""" Extract client information from http user agent The module does not try to detect all capabilities of browser in current form (it can easily be extended though). Tries to * be fast * very easy to extend * reliable enough for practical purposes * assist python web apps to detect clients. """ __version__ = '1.7.8' class DetectorsHub(dict): _known_types = ['os', 'dist', 'flavor', 'browser'] def __init__(self, *args, **kw): dict.__init__(self, *args, **kw) for typ in self._known_types: self.setdefault(typ, []) self.registerDetectors() def register(self, detector): if detector.info_type not in self._known_types: self[detector.info_type] = [detector] self._known_types.insert(detector.order, detector.info_type) else: self[detector.info_type].append(detector) def __iter__(self): return iter(self._known_types) def registerDetectors(self): detectors = [v() for v in globals().values() if DetectorBase in getattr(v, '__mro__', [])] for d in detectors: if d.can_register: self.register(d) class DetectorBase(object): name = "" # "to perform match in DetectorsHub object" info_type = "override me" result_key = "override me" order = 10 # 0 is highest look_for = "string to look for" skip_if_found = [] # strings if present stop processin can_register = False version_markers = [("/", " ")] allow_space_in_version = False _suggested_detectors = None platform = None bot = False def __init__(self): if not self.name: self.name = self.__class__.__name__ self.can_register = (self.__class__.__dict__.get('can_register', True)) def detect(self, agent, result): # -> True/None word = self.checkWords(agent) if word: result[self.info_type] = dict(name=self.name) result['bot'] = self.bot version = self.getVersion(agent, word) if version: result[self.info_type]['version'] = version if self.platform: result['platform'] = {'name': self.platform, 'version': version} return True def checkWords(self, agent): # -> True/None for w in self.skip_if_found: if w in agent: return False if isinstance(self.look_for, (tuple, list)): for word in self.look_for: if word in agent: return word elif self.look_for in agent: return self.look_for def getVersion(self, agent, word): """ => version string /None """ version_markers = self.version_markers if \ isinstance(self.version_markers[0], (list, tuple)) else [self.version_markers] version_part = agent.split(word, 1)[-1] for start, end in version_markers: if version_part.startswith(start) and end in version_part: version = version_part[1:] if end: # end could be empty string version = version.split(end)[0] if not self.allow_space_in_version: version = version.split()[0] return version class OS(DetectorBase): info_type = "os" can_register = False version_markers = [";", " "] allow_space_in_version = True platform = None class Dist(DetectorBase): info_type = "dist" can_register = False platform = None class Flavor(DetectorBase): info_type = "flavor" can_register = False platform = None class Browser(DetectorBase): info_type = "browser" can_register = False class Firefox(Browser): look_for = "Firefox" version_markers = [('/', '')] skip_if_found = ["SeaMonkey", "web/snippet"] class SeaMonkey(Browser): look_for = "SeaMonkey" version_markers = [('/', '')] class Konqueror(Browser): look_for = "Konqueror" version_markers = ["/", ";"] class OperaMobile(Browser): look_for = "Opera Mobi" name = "Opera Mobile" def getVersion(self, agent, word): try: look_for = "Version" return agent.split(look_for)[1][1:].split(' ')[0] except IndexError: look_for = "Opera" return agent.split(look_for)[1][1:].split(' ')[0] class Opera(Browser): look_for = "Opera" def getVersion(self, agent, word): try: look_for = "Version" return agent.split(look_for)[1][1:].split(' ')[0] except IndexError: look_for = "Opera" version = agent.split(look_for)[1][1:].split(' ')[0] return version.split('(')[0] class OperaNew(Browser): """ Opera after version 15 """ name = "Opera" look_for = "OPR" version_markers = [('/', '')] class Netscape(Browser): look_for = "Netscape" version_markers = [("/", '')] class Trident(Browser): look_for = "Trident" skip_if_found = ["MSIE", "Opera"] name = "Microsoft Internet Explorer" version_markers = ["/", ";"] trident_to_ie_versions = { '4.0': '8.0', '5.0': '9.0', '6.0': '10.0', '7.0': '11.0', } def getVersion(self, agent, word): return self.trident_to_ie_versions.get(super(Trident, self).getVersion(agent, word)) class MSIE(Browser): look_for = "MSIE" skip_if_found = ["Opera"] name = "Microsoft Internet Explorer" version_markers = [" ", ";"] class MSEdge(Browser): look_for = "Edge" skip_if_found = ["MSIE"] version_markers = ["/", ""] class Galeon(Browser): look_for = "Galeon" class WOSBrowser(Browser): look_for = "wOSBrowser" def getVersion(self, agent, word): pass class Safari(Browser): look_for = "Safari" skip_if_found = ["Edge"] def checkWords(self, agent): unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android"] if self.look_for in agent: for word in unless_list: if word in agent: return False return self.look_for def getVersion(self, agent, word): if "Version/" in agent: return agent.split('Version/')[-1].split(' ')[0].strip() if "Safari/" in agent: return agent.split('Safari/')[-1].split(' ')[0].strip() else: return agent.split('Safari ')[-1].split(' ')[0].strip() # Mobile Safari class GoogleBot(Browser): # https://support.google.com/webmasters/answer/1061943 look_for = ["Googlebot", "Googlebot-News", "Googlebot-Image", "Googlebot-Video", "Googlebot-Mobile", "Mediapartners-Google", "Mediapartners", "AdsBot-Google", "web/snippet"] bot = True version_markers = [('/', ';'), ('/', ' ')] class GoogleFeedFetcher(Browser): look_for = "Feedfetcher-Google" bot = True def get_version(self, agent): pass class RunscopeRadar(Browser): look_for = "runscope-radar" bot = True class GoogleAppEngine(Browser): look_for = "AppEngine-Google" bot = True def get_version(self, agent): pass class GoogleApps(Browser): look_for = "GoogleApps script" bot = True def get_version(self, agent): pass class TwitterBot(Browser): look_for = "Twitterbot" bot = True class MJ12Bot(Browser): look_for = "MJ12bot" bot = True class YandexBot(Browser): # http://help.yandex.com/search/robots/agent.xml look_for = "Yandex" bot = True def getVersion(self, agent, word): return agent[agent.index('Yandex'):].split('/')[-1].split(')')[0].strip() class BingBot(Browser): look_for = "bingbot" version_markers = ["/", ";"] bot = True class BaiduBot(Browser): # http://help.baidu.com/question?prod_en=master&class=1&id=1000973 look_for = ["Baiduspider", "Baiduspider-image", "Baiduspider-video", "Baiduspider-news", "Baiduspider-favo", "Baiduspider-cpro", "Baiduspider-ads"] bot = True version_markers = ('/', ';') class LinkedInBot(Browser): look_for = "LinkedInBot" bot = True class ArchiveDotOrgBot(Browser): look_for = "archive.org_bot" bot = True class YoudaoBot(Browser): look_for = "YoudaoBot" bot = True class YoudaoBotImage(Browser): look_for = "YodaoBot-Image" bot = True class RogerBot(Browser): look_for = "rogerbot" bot = True class TweetmemeBot(Browser): look_for = "TweetmemeBot" bot = True class WebshotBot(Browser): look_for = "WebshotBot" bot = True class SensikaBot(Browser): look_for = "SensikaBot" bot = True class YesupBot(Browser): look_for = "YesupBot" bot = True class DotBot(Browser): look_for = "DotBot" bot = True class PhantomJS(Browser): look_for = "Browser/Phantom" bot = True class FacebookExternalHit(Browser): look_for = 'facebookexternalhit' bot = True class NokiaOvi(Browser): look_for = "S40OviBrowser" class UCBrowser(Browser): look_for = "UCBrowser" class BrowserNG(Browser): look_for = "BrowserNG" class Dolfin(Browser): look_for = 'Dolfin' class NetFront(Browser): look_for = 'NetFront' class Jasmine(Browser): look_for = 'Jasmine' class Openwave(Browser): look_for = 'Openwave' class UPBrowser(Browser): look_for = 'UP.Browser' class OneBrowser(Browser): look_for = 'OneBrowser' class ObigoInternetBrowser(Browser): look_for = 'ObigoInternetBrowser' class TelecaBrowser(Browser): look_for = 'TelecaBrowser' class MAUI(Browser): look_for = 'Browser/MAUI' def getVersion(self, agent, word): version = agent.split("Release/")[-1][:10] return version class NintendoBrowser(Browser): look_for = 'NintendoBrowser' class AndroidBrowser(Browser): look_for = "Android" skip_if_found = ['Chrome', 'Windows Phone'] # http://decadecity.net/blog/2013/11/21/android-browser-versions def getVersion(self, agent, word): pass class Linux(OS): look_for = 'Linux' platform = 'Linux' def getVersion(self, agent, word): pass class Blackberry(OS): look_for = 'BlackBerry' platform = 'BlackBerry' def getVersion(self, agent, word): pass class BlackberryPlaybook(Dist): look_for = 'PlayBook' platform = 'BlackBerry' def getVersion(self, agent, word): pass class WindowsPhone(OS): name = "Windows Phone" platform = 'Windows' look_for = ["Windows Phone OS", "Windows Phone"] version_markers = [(" ", ";"), (" ", ")")] class iOS(OS): look_for = ('iPhone', 'iPad') skip_if_found = ['like iPhone'] class iPhone(Dist): look_for = 'iPhone' platform = 'iOS' skip_if_found = ['like iPhone'] def getVersion(self, agent, word): version_end_chars = [' '] if not "iPhone OS" in agent: return None part = agent.split('iPhone OS')[-1].strip() for c in version_end_chars: if c in part: version = part.split(c)[0] return version.replace('_', '.') return None class IPad(Dist): look_for = 'iPad;' platform = 'iOS' def getVersion(self, agent, word): version_end_chars = [' '] if not "CPU OS " in agent: return None part = agent.split('CPU OS ')[-1].strip() for c in version_end_chars: if c in part: version = part.split(c)[0] return version.replace('_', '.') return None class Macintosh(OS): look_for = 'Macintosh' def getVersion(self, agent, word): pass class MacOS(Flavor): look_for = 'Mac OS' platform = 'Mac OS' skip_if_found = ['iPhone', 'iPad'] def getVersion(self, agent, word): version_end_chars = [';', ')'] part = agent.split('Mac OS')[-1].strip() for c in version_end_chars: if c in part: version = part.split(c)[0] return version.replace('_', '.') return '' class Windows(Dist): look_for = 'Windows' platform = 'Windows' class Windows(OS): look_for = 'Windows' platform = 'Windows' skip_if_found = ["Windows Phone"] win_versions = { "NT 10.0": "10", "NT 6.3": "8.1", "NT 6.2": "8", "NT 6.1": "7", "NT 6.0": "Vista", "NT 5.2": "Server 2003 / XP x64", "NT 5.1": "XP", "NT 5.01": "2000 SP1", "NT 5.0": "2000", "98; Win 9x 4.90": "Me" } def getVersion(self, agent, word): v = agent.split('Windows')[-1].split(';')[0].strip() if ')' in v: v = v.split(')')[0] v = self.win_versions.get(v, v) return v class Ubuntu(Dist): look_for = 'Ubuntu' version_markers = ["/", " "] class Debian(Dist): look_for = 'Debian' version_markers = ["/", " "] class Chrome(Browser): look_for = "Chrome" version_markers = ["/", " "] skip_if_found = ["OPR", "Edge"] def getVersion(self, agent, word): part = agent.split(word + self.version_markers[0])[-1] version = part.split(self.version_markers[1])[0] if '+' in version: version = part.split('+')[0] return version.strip() class ChromeiOS(Browser): look_for = "CriOS" version_markers = ["/", " "] class ChromeOS(OS): look_for = "CrOS" platform = ' ChromeOS' version_markers = [" ", " "] def getVersion(self, agent, word): version_markers = self.version_markers if word + '+' in agent: version_markers = ['+', '+'] return agent.split(word + version_markers[0])[-1].split(version_markers[1])[1].strip()[:-1] class Android(Dist): look_for = 'Android' platform = 'Android' skip_if_found = ['Windows Phone'] def getVersion(self, agent, word): return agent.split(word)[-1].split(';')[0].strip() class WebOS(Dist): look_for = 'hpwOS' def getVersion(self, agent, word): return agent.split('hpwOS/')[-1].split(';')[0].strip() class NokiaS40(OS): look_for = 'Series40' platform = 'Nokia S40' def getVersion(self, agent, word): pass class Symbian(OS): look_for = ['Symbian', 'SymbianOS'] platform = 'Symbian' class PlayStation(OS): look_for = ['PlayStation', 'PLAYSTATION'] platform = 'PlayStation' version_markers = [" ", ")"] class prefs: # experimental os = dict( Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])), BlackBerry=dict(dist=[BlackberryPlaybook]), Macintosh=dict(flavor=[MacOS]), Windows=dict(browser=[MSIE, Firefox]), ChromeOS=dict(browser=[Chrome]), Debian=dict(browser=[Firefox]) ) dist = dict( Ubuntu=dict(browser=[Firefox]), Android=dict(browser=[Safari]), IPhone=dict(browser=[Safari]), IPad=dict(browser=[Safari]), ) flavor = dict( MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE]) ) detectorshub = DetectorsHub() def detect(agent, fill_none=False): """ fill_none: if name/version is not detected respective key is still added to the result with value None """ result = dict(platform=dict(name=None, version=None)) _suggested_detectors = [] for info_type in detectorshub: detectors = _suggested_detectors or detectorshub[info_type] for detector in detectors: try: detector.detect(agent, result) except Exception as _err: pass if fill_none: attrs_d = {'name': None, 'version': None} for key in ('os', 'browser'): if key not in result: result[key] = attrs_d else: for k, v in attrs_d.items(): result[k] = v return result def simple_detect(agent): """ -> (os, browser) # tuple of strings """ result = detect(agent) os_list = [] if 'flavor' in result: os_list.append(result['flavor']['name']) if 'dist' in result: os_list.append(result['dist']['name']) if 'os' in result: os_list.append(result['os']['name']) os = os_list and " ".join(os_list) or "Unknown OS" os_version = os_list and (result.get('flavor') and result['flavor'].get('version')) or \ (result.get('dist') and result['dist'].get('version')) or (result.get('os') and result['os'].get('version')) or "" browser = 'browser' in result and result['browser'].get('name') or 'Unknown Browser' browser_version = 'browser' in result and result['browser'].get('version') or "" if browser_version: browser = " ".join((browser, browser_version)) if os_version: os = " ".join((os, os_version)) return os, browser class mobilize(object): """ Decorator for controller functions so they use different views for mobile devices. WARNING: If you update httpagentparser make sure to leave mobilize for backwards compatibility. """ def __init__(self, func): self.func = func def __call__(self): from gluon import current user_agent = current.request.user_agent() if user_agent.is_mobile: items = current.response.view.split('.') items.insert(-1, 'mobile') current.response.view = '.'.join(items) return self.func()