codekingpro/portable-devtools
115k
1"""2Extract client information from http user agent3The module does not try to detect all capabilities of browser in current form (it can easily be extended though).4Tries to5 * be fast6 * very easy to extend7 * reliable enough for practical purposes8 * assist python web apps to detect clients.9"""10 11__version__ = '1.9.5'12 13 14class DetectorsHub(dict):15 _known_types = ['os', 'dist', 'flavor', 'browser']16 17 def __init__(self, *args, **kw):18 dict.__init__(self, *args, **kw)19 for typ in self._known_types:20 self.setdefault(typ, [])21 self.registerDetectors()22 23 def register(self, detector):24 if detector.info_type not in self._known_types:25 self[detector.info_type] = [detector]26 self._known_types.insert(detector.order, detector.info_type)27 else:28 self[detector.info_type].append(detector)29 30 def __iter__(self):31 return iter(self._known_types)32 33 def registerDetectors(self):34 detectors = [v() for v in list(globals().values()) if DetectorBase in getattr(v, '__mro__', [])]35 for d in detectors:36 if d.can_register:37 self.register(d)38 39 40class DetectorBase(object):41 name = "" # "to perform match in DetectorsHub object"42 info_type = "override me"43 result_key = "override me"44 order = 10 # 0 is highest45 look_for = "string to look for"46 skip_if_found = [] # strings if present stop processin47 can_register = False48 version_markers = [("/", " ")]49 allow_space_in_version = False50 _suggested_detectors = None51 platform = None52 bot = False53 54 def __init__(self):55 if not self.name:56 self.name = self.__class__.__name__57 self.can_register = (self.__class__.__dict__.get('can_register', True))58 59 def detect(self, agent, result):60 # -> True/None61 word = self.checkWords(agent)62 if word:63 result[self.info_type] = dict(name=self.name)64 result['bot'] = self.bot65 version = self.getVersion(agent, word)66 if version:67 result[self.info_type]['version'] = version68 if self.platform:69 result['platform'] = {'name': self.platform, 'version': version}70 return True71 72 def checkWords(self, agent):73 # -> True/None74 for w in self.skip_if_found:75 if w in agent:76 return False77 if isinstance(self.look_for, (tuple, list)):78 for word in self.look_for:79 if word in agent:80 return word81 elif self.look_for in agent:82 return self.look_for83 84 def getVersion(self, agent, word):85 """86 => version string /None87 """88 version_markers = self.version_markers if \89 isinstance(self.version_markers[0], (list, tuple)) else [self.version_markers]90 version_part = agent.split(word, 1)[-1]91 for start, end in version_markers:92 if version_part.startswith(start) and end in version_part:93 version = version_part[1:]94 if end: # end could be empty string95 version = version.split(end)[0]96 if not self.allow_space_in_version:97 version = version.split()[0]98 return version99 100 101class OS(DetectorBase):102 info_type = "os"103 can_register = False104 version_markers = [";", " "]105 allow_space_in_version = True106 platform = None107 108 109class Dist(DetectorBase):110 info_type = "dist"111 can_register = False112 platform = None113 114 115class Flavor(DetectorBase):116 info_type = "flavor"117 can_register = False118 platform = None119 120 121class Browser(DetectorBase):122 info_type = "browser"123 can_register = False124 125 126class Konqueror(Browser):127 look_for = "Konqueror"128 version_markers = ["/", ";"]129 130 131class OperaMobile(Browser):132 look_for = "Opera Mobi"133 name = "Opera Mobile"134 135 def getVersion(self, agent, word):136 try:137 look_for = "Version"138 return agent.split(look_for)[1][1:].split(' ')[0]139 except IndexError:140 look_for = "Opera"141 return agent.split(look_for)[1][1:].split(' ')[0]142 143 144class Opera(Browser):145 look_for = "Opera"146 skip_if_found = ['Opera Mobi']147 148 def getVersion(self, agent, word):149 try:150 look_for = "Version"151 return agent.split(look_for)[1][1:].split(' ')[0]152 except IndexError:153 look_for = "Opera"154 version = agent.split(look_for)[1][1:].split(' ')[0]155 return version.split('(')[0]156 157 158class OperaNew(Browser):159 """160 Opera after version 15161 """162 name = "Opera"163 look_for = "OPR"164 skip_if_found = ["Build/OPR"]165 version_markers = [('/', '')]166 167 168class Netscape(Browser):169 look_for = "Netscape"170 version_markers = [("/", '')]171 172 173class Trident(Browser):174 look_for = "Trident"175 skip_if_found = ["MSIE", "Opera"]176 name = "Microsoft Internet Explorer"177 version_markers = ["/", ";"]178 trident_to_ie_versions = {179 '4.0': '8.0',180 '5.0': '9.0',181 '6.0': '10.0',182 '7.0': '11.0',183 }184 185 def getVersion(self, agent, word):186 return self.trident_to_ie_versions.get(super(Trident, self).getVersion(agent, word))187 188 189class MSIE(Browser):190 look_for = "MSIE"191 skip_if_found = ["Opera"]192 name = "Microsoft Internet Explorer"193 version_markers = [" ", ";"]194 195class MSEdge(Browser):196 look_for = "Edge"197 skip_if_found = ["MSIE"]198 version_markers = ["/", ""]199 200class ChromiumEdge(Browser):201 look_for = "Edg/"202 203 def getVersion(self, agent, word):204 if "Edg/" in agent:205 return agent.split('Edg/')[-1].strip()206 207 208class Galeon(Browser):209 look_for = "Galeon"210 211 212class WOSBrowser(Browser):213 look_for = "wOSBrowser"214 215 def getVersion(self, agent, word):216 pass217 218 219class Safari(Browser):220 look_for = "Safari"221 skip_if_found = ["Edge", "YaBrowser", "FxiOS"]222 223 def checkWords(self, agent):224 unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android", "CriOS"]225 if self.look_for in agent:226 for word in unless_list:227 if word in agent:228 return False229 return self.look_for230 231 def getVersion(self, agent, word):232 if "Version/" in agent:233 return agent.split('Version/')[-1].split(' ')[0].strip()234 if "Safari/" in agent:235 return agent.split('Safari/')[-1].split(' ')[0].strip()236 else:237 return agent.split('Safari ')[-1].split(' ')[0].strip() # Mobile Safari238 239class GoogleBot(Browser):240 # https://support.google.com/webmasters/answer/1061943241 look_for = ["Googlebot", "Googlebot-News", "Googlebot-Image",242 "Googlebot-Video", "Googlebot-Mobile", "Mediapartners-Google",243 "Mediapartners", "AdsBot-Google", "web/snippet"]244 bot = True245 version_markers = [('/', ';'), ('/', ' ')]246 247class GoogleFeedFetcher(Browser):248 look_for = "Feedfetcher-Google"249 bot = True250 251 def get_version(self, agent):252 pass253 254class RunscopeRadar(Browser):255 look_for = "runscope-radar"256 bot = True257 258class GoogleAppEngine(Browser):259 look_for = "AppEngine-Google"260 bot = True261 262 def get_version(self, agent):263 pass264 265class GoogleApps(Browser):266 look_for = "GoogleApps script"267 bot = True268 269 def get_version(self, agent):270 pass271 272class TwitterBot(Browser):273 look_for = "Twitterbot"274 bot = True275 276class TelegramBot(Browser):277 look_for = "TelegramBot"278 bot = True279 280class MJ12Bot(Browser):281 look_for = "MJ12bot"282 bot = True283 284class YandexBot(Browser):285 # http://help.yandex.com/search/robots/agent.xml286 look_for = "Yandex"287 bot = True288 289 def getVersion(self, agent, word):290 return agent[agent.index('Yandex'):].split('/')[-1].split(')')[0].strip()291 292class BingBot(Browser):293 look_for = "bingbot"294 version_markers = ["/", ";"]295 bot = True296 297 298class BaiduBot(Browser):299 # http://help.baidu.com/question?prod_en=master&class=1&id=1000973300 look_for = ["Baiduspider", "Baiduspider-image", "Baiduspider-video",301 "Baiduspider-news", "Baiduspider-favo", "Baiduspider-cpro",302 "Baiduspider-ads"]303 bot = True304 version_markers = ('/', ';')305 306 307class LinkedInBot(Browser):308 look_for = "LinkedInBot"309 bot = True310 311class ArchiveDotOrgBot(Browser):312 look_for = "archive.org_bot"313 bot = True314 315class YoudaoBot(Browser):316 look_for = "YoudaoBot"317 bot = True318 319class YoudaoBotImage(Browser):320 look_for = "YodaoBot-Image"321 bot = True322 323class RogerBot(Browser):324 look_for = "rogerbot"325 bot = True326 327class TweetmemeBot(Browser):328 look_for = "TweetmemeBot"329 bot = True330 331class WebshotBot(Browser):332 look_for = "WebshotBot"333 bot = True334 335class SensikaBot(Browser):336 look_for = "SensikaBot"337 bot = True338 339class YesupBot(Browser):340 look_for = "YesupBot"341 bot = True342 343class DotBot(Browser):344 look_for = "DotBot"345 bot = True346 347class PhantomJS(Browser):348 look_for = "Browser/Phantom"349 bot = True350 351class FacebookExternalHit(Browser):352 look_for = 'facebookexternalhit'353 bot = True354 355 356class NokiaOvi(Browser):357 look_for = "S40OviBrowser"358 359class UCBrowser(Browser):360 look_for = "UCBrowser"361 362class BrowserNG(Browser):363 look_for = "BrowserNG"364 365class Dolfin(Browser):366 look_for = 'Dolfin'367 368class NetFront(Browser):369 look_for = 'NetFront'370 371class Jasmine(Browser):372 look_for = 'Jasmine'373 374class Openwave(Browser):375 look_for = 'Openwave'376 377class UPBrowser(Browser):378 look_for = 'UP.Browser'379 380class OneBrowser(Browser):381 look_for = 'OneBrowser'382 383class ObigoInternetBrowser(Browser):384 look_for = 'ObigoInternetBrowser'385 386class TelecaBrowser(Browser):387 look_for = 'TelecaBrowser'388 389class MAUI(Browser):390 look_for = 'Browser/MAUI'391 392 def getVersion(self, agent, word):393 version = agent.split("Release/")[-1][:10]394 return version395 396 397class NintendoBrowser(Browser):398 look_for = 'NintendoBrowser'399 400 401class AndroidBrowser(Browser):402 look_for = "Android"403 skip_if_found = ['Chrome', 'Windows Phone', 'Opera', 'Firefox']404 405 # http://decadecity.net/blog/2013/11/21/android-browser-versions406 def getVersion(self, agent, word):407 pass408 409 410class Firefox(Browser):411 look_for = ["Firefox", "FxiOS"]412 version_markers = [('/', '')]413 skip_if_found = ["SeaMonkey", "web/snippet"]414 415 416class SeaMonkey(Browser):417 look_for = "SeaMonkey"418 version_markers = [('/', '')]419 420 421class Linux(OS):422 look_for = 'Linux'423 platform = 'Linux'424 425 def getVersion(self, agent, word):426 pass427 428 429class Blackberry(OS):430 look_for = 'BlackBerry'431 platform = 'BlackBerry'432 433 def getVersion(self, agent, word):434 pass435 436 437class BlackberryPlaybook(Dist):438 look_for = 'PlayBook'439 platform = 'BlackBerry'440 441 def getVersion(self, agent, word):442 pass443 444 445class WindowsPhone(OS):446 name = "Windows Phone"447 platform = 'Windows'448 look_for = ["Windows Phone OS", "Windows Phone"]449 version_markers = [(" ", ";"), (" ", ")")]450 451 452class iOS(OS):453 look_for = ('iPhone', 'iPad')454 skip_if_found = ['like iPhone']455 456 457class iPhone(Dist):458 look_for = 'iPhone'459 platform = 'iOS'460 skip_if_found = ['like iPhone']461 462 def getVersion(self, agent, word):463 version_end_chars = [' ']464 if not "iPhone OS" in agent:465 return None466 part = agent.split('iPhone OS')[-1].strip()467 for c in version_end_chars:468 if c in part:469 version = part.split(c)[0]470 return version.replace('_', '.')471 return None472 473 474class IPad(Dist):475 look_for = 'iPad;'476 platform = 'iOS'477 478 def getVersion(self, agent, word):479 version_end_chars = [' ']480 if not "CPU OS " in agent:481 return None482 part = agent.split('CPU OS ')[-1].strip()483 for c in version_end_chars:484 if c in part:485 version = part.split(c)[0]486 return version.replace('_', '.')487 return None488 489 490class Macintosh(OS):491 look_for = 'Macintosh'492 493 def getVersion(self, agent, word):494 pass495 496 497class MacOS(Flavor):498 look_for = 'Mac OS'499 platform = 'Mac OS'500 skip_if_found = ['iPhone', 'iPad']501 502 def getVersion(self, agent, word):503 version_end_chars = [';', ')']504 part = agent.split('Mac OS')[-1].strip()505 for c in version_end_chars:506 if c in part:507 version = part.split(c)[0]508 return version.replace('_', '.')509 return ''510 511 512class Windows(Dist):513 look_for = 'Windows'514 platform = 'Windows'515 516 517class Windows(OS):518 look_for = 'Windows'519 platform = 'Windows'520 skip_if_found = ["Windows Phone"]521 win_versions = {522 "NT 10.0": "10",523 "NT 6.3": "8.1",524 "NT 6.2": "8",525 "NT 6.1": "7",526 "NT 6.0": "Vista",527 "NT 5.2": "Server 2003 / XP x64",528 "NT 5.1": "XP",529 "NT 5.01": "2000 SP1",530 "NT 5.0": "2000",531 "98; Win 9x 4.90": "Me"532 }533 534 def getVersion(self, agent, word):535 v = agent.split('Windows')[-1].split(';')[0].strip()536 if ')' in v:537 v = v.split(')')[0]538 v = self.win_versions.get(v, v)539 return v540 541 542class Ubuntu(Dist):543 look_for = 'Ubuntu'544 version_markers = ["/", " "]545 546 547class Debian(Dist):548 look_for = 'Debian'549 version_markers = ["/", " "]550 551class Chrome(Browser):552 look_for = "Chrome"553 version_markers = ["/", " "]554 skip_if_found = [" OPR", "Edge", "YaBrowser", "Edg/"]555 556 def getVersion(self, agent, word):557 part = agent.split(word + self.version_markers[0])[-1]558 version = part.split(self.version_markers[1])[0]559 if '+' in version:560 version = version.split('+')[0]561 return version.strip()562 563class YaBrowser(Browser):564 look_for = "YaBrowser"565 name = "Yandex.Browser"566 version_markers = ["/", " "]567 568 def getVersion(self, agent, word):569 part = agent.split(word + self.version_markers[0])[-1]570 version = part.split(self.version_markers[1])[0]571 if '+' in version:572 version = version.split('+')[0]573 return version.strip()574 575class ChromeiOS(Browser):576 look_for = "CriOS"577 version_markers = ["/", " "]578 579 580class ChromeOS(OS):581 look_for = "CrOS"582 platform = ' ChromeOS'583 version_markers = [" ", " "]584 585 def getVersion(self, agent, word):586 version_markers = self.version_markers587 if word + '+' in agent:588 version_markers = ['+', '+']589 return agent.split(word + version_markers[0])[-1].split(version_markers[1])[1].strip()[:-1]590 591 592class Android(Dist):593 look_for = 'Android'594 platform = 'Android'595 skip_if_found = ['Windows Phone']596 597 def getVersion(self, agent, word):598 return agent.split(word)[-1].replace(')', ';').split(';')[0].strip()599 600 601class WebOS(Dist):602 look_for = 'hpwOS'603 604 def getVersion(self, agent, word):605 return agent.split('hpwOS/')[-1].split(';')[0].strip()606 607 608class NokiaS40(OS):609 look_for = 'Series40'610 platform = 'Nokia S40'611 612 def getVersion(self, agent, word):613 pass614 615 616class Symbian(OS):617 look_for = ['Symbian', 'SymbianOS']618 platform = 'Symbian'619 620 621class PlayStation(OS):622 look_for = ['PlayStation', 'PLAYSTATION']623 platform = 'PlayStation'624 version_markers = [" ", ")"]625 626 627class prefs: # experimental628 os = dict(629 Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])),630 BlackBerry=dict(dist=[BlackberryPlaybook]),631 Macintosh=dict(flavor=[MacOS]),632 Windows=dict(browser=[MSIE, Firefox]),633 ChromeOS=dict(browser=[Chrome]),634 Debian=dict(browser=[Firefox])635 )636 dist = dict(637 Ubuntu=dict(browser=[Firefox]),638 Android=dict(browser=[Safari]),639 IPhone=dict(browser=[Safari]),640 IPad=dict(browser=[Safari]),641 )642 flavor = dict(643 MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE])644 )645 646 647detectorshub = DetectorsHub()648 649 650def detect(agent, fill_none=False):651 """652 fill_none: if name/version is not detected respective key is still added to the result with value None653 """654 result = dict(platform=dict(name=None, version=None))655 _suggested_detectors = []656 657 for info_type in detectorshub:658 detectors = _suggested_detectors or detectorshub[info_type]659 for detector in detectors:660 try:661 detector.detect(agent, result)662 except Exception as _err:663 pass664 665 if fill_none:666 for outer_key in ('os', 'browser'):667 outer_value = result.setdefault(outer_key, dict())668 for inner_key in ('name', 'version'):669 outer_value.setdefault(inner_key, None)670 671 return result672 673 674UNKNOWN_OS_NAME = 'Unknown OS'675UNKNOWN_BROWSER_NAME = 'Unknown Browser'676 677 678def simple_detect_tuple(agent, parsed_agent=None):679 """680 @params:681 agent::str682 parsed_agent::dict683 The result of detect, used to save calculations684 685 @return:686 (os_name, os_version, browser_name, browser_version)::Tuple(str)687 """688 result = parsed_agent or detect(agent)689 os_list = []690 if 'flavor' in result:691 os_list.append(result['flavor']['name'])692 if 'dist' in result:693 os_list.append(result['dist']['name'])694 if 'os' in result:695 os_list.append(result['os']['name'])696 697 os = os_list and " ".join(os_list) or UNKNOWN_OS_NAME698 os_version = os_list and (result.get('flavor') and result['flavor'].get('version')) or \699 (result.get('dist') and result['dist'].get('version')) or (result.get('os') and result['os'].get('version')) or ""700 browser = 'browser' in result and result['browser'].get('name') or UNKNOWN_BROWSER_NAME701 browser_version = 'browser' in result and result['browser'].get('version') or ""702 703 return os, os_version, browser, browser_version704 705 706def simple_detect(agent, parsed_agent=None):707 """708 @params:709 agent::str710 parsed_agent::dict711 The result of detect, used to save calculations712 713 @return:714 (os_name_version, browser_name_version)::Tuple(str)715 """716 os, os_version, browser, browser_version = simple_detect_tuple(agent, parsed_agent=parsed_agent)717 if browser_version:718 browser = " ".join((browser, browser_version))719 if os_version:720 os = " ".join((os, os_version))721 return os, browser722 