def __init__(self, configFile=None): self._Store = HDStore() self._Device = HDDevice() if configFile is not None: self.readConfig(configFile) elif os.path.isfile(self.configFile): self.readConfig(self.configFile)
def test_FetchDevices(self): store = HDStore({'config': {'filesdir': '/tmp/hd40test'}}) store.write('Device_' + str(self._deviceA['_id']), self._deviceA) store.write('Device_' + str(self._deviceB['_id']), self._deviceB) devices = store.fetchDevices() count = 0 for key in devices: count += 1 self.assertEqual(2, count)
def test_FetchDevices(self): store = HDStore({"config": {"filesdir": "/tmp/hd40test"}}) store.write("Device_" + str(self._deviceA["_id"]), self._deviceA) store.write("Device_" + str(self._deviceB["_id"]), self._deviceB) devices = store.fetchDevices() count = 0 for key in devices: count += 1 self.assertEqual(2, count)
def test_readWrite(self): key = 'readkey-' + self._token store = HDStore() store.write(key, self._data) data = store.read(key) self.assertEqual(data, self._data) # Verify data is cached data = store._Cache.read(key) self.assertEqual(data, self._data) # Verify data is on disk exists = os.path.isfile(os.path.join(store._directory, key + '.json')) self.assertTrue(exists)
def test_storeFetch(self): key = 'storekey-' + self._token store = HDStore() store.store(key, self._data) data = store.fetch(key) self.assertEqual(self._data, data) # Verify data is not cached data = store._Cache.read(key) self.assertEqual(data, None) # Verify data is on disk exists = os.path.isfile(os.path.join(store._directory, key + '.json')) self.assertTrue(exists)
def test_storeFetch(self): key = "storekey-" + self._token store = HDStore() store.store(key, self._data) data = store.fetch(key) self.assertEqual(self._data, data) # Verify data is not cached data = store._Cache.read(key) self.assertEqual(data, None) # Verify data is on disk exists = os.path.isfile(os.path.join(store._directory, key + ".json")) self.assertTrue(exists)
def test_readWrite(self): key = "readkey-" + self._token store = HDStore() store.write(key, self._data) data = store.read(key) self.assertEqual(data, self._data) # Verify data is cached data = store._Cache.read(key) self.assertEqual(data, self._data) # Verify data is on disk exists = os.path.isfile(os.path.join(store._directory, key + ".json")) self.assertTrue(exists)
def __init__(self, configFile = None): self._Store = HDStore() self._Device = HDDevice() if configFile is not None: self.readConfig(configFile) elif os.path.isfile(self.configFile): self.readConfig(self.configFile)
def test_moveIn(self): store = HDStore({'config': {'filesdir': '/tmp/hd40test'}}) store.purge() deviceA = store.read('Device_' + str(self._deviceA['_id'])) self.assertEqual(None, deviceA) # Save dict as Json - try moving it into the store, then fetching it back out. fileName = 'Device_' + str(self._deviceA['_id']) + '.json' jsonFile = open(fileName, "w") jsonFile.write(json.dumps(self._deviceA)) jsonFile.close() store.moveIn(fileName, fileName) deviceA = store.fetch('Device_' + str(self._deviceA['_id'])) self.assertEqual(self._deviceA, deviceA) # Cleanup store.purge() pass
def test_moveIn(self): store = HDStore({"config": {"filesdir": "/tmp/hd40test"}}) store.purge() deviceA = store.read("Device_" + str(self._deviceA["_id"])) self.assertEqual(None, deviceA) # Save dict as Json - try moving it into the store, then fetching it back out. fileName = "Device_" + str(self._deviceA["_id"]) + ".json" jsonFile = open(fileName, "w") jsonFile.write(json.dumps(self._deviceA)) jsonFile.close() store.moveIn(fileName, fileName) deviceA = store.fetch("Device_" + str(self._deviceA["_id"])) self.assertEqual(self._deviceA, deviceA) # Cleanup store.purge() pass
def __init__(self, config=None): self._Store = HDStore() if config is not None: self.setConfig(config)
class HDExtra(HDBase): _data = None _Store = None config = {} def __init__(self, config=None): self._Store = HDStore() if config is not None: self.setConfig(config) def setConfig(self, config): for key in config['config']: self.config[key] = config['config'][key] self._Store.setConfig(config) def set(self, data): self._data = data def matchExtra(self, category, headers): """ Matches all HTTP header extras - platform, browser and app """ order = self._detectionConfig[category + '-ua-order'] # Match nominated headers ahead of x- headers, but do check x- headers for k in headers: if k not in order and k.startswith("x-"): order.append(k) for item in order: if item in headers: self.log("Trying user-agent match on header " + item) _id = self.getMatch('user-agent', headers[item], category, item, category) if _id is not None: return self.findById(_id) return None def findById(self, _id): """ Find an extra by its id """ return self._Store.read("Extra_" + _id) def matchLanguage(self, category, headers): """ Find the language, preferebly from a language header, but try the user-agent as a last resort """ extra = {} # Mock up a fake Extra for merge into detection reply. extra['_id'] = '0' extra['Extra'] = {} extra['Extra']['hd_specs'] = {} extra['Extra']['hd_specs']['general_language'] = '' extra['Extra']['hd_specs']['general_language_full'] = '' # Try directly from http header first if 'language' in headers: candidate = headers['language'] if candidate in self._detectionLanguages: extra['Extra']['hd_specs']['general_language'] = candidate extra['Extra']['hd_specs'][ 'general_language_full'] = self._detectionLanguages[ candidate] return extra order = self._detectionConfig['language-ua-order'] for k in headers: if k not in order: order.append(k) languageList = self._detectionLanguages for headerKey in order: if headerKey in headers: agent = headers[headerKey] if agent is not None: for code in languageList: if re.search('[; \(]' + code + '[; \)]', agent, re.I) is not None: extra['Extra']['hd_specs'][ 'general_language'] = code extra['Extra']['hd_specs'][ 'general_language_full'] = languageList[code] return extra return None def verifyPlatform(self, specs): """ Returns false if this device definitively cannot run this platform and platform version. Returns true if its possible of if there is any doubt. Note : The detected platform must match the device platform. This is the stock OS as shipped on the device. If someone is running a variant (eg CyanogenMod) then all bets are off. """ if self._data is None: return None platform = self._data platformName = platform['Extra']['hd_specs']['general_platform'].strip( ).lower() platformVersion = platform['Extra']['hd_specs'][ 'general_platform_version'].strip().lower() devicePlatformName = specs['general_platform'].strip().lower() devicePlatformVersionMin = specs['general_platform_version'].strip( ).lower() devicePlatformVersionMax = specs['general_platform_version_max'].strip( ).lower() # Its possible that we didnt pickup the platform correctly or the device has no platform info # Return true in this case because we cant give a concrete false (it might run this version). if platform is None or platformName == '' or devicePlatformName == '': return True # Make sure device is running stock OS / Platform # Return true in this case because its possible the device can run a different OS (mods / hacks etc..) if platformName != devicePlatformName: return True # Detected version is lower than the min version - so definetly false. if platformVersion != '' and devicePlatformVersionMin != '' and self.comparePlatformVersions( platformVersion, devicePlatformVersionMin) <= -1: return False # Detected version is greater than the max version - so definetly false. if platformVersion != '' and devicePlatformVersionMax != '' and self.comparePlatformVersions( platformVersion, devicePlatformVersionMax) >= 1: return False # Maybe Ok .. return True def comparePlatformVersions(self, va, vb): """ Compares two platform version numbers return < 0 if a < b, 0 if a == b and > 0 if a > b : Also returns 0 if data is absent from either. """ if va is None or va == '' or vb is None or vb == '': return 0 versionA = self.breakVersionApart(va) versionB = self.breakVersionApart(vb) major = self.compareSmartly(versionA['major'], versionB['major']) minor = self.compareSmartly(versionA['minor'], versionB['minor']) point = self.compareSmartly(versionA['point'], versionB['point']) if major != 0: return major if minor != 0: return minor if point != 0: return point return 0 def breakVersionApart(self, versionNumber): """ Breaks a version number apart into its major, minor and point release numbers for comparison. Big Assumption : That version numbers separate their release bits by '.' !!! might need to do some analysis on the string to rip it up right, but this assumption seems to hold... for now. """ versionNumber = versionNumber + '.0.0.0' tmp = versionNumber.split('.', 4) reply = {} reply['major'] = tmp[0] if tmp[0] is not None and tmp[0] != '' else '0' reply['minor'] = tmp[1] if tmp[1] is not None and tmp[1] != '' else '0' reply['point'] = tmp[2] if tmp[2] is not None and tmp[2] != '' else '0' return reply def compareSmartly(self, a, b): """ Helper for comparing two strings (numerically if possible) """ if sys.version_info[0] == 2: # Python version 2 a = unicode(a) b = unicode(b) else: a = str(a) b = str(b) if a.isnumeric() and b.isnumeric(): return int(a) - int(b) if a < b: return -1 if a > b: return 1 return 0
def __init__(self, config = None): self._Store = HDStore() if config is not None: self.setConfig(config)
class HDExtra(HDBase): _data = None _Store = None config = {} def __init__(self, config = None): self._Store = HDStore() if config is not None: self.setConfig(config) def setConfig(self, config): for key in config['config']: self.config[key] = config['config'][key] self._Store.setConfig(config) def set(self, data): self._data = data def matchExtra(self, category, headers): """ Matches all HTTP header extras - platform, browser and app """ order = self._detectionConfig[category + '-ua-order'] # Match nominated headers ahead of x- headers, but do check x- headers for k in headers: if k not in order and k.startswith("x-"): order.append(k) for item in order: if item in headers: self.log("Trying user-agent match on header " + item) _id = self.getMatch('user-agent', headers[item], category, item, category) if _id is not None: return self.findById(_id) return None def findById(self, _id): """ Find an extra by its id """ return self._Store.read("Extra_" + _id) def matchLanguage(self, category, headers): """ Find the language, preferebly from a language header, but try the user-agent as a last resort """ extra = {} # Mock up a fake Extra for merge into detection reply. extra['_id'] = '0' extra['Extra'] = {} extra['Extra']['hd_specs'] = {} extra['Extra']['hd_specs']['general_language'] = '' extra['Extra']['hd_specs']['general_language_full'] = '' # Try directly from http header first if 'language' in headers: candidate = headers['language'] if candidate in self._detectionLanguages: extra['Extra']['hd_specs']['general_language'] = candidate extra['Extra']['hd_specs']['general_language_full'] = self._detectionLanguages[candidate] return extra order = self._detectionConfig['language-ua-order'] for k in headers: if k not in order: order.append(k) languageList = self._detectionLanguages for headerKey in order: if headerKey in headers: agent = headers[headerKey] if agent is not None: for code in languageList: if re.search('[; \(]' + code + '[; \)]', agent, re.I) is not None: extra['Extra']['hd_specs']['general_language'] = code extra['Extra']['hd_specs']['general_language_full'] = languageList[code] return extra return None def verifyPlatform(self, specs): """ Returns false if this device definitively cannot run this platform and platform version. Returns true if its possible of if there is any doubt. Note : The detected platform must match the device platform. This is the stock OS as shipped on the device. If someone is running a variant (eg CyanogenMod) then all bets are off. """ if self._data is None: return None platform = self._data platformName = platform['Extra']['hd_specs']['general_platform'].strip().lower() platformVersion = platform['Extra']['hd_specs']['general_platform_version'].strip().lower() devicePlatformName = specs['general_platform'].strip().lower() devicePlatformVersionMin = specs['general_platform_version'].strip().lower() devicePlatformVersionMax = specs['general_platform_version_max'].strip().lower() # Its possible that we didnt pickup the platform correctly or the device has no platform info # Return true in this case because we cant give a concrete false (it might run this version). if platform is None or platformName == '' or devicePlatformName == '': return True # Make sure device is running stock OS / Platform # Return true in this case because its possible the device can run a different OS (mods / hacks etc..) if platformName != devicePlatformName: return True # Detected version is lower than the min version - so definetly false. if platformVersion != '' and devicePlatformVersionMin != '' and self.comparePlatformVersions(platformVersion, devicePlatformVersionMin) <= -1: return False # Detected version is greater than the max version - so definetly false. if platformVersion != '' and devicePlatformVersionMax != '' and self.comparePlatformVersions(platformVersion, devicePlatformVersionMax) >= 1: return False # Maybe Ok .. return True def comparePlatformVersions(self, va, vb): """ Compares two platform version numbers return < 0 if a < b, 0 if a == b and > 0 if a > b : Also returns 0 if data is absent from either. """ if va is None or va == '' or vb is None or vb == '': return 0 versionA = self.breakVersionApart(va) versionB = self.breakVersionApart(vb) major = self.compareSmartly(versionA['major'], versionB['major']) minor = self.compareSmartly(versionA['minor'], versionB['minor']) point = self.compareSmartly(versionA['point'], versionB['point']) if major != 0: return major if minor != 0: return minor if point != 0: return point return 0 def breakVersionApart(self, versionNumber): """ Breaks a version number apart into its major, minor and point release numbers for comparison. Big Assumption : That version numbers separate their release bits by '.' !!! might need to do some analysis on the string to rip it up right, but this assumption seems to hold... for now. """ versionNumber = versionNumber + '.0.0.0' tmp = versionNumber.split('.', 4) reply = {} reply['major'] = tmp[0] if tmp[0] is not None and tmp[0] != '' else '0' reply['minor'] = tmp[1] if tmp[1] is not None and tmp[1] != '' else '0' reply['point'] = tmp[2] if tmp[2] is not None and tmp[2] != '' else '0' return reply def compareSmartly(self, a, b): """ Helper for comparing two strings (numerically if possible) """ if sys.version_info[0] == 2: # Python version 2 a = unicode(a) b = unicode(b) else: a = str(a) b = str(b) if a.isnumeric() and b.isnumeric(): return int(a) - int(b) if a < b: return -1 if a > b: return 1 return 0
class HandsetDetection(object): "An object for accessing the Handset Detection API v4.0" configFile = 'hd_config.yml' config = { 'api_username': '', 'api_secret': '', 'api_server': 'api.handsetdetection.com', 'site_id': '0', 'use_local': False, 'filesdir': '/tmp', 'debug': False, 'cache_requests': False, 'timeout': 5, 'use_proxy': False, 'proxy_server': '', 'proxy_port' : '', 'proxy_user' : '', 'proxy_pass': '', 'retries': 3 } _api_realm = 'APIv4' _urlPathFragment = "/apiv4" _Store = None _raw_reply = '' # Initialization def __init__(self, configFile = None): self._Store = HDStore() self._Device = HDDevice() if configFile is not None: self.readConfig(configFile) elif os.path.isfile(self.configFile): self.readConfig(self.configFile) # Public Methods def getReply(self): return self.reply def getRawReply(self): return self._raw_reply def readConfig(self, configFileName): """Configure the api kit with details from a config file. configFileName string - The name of config file """ yamlFile = open(configFileName) config = yaml.safe_load(yamlFile) yamlFile.close() self.setConfig(config) def setConfig(self, config): """Set the username and generate the user token. api_username string - your registered e-mail address api_secret string - the auto-generated API version 2 secret """ for key in config['config']: self.config[key] = config['config'][key] self._Store.setConfig(config) self._Device.setConfig(config) def deviceVendors(self): "Returns a list of vendors." if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localVendors() self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "vendors", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceModels(self, vendorName): """Returns a list of models for the given vendor. vendorName string - The name of a vendor eg. Nokia """ if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localModels(vendorName) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "models", vendorName, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceWhatHas(self, key, value): if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localWhatHas(key, value) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "whathas", key, value, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceView(self, vendor, model): if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localView(vendor, model) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "view", vendor, model, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceDetect(self, data, options = None): """Detect a handset with the User-Agent and/or other information. data - a dictionary containing a User-Agent, IP address, x-wap-profile, and any other information that may be useful in identifying the handset options - a string or list of options to be returned from your query, e.g., "geoip,product_info,display" or ["geoip", "product_info"], etc. """ assert isinstance(data, dict) if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localDetect(data) self._raw_reply = json.dumps(self.reply) else: # Set Content-type header headers = {} headers["Content-type"] = "application/json" uriParts = [ self._urlPathFragment, "device", "detect", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" if isinstance(options, list): options = ",".join(options) data["options"] = options self._do_request(uri, headers, data, "json") return self.reply def deviceFetchArchive(self): """Fetch an archive of all the device specs and install it """ uriParts = [ self._urlPathFragment, "device", "fetcharchive", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" result = self._do_request(uri, {}, {}, "zip") if result == None: return None # Save Archive zipFileName = os.path.join(self.config['filesdir'], "ultimate-full.zip") with open(zipFileName, 'wb') as zipFile: zipFile.write(self._raw_reply) zipFile.close() # Install archive self.installArchive(zipFileName) def communityFetchArchive(self): """Fetch the community device archive. """ uriParts = [ self._urlPathFragment, "community", "fetcharchive", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" result = self._do_request(uri, {}, {}, "zip") if result == None: return None # Save Archive zipFileName = os.path.join(self.config['filesdir'], "ultimate-community.zip") with open(zipFileName, 'wb') as zipFile: zipFile.write(self._raw_reply) zipFile.close() # Install Archive self.installArchive(zipFileName) def installArchive(self, fileName): "Install an archive" zf = zipfile.ZipFile(fileName, 'r') for archiveFile in zf.namelist(): zf.extract(archiveFile, self.config['filesdir']) srcFileName = os.path.join(self.config['filesdir'], archiveFile) self._Store.moveIn(srcFileName, archiveFile) # Private methods def _do_request(self, uri, headers, data, replyType): "Send the request to Handset Detection." self._raw_reply = '' self.reply = {} postdata = json.dumps(data) url = "http://" + self.config['api_server'] + uri # Note : Precompute the auth digest to avoid the challenge turnaround request # Speeds up network turnaround requests by 50% realm = self._api_realm username = self.config['api_username'] secret = str(self.config['api_secret']) nc = "00000001" snonce = self._api_realm cnonceBase = (str(int(time.time())) + secret).encode('utf8') cnonce = hashlib.md5(cnonceBase).hexdigest() qop = 'auth' # AuthDigest Components # http://en.wikipedia.org/wiki/Digest_access_authentication ha1 = hashlib.md5((username + ':' + realm + ':' + secret).encode('utf8')).hexdigest() ha2 = hashlib.md5(str('POST:' + uri).encode('utf8')).hexdigest() response = hashlib.md5(str(ha1 + ':' + snonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2).encode('utf8')).hexdigest() # # Conventional HTTP Digest handling #auth = urllib2.HTTPDigestAuthHandler() #auth.add_password(self._api_realm, url, self.config['api_username'], self.config['api_secret']) #opener = urllib2.build_opener(auth, urllib2.HTTPHandler(debuglevel=1)) #urllib2.install_opener(opener) # Python 2/3 url handling try: # Python 3 import urllib.request request = urllib.request.Request(url, postdata.encode('utf-8'), headers) request.add_header('Authorization', 'Digest ' + 'username="******", ' + 'realm="' + realm + '", ' + 'nonce="' + snonce + '", ' + 'uri="' + uri + '", ' + 'qop=' + qop + ', ' + 'nc=' + nc + ', ' + 'cnonce="' + cnonce + '", ' + 'response="' + response + '", ' + 'opaque="' + realm + '"' ) response = urllib.request.urlopen(request) if replyType == 'json': rawReply = response.read().decode("utf-8") else: rawReply = response.read() except ImportError: # Python 2 socket.setdefaulttimeout(self.config['timeout']) import urllib2 request = urllib2.Request(url, postdata, headers) request.add_header('Authorization', 'Digest ' + 'username="******", ' + 'realm="' + realm + '", ' + 'nonce="' + snonce + '", ' + 'uri="' + uri + '", ' + 'qop=' + qop + ', ' + 'nc=' + nc + ', ' + 'cnonce="' + cnonce + '", ' + 'response="' + response + '", ' + 'opaque="' + realm + '"' ) response = urllib2.urlopen(request) rawReply = response.read() response.close() if response.code == 200: #print rawReply self._raw_reply = rawReply if replyType == "json": self.reply = json.loads(rawReply) return True assert False, ("response.code != 200 - urllib2 should have thrown an exception.")
class HDDevice(HDBase): DETECTIONV4_STANDARD = '0' DETECTIONV4_GENERIC = '1' _device = None _platform = None _browser = None _app = None _ratingResult = None _Extra = None _Store = None config = {} def __init__(self, config = None): self._Extra = HDExtra() self._Store = HDStore() if config is not None: self.setConfig(config) def setConfig(self, config): for key in config['config']: self.config[key] = config['config'][key] self._Store.setConfig(config) self._Extra.setConfig(config) def localVendors(self): self.reply = {} self.reply['vendor'] = [] self.setError(301, 'Nothing Found'); deviceList = self._fetchDevices() if deviceList is None: return self.reply replySet = set() for device in deviceList: replySet.add(device['Device']['hd_specs']['general_vendor']) tmpList = list(replySet) tmpList.sort() self.reply['vendor'] = tmpList self.setError(0, 'OK') return self.reply def localModels(self, vendor): self.reply = {} self.reply['model'] = [] self.setError(301, 'Nothing Found'); deviceList = self._fetchDevices() if deviceList is None: return self.reply vendor = vendor.lower() replySet = set() for device in deviceList: deviceVendor = device['Device']['hd_specs']['general_vendor'].lower() if deviceVendor == vendor: replySet.add(device['Device']['hd_specs']['general_model']) searchKey = vendor + " " for alias in device['Device']['hd_specs']['general_aliases']: alias = alias.lower() if alias.find(searchKey) is 0: replySet.add(alias.replace(searchKey, '')) tmpList = list(replySet) tmpList.sort() self.reply['model'] = tmpList self.setError(0, 'OK') return self.reply def localView(self, vendor, model): self.reply = {} self.reply['device'] = {} self.setError(301, 'Nothing Found'); deviceList = self._fetchDevices() if deviceList is None: return self.reply vendor = vendor.lower() model = model.lower() for device in deviceList: if device['Device']['hd_specs']['general_vendor'].lower() == vendor and device['Device']['hd_specs']['general_model'].lower() == model: self.reply['device'] = device['Device']['hd_specs'] self.setError(0, 'OK') return self.reply def localWhatHas(self, key, value): self.reply = {} self.reply['devices'] = [] self.setError(301, 'Nothing Found'); deviceList = self._fetchDevices() if deviceList is None: return self.reply value = value.lower() for device in deviceList: if device['Device']['hd_specs'][key] is None: continue match = None if isinstance(device['Device']['hd_specs'][key], list): for item in device['Device']['hd_specs'][key]: if value.find(item.lower()) > -1: match = True elif value.find(device['Device']['hd_specs'][key].lower()) > -1: match = True if match == True: tmp = {} tmp['_id'] = device['Device']['_id'] tmp['general_vendor'] = device['Device']['hd_specs']['general_vendor'] tmp['general_model'] = device['Device']['hd_specs']['general_model'] self.reply['devices'].append(tmp) self.setError(0, 'OK') return self.reply def _fetchDevices(self): deviceList = self._Store.fetchDevices() if deviceList is None: return self.setError(299, "Error : fetchDevices cannot read files from store.") return deviceList def localDetect(self, headers): newHeaders = {} hardwareInfo = '' for k in headers: newHeaders[k.lower()] = headers[k] if 'x-local-hardwareinfo' in newHeaders: hardwareInfo = newHeaders['x-local-hardwareinfo'] del newHeaders['x-local-hardwareinfo'] if self.hasBiKeys(newHeaders) is not None: return self.v4MatchBuildInfo(newHeaders) return self.v4MatchHttpHeaders(newHeaders, hardwareInfo) def v4MatchBuildInfo(self, headers): self._device = None self._platform = None self._browser = None self._app = None self._detectedRuleKey = None self._ratingResult = None self.reply = {} self.setError(301, 'Not Found'); if headers is None: return self.reply self._headers = headers self._device = self.v4MatchBiHelper(headers, 'device') if self._device is None: return None self._platform = self.v4MatchBiHelper(headers, 'platform') if self._platform is not None: self.specsOverlay('platform', self._device, self._platform) self.reply['hd_specs'] = self._device['Device']['hd_specs'] self.setError(0, 'OK') return self.reply def v4MatchBiHelper(self, headers, category): confBiKeys = self._detectionConfig[category + '-bi-order'] if confBiKeys is None or headers is None: return None hints = [] for platform in confBiKeys: value = '' for items in confBiKeys[platform]: checking = True for item in items: if item in headers: value = headers[item] if value == '' else value + '|' + headers[item] else: checking = False break if checking == True: value.strip("| \t\n\r\0\x0B") hints.append(value) subtree = self.DETECTIONV4_STANDARD if category == 'device' else category _id = self.getMatch('buildinfo', value, subtree, 'buildinfo', category) if _id is not None: return self.findById(_id) if category == 'device' else self._Extra.findById(_id) # If we get this far without a result then try a generic match platform = self.hasBiKeys(headers) if platform is not None: tryList = [] tryList.append("generic|" + platform) tryList.append(platform + "|generic") for attempt in tryList: subtree = self.DETECTIONV4_GENERIC if category == 'device' else category _id = self.getMatch('buildinfo', value, subtree, 'buildinfo', category) if _id is not None: return self.findById(_id) if category == 'device' else self._Extra.findById(_id) return None def v4MatchHttpHeaders(self, headers, hardwareInfo): self._device = {} self._platform = {} self._browser = {} self._app = {} self._language = {} self._detectedRuleKey = {} self._ratingResult = {} self.reply = {} self.setError(301, 'Not Found'); hwProps = {} deviceHeaders = {} extraHeaders = {} if headers is None: return self.reply if 'ip' in headers: del headers['ip'] if 'host' in headers: del headers['host'] # Sanitize headers & cleanup language (you filthy animal) :) for k in headers: v = headers[k].lower() if k == 'accept-language' or k == 'content-language': tmp = re.split("[,;]", v) k = 'language' if tmp is not None and len(tmp) > 0: v = tmp[0] else: continue deviceHeaders[k] = self.cleanStr(v) extraHeaders[k] = self.extraCleanStr(v) self._device = self.matchDevice(deviceHeaders) if self._device is None: return self.reply if hardwareInfo != '': hwProps = self.infoStringToArray(hardwareInfo) # Stop on detect set - Tidy up and return if self._device['Device']['hd_ops']['stop_on_detect'] == '1': if self._device['Device']['hd_ops']['overlay_result_specs'] == '1': self.hardwareInfoOverlay(self._device, hwProps) self.reply['hd_specs'] = self._device['Device']['hd_specs'] self.setError(0, 'OK') return self.reply # Get extra info self._platform = self._Extra.matchExtra('platform', extraHeaders) self._browser = self._Extra.matchExtra('browser', extraHeaders) self._app = self._Extra.matchExtra('app', extraHeaders) self._language = self._Extra.matchLanguage('language', extraHeaders) # Find out if there is any contention on the detected rule. deviceList = self.getHighAccuracyCandidates() if deviceList is not None: # Resolve contention with OS check self._Extra._data = self._platform pass1List = [] for _id in deviceList: tryDevice = self.findById(_id) if self._Extra.verifyPlatform(tryDevice['Device']['hd_specs']) is True: pass1List.append(_id) # Contention still not resolved .. check hardware if len(pass1List) >= 2 and hwProps is not None and len(hwProps) > 0: # Score the list based on hardware ratedResult = []; for _id in pass1List: tmp = self.findRating(_id, hwProps) if tmp is not None: ratedResult.append(tmp); # Find winning device by picking the one with the highest score. # If scores are even choose the one with the lowest distance winningDevice = None for tmpDevice in ratedResult: if winningDevice == None: winningDevice = tmpDevice else: if tmpDevice['score'] > winningDevice['score'] or (tmpDevice['score'] == winningDevice['score'] and tmpDevice['distance'] < winningDevice['distance']): winningDevice = tmpDevice; self._device = self.findById(winningDevice['_id']) # Overlay specs if self._platform is not None and 'Extra' in self._platform: self.specsOverlay('platform', self._device, self._platform['Extra']) if self._browser is not None and 'Extra' in self._browser: self.specsOverlay('browser', self._device, self._browser['Extra']) if self._app is not None and 'Extra' in self._app: self.specsOverlay('app', self._device, self._app['Extra']) if self._language is not None and 'Extra' in self._language: self.specsOverlay('language', self._device, self._language['Extra']) # Overlay hardware info result if required if (self._device['Device']['hd_ops']['overlay_result_specs'] == 1 or self._device['Device']['hd_ops']['overlay_result_specs'] == '1') and hardwareInfo != '': self.hardwareInfoOverlay(self._device, hwProps) self.reply['hd_specs'] = self._device['Device']['hd_specs'] self.setError(0, 'OK') return self.reply def findRating(self, deviceId, props): """ Rates a device as compared to a set of hardware properties deviceId string - A device ID props dict - A dictionary of properties return dictionary """ device = self.findById(deviceId) if device is None: return None specs = device['Device']['hd_specs'] total = 0; result = {} # Display Resolution - Worth 40 points if correct if 'display_x' in props and 'display_y' in props: total += 40 if int(specs['display_x']) == int(props['display_x']) and int(specs['display_y']) == int(props['display_y']): result['resolution'] = 40 elif int(specs['display_x']) == int(props['display_y']) and int(specs['display_y']) == int(props['display_x']): result['resolution'] = 40 elif float(specs['display_pixel_ratio']) > 1.0: # The resolution can be scaled by the pixel ratio for some devices adjX = int(int(props['display_x']) * float(specs['display_pixel_ratio'])) adjY = int(int(props['display_y']) * float(specs['display_pixel_ratio'])) if int(specs['display_x']) == adjX and int(specs['display_y']) == adjY: result['resolution'] = 40 elif int(specs['display_x']) == adjY and int(specs['display_y']) == adjX: result['resolution'] = 40 # Display pixel ratio - Also worth 40 points if 'display_pixel_ratio' in props: total += 40; # Note : display_pixel_ratio will be a string stored as 1.33 or 1.5 or 2, perhaps 2.0 .. if specs['display_pixel_ratio'] == str(round(props['display_pixel_ratio']/100, 2)): result['display_pixel_ratio'] = 40; # Benchmark - 10 points - Enough to tie break but not enough to overrule display or pixel ratio. if 'benchmark' in props: total += 10; if 'benchmark_min' in specs and 'benchmark_max' in specs: if int(props['benchmark']) >= int(specs['benchmark_min']) and int(props['benchmark']) <= int(specs['benchmark_max']): # Inside range result['benchmark'] = 10 else: # Outside range result['benchmark'] = 0 result['score'] = 0 if total == 0 else int(sum(result.values())) result['possible'] = total; # Distance from mean used in tie breaking situations if two devices have the same score. result['distance'] = 100000 if 'benchmark_min' in specs and 'benchmark_max' in specs and 'benchmark' in props: result['distance'] = int(abs(((specs['benchmark_min'] + specs['benchmark_max'])/2) - props['benchmark'])) result['_id'] = deviceId return result def specsOverlay(self, category, device, specs): """Overlays specs onto a device category string - 'platform' | 'browser' | 'app' | 'language device object - A device dictionary """ if category == "platform": if specs['hd_specs']['general_platform'] != "": device['Device']['hd_specs']['general_platform'] = specs['hd_specs']['general_platform']; device['Device']['hd_specs']['general_platform_version'] = specs['hd_specs']['general_platform_version']; elif category == 'browser': if specs['hd_specs']['general_browser'] != "": device['Device']['hd_specs']['general_browser'] = specs['hd_specs']['general_browser']; device['Device']['hd_specs']['general_browser_version'] = specs['hd_specs']['general_browser_version']; elif category == 'app': if specs['hd_specs']['general_app'] != "": device['Device']['hd_specs']['general_app'] = specs['hd_specs']['general_app']; device['Device']['hd_specs']['general_app_version'] = specs['hd_specs']['general_app_version']; device['Device']['hd_specs']['general_app_category'] = specs['hd_specs']['general_app_category']; elif category == 'language': if specs['hd_specs']['general_language'] != "": device['Device']['hd_specs']['general_language'] = specs['hd_specs']['general_language']; device['Device']['hd_specs']['general_language_full'] = specs['hd_specs']['general_language_full']; #return device ?? def infoStringToArray(self, hardwareInfo): """ Takes a string of onDeviceInformation and turns it into something that can be used for high accuracy checking. Strings a usually generated from cookies, but may also be supplied in headers. The format is $w:$h:$r:$b where w is the display width, h is the display height, r is the pixel ratio and b is the benchmark. display_x, display_y, display_pixel_ratio, general_benchmark @param string $hardwareInfo String of light weight device property information, separated by ':' @return dictionary partial specs array of information we can use to improve detection accuracy """ # Remove the header or cookie name from the string 'x-specs1a=' if hardwareInfo.find('=') > -1: cookieName, hardwareInfo = hardwareInfo.split('=') reply = {} info = hardwareInfo.split(":") if len(info) != 4: return reply reply['display_x'] = int(info[0]) reply['display_y'] = int(info[1]) reply['display_pixel_ratio'] = int(info[2]) reply['benchmark'] = int(info[3]) return reply; def hardwareInfoOverlay(self, device, info): """Overlays hardware info onto a device - Used in generic replys device dictionary info dictionary """ if 'display_x' in info and info['display_x'] != 0: device['Device']['hd_specs']['display_x'] = info['display_x'] if 'display_y' in info and info['display_y'] != 0: device['Device']['hd_specs']['display_y'] = info['display_y'] if 'display_pixel_ratio' in info and info['display_pixel_ratio'] != 0: device['Device']['hd_specs']['display_pixel_ratio'] = str(round(float(info['display_pixel_ratio'] / 100), 2)) # return device ?? def matchDevice(self, headers): """ Device matching Plan of attack : 1) Look for opera headers first - as they're definitive 2) Try profile match - only devices which have unique profiles will match. 3) Try user-agent match 4) Try other x-headers 5) Try all remaining headers """ # May need to assign headers to an interneal variable - not sure if its pass by ref or value ... # Remember the agent for generic matching later. agent = "" # Opera mini sometimes puts the vendor # model in the header - nice! ... sometimes it puts ? # ? in as well if 'x-operamini-phone' in headers and headers['x-operamini-phone'] != "? # ?": _id = self.getMatch('x-operamini-phone', headers['x-operamini-phone'], self.DETECTIONV4_STANDARD, 'x-operamini-phone', 'device') if _id is not None: return self.findById(_id) agent = headers['x-operamini-phone'] del headers['x-operamini-phone'] # Profile header matching if 'profile' in headers: _id = self.getMatch('profile', headers['profile'], self.DETECTIONV4_STANDARD, 'profile', 'device') if _id is not None: return self.findById(_id) del headers['profile'] # Profile header matching - native header name if 'x-wap-profile' in headers: _id = self.getMatch('profile', headers['x-wap-profile'], self.DETECTIONV4_STANDARD, 'x-wap-profile', 'device') if _id is not None: return self.findById(_id) del headers['x-wap-profile'] # Match nominated headers ahead of x- headers, but do check x- headers order = self._detectionConfig['device-ua-order'] for k in headers: if k not in order and k.startswith("x-"): order.append(k) for item in order: if item in headers: self.log("Trying user-agent match on header " + item) _id = self.getMatch('user-agent', headers[item], self.DETECTIONV4_STANDARD, item, 'device') if _id is not None: return self.findById(_id) # Generic matching - Match of last resort self.log('Trying Generic Match') _id = None if 'x-operamini-phone-ua' in headers: _id = self.getMatch('agent', headers['x-operamini-phone-ua'], self.DETECTIONV4_GENERIC, 'agent', 'device') if 'agent' in headers and _id is None: _id = self.getMatch('user-agent', headers['agent'], self.DETECTIONV4_GENERIC, 'agent', 'device') if 'user-agent' in headers and _id is None: _id = self.getMatch('user-agent', headers['user-agent'], self.DETECTIONV4_GENERIC, 'agent', 'device') if _id is not None: return self.findById(_id) return None def findById(self, _id): return self._Store.read("Device_" + _id) def getHighAccuracyCandidates(self): """ Determines if High Accuracy checks are available on the device which was just detected """ branch = self.getBranch('hachecks') ruleKey = self._detectedRuleKey['device'] if 'device' in self._detectedRuleKey else None; if ruleKey in branch: return branch[ruleKey] return None def isHelperUseful(self, headers): """ Determines of hd4Helper would prove more accurate detection results """ if headers is None: return None if self.localDetect(headers) is None: return None if self.getHighAccuracyCandidates() is None: return None return True
class HandsetDetection(object): "An object for accessing the Handset Detection API v4.0" configFile = 'hd_config.yml' config = { 'api_username': '', 'api_secret': '', 'api_server': 'api.handsetdetection.com', 'site_id': '0', 'use_local': False, 'filesdir': '/tmp', 'debug': False, 'cache_requests': False, 'timeout': 5, 'use_proxy': False, 'proxy_server': '', 'proxy_port': '', 'proxy_user': '', 'proxy_pass': '', 'retries': 3 } _api_realm = 'APIv4' _urlPathFragment = "/apiv4" _Store = None _raw_reply = '' # Initialization def __init__(self, configFile=None): self._Store = HDStore() self._Device = HDDevice() if configFile is not None: self.readConfig(configFile) elif os.path.isfile(self.configFile): self.readConfig(self.configFile) # Public Methods def getReply(self): return self.reply def getRawReply(self): return self._raw_reply def readConfig(self, configFileName): """Configure the api kit with details from a config file. configFileName string - The name of config file """ yamlFile = open(configFileName) config = yaml.safe_load(yamlFile) yamlFile.close() self.setConfig(config) def setConfig(self, config): """Set the username and generate the user token. api_username string - your registered e-mail address api_secret string - the auto-generated API version 2 secret """ for key in config['config']: self.config[key] = config['config'][key] self._Store.setConfig(config) self._Device.setConfig(config) def deviceVendors(self): "Returns a list of vendors." if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localVendors() self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "vendors", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceModels(self, vendorName): """Returns a list of models for the given vendor. vendorName string - The name of a vendor eg. Nokia """ if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localModels(vendorName) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "models", vendorName, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceWhatHas(self, key, value): if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localWhatHas(key, value) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "whathas", key, value, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceView(self, vendor, model): if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localView(vendor, model) self._raw_reply = json.dumps(self.reply) else: uriParts = [ self._urlPathFragment, "device", "view", vendor, model, self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" self._do_request(uri, {}, {}, "json") return self.reply def deviceDetect(self, data, options=None): """Detect a handset with the User-Agent and/or other information. data - a dictionary containing a User-Agent, IP address, x-wap-profile, and any other information that may be useful in identifying the handset options - a string or list of options to be returned from your query, e.g., "geoip,product_info,display" or ["geoip", "product_info"], etc. """ assert isinstance(data, dict) if self.config['use_local'] is True or self.config['use_local'] is 1: self.reply = self._Device.localDetect(data) self._raw_reply = json.dumps(self.reply) else: # Set Content-type header headers = {} headers["Content-type"] = "application/json" uriParts = [ self._urlPathFragment, "device", "detect", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" if isinstance(options, list): options = ",".join(options) data["options"] = options self._do_request(uri, headers, data, "json") return self.reply def deviceFetchArchive(self): """Fetch an archive of all the device specs and install it """ uriParts = [ self._urlPathFragment, "device", "fetcharchive", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" result = self._do_request(uri, {}, {}, "zip") if result == None: return None # Save Archive zipFileName = os.path.join(self.config['filesdir'], "ultimate-full.zip") with open(zipFileName, 'wb') as zipFile: zipFile.write(self._raw_reply) zipFile.close() # Install archive self.installArchive(zipFileName) def communityFetchArchive(self): """Fetch the community device archive. """ uriParts = [ self._urlPathFragment, "community", "fetcharchive", self.config['site_id'] ] uri = '/'.join(uriParts) + ".json" result = self._do_request(uri, {}, {}, "zip") if result == None: return None # Save Archive zipFileName = os.path.join(self.config['filesdir'], "ultimate-community.zip") with open(zipFileName, 'wb') as zipFile: zipFile.write(self._raw_reply) zipFile.close() # Install Archive self.installArchive(zipFileName) def installArchive(self, fileName): "Install an archive" zf = zipfile.ZipFile(fileName, 'r') for archiveFile in zf.namelist(): zf.extract(archiveFile, self.config['filesdir']) srcFileName = os.path.join(self.config['filesdir'], archiveFile) self._Store.moveIn(srcFileName, archiveFile) # Private methods def _do_request(self, uri, headers, data, replyType): "Send the request to Handset Detection." self._raw_reply = '' self.reply = {} postdata = json.dumps(data) url = "http://" + self.config['api_server'] + uri # Note : Precompute the auth digest to avoid the challenge turnaround request # Speeds up network turnaround requests by 50% realm = self._api_realm username = self.config['api_username'] secret = str(self.config['api_secret']) nc = "00000001" snonce = self._api_realm cnonceBase = (str(int(time.time())) + secret).encode('utf8') cnonce = hashlib.md5(cnonceBase).hexdigest() qop = 'auth' # AuthDigest Components # http://en.wikipedia.org/wiki/Digest_access_authentication ha1 = hashlib.md5((username + ':' + realm + ':' + secret).encode('utf8')).hexdigest() ha2 = hashlib.md5(str('POST:' + uri).encode('utf8')).hexdigest() response = hashlib.md5( str(ha1 + ':' + snonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2).encode('utf8')).hexdigest() # # Conventional HTTP Digest handling #auth = urllib2.HTTPDigestAuthHandler() #auth.add_password(self._api_realm, url, self.config['api_username'], self.config['api_secret']) #opener = urllib2.build_opener(auth, urllib2.HTTPHandler(debuglevel=1)) #urllib2.install_opener(opener) # Python 2/3 url handling try: # Python 3 import urllib.request request = urllib.request.Request(url, postdata.encode('utf-8'), headers) request.add_header( 'Authorization', 'Digest ' + 'username="******", ' + 'realm="' + realm + '", ' + 'nonce="' + snonce + '", ' + 'uri="' + uri + '", ' + 'qop=' + qop + ', ' + 'nc=' + nc + ', ' + 'cnonce="' + cnonce + '", ' + 'response="' + response + '", ' + 'opaque="' + realm + '"') response = urllib.request.urlopen(request) if replyType == 'json': rawReply = response.read().decode("utf-8") else: rawReply = response.read() except ImportError: # Python 2 socket.setdefaulttimeout(self.config['timeout']) import urllib2 request = urllib2.Request(url, postdata, headers) request.add_header( 'Authorization', 'Digest ' + 'username="******", ' + 'realm="' + realm + '", ' + 'nonce="' + snonce + '", ' + 'uri="' + uri + '", ' + 'qop=' + qop + ', ' + 'nc=' + nc + ', ' + 'cnonce="' + cnonce + '", ' + 'response="' + response + '", ' + 'opaque="' + realm + '"') response = urllib2.urlopen(request) rawReply = response.read() response.close() if response.code == 200: #print rawReply self._raw_reply = rawReply if replyType == "json": self.reply = json.loads(rawReply) return True assert False, ( "response.code != 200 - urllib2 should have thrown an exception.")