handset_detection 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/handset_detection/base.rb +554 -0
- data/lib/handset_detection/cache/filesystem.rb +87 -0
- data/lib/handset_detection/cache/memcached.rb +70 -0
- data/lib/handset_detection/cache/none.rb +60 -0
- data/lib/handset_detection/cache/rails.rb +59 -0
- data/lib/handset_detection/cache.rb +105 -0
- data/lib/handset_detection/device.rb +667 -0
- data/lib/handset_detection/extra.rb +221 -0
- data/lib/handset_detection/hd4.rb +458 -0
- data/lib/handset_detection/store.rb +178 -0
- data/lib/handset_detection.rb +27 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 171d53310321c9a9aa5173e52adafaf3e50a222e
|
4
|
+
data.tar.gz: 0d30b245f08d3b079dbfc0d3b6f4404a24029283
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a88fde905056abb3cb011b6e723caa741276133c283a19f9b4c3b79bb35932d4c76b747cd08152de29e17ca9e97776a0b314179a1a0e6d1cb8cd8a375132a385
|
7
|
+
data.tar.gz: e2f4e48c3cb022b17f8f939d6f4d61e5dcbdade57b8e44c553c115b2f17e08f8927b756caa02f23b8af3ebf9885bc60f698dbc8fcddf0debbb3fe7bb60c63e68
|
@@ -0,0 +1,554 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) Richard Uren 2016 <richard@teleport.com.au>
|
3
|
+
# All Rights Reserved
|
4
|
+
#
|
5
|
+
# LICENSE: Redistribution and use in source and binary forms, with or
|
6
|
+
# without modification, are permitted provided that the following
|
7
|
+
# conditions are met: Redistributions of source code must retain the
|
8
|
+
# above copyright notice, this list of conditions and the following
|
9
|
+
# disclaimer. Redistributions in binary form must reproduce the above
|
10
|
+
# copyright notice, this list of conditions and the following disclaimer
|
11
|
+
# in the documentation and/or other materials provided with the
|
12
|
+
# distribution.
|
13
|
+
#
|
14
|
+
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
15
|
+
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
16
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
17
|
+
# NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
18
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
19
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
20
|
+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
21
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
22
|
+
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
23
|
+
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
24
|
+
# DAMAGE.
|
25
|
+
#++
|
26
|
+
|
27
|
+
require 'active_support/core_ext/object/blank'
|
28
|
+
require 'json'
|
29
|
+
require 'digest/md5'
|
30
|
+
require 'socket'
|
31
|
+
require 'tcp_timeout'
|
32
|
+
require 'uri'
|
33
|
+
|
34
|
+
DETECTIONV4_STANDARD = 0
|
35
|
+
DETECTIONV4_GENERIC = 1
|
36
|
+
|
37
|
+
class Base
|
38
|
+
def initialize
|
39
|
+
@config = {}
|
40
|
+
@api_base = '/apiv4/'
|
41
|
+
@detected_rule_key = {}
|
42
|
+
@device_ua_filter = /[ _\\#\-,.\/:"']/
|
43
|
+
@extra_ua_filter = /[ ]/
|
44
|
+
@apikit = 'Ruby 4.0.0'
|
45
|
+
@logger_host = 'logger.handsetdetection.com'
|
46
|
+
@logger_port = 80
|
47
|
+
@reply = {}
|
48
|
+
@tree = {}
|
49
|
+
@detection_config = {
|
50
|
+
'device-ua-order' => ['x-operamini-phone-ua', 'x-mobile-ua', 'device-stock-ua', 'user-agent', 'agent'],
|
51
|
+
'platform-ua-order' => ['x-operamini-phone-ua', 'x-mobile-ua', 'device-stock-ua', 'user-agent', 'agent'],
|
52
|
+
'browser-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
|
53
|
+
'app-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
|
54
|
+
'language-ua-order' => ['user-agent', 'agent', 'device-stock-ua'],
|
55
|
+
'device-bi-order' => {
|
56
|
+
'android' => [
|
57
|
+
['ro.product.brand','ro.product.model'],
|
58
|
+
['ro.product.manufacturer','ro.product.model'],
|
59
|
+
['ro-product-brand','ro-product-model'],
|
60
|
+
['ro-product-manufacturer','ro-product-model'],
|
61
|
+
],
|
62
|
+
'ios' => [
|
63
|
+
['utsname.brand','utsname.machine']
|
64
|
+
],
|
65
|
+
'windows phone' => [
|
66
|
+
['devicemanufacturer','devicename']
|
67
|
+
]
|
68
|
+
},
|
69
|
+
'platform-bi-order' => {
|
70
|
+
'android' => [
|
71
|
+
['ro.build.id', 'ro.build.version.release'],
|
72
|
+
['ro-build-id', 'ro-build-version-release'],
|
73
|
+
],
|
74
|
+
'ios' => [
|
75
|
+
['uidevice.systemName','uidevice.systemversion']
|
76
|
+
],
|
77
|
+
'windows phone' => [
|
78
|
+
['osname','osversion']
|
79
|
+
]
|
80
|
+
},
|
81
|
+
'browser-bi-order' => [],
|
82
|
+
'app-bi-order' => []
|
83
|
+
}
|
84
|
+
@detection_languages = {
|
85
|
+
'af' => 'Afrikaans',
|
86
|
+
'sq' => 'Albanian',
|
87
|
+
'ar-dz' => 'Arabic (Algeria)',
|
88
|
+
'ar-bh' => 'Arabic (Bahrain)',
|
89
|
+
'ar-eg' => 'Arabic (Egypt)',
|
90
|
+
'ar-iq' => 'Arabic (Iraq)',
|
91
|
+
'ar-jo' => 'Arabic (Jordan)',
|
92
|
+
'ar-kw' => 'Arabic (Kuwait)',
|
93
|
+
'ar-lb' => 'Arabic (Lebanon)',
|
94
|
+
'ar-ly' => 'Arabic (libya)',
|
95
|
+
'ar-ma' => 'Arabic (Morocco)',
|
96
|
+
'ar-om' => 'Arabic (Oman)',
|
97
|
+
'ar-qa' => 'Arabic (Qatar)',
|
98
|
+
'ar-sa' => 'Arabic (Saudi Arabia)',
|
99
|
+
'ar-sy' => 'Arabic (Syria)',
|
100
|
+
'ar-tn' => 'Arabic (Tunisia)',
|
101
|
+
'ar-ae' => 'Arabic (U.A.E.)',
|
102
|
+
'ar-ye' => 'Arabic (Yemen)',
|
103
|
+
'ar' => 'Arabic',
|
104
|
+
'hy' => 'Armenian',
|
105
|
+
'as' => 'Assamese',
|
106
|
+
'az' => 'Azeri',
|
107
|
+
'eu' => 'Basque',
|
108
|
+
'be' => 'Belarusian',
|
109
|
+
'bn' => 'Bengali',
|
110
|
+
'bg' => 'Bulgarian',
|
111
|
+
'ca' => 'Catalan',
|
112
|
+
'zh-cn' => 'Chinese (China)',
|
113
|
+
'zh-hk' => 'Chinese (Hong Kong SAR)',
|
114
|
+
'zh-mo' => 'Chinese (Macau SAR)',
|
115
|
+
'zh-sg' => 'Chinese (Singapore)',
|
116
|
+
'zh-tw' => 'Chinese (Taiwan)',
|
117
|
+
'zh' => 'Chinese',
|
118
|
+
'hr' => 'Croatian',
|
119
|
+
'cs' => 'Czech',
|
120
|
+
'da' => 'Danish',
|
121
|
+
'da-dk' => 'Danish',
|
122
|
+
'div' => 'Divehi',
|
123
|
+
'nl-be' => 'Dutch (Belgium)',
|
124
|
+
'nl' => 'Dutch (Netherlands)',
|
125
|
+
'en-au' => 'English (Australia)',
|
126
|
+
'en-bz' => 'English (Belize)',
|
127
|
+
'en-ca' => 'English (Canada)',
|
128
|
+
'en-ie' => 'English (Ireland)',
|
129
|
+
'en-jm' => 'English (Jamaica)',
|
130
|
+
'en-nz' => 'English (New Zealand)',
|
131
|
+
'en-ph' => 'English (Philippines)',
|
132
|
+
'en-za' => 'English (South Africa)',
|
133
|
+
'en-tt' => 'English (Trinidad)',
|
134
|
+
'en-gb' => 'English (United Kingdom)',
|
135
|
+
'en-us' => 'English (United States)',
|
136
|
+
'en-zw' => 'English (Zimbabwe)',
|
137
|
+
'en' => 'English',
|
138
|
+
'us' => 'English (United States)',
|
139
|
+
'et' => 'Estonian',
|
140
|
+
'fo' => 'Faeroese',
|
141
|
+
'fa' => 'Farsi',
|
142
|
+
'fi' => 'Finnish',
|
143
|
+
'fr-be' => 'French (Belgium)',
|
144
|
+
'fr-ca' => 'French (Canada)',
|
145
|
+
'fr-lu' => 'French (Luxembourg)',
|
146
|
+
'fr-mc' => 'French (Monaco)',
|
147
|
+
'fr-ch' => 'French (Switzerland)',
|
148
|
+
'fr' => 'French (France)',
|
149
|
+
'mk' => 'FYRO Macedonian',
|
150
|
+
'gd' => 'Gaelic',
|
151
|
+
'ka' => 'Georgian',
|
152
|
+
'de-at' => 'German (Austria)',
|
153
|
+
'de-li' => 'German (Liechtenstein)',
|
154
|
+
'de-lu' => 'German (Luxembourg)',
|
155
|
+
'de-ch' => 'German (Switzerland)',
|
156
|
+
'de-de' => 'German (Germany)',
|
157
|
+
'de' => 'German (Germany)',
|
158
|
+
'el' => 'Greek',
|
159
|
+
'gu' => 'Gujarati',
|
160
|
+
'he' => 'Hebrew',
|
161
|
+
'hi' => 'Hindi',
|
162
|
+
'hu' => 'Hungarian',
|
163
|
+
'is' => 'Icelandic',
|
164
|
+
'id' => 'Indonesian',
|
165
|
+
'it-ch' => 'Italian (Switzerland)',
|
166
|
+
'it' => 'Italian (Italy)',
|
167
|
+
'it-it' => 'Italian (Italy)',
|
168
|
+
'ja' => 'Japanese',
|
169
|
+
'kn' => 'Kannada',
|
170
|
+
'kk' => 'Kazakh',
|
171
|
+
'kok' => 'Konkani',
|
172
|
+
'ko' => 'Korean',
|
173
|
+
'kz' => 'Kyrgyz',
|
174
|
+
'lv' => 'Latvian',
|
175
|
+
'lt' => 'Lithuanian',
|
176
|
+
'ms' => 'Malay',
|
177
|
+
'ml' => 'Malayalam',
|
178
|
+
'mt' => 'Maltese',
|
179
|
+
'mr' => 'Marathi',
|
180
|
+
'mn' => 'Mongolian (Cyrillic)',
|
181
|
+
'ne' => 'Nepali (India)',
|
182
|
+
'nb-no' => 'Norwegian (Bokmal)',
|
183
|
+
'nn-no' => 'Norwegian (Nynorsk)',
|
184
|
+
'no' => 'Norwegian (Bokmal)',
|
185
|
+
'or' => 'Oriya',
|
186
|
+
'pl' => 'Polish',
|
187
|
+
'pt-br' => 'Portuguese (Brazil)',
|
188
|
+
'pt' => 'Portuguese (Portugal)',
|
189
|
+
'pa' => 'Punjabi',
|
190
|
+
'rm' => 'Rhaeto-Romanic',
|
191
|
+
'ro-md' => 'Romanian (Moldova)',
|
192
|
+
'ro' => 'Romanian',
|
193
|
+
'ru-md' => 'Russian (Moldova)',
|
194
|
+
'ru' => 'Russian',
|
195
|
+
'sa' => 'Sanskrit',
|
196
|
+
'sr' => 'Serbian',
|
197
|
+
'sk' => 'Slovak',
|
198
|
+
'ls' => 'Slovenian',
|
199
|
+
'sb' => 'Sorbian',
|
200
|
+
'es-ar' => 'Spanish (Argentina)',
|
201
|
+
'es-bo' => 'Spanish (Bolivia)',
|
202
|
+
'es-cl' => 'Spanish (Chile)',
|
203
|
+
'es-co' => 'Spanish (Colombia)',
|
204
|
+
'es-cr' => 'Spanish (Costa Rica)',
|
205
|
+
'es-do' => 'Spanish (Dominican Republic)',
|
206
|
+
'es-ec' => 'Spanish (Ecuador)',
|
207
|
+
'es-sv' => 'Spanish (El Salvador)',
|
208
|
+
'es-gt' => 'Spanish (Guatemala)',
|
209
|
+
'es-hn' => 'Spanish (Honduras)',
|
210
|
+
'es-mx' => 'Spanish (Mexico)',
|
211
|
+
'es-ni' => 'Spanish (Nicaragua)',
|
212
|
+
'es-pa' => 'Spanish (Panama)',
|
213
|
+
'es-py' => 'Spanish (Paraguay)',
|
214
|
+
'es-pe' => 'Spanish (Peru)',
|
215
|
+
'es-pr' => 'Spanish (Puerto Rico)',
|
216
|
+
'es-us' => 'Spanish (United States)',
|
217
|
+
'es-uy' => 'Spanish (Uruguay)',
|
218
|
+
'es-ve' => 'Spanish (Venezuela)',
|
219
|
+
'es' => 'Spanish (Traditional Sort)',
|
220
|
+
'es-es' => 'Spanish (Traditional Sort)',
|
221
|
+
'sx' => 'Sutu',
|
222
|
+
'sw' => 'Swahili',
|
223
|
+
'sv-fi' => 'Swedish (Finland)',
|
224
|
+
'sv' => 'Swedish',
|
225
|
+
'syr' => 'Syriac',
|
226
|
+
'ta' => 'Tamil',
|
227
|
+
'tt' => 'Tatar',
|
228
|
+
'te' => 'Telugu',
|
229
|
+
'th' => 'Thai',
|
230
|
+
'ts' => 'Tsonga',
|
231
|
+
'tn' => 'Tswana',
|
232
|
+
'tr' => 'Turkish',
|
233
|
+
'uk' => 'Ukrainian',
|
234
|
+
'ur' => 'Urdu',
|
235
|
+
'uz' => 'Uzbek',
|
236
|
+
'vi' => 'Vietnamese',
|
237
|
+
'xh' => 'Xhosa',
|
238
|
+
'yi' => 'Yiddish',
|
239
|
+
'zu' => 'Zulu'
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
# Get reply status
|
244
|
+
#
|
245
|
+
# +param+ void
|
246
|
+
# +return+ int error status, 0 is Ok, anything else is probably not Ok
|
247
|
+
#
|
248
|
+
def get_status
|
249
|
+
@reply['status']
|
250
|
+
end
|
251
|
+
|
252
|
+
# Get reply message
|
253
|
+
#
|
254
|
+
# +param+ void
|
255
|
+
# +return+ string A message
|
256
|
+
#
|
257
|
+
def get_message
|
258
|
+
@reply['message']
|
259
|
+
end
|
260
|
+
|
261
|
+
# Get reply payload in array assoc format
|
262
|
+
#
|
263
|
+
# +param+ void
|
264
|
+
# +return+ array
|
265
|
+
#
|
266
|
+
def get_reply
|
267
|
+
@reply
|
268
|
+
end
|
269
|
+
|
270
|
+
# Set a reply payload
|
271
|
+
#
|
272
|
+
# +param+ array reply
|
273
|
+
# +return+ void
|
274
|
+
#
|
275
|
+
def set_reply(reply)
|
276
|
+
@reply = reply
|
277
|
+
end
|
278
|
+
|
279
|
+
# Error handling helper. Sets a message and an error code.
|
280
|
+
#
|
281
|
+
# +param+ int status
|
282
|
+
# +param+ string msg
|
283
|
+
# +return+ true if no error, or false otherwise.
|
284
|
+
#
|
285
|
+
def set_error(status, msg)
|
286
|
+
@error = msg
|
287
|
+
@reply['status'] = status
|
288
|
+
@reply['message'] = msg
|
289
|
+
status == 0
|
290
|
+
end
|
291
|
+
|
292
|
+
# String cleanse for extras matching.
|
293
|
+
#
|
294
|
+
# +param+ string str
|
295
|
+
# +return+ string Cleansed string
|
296
|
+
#
|
297
|
+
def extra_clean_str(str)
|
298
|
+
str = str.downcase.gsub @extra_ua_filter, ''
|
299
|
+
str = str.gsub(/[^\x20-\x7F]/, '')
|
300
|
+
str.strip
|
301
|
+
end
|
302
|
+
|
303
|
+
# Standard string cleanse for device matching
|
304
|
+
#
|
305
|
+
# +param+ string str
|
306
|
+
# +return+ string cleansed string
|
307
|
+
#
|
308
|
+
def clean_str(str)
|
309
|
+
str = str.downcase.gsub @device_ua_filter, ''
|
310
|
+
str = str.gsub(/[^\x20-\x7F]/, '')
|
311
|
+
str.strip
|
312
|
+
end
|
313
|
+
|
314
|
+
# Log function - User defined functions can be supplied in the 'logger' config variable.
|
315
|
+
#
|
316
|
+
def log(msg)
|
317
|
+
Syslog.log(Syslog::LOG_NOTICE, "#{Time.now.to_f} #{msg}")
|
318
|
+
if @config.key?('logger') and @config['logger'].is_a?(Proc)
|
319
|
+
@config['logger'].call(msg)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Makes requests to the various web services of Handset Detection.
|
324
|
+
#
|
325
|
+
# Note : suburl - the url fragment of the web service eg site/detect/${site_id}
|
326
|
+
#
|
327
|
+
# +param+ string suburl
|
328
|
+
# +param+ string data
|
329
|
+
# +param+ string filetype
|
330
|
+
# +param+ boolean auth_required - Is authentication required ?
|
331
|
+
# +return+ bool true on success, false otherwise
|
332
|
+
#
|
333
|
+
def remote(suburl, data, filetype='json', auth_required=true)
|
334
|
+
@reply = {}
|
335
|
+
@raw_reply = {}
|
336
|
+
set_error 0, ''
|
337
|
+
|
338
|
+
if data.blank?
|
339
|
+
data = []
|
340
|
+
end
|
341
|
+
|
342
|
+
url = @api_base + suburl
|
343
|
+
attempts = @config['retries'] + 1
|
344
|
+
trys = 0
|
345
|
+
|
346
|
+
requestdata = JSON.generate(data)
|
347
|
+
|
348
|
+
success = false
|
349
|
+
while (trys+=1) < attempts and success == false
|
350
|
+
@raw_reply = post @config['api_server'], url, requestdata, auth_required
|
351
|
+
if @raw_reply == false
|
352
|
+
set_error 299, "Error : Connection to #{url} failed"
|
353
|
+
elsif
|
354
|
+
if filetype == 'json'
|
355
|
+
@reply = JSON.parse @raw_reply
|
356
|
+
|
357
|
+
if @reply.blank?
|
358
|
+
set_error 299, "Error : Empty Reply."
|
359
|
+
elsif not @reply.key? 'status'
|
360
|
+
set_error 299, "Error : No status set in reply"
|
361
|
+
elsif @reply['status'].to_i != 0
|
362
|
+
set_error @reply['status'], @reply['message']
|
363
|
+
trys = attempts + 1
|
364
|
+
else
|
365
|
+
success = true
|
366
|
+
end
|
367
|
+
else
|
368
|
+
success = true
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
success
|
373
|
+
end
|
374
|
+
|
375
|
+
# Post data to remote server
|
376
|
+
#
|
377
|
+
# Modified version of PHP Post from From http://www.enyem.com/wiki/index.php/Send_POST_request_(PHP)
|
378
|
+
# Thanks dude !
|
379
|
+
#
|
380
|
+
# +param+ string server Server name
|
381
|
+
# +param+ string url URL name
|
382
|
+
# +param+ string jsondata Data in json format
|
383
|
+
# +param+ boolean auth_required Is suthentication reguired ?
|
384
|
+
# +return+ false on failue (sets error), or string on success.
|
385
|
+
#
|
386
|
+
def post(server, url, jsondata, auth_required=true)
|
387
|
+
host = server
|
388
|
+
port = 80
|
389
|
+
uri = URI.parse url.gsub(/ /, '%20')
|
390
|
+
realm = @realm
|
391
|
+
username =@config['username']
|
392
|
+
nc = "00000001"
|
393
|
+
snonce = @realm
|
394
|
+
cnonce = Digest::MD5.hexdigest Time.now.to_i.to_s + @config['secret']
|
395
|
+
qop = 'auth'
|
396
|
+
|
397
|
+
if @config['use_proxy']
|
398
|
+
host = @config['proxy_server']
|
399
|
+
port = @config['proxy_port']
|
400
|
+
end
|
401
|
+
|
402
|
+
# AuthDigest Components
|
403
|
+
# http://en.wikipedia.org/wiki/Digest_access_authentication
|
404
|
+
ha1 = Digest::MD5.hexdigest username + ':' + realm + ':' + @config['secret']
|
405
|
+
ha2 = Digest::MD5.hexdigest 'POST:' + uri.path
|
406
|
+
response = Digest::MD5.hexdigest ha1 + ':' + snonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2
|
407
|
+
|
408
|
+
out = "POST #{url} HTTP/1.0\r\n"
|
409
|
+
out += "Host: #{server}\r\n"
|
410
|
+
if @config['use_proxy'] and not @config['proxy_user'].blank? and not @config['proxy_pass'].blank?
|
411
|
+
out += "Proxy-Authorization:Basic " + base64_encode(@config['proxy_user'] + ':' + @config['proxy_pass']) + "\r\n"
|
412
|
+
end
|
413
|
+
out += "Content-Type: application/json\r\n"
|
414
|
+
# Pre-computed auth credentials, saves waiting for the auth challenge hence makes things round trip time 50% faster.
|
415
|
+
if auth_required
|
416
|
+
out += 'Authorization: Digest ' +
|
417
|
+
'username="' + username + '", ' +
|
418
|
+
'realm="' + realm + '", ' +
|
419
|
+
'nonce="' + snonce + '", ' +
|
420
|
+
'uri="' + uri.path + '", ' +
|
421
|
+
'qop=' + qop + ', ' +
|
422
|
+
'nc=' + nc + ', ' +
|
423
|
+
'cnonce="' + cnonce + '", ' +
|
424
|
+
'response="' + response + '", ' +
|
425
|
+
'opaque="' + realm + '"' +
|
426
|
+
"\r\n"
|
427
|
+
end
|
428
|
+
out += "Content-length: " + jsondata.length.to_s + "\r\n\r\n"
|
429
|
+
out += jsondata + "\r\n\r\n"
|
430
|
+
|
431
|
+
socket = nil
|
432
|
+
begin
|
433
|
+
socket = TCPTimeout::TCPSocket.new(host, port,
|
434
|
+
connect_timeout: @config['timeout'], read_timeout: @config['timeout'], write_timeout: @config['timeout'])
|
435
|
+
socket.write(out)
|
436
|
+
reply = socket.read 1000000000
|
437
|
+
rescue SocketError => e
|
438
|
+
return set_error 299, e.to_s
|
439
|
+
ensure
|
440
|
+
socket.close unless socket.nil?
|
441
|
+
end
|
442
|
+
hunks = reply.split("\r\n\r\n")
|
443
|
+
return set_error(299, "Error : Reply is too short.") if hunks.length < 2
|
444
|
+
# header = hunks[hunks.length - 2]
|
445
|
+
# headers = header.split("\n")
|
446
|
+
body = hunks[hunks.length - 1]
|
447
|
+
return set_error(299, "Error : Reply body is empty.") if body.blank?
|
448
|
+
body
|
449
|
+
end
|
450
|
+
|
451
|
+
# Helper for determining if a header has BiKeys
|
452
|
+
#
|
453
|
+
# +param+ hash header
|
454
|
+
# +return+ platform name on success, false otherwise
|
455
|
+
#
|
456
|
+
def has_bi_keys(headers)
|
457
|
+
bi_keys = @detection_config['device-bi-order']
|
458
|
+
data_keys = {}
|
459
|
+
headers.each { |k, v| data_keys[k.downcase] = v }
|
460
|
+
|
461
|
+
# Fast check
|
462
|
+
return false if headers.key? 'agent'
|
463
|
+
return false if headers.key? 'user-agent'
|
464
|
+
bi_keys.each do |platform, s|
|
465
|
+
s.each do |tuple|
|
466
|
+
count = 0
|
467
|
+
total = tuple.length
|
468
|
+
tuple.each do |item|
|
469
|
+
count += 1 if data_keys.include? item
|
470
|
+
return platform if count == total
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
false
|
475
|
+
end
|
476
|
+
|
477
|
+
# The heart of the detection process
|
478
|
+
#
|
479
|
+
# +param+ string header The type of header we're matching against - user-agent type headers use a sieve matching, all others are hash matching.
|
480
|
+
# +param+ string newvalue The http header's value (could be a user-agent or some other x- header value)
|
481
|
+
# +param+ string treetag The branch name eg : user-agent0, user-agent1, user-agentplatform, user-agentbrowser
|
482
|
+
# +return+ int node (which is an id) on success, false otherwise
|
483
|
+
#
|
484
|
+
def get_match(header, value, subtree="0", actual_header='', cls='device')
|
485
|
+
f = 0
|
486
|
+
r = 0
|
487
|
+
if cls == 'device'
|
488
|
+
value = clean_str value
|
489
|
+
else
|
490
|
+
value = extra_clean_str value
|
491
|
+
end
|
492
|
+
treetag = "#{header}#{subtree}"
|
493
|
+
|
494
|
+
return false if value.length < 4
|
495
|
+
|
496
|
+
branch = get_branch treetag
|
497
|
+
return false if branch.blank?
|
498
|
+
if header == 'user-agent'
|
499
|
+
# Sieve matching strategy
|
500
|
+
branch.each do |order, filters|
|
501
|
+
filters.each do |filter, matches|
|
502
|
+
f += 1
|
503
|
+
if value.include? filter
|
504
|
+
matches.each do |match, node|
|
505
|
+
r += 1
|
506
|
+
if value.include? match
|
507
|
+
@detected_rule_key[cls] = clean_str(header) + ':' + clean_str(filter) + ':' + clean_str(match)
|
508
|
+
return node
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
else
|
515
|
+
# Hash matching strategy
|
516
|
+
unless branch[value].blank?
|
517
|
+
node = branch[value]
|
518
|
+
return node
|
519
|
+
end
|
520
|
+
end
|
521
|
+
false
|
522
|
+
end
|
523
|
+
|
524
|
+
# Find a branch for the matching process
|
525
|
+
#
|
526
|
+
# +param+ string branch The name of the branch to find
|
527
|
+
# +return+ an assoc array on success, false otherwise.
|
528
|
+
#
|
529
|
+
def get_branch(branch)
|
530
|
+
return @tree[branch] unless @tree[branch].blank?
|
531
|
+
tmp = @store.read branch
|
532
|
+
if tmp != false
|
533
|
+
@tree[branch] = tmp
|
534
|
+
return tmp
|
535
|
+
end
|
536
|
+
false
|
537
|
+
end
|
538
|
+
|
539
|
+
# UDP Syslog via https://gist.github.com/troy/2220679 - Thanks Troy
|
540
|
+
#
|
541
|
+
# Send a message via UDP, used if log_unknown is set in config && running in Ultimate (local) mode.
|
542
|
+
#
|
543
|
+
# +param+ array $headers
|
544
|
+
# +return+ void
|
545
|
+
#
|
546
|
+
def send_remote_syslog(headers)
|
547
|
+
headers['version'] = RUBY_VERSION
|
548
|
+
headers['apikit'] = @apikit
|
549
|
+
sock = UDPSocket.new(Socket::AF_INET)
|
550
|
+
message = JSON.generate headers
|
551
|
+
sock.send '<22> ' + message, 0, @logger_host, @logger_port
|
552
|
+
sock.close
|
553
|
+
end
|
554
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) Richard Uren 2016 <richard@teleport.com.au>
|
3
|
+
# All Rights Reserved
|
4
|
+
#
|
5
|
+
# LICENSE: Redistribution and use in source and binary forms, with or
|
6
|
+
# without modification, are permitted provided that the following
|
7
|
+
# conditions are met: Redistributions of source code must retain the
|
8
|
+
# above copyright notice, this list of conditions and the following
|
9
|
+
# disclaimer. Redistributions in binary form must reproduce the above
|
10
|
+
# copyright notice, this list of conditions and the following disclaimer
|
11
|
+
# in the documentation and/or other materials provided with the
|
12
|
+
# distribution.
|
13
|
+
#
|
14
|
+
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
15
|
+
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
16
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
17
|
+
# NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
18
|
+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
19
|
+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
20
|
+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
21
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
22
|
+
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
23
|
+
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
24
|
+
# DAMAGE.
|
25
|
+
#++
|
26
|
+
|
27
|
+
class FileSystem
|
28
|
+
# Construct a new File cache object.
|
29
|
+
#
|
30
|
+
# +param+ string dir
|
31
|
+
#
|
32
|
+
def initialize(config={})
|
33
|
+
if config.include?('cache') and config['cache'].include?('file') and not config['cache']['file']['directory'].blank?
|
34
|
+
dir = config['cache']['file']['directory']
|
35
|
+
else
|
36
|
+
dir = Dir.tmpdir
|
37
|
+
end
|
38
|
+
dir += File::SEPARATOR unless dir.match(/#{File::SEPARATOR}$/)
|
39
|
+
raise 'Directory does not exist.' unless File.directory? dir
|
40
|
+
raise 'Directory is not writable.' unless File.writable? dir
|
41
|
+
@dir = dir
|
42
|
+
@prefix = (config.include?('cache') and config['cache'].include?('prefix')) ? config['cache']['prefix'] : 'hd40'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get key
|
46
|
+
#
|
47
|
+
def get(key)
|
48
|
+
fname = get_file_path key
|
49
|
+
return nil unless File.file? fname
|
50
|
+
|
51
|
+
data = File.readlines fname
|
52
|
+
exp = data.shift.to_i
|
53
|
+
return nil if Time.now.to_i > exp and exp != -1
|
54
|
+
|
55
|
+
Marshal::load data.join('')
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set key
|
59
|
+
#
|
60
|
+
def set(key, data, ttl)
|
61
|
+
fname = get_file_path key
|
62
|
+
File.open(fname, 'w') { |f| f.write((Time.now.to_i + ttl).to_s + "\n" + Marshal::dump(data)) }
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Delete key
|
67
|
+
#
|
68
|
+
def del(key)
|
69
|
+
fname = get_file_path key
|
70
|
+
return true unless File.file? fname
|
71
|
+
File.unlink fname
|
72
|
+
end
|
73
|
+
|
74
|
+
# Flush cache
|
75
|
+
def flush
|
76
|
+
files = Dir.glob @dir + @prefix + '*'
|
77
|
+
files.each do |file|
|
78
|
+
File.unlink file if File.file? file
|
79
|
+
end
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get fully qualified path to file
|
84
|
+
def get_file_path(key)
|
85
|
+
@dir + key
|
86
|
+
end
|
87
|
+
end
|