├── .github ├── ISSUE_TEMPLATE │ └── custom.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── all_test.go ├── bot.go ├── browser.go ├── go.mod ├── model.go ├── operating_systems.go └── user_agent.go /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: General issues, both bugs and features. 4 | title: '' 5 | labels: '' 6 | assignees: mssola 7 | --- 8 | 9 | ### Description 10 | 11 | Check out the [contribution guidelines](../CONTRIBUTING.md) file for some considerations before submitting a new issue. 12 | 13 | ### Steps to reproduce 14 | 15 | 1. First I did this... 16 | 2. Then that... 17 | 3. And this happened! 18 | 19 | - **Expected behavior**: I expected this to happen! 20 | - **Actual behavior**: But this happened... 21 | 22 | ### user_agent version 23 | 24 | With a git commit SHA if possible. 25 | 26 | ### Go version and interpreter 27 | 28 | ```bash 29 | $ go version 30 | ``` 31 | 32 | ### Operating system 33 | 34 | The operating system and the exact version you are using. If you are using Linux, it may be useful to know which distribution you are using and what did you do in order to install go. 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Provide a general description of the changes in your pull request. If this pull request fixes a known issue, please tag it as well (e.g.: `Fixes #1`). 2 | 3 | Before submitting a PR make sure the following things have been done (and denote this by checking the relevant checkboxes): 4 | 5 | - [ ] The commits are consistent with the [contribution guidelines](../CONTRIBUTING.md). 6 | - [ ] You've added tests (if possible) to cover your change(s). 7 | - [ ] All tests and style checkers are passing (`make ci`). 8 | - [ ] You've updated the [changelog](../CHANGELOG.md). 9 | - [ ] You've updated the [readme](../README.md) (if relevant). 10 | 11 | Thanks for contributing to user_agent! 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go: ['1.13', '1.14', '1.15', '1.16', '1.17'] 15 | 16 | name: Go ${{ matrix.go }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Lint 26 | uses: golangci/golangci-lint-action@v2 27 | 28 | - name: Test 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.0 4 | 5 | - Added information on the model of mobile devices. See [746647ad73b5](https://github.com/mssola/user_agent/commit/746647ad73b5ad8648175bbd07319c0a8ac559c6). 6 | - Added support for PhantomJS. See [6b5e6f6ebfa8](https://github.com/mssola/user_agent/commit/6b5e6f6ebfa87464ccdb42bac5448cbf46ce1ba1). 7 | 8 | ## 0.5.4 9 | 10 | - Add detection of Coc Coc Browser. See [897eb45aec23](https://github.com/mssola/user_agent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9). 11 | - Add detection of Headless Chrome. See [897eb45aec23](https://github.com/mssola/user_agent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9). 12 | - Add detection of iOS WebViews. See [897eb45aec23](https://github.com/mssola/user_agent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9). 13 | 14 | ## 0.5.3 15 | 16 | - Fix detection of Firefox on iPad. See [42e4a8f39125](https://github.com/mssola/user_agent/commit/42e4a8f39125a6680fb5367a4602963f1351e069). 17 | - Fix detection of Linux ARM-based Android. See [3b0e113c8047](https://github.com/mssola/user_agent/commit/3b0e113c804708c01de00c27aae07d2acfee40d8). 18 | - Add detection of Chromium Edge on Windows. See [ea81f1e9d61c](https://github.com/mssola/user_agent/commit/ea81f1e9d61c094df4156690a8f4d5481b0d6c4a). 19 | - Add detection of OkHttp. See [6b33e248e796](https://github.com/mssola/user_agent/commit/6b33e248e7969cf3e76128a34d33be88d4eb0dc8). 20 | 21 | ## 0.5.2 22 | 23 | - Detect Electron. See [commit](https://github.com/mssola/user_agent/commit/1a36963d74c0efca7de80dc7518a0958c66b3c4f). 24 | - Add support for both http and https site urls. See [commit](https://github.com/mssola/user_agent/commit/d78bf2c5886a0ab7e1cf90b68c808fe3e3ab6f8c). 25 | - Add more support for BingBot. See [commit](https://github.com/mssola/user_agent/commit/c6402a7b8aefdc4acfbf1e7f3b43eac0b266e49e). 26 | - Add a test case for Firefox focus on iOS. See [commit](https://github.com/mssola/user_agent/commit/a1e9c19d5a6887a17cef1d249118ccbd45cf4c0b). 27 | - Detect iMessage-Preview. See [commit](https://github.com/mssola/user_agent/commit/e8f5e19ded9711ee1f4b43218b9d57d00ef5c26a). 28 | 29 | ## 0.5.1 30 | 31 | - add Firefox for iOS. See [commit](https://github.com/mssola/user_agent/commit/00a868fa17e7). 32 | - Add go.mod. See [commit](https://github.com/mssola/user_agent/commit/8c16c37f4e07). 33 | - Use CodeLingo to Address Further Issues. See [commit](https://github.com/mssola/user_agent/commit/7e313fc62553). 34 | - Fix function comments based on best practices from Effective Go. See [commit](https://github.com/mssola/user_agent/commit/95b0c164394f). 35 | - test: mobile Yandex Browser. See [commit](https://github.com/mssola/user_agent/commit/1df9e04ee4f5). 36 | - Add Yandex browser. See [commit](https://github.com/mssola/user_agent/commit/6eb76c60b5e8). 37 | - Updating license notice. See [commit](https://github.com/mssola/user_agent/commit/8b3999083770). 38 | - Detect Chrome for iOS correctly. See [commit](https://github.com/mssola/user_agent/commit/82f141dea4a8). 39 | - Facebook App Handling. See [commit](https://github.com/mssola/user_agent/commit/5723c361ed97). 40 | - Add a new google bot user agent format. See [commit](https://github.com/mssola/user_agent/commit/57c32981bd5f). 41 | 42 | ## 0.5.0 43 | 44 | ### Newly supported and improvements 45 | 46 | - Added support for Microsoft Edge. See [commit](https://github.com/mssola/user_agent/commit/f659b9863849). 47 | - Precompile regular expressions. See [commit](https://github.com/mssola/user_agent/commit/783ec61292ae). 48 | - Added support for Dalvik user agent parsing. See [commit](https://github.com/mssola/user_agent/commit/78413629666f). 49 | - Improved bot support (also e25e612b37a4). See [commit](https://github.com/mssola/user_agent/commit/0319fcf00bfd). 50 | - Add Chromium support and Ubuntu specific tests. See [commit](https://github.com/mssola/user_agent/commit/6e7843e05771). 51 | - Add OSInfo function to user agent (also 7286ca6abc28). See [commit](https://github.com/mssola/user_agent/commit/3335cae017e7). 52 | - Detect updated UA for Googlebot. See [commit](https://github.com/mssola/user_agent/commit/6fe362d7cd64). 53 | - Adds the Adsense bot (mobile). See [commit](https://github.com/mssola/user_agent/commit/1438bfba89d7). 54 | 55 | ### Fixes 56 | 57 | - Fixed bug when extracting windows 10. See [commit](https://github.com/mssola/user_agent/commit/8d86c2cf88bf). 58 | - Fixed bug on mobile Firefox browsers running on Android OS versions that report their version number inline.. See [commit](https://github.com/mssola/user_agent/commit/9d00ff9e4202). 59 | 60 | ### Other 61 | 62 | - Improved testing infrastructure. See [commit](https://github.com/mssola/user_agent/commit/63395b193f8812526305bec75ea7117262a124aa). 63 | 64 | ## Older releases 65 | 66 | See the description on each release 67 | [here](https://github.com/mssola/user_agent/releases). 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | THIS PROJECT HAS BEEN MOVED INTO https://github.com/mssola/useragent 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2023 Miquel Sabaté Solà 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GO_SRC = $(shell find . -name \*.go) 3 | 4 | .DEFAULT: build 5 | all: test lint 6 | 7 | .PHONY: test 8 | test: 9 | @$(GO) test 10 | 11 | .PHONY: bench 12 | bench: 13 | @$(GO) test -bench=. 14 | 15 | .PHONY: lint 16 | lint: git-validation cilint 17 | 18 | EPOCH_COMMIT ?= 834b6d4d9e84 19 | .PHONY: git-validation 20 | git-validation: 21 | @git-validation -v -D -range $(EPOCH_COMMIT)..HEAD 22 | 23 | .PHONY: cilint 24 | cilint: 25 | @golangci-lint run 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **IMPORTANT: THIS PROJECT HAS MIGRATED TO https://github.com/mssola/useragent** 3 | 4 | ## License 5 | 6 | ``` 7 | Copyright (c) 2012-2023 Miquel Sabaté Solà 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining 10 | a copy of this software and associated documentation files (the 11 | "Software"), to deal in the Software without restriction, including 12 | without limitation the rights to use, copy, modify, merge, publish, 13 | distribute, sublicense, and/or sell copies of the Software, and to 14 | permit persons to whom the Software is furnished to do so, subject to 15 | the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be 18 | included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 24 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 26 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | ``` 28 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012-2023 Miquel Sabaté Solà 2 | // This file is licensed under the MIT license. 3 | // See the LICENSE file. 4 | 5 | package user_agent 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | // Slice that contains all the tests. Each test is contained in a struct 14 | // that groups the title of the test, the User-Agent string to be tested and the expected value. 15 | var uastrings = []struct { 16 | title string 17 | ua string 18 | expected string 19 | expectedOS *OSInfo 20 | }{ 21 | // Bots 22 | { 23 | title: "GoogleBot", 24 | ua: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 25 | expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:false", 26 | }, 27 | { 28 | title: "GoogleBotSmartphone (iPhone)", 29 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 30 | expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:true", 31 | }, 32 | { 33 | title: "GoogleBotSmartphone (Android)", 34 | ua: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", 35 | expected: "Mozilla:5.0 Model:Nexus 5X Browser:Googlebot-2.1 Bot:true Mobile:true", 36 | }, 37 | { 38 | title: "GoogleBotEmulateMozilla", 39 | ua: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36", 40 | expected: "Mozilla:5.0 Browser:Googlebot-2.1 Bot:true Mobile:false", 41 | }, 42 | { 43 | title: "BingBot", 44 | ua: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", 45 | expected: "Mozilla:5.0 Browser:bingbot-2.0 Bot:true Mobile:false", 46 | }, 47 | { 48 | title: "BingBotSmartphone(iPhone)", 49 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", 50 | expected: "Mozilla:5.0 Browser:bingbot-2.0 Bot:true Mobile:true", 51 | }, 52 | { 53 | title: "BingBotSmartphone(Android)", 54 | ua: "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 Edg/80.0.345.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", 55 | expected: "Mozilla:5.0 Model:Nexus 5X Browser:bingbot-2.0 Bot:true Mobile:true", 56 | }, 57 | { 58 | title: "BingBotEmulateMozilla", 59 | ua: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/41.0.2272.96 Mobile Safari/537.36 Edg/80.0.345.0", 60 | expected: "Mozilla:5.0 Browser:bingbot-2.0 Bot:true Mobile:true", 61 | }, 62 | { 63 | title: "BaiduBot", 64 | ua: "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)", 65 | expected: "Mozilla:5.0 Browser:Baiduspider-2.0 Bot:true Mobile:false", 66 | }, 67 | { 68 | title: "Twitterbot", 69 | ua: "Twitterbot", 70 | expected: "Browser:Twitterbot Bot:true Mobile:false", 71 | }, 72 | { 73 | title: "YahooBot", 74 | ua: "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", 75 | expected: "Mozilla:5.0 Browser:Yahoo! Slurp Bot:true Mobile:false", 76 | }, 77 | { 78 | title: "FacebookExternalHit", 79 | ua: "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", 80 | expected: "Browser:facebookexternalhit-1.1 Bot:true Mobile:false", 81 | }, 82 | { 83 | title: "FacebookPlatform", 84 | ua: "facebookplatform/1.0 (+http://developers.facebook.com)", 85 | expected: "Browser:facebookplatform-1.0 Bot:true Mobile:false", 86 | }, 87 | { 88 | title: "FaceBot", 89 | ua: "Facebot", 90 | expected: "Browser:Facebot Bot:true Mobile:false", 91 | }, 92 | { 93 | title: "NutchCVS", 94 | ua: "NutchCVS/0.8-dev (Nutch; http://lucene.apache.org/nutch/bot.html; nutch-agent@lucene.apache.org)", 95 | expected: "Browser:NutchCVS Bot:true Mobile:false", 96 | }, 97 | { 98 | title: "MJ12bot", 99 | ua: "Mozilla/5.0 (compatible; MJ12bot/v1.2.4; http://www.majestic12.co.uk/bot.php?+)", 100 | expected: "Mozilla:5.0 Browser:MJ12bot-v1.2.4 Bot:true Mobile:false", 101 | }, 102 | { 103 | title: "MJ12bot", 104 | ua: "MJ12bot/v1.0.8 (http://majestic12.co.uk/bot.php?+)", 105 | expected: "Browser:MJ12bot Bot:true Mobile:false", 106 | }, 107 | { 108 | title: "AhrefsBot", 109 | ua: "Mozilla/5.0 (compatible; AhrefsBot/4.0; +http://ahrefs.com/robot/)", 110 | expected: "Mozilla:5.0 Browser:AhrefsBot-4.0 Bot:true Mobile:false", 111 | }, 112 | { 113 | title: "AdsBotGoogle", 114 | ua: "AdsBot-Google (+http://www.google.com/adsbot.html)", 115 | expected: "Browser:AdsBot-Google Bot:true Mobile:false", 116 | }, 117 | { 118 | title: "AdsBotGoogleMobile", 119 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", 120 | expected: "Mozilla:5.0 Browser:AdsBot-Google-Mobile Bot:true Mobile:true", 121 | }, 122 | { 123 | title: "APIs-Google", 124 | ua: "APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)", 125 | expected: "Browser:APIs-Google Bot:true Mobile:false", 126 | }, 127 | { 128 | title: "iMessage-preview", 129 | ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0", 130 | expected: "Mozilla:5.0 Platform:Macintosh Browser:iMessage-Preview-9.0.1 Bot:true Mobile:false", 131 | }, 132 | 133 | // Internet Explorer 134 | { 135 | title: "IE10", 136 | ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)", 137 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", 138 | expectedOS: &OSInfo{"Windows 8", "Windows", "8"}, 139 | }, 140 | { 141 | title: "Tablet", 142 | ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; ARM; Trident/6.0; Touch; .NET4.0E; .NET4.0C; Tablet PC 2.0)", 143 | expected: "Mozilla:4.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", 144 | }, 145 | { 146 | title: "Touch", 147 | ua: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ARM; Trident/6.0; Touch)", 148 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 8 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", 149 | }, 150 | { 151 | title: "Phone", 152 | ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0; SAMSUNG; SGH-i917)", 153 | expected: "Mozilla:4.0 Platform:Windows OS:Windows Phone OS 7.0 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:true", 154 | expectedOS: &OSInfo{"Windows Phone OS 7.0", "Windows Phone OS", "7.0"}, 155 | }, 156 | { 157 | title: "IE6", 158 | ua: "Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.0; .NET CLR 1.1.4322)", 159 | expected: "Mozilla:4.0 Platform:Windows OS:Windows 2000 Browser:Internet Explorer-6.0 Engine:Trident Bot:false Mobile:false", 160 | expectedOS: &OSInfo{"Windows 2000", "Windows", "2000"}, 161 | }, 162 | { 163 | title: "IE8Compatibility", 164 | ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)", 165 | expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-8.0 Engine:Trident Bot:false Mobile:false", 166 | expectedOS: &OSInfo{"Windows 7", "Windows", "7"}, 167 | }, 168 | { 169 | title: "IE10Compatibility", 170 | ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3; MS-RTC LM 8)", 171 | expected: "Mozilla:4.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-10.0 Engine:Trident Bot:false Mobile:false", 172 | }, 173 | { 174 | title: "IE11Win81", 175 | ua: "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", 176 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", 177 | expectedOS: &OSInfo{"Windows 8.1", "Windows", "8.1"}, 178 | }, 179 | { 180 | title: "IE11Win7", 181 | ua: "Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", 182 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", 183 | }, 184 | { 185 | title: "IE11b32Win7b64", 186 | ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko", 187 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", 188 | }, 189 | { 190 | title: "IE11b32Win7b64MDDRJS", 191 | ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; MDDRJS; rv:11.0) like Gecko", 192 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Internet Explorer-11.0 Engine:Trident Bot:false Mobile:false", 193 | }, 194 | { 195 | title: "IE11Compatibility", 196 | ua: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0)", 197 | expected: "Mozilla:4.0 Platform:Windows OS:Windows 8.1 Browser:Internet Explorer-7.0 Engine:Trident Bot:false Mobile:false", 198 | }, 199 | 200 | // Microsoft Edge 201 | { 202 | title: "EdgeDesktop", 203 | ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240", 204 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 10 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:false", 205 | expectedOS: &OSInfo{"Windows 10", "Windows", "10"}, 206 | }, 207 | { 208 | title: "EdgeMobile", 209 | ua: "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; DEVICE INFO) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Mobile Safari/537.36 Edge/12.10240", 210 | expected: "Mozilla:5.0 Platform:Windows OS:Windows Phone 10.0 Browser:Edge-12.10240 Engine:EdgeHTML Bot:false Mobile:true", 211 | }, 212 | 213 | // Microsoft Chromium Edge 214 | { 215 | title: "EdgeDesktop", 216 | ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37", 217 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 10 Browser:Edge-83.0.478.37 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 218 | expectedOS: &OSInfo{"Windows 10", "Windows", "10"}, 219 | }, 220 | 221 | // Gecko 222 | { 223 | title: "FirefoxMac", 224 | ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8", 225 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Browser:Firefox-4.0b8 Engine:Gecko-20100101 Bot:false Mobile:false", 226 | expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"}, 227 | }, 228 | { 229 | title: "FirefoxMacLoc", 230 | ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13", 231 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:Firefox-3.6.13 Engine:Gecko-20101203 Bot:false Mobile:false", 232 | expectedOS: &OSInfo{"Intel Mac OS X 10.6", "Mac OS X", "10.6"}, 233 | }, 234 | { 235 | title: "FirefoxLinux", 236 | ua: "Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0", 237 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Firefox-17.0 Engine:Gecko-20100101 Bot:false Mobile:false", 238 | expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, 239 | }, 240 | { 241 | title: "FirefoxLinux - Ubuntu V50", 242 | ua: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/50.0", 243 | expected: "Mozilla:5.0 Platform:X11 OS:Ubuntu Browser:Firefox-50.0 Engine:Gecko-20100101 Bot:false Mobile:false", 244 | expectedOS: &OSInfo{"Ubuntu", "Ubuntu", ""}, 245 | }, 246 | { 247 | title: "FirefoxWin", 248 | ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14", 249 | expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-2.0.0.14 Engine:Gecko-20080404 Bot:false Mobile:false", 250 | expectedOS: &OSInfo{"Windows XP", "Windows", "XP"}, 251 | }, 252 | { 253 | title: "Firefox29Win7", 254 | ua: "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0", 255 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Firefox-29.0 Engine:Gecko-20100101 Bot:false Mobile:false", 256 | }, 257 | { 258 | title: "CaminoMac", 259 | ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.14) Gecko/20080409 Camino/1.6 (like Firefox/2.0.0.14)", 260 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Camino-1.6 Engine:Gecko-20080409 Bot:false Mobile:false", 261 | expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""}, 262 | }, 263 | { 264 | title: "Iceweasel", 265 | ua: "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Iceweasel/2.0 (Debian-2.0+dfsg-1)", 266 | expected: "Mozilla:5.0 Platform:X11 OS:Linux i686 Localization:en-US Browser:Iceweasel-2.0 Engine:Gecko-20061024 Bot:false Mobile:false", 267 | expectedOS: &OSInfo{"Linux i686", "Linux", ""}, 268 | }, 269 | { 270 | title: "SeaMonkey", 271 | ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.1.4) Gecko/20091017 SeaMonkey/2.0", 272 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10.6 Localization:en-US Browser:SeaMonkey-2.0 Engine:Gecko-20091017 Bot:false Mobile:false", 273 | }, 274 | { 275 | title: "AndroidFirefox", 276 | ua: "Mozilla/5.0 (Android; Mobile; rv:17.0) Gecko/17.0 Firefox/17.0", 277 | expected: "Mozilla:5.0 Platform:Mobile OS:Android Browser:Firefox-17.0 Engine:Gecko-17.0 Bot:false Mobile:true", 278 | }, 279 | { 280 | title: "AndroidFirefoxNougat", 281 | ua: "Mozilla/5.0 (Android 7.0; Mobile; rv:60.0) Gecko/60.0 Firefox/60.0", 282 | expected: "Mozilla:5.0 Platform:Mobile OS:Android 7.0 Browser:Firefox-60.0 Engine:Gecko-60.0 Bot:false Mobile:true", 283 | }, 284 | { 285 | title: "AndroidFirefoxTablet", 286 | ua: "Mozilla/5.0 (Android; Tablet; rv:26.0) Gecko/26.0 Firefox/26.0", 287 | expected: "Mozilla:5.0 Platform:Tablet OS:Android Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", 288 | expectedOS: &OSInfo{"Android", "Android", ""}, 289 | }, 290 | { 291 | title: "FirefoxOS", 292 | ua: "Mozilla/5.0 (Mobile; rv:26.0) Gecko/26.0 Firefox/26.0", 293 | expected: "Mozilla:5.0 Platform:Mobile OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", 294 | expectedOS: &OSInfo{"FirefoxOS", "FirefoxOS", ""}, 295 | }, 296 | { 297 | title: "FirefoxOSTablet", 298 | ua: "Mozilla/5.0 (Tablet; rv:26.0) Gecko/26.0 Firefox/26.0", 299 | expected: "Mozilla:5.0 Platform:Tablet OS:FirefoxOS Browser:Firefox-26.0 Engine:Gecko-26.0 Bot:false Mobile:true", 300 | }, 301 | { 302 | title: "FirefoxWinXP", 303 | ua: "Mozilla/5.0 (Windows NT 5.2; rv:31.0) Gecko/20100101 Firefox/31.0", 304 | expected: "Mozilla:5.0 Platform:Windows OS:Windows XP x64 Edition Browser:Firefox-31.0 Engine:Gecko-20100101 Bot:false Mobile:false", 305 | expectedOS: &OSInfo{"Windows XP x64 Edition", "Windows", "XP"}, 306 | }, 307 | { 308 | title: "FirefoxMRA", 309 | ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:24.0) Gecko/20130405 MRA 5.5 (build 02842) Firefox/24.0 (.NET CLR 3.5.30729)", 310 | expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en-US Browser:Firefox-24.0 Engine:Gecko-20130405 Bot:false Mobile:false", 311 | }, 312 | 313 | // Opera 314 | { 315 | title: "OperaMac", 316 | ua: "Opera/9.27 (Macintosh; Intel Mac OS X; U; en)", 317 | expected: "Platform:Macintosh OS:Intel Mac OS X Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false", 318 | expectedOS: &OSInfo{"Intel Mac OS X", "Mac OS X", ""}, 319 | }, 320 | { 321 | title: "OperaWin", 322 | ua: "Opera/9.27 (Windows NT 5.1; U; en)", 323 | expected: "Platform:Windows OS:Windows XP Localization:en Browser:Opera-9.27 Engine:Presto Bot:false Mobile:false", 324 | }, 325 | { 326 | title: "OperaWinNoLocale", 327 | ua: "Opera/9.80 (Windows NT 5.1) Presto/2.12.388 Version/12.10", 328 | expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", 329 | }, 330 | { 331 | title: "OperaWin2Comment", 332 | ua: "Opera/9.80 (Windows NT 6.0; WOW64) Presto/2.12.388 Version/12.15", 333 | expected: "Platform:Windows OS:Windows Vista Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", 334 | expectedOS: &OSInfo{"Windows Vista", "Windows", "Vista"}, 335 | }, 336 | { 337 | title: "OperaMinimal", 338 | ua: "Opera/9.80", 339 | expected: "Browser:Opera-9.80 Engine:Presto Bot:false Mobile:false", 340 | }, 341 | { 342 | title: "OperaFull", 343 | ua: "Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10", 344 | expected: "Platform:Windows OS:Windows Vista Localization:en Browser:Opera-9.80 Engine:Presto-2.2.15 Bot:false Mobile:false", 345 | }, 346 | { 347 | title: "OperaLinux", 348 | ua: "Opera/9.80 (X11; Linux x86_64) Presto/2.12.388 Version/12.10", 349 | expected: "Platform:X11 OS:Linux x86_64 Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", 350 | }, 351 | { 352 | title: "OperaLinux - Ubuntu V41", 353 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36 OPR/41.0.2353.69", 354 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-41.0.2353.69 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 355 | expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, 356 | }, 357 | { 358 | title: "OperaAndroid", 359 | ua: "Opera/9.80 (Android 4.2.1; Linux; Opera Mobi/ADR-1212030829) Presto/2.11.355 Version/12.10", 360 | expected: "Platform:Android 4.2.1 OS:Linux Browser:Opera-9.80 Engine:Presto-2.11.355 Bot:false Mobile:true", 361 | expectedOS: &OSInfo{"Linux", "Linux", ""}, 362 | }, 363 | { 364 | title: "OperaNested", 365 | ua: "Opera/9.80 (Windows NT 5.1; MRA 6.0 (build 5831)) Presto/2.12.388 Version/12.10", 366 | expected: "Platform:Windows OS:Windows XP Browser:Opera-9.80 Engine:Presto-2.12.388 Bot:false Mobile:false", 367 | }, 368 | { 369 | title: "OperaMRA", 370 | ua: "Opera/9.80 (Windows NT 6.1; U; MRA 5.8 (build 4139); en) Presto/2.9.168 Version/11.50", 371 | expected: "Platform:Windows OS:Windows 7 Localization:en Browser:Opera-9.80 Engine:Presto-2.9.168 Bot:false Mobile:false", 372 | }, 373 | 374 | // Yandex Browser 375 | { 376 | title: "YandexBrowserLinux", 377 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 YaBrowser/19.1.0.2494 (beta) Yowser/2.5 Safari/537.36", 378 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:YaBrowser-19.1.0.2494 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 379 | expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, 380 | }, 381 | 382 | { 383 | title: "YandexBrowserWindows", 384 | ua: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 YaBrowser/17.3.1.840 Yowser/2.5 Safari/537.36", 385 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:YaBrowser-17.3.1.840 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 386 | }, 387 | 388 | { 389 | title: "YandexBrowserAndroid", 390 | ua: "Mozilla/5.0 (Linux; Android 4.4.4; GT-I9300I Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 YaBrowser/17.9.0.523.00 Mobile Safari/537.36", 391 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.4.4 Model:GT-I9300I Browser:YaBrowser-17.9.0.523.00 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 392 | }, 393 | 394 | { 395 | title: "YandexBrowserIOS", 396 | ua: "Mozilla/5.0 (iPad; CPU OS 10_1_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 YaBrowser/16.11.1.716.11 Mobile/14B100 Safari/602.1", 397 | expected: "Mozilla:5.0 Platform:iPad OS:CPU OS 10_1_1 like Mac OS X Model:iPad Browser:YaBrowser-16.11.1.716.11 Engine:AppleWebKit-602.1.50 Bot:false Mobile:true", 398 | }, 399 | // Other 400 | { 401 | title: "Empty", 402 | ua: "", 403 | expected: "Bot:false Mobile:false", 404 | }, 405 | { 406 | title: "Nil", 407 | ua: "nil", 408 | expected: "Browser:nil Bot:false Mobile:false", 409 | }, 410 | { 411 | title: "Compatible", 412 | ua: "Mozilla/4.0 (compatible)", 413 | expected: "Browser:Mozilla-4.0 Bot:false Mobile:false", 414 | }, 415 | { 416 | title: "Mozilla", 417 | ua: "Mozilla/5.0", 418 | expected: "Browser:Mozilla-5.0 Bot:false Mobile:false", 419 | }, 420 | { 421 | title: "Amaya", 422 | ua: "amaya/9.51 libwww/5.4.0", 423 | expected: "Browser:amaya-9.51 Engine:libwww-5.4.0 Bot:false Mobile:false", 424 | }, 425 | { 426 | title: "Rails", 427 | ua: "Rails Testing", 428 | expected: "Browser:Rails Engine:Testing Bot:false Mobile:false", 429 | }, 430 | { 431 | title: "Python", 432 | ua: "Python-urllib/2.7", 433 | expected: "Browser:Python-urllib-2.7 Bot:false Mobile:false", 434 | }, 435 | { 436 | title: "Curl", 437 | ua: "curl/7.28.1", 438 | expected: "Browser:curl-7.28.1 Bot:false Mobile:false", 439 | }, 440 | { 441 | title: "OkHttp", 442 | ua: "okhttp/4.2.2", 443 | expected: "Browser:OkHttp-4.2.2 Bot:false Mobile:true", 444 | }, 445 | 446 | // WebKit 447 | { 448 | title: "ChromeLinux", 449 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11", 450 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-23.0.1271.97 Engine:AppleWebKit-537.11 Bot:false Mobile:false", 451 | expectedOS: &OSInfo{"Linux x86_64", "Linux", ""}, 452 | }, 453 | { 454 | title: "ChromeLinux - Ubuntu V55", 455 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.75 Safari/537.36", 456 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chrome-55.0.2883.75 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 457 | }, 458 | { 459 | title: "ChromeWin7", 460 | ua: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.168 Safari/535.19", 461 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 7 Browser:Chrome-18.0.1025.168 Engine:AppleWebKit-535.19 Bot:false Mobile:false", 462 | }, 463 | { 464 | title: "ChromeMinimal", 465 | ua: "Mozilla/5.0 AppleWebKit/534.10 Chrome/8.0.552.215 Safari/534.10", 466 | expected: "Mozilla:5.0 Browser:Chrome-8.0.552.215 Engine:AppleWebKit-534.10 Bot:false Mobile:false", 467 | }, 468 | { 469 | title: "ChromeMac", 470 | ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.231 Safari/534.10", 471 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_5 Localization:en-US Browser:Chrome-8.0.552.231 Engine:AppleWebKit-534.10 Bot:false Mobile:false", 472 | expectedOS: &OSInfo{"Intel Mac OS X 10_6_5", "Mac OS X", "10.6.5"}, 473 | }, 474 | { 475 | title: "Headless Chrome", 476 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/92.0.4515.107 Safari/537.36", 477 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Headless Chrome-92.0.4515.107 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 478 | }, 479 | { 480 | title: "PhantomJS", 481 | ua: "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1", 482 | expected: "Mozilla:5.0 Platform:Unknown OS:Linux x86_64 Browser:PhantomJS-2.1.1 Engine:AppleWebKit-538.1 Bot:false Mobile:false", 483 | }, 484 | { 485 | title: "SafariMac", 486 | ua: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16", 487 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_6_3 Localization:en-us Browser:Safari-5.0 Engine:AppleWebKit-533.16 Bot:false Mobile:false", 488 | }, 489 | { 490 | title: "SafariWin", 491 | ua: "Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8", 492 | expected: "Mozilla:5.0 Platform:Windows OS:Windows XP Localization:en Browser:Safari-4.0dp1 Engine:AppleWebKit-526.9 Bot:false Mobile:false", 493 | }, 494 | { 495 | title: "iPhone7", 496 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53", 497 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 7_0_3 like Mac OS X Model:iPhone Browser:Safari-7.0 Engine:AppleWebKit-537.51.1 Bot:false Mobile:true", 498 | expectedOS: &OSInfo{"CPU iPhone OS 7_0_3 like Mac OS X", "iPhone OS", "7.0.3"}, 499 | }, 500 | { 501 | title: "iPhone", 502 | ua: "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419", 503 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU like Mac OS X Localization:en Model:iPhone Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true", 504 | }, 505 | { 506 | title: "iPod", 507 | ua: "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/4A102 Safari/419", 508 | expected: "Mozilla:5.0 Platform:iPod OS:CPU like Mac OS X Localization:en Browser:Safari-3.0 Engine:AppleWebKit-420.1 Bot:false Mobile:true", 509 | }, 510 | { 511 | title: "iPad", 512 | ua: "Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10", 513 | expected: "Mozilla:5.0 Platform:iPad OS:CPU OS 3_2 like Mac OS X Localization:en-us Model:iPad Browser:Safari-4.0.4 Engine:AppleWebKit-531.21.10 Bot:false Mobile:true", 514 | }, 515 | { 516 | title: "webOS", 517 | ua: "Mozilla/5.0 (webOS/1.4.0; U; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Version/1.0 Safari/532.2 Pre/1.1", 518 | expected: "Mozilla:5.0 Platform:webOS OS:Palm Localization:en-US Browser:webOS-1.0 Engine:AppleWebKit-532.2 Bot:false Mobile:true", 519 | }, 520 | { 521 | title: "Android", 522 | ua: "Mozilla/5.0 (Linux; U; Android 1.5; de-; HTC Magic Build/PLAT-RC33) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1", 523 | expected: "Mozilla:5.0 Platform:Linux OS:Android 1.5 Localization:de- Model:HTC Magic Browser:Android-3.1.2 Engine:AppleWebKit-528.5+ Bot:false Mobile:true", 524 | }, 525 | { 526 | title: "BlackBerry", 527 | ua: "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, Like Gecko) Version/6.0.0.141 Mobile Safari/534.1+", 528 | expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry 9800 Localization:en Browser:BlackBerry-6.0.0.141 Engine:AppleWebKit-534.1+ Bot:false Mobile:true", 529 | expectedOS: &OSInfo{"BlackBerry 9800", "BlackBerry", "9800"}, 530 | }, 531 | { 532 | title: "BB10", 533 | ua: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.3+ (KHTML, like Gecko) Version/10.0.9.388 Mobile Safari/537.3+", 534 | expected: "Mozilla:5.0 Platform:BlackBerry OS:BlackBerry Browser:BlackBerry-10.0.9.388 Engine:AppleWebKit-537.3+ Bot:false Mobile:true", 535 | }, 536 | { 537 | title: "Ericsson", 538 | ua: "Mozilla/5.0 (SymbianOS/9.4; U; Series60/5.0 Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) Version/3.0 Safari/525", 539 | expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.4 Browser:Symbian-3.0 Engine:AppleWebKit-525 Bot:false Mobile:true", 540 | expectedOS: &OSInfo{"SymbianOS/9.4", "SymbianOS", "9.4"}, 541 | }, 542 | { 543 | title: "ChromeAndroid", 544 | ua: "Mozilla/5.0 (Linux; Android 4.2.1; Galaxy Nexus Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19", 545 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.1 Model:Galaxy Nexus Browser:Chrome-18.0.1025.166 Engine:AppleWebKit-535.19 Bot:false Mobile:true", 546 | }, 547 | { 548 | title: "Chrome for iOS", 549 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_1 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) CriOS/67.0.3396.87 Mobile/15E302 Safari/604.1", 550 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 11_3_1 like Mac OS X Model:iPhone Browser:Chrome-67.0.3396.87 Engine:AppleWebKit-604.1.34 Bot:false Mobile:true", 551 | }, 552 | { 553 | title: "WebkitNoPlatform", 554 | ua: "Mozilla/5.0 (en-us) AppleWebKit/525.13 (KHTML, like Gecko; Google Web Preview) Version/3.1 Safari/525.13", 555 | expected: "Mozilla:5.0 Platform:en-us Localization:en-us Browser:Safari-3.1 Engine:AppleWebKit-525.13 Bot:false Mobile:false", 556 | }, 557 | { 558 | title: "OperaWebkitMobile", 559 | ua: "Mozilla/5.0 (Linux; Android 4.2.2; Galaxy Nexus Build/JDQ39) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Mobile Safari/537.31 OPR/14.0.1074.57453", 560 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Model:Galaxy Nexus Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:true", 561 | }, 562 | { 563 | title: "OperaWebkitDesktop", 564 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.58 Safari/537.31 OPR/14.0.1074.57453", 565 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Opera-14.0.1074.57453 Engine:AppleWebKit-537.31 Bot:false Mobile:false", 566 | }, 567 | { 568 | title: "ChromeNothingAfterU", 569 | ua: "Mozilla/5.0 (Linux; U) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4", 570 | expected: "Mozilla:5.0 Platform:Linux OS:Linux Browser:Chrome-22.0.1229.79 Engine:AppleWebKit-537.4 Bot:false Mobile:false", 571 | }, 572 | { 573 | title: "SafariOnSymbian", 574 | ua: "Mozilla/5.0 (SymbianOS/9.1; U; [en-us]) AppleWebKit/413 (KHTML, like Gecko) Safari/413", 575 | expected: "Mozilla:5.0 Platform:Symbian OS:SymbianOS/9.1 Browser:Symbian-413 Engine:AppleWebKit-413 Bot:false Mobile:true", 576 | }, 577 | { 578 | title: "Chromium - Ubuntu V49", 579 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36", 580 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-49.0.2623.108 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 581 | }, 582 | { 583 | title: "Chromium - Ubuntu V55", 584 | ua: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36", 585 | expected: "Mozilla:5.0 Platform:X11 OS:Linux x86_64 Browser:Chromium-53.0.2785.143 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 586 | }, 587 | { 588 | title: "Firefox for iOS", 589 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4", 590 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 8_3 like Mac OS X Model:iPhone Browser:Firefox-1.0 Engine:AppleWebKit-600.1.4 Bot:false Mobile:true", 591 | }, 592 | { 593 | title: "Firefox Focus for iOS", 594 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/7.0.4 Mobile/16B91 Safari/605.1.15", 595 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 12_1 like Mac OS X Model:iPhone Browser:Firefox-7.0.4 Engine:AppleWebKit-605.1.15 Bot:false Mobile:true", 596 | }, 597 | { 598 | title: "Firefox on iPad", 599 | ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/24.1 Safari/605.1.15", 600 | expected: "Mozilla:5.0 Platform:iPad OS:Intel Mac OS X 10.15 Model:iPad Browser:Firefox-24.1 Engine:AppleWebKit-605.1.15 Bot:false Mobile:true", 601 | }, 602 | { 603 | title: "Electron", 604 | ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) CozyDrive/3.17.0 Chrome/73.0.3683.119 Electron/5.0.0 Safari/537.36", 605 | expected: "Mozilla:5.0 Platform:Windows OS:Windows 10 Browser:Electron-5.0.0 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 606 | }, 607 | { 608 | title: "Coc Coc", 609 | ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/96.0.230 Chrome/90.0.4430.230 Safari/537.36", 610 | expected: "Mozilla:5.0 Platform:Macintosh OS:Intel Mac OS X 10_15_7 Browser:Coc Coc-96.0.230 Engine:AppleWebKit-537.36 Bot:false Mobile:false", 611 | }, 612 | { 613 | title: "LinkedInApp", 614 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [LinkedInApp]", 615 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 14_6 like Mac OS X Model:iPhone Browser:Mobile App Engine:AppleWebKit-605.1.15 Bot:false Mobile:true", 616 | }, 617 | { 618 | title: "Google App for iOS", 619 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/170.0.386351093 Mobile/15E148 Safari/604.1", 620 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 14_1 like Mac OS X Model:iPhone Browser:Google App-170.0.386351093 Engine:AppleWebKit-605.1.15 Bot:false Mobile:true", 621 | }, 622 | { 623 | title: "DuckDuckGo Browser for iOS", 624 | ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.6 Mobile/15E148 DuckDuckGo/7 Safari/605.1.15", 625 | expected: "Mozilla:5.0 Platform:iPhone OS:CPU iPhone OS 14_6 like Mac OS X Model:iPhone Browser:DuckDuckGo-7 Engine:AppleWebKit-605.1.15 Bot:false Mobile:true", 626 | }, 627 | 628 | // Dalvik 629 | { 630 | title: "Dalvik - Dell:001DL", 631 | ua: "Dalvik/1.2.0 (Linux; U; Android 2.2.2; 001DL Build/FRG83G)", 632 | expected: "Mozilla:5.0 Platform:Linux OS:Android 2.2.2 Model:001DL Bot:false Mobile:true", 633 | }, 634 | { 635 | title: "Dalvik - HTC:001HT", 636 | ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.3; 001HT Build/GRI40)", 637 | expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.3 Model:001HT Bot:false Mobile:true", 638 | }, 639 | { 640 | title: "Dalvik - ZTE:009Z", 641 | ua: "Dalvik/1.4.0 (Linux; U; Android 2.3.4; 009Z Build/GINGERBREAD)", 642 | expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.4 Model:009Z Bot:false Mobile:true", 643 | }, 644 | { 645 | title: "Dalvik - A850", 646 | ua: "Dalvik/1.6.0 (Linux; U; Android 4.2.2; A850 Build/JDQ39) Configuration/CLDC-1.1; Opera Mini/att/4.2", 647 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.2.2 Model:A850 Bot:false Mobile:true", 648 | }, 649 | { 650 | title: "Dalvik - Asus:T00Q", 651 | ua: "Dalvik/1.6.0 (Linux; U; Android 4.4.2; ASUS_T00Q Build/KVT49L)/CLDC-1.1", 652 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.4.2 Model:ASUS_T00Q Bot:false Mobile:true", 653 | expectedOS: &OSInfo{"Android 4.4.2", "Android", "4.4.2"}, 654 | }, 655 | { 656 | title: "Dalvik - W2430", 657 | ua: "Dalvik/1.6.0 (Linux; U; Android 4.0.4; W2430 Build/IMM76D)014; Profile/MIDP-2.1 Configuration/CLDC-1", 658 | expected: "Mozilla:5.0 Platform:Linux OS:Android 4.0.4 Model:W2430 Bot:false Mobile:true", 659 | }, 660 | { 661 | title: "Samsung S5 Facebook App", 662 | ua: "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.121 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/35.0.0.48.273;]", 663 | expected: "Mozilla:5.0 Platform:Linux OS:Android 5.0 Localization:wv Model:SM-G900P Browser:Android-4.0 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 664 | }, 665 | { 666 | title: "Facebook - No Browser Or OS", 667 | ua: "[FBAN/FB4A;FBAV/16.0.0.20.15;FBBV/4061184;FBDM/{density=1.5,width=540,height=960};FBLC/en_US;FB_FW/2;FBCR/MY CELCOM;FBPN/com.facebook.katana;FBDV/Lenovo A850+;FBSV/4.2.2;FBOP/1;FBCA/armeabi-v7a:armeabi;]", 668 | expected: "Bot:false Mobile:false", 669 | }, 670 | 671 | // arm_64 672 | { 673 | title: "Samsung S7 Edge - YaBrowser", 674 | ua: "Mozilla/5.0 (Linux; arm_64; Android 8.0.0; SM-G935F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 YaBrowser/19.12.3.101.00 Mobile Safari/537.36", 675 | expected: "Mozilla:5.0 Platform:Linux OS:Android 8.0.0 Localization:SM-G935F Model:SM-G935F Browser:YaBrowser-19.12.3.101.00 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 676 | }, 677 | 678 | // Get Phone Model 679 | { 680 | title: "HUAWEI P20 lite - YaBrowser", 681 | ua: "Mozilla/5.0 (Linux; arm_64; Android 9; ANE-LX2J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.136 YaBrowser/20.2.6.114.00 Mobile Safari/537.36", 682 | expected: "Mozilla:5.0 Platform:Linux OS:Android 9 Localization:ANE-LX2J Model:ANE-LX2J Browser:YaBrowser-20.2.6.114.00 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 683 | }, 684 | { 685 | title: "OPPO R9sk", 686 | ua: "Mozilla/5.0 (Linux; Android 7.1.1; OPPO R9sk) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.111 Mobile Safari/537.36", 687 | expected: "Mozilla:5.0 Platform:Linux OS:Android 7.1.1 Model:OPPO R9sk Browser:Chrome-76.0.3809.111 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 688 | }, 689 | { 690 | title: "Nexus One", 691 | ua: "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", 692 | expected: "Mozilla:5.0 Platform:Linux OS:Android 2.3.7 Localization:en-us Model:Nexus One Browser:Android-4.0 Engine:AppleWebKit-533.1 Bot:false Mobile:true", 693 | }, 694 | { 695 | title: "HUAWEIELE", 696 | ua: "Mozilla/5.0 (Linux; Android 9; ELE-AL00 Build/HUAWEIELE-AL0001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.83 Mobile Safari/537.36 T7/11.15 baiduboxapp/11.15.5.10 (Baidu; P1 9)", 697 | expected: "Mozilla:5.0 Platform:Linux OS:Android 9 Localization:wv Model:ELE-AL00 Browser:Android-4.0 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 698 | }, 699 | { 700 | title: "Redmi Note 3", 701 | ua: "Mozilla/5.0 (Linux; U; Android 5.0.2; zh-cn; Redmi Note 3 Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.146 Mobile Safari/537.36 XiaoMi/MiuiBrowser/8.8.7", 702 | expected: "Mozilla:5.0 Platform:Linux OS:Android 5.0.2 Localization:zh-cn Model:Redmi Note 3 Browser:Android-4.0 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 703 | }, 704 | { 705 | title: "Redmi K40", 706 | ua: "Mozilla/5.0 (Linux; U; Android 12; zh-cn; M2012K11AC Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.116 Mobile Safari/537.36 XiaoMi/MiuiBrowser/16.4.20 swan-mibrowser", 707 | expected: "Mozilla:5.0 Platform:Linux OS:Android 12 Localization:zh-cn Model:M2012K11AC Browser:Android-4.0 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 708 | }, 709 | { 710 | title: "XiaoMi 6", 711 | ua: "Mozilla/5.0 (Linux; Android 8.0.0; MI 6 Build/OPR1.170623.027; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/76.0.3809.89 Mobile Safari/537.36 T7/11.12 swan/2.11.0 baiduboxapp/11.15.0.0 (Baidu; P1 8.0.0)", 712 | expected: "Mozilla:5.0 Platform:Linux OS:Android 8.0.0 Localization:wv Model:MI 6 Browser:Android-4.0 Engine:AppleWebKit-537.36 Bot:false Mobile:true", 713 | }, 714 | { 715 | title: "HTC_Wildfire_A3333", 716 | ua: "Mozilla/5.0 (Linux; U; Android 2.2.1; zh-cn; HTC_Wildfire_A3333 Build/FRG83D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", 717 | expected: "Mozilla:5.0 Platform:Linux OS:Android 2.2.1 Localization:zh-cn Model:HTC_Wildfire_A3333 Browser:Android-4.0 Engine:AppleWebKit-533.1 Bot:false Mobile:true", 718 | }, 719 | } 720 | 721 | // Internal: beautify the UserAgent reference into a string so it can be 722 | // tested later on. 723 | // 724 | // ua - a UserAgent reference. 725 | // 726 | // Returns a string that contains the beautified representation. 727 | func beautify(ua *UserAgent) (s string) { 728 | if len(ua.Mozilla()) > 0 { 729 | s += "Mozilla:" + ua.Mozilla() + " " 730 | } 731 | if len(ua.Platform()) > 0 { 732 | s += "Platform:" + ua.Platform() + " " 733 | } 734 | if len(ua.OS()) > 0 { 735 | s += "OS:" + ua.OS() + " " 736 | } 737 | if len(ua.Localization()) > 0 { 738 | s += "Localization:" + ua.Localization() + " " 739 | } 740 | if len(ua.Model()) > 0 { 741 | s += "Model:" + ua.Model() + " " 742 | } 743 | str1, str2 := ua.Browser() 744 | if len(str1) > 0 { 745 | s += "Browser:" + str1 746 | if len(str2) > 0 { 747 | s += "-" + str2 + " " 748 | } else { 749 | s += " " 750 | } 751 | } 752 | str1, str2 = ua.Engine() 753 | if len(str1) > 0 { 754 | s += "Engine:" + str1 755 | if len(str2) > 0 { 756 | s += "-" + str2 + " " 757 | } else { 758 | s += " " 759 | } 760 | } 761 | s += "Bot:" + fmt.Sprintf("%v", ua.Bot()) + " " 762 | s += "Mobile:" + fmt.Sprintf("%v", ua.Mobile()) 763 | return s 764 | } 765 | 766 | // The test suite. 767 | func TestUserAgent(t *testing.T) { 768 | for _, tt := range uastrings { 769 | ua := New(tt.ua) 770 | got := beautify(ua) 771 | if tt.expected != got { 772 | t.Errorf("\nTest %v\ngot: %q\nexpected %q\n", tt.title, got, tt.expected) 773 | } 774 | 775 | if tt.expectedOS != nil { 776 | gotOSInfo := ua.OSInfo() 777 | if !reflect.DeepEqual(tt.expectedOS, &gotOSInfo) { 778 | t.Errorf("\nTest %v\ngot: %#v\nexpected %#v\n", tt.title, gotOSInfo, tt.expectedOS) 779 | } 780 | } 781 | 782 | } 783 | } 784 | 785 | // Benchmark: it parses each User-Agent string on the uastrings slice b.N times. 786 | func BenchmarkUserAgent(b *testing.B) { 787 | for i := 0; i < b.N; i++ { 788 | b.StopTimer() 789 | for _, tt := range uastrings { 790 | ua := new(UserAgent) 791 | b.StartTimer() 792 | ua.Parse(tt.ua) 793 | } 794 | } 795 | } 796 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2014-2023 Miquel Sabaté Solà 2 | // This file is licensed under the MIT license. 3 | // See the LICENSE file. 4 | 5 | package user_agent 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var botFromSiteRegexp = regexp.MustCompile(`http[s]?://.+\.\w+`) 13 | 14 | // Get the name of the bot from the website that may be in the given comment. If 15 | // there is no website in the comment, then an empty string is returned. 16 | func getFromSite(comment []string) string { 17 | if len(comment) == 0 { 18 | return "" 19 | } 20 | 21 | // Where we should check the website. 22 | idx := 2 23 | if len(comment) < 3 { 24 | idx = 0 25 | } else if len(comment) == 4 { 26 | idx = 3 27 | } 28 | 29 | // Pick the site. 30 | results := botFromSiteRegexp.FindStringSubmatch(comment[idx]) 31 | if len(results) == 1 { 32 | // If it's a simple comment, just return the name of the site. 33 | if idx == 0 { 34 | return results[0] 35 | } 36 | 37 | // This is a large comment, usually the name will be in the previous 38 | // field of the comment. 39 | return strings.TrimSpace(comment[idx-1]) 40 | } 41 | return "" 42 | } 43 | 44 | // Returns true if the info that we currently have corresponds to the Google 45 | // or Bing mobile bot. This function also modifies some attributes in the receiver 46 | // accordingly. 47 | func (p *UserAgent) googleOrBingBot() bool { 48 | // This is a hackish way to detect 49 | // Google's mobile bot (Googlebot, AdsBot-Google-Mobile, etc.) 50 | // (See https://support.google.com/webmasters/answer/1061943) 51 | // and Bing's mobile bot 52 | // (See https://www.bing.com/webmaster/help/which-crawlers-does-bing-use-8c184ec0) 53 | if strings.Contains(p.ua, "Google") || strings.Contains(p.ua, "bingbot") { 54 | p.platform = "" 55 | p.undecided = true 56 | } 57 | return p.undecided 58 | } 59 | 60 | // Returns true if we think that it is iMessage-Preview. This function also 61 | // modifies some attributes in the receiver accordingly. 62 | func (p *UserAgent) iMessagePreview() bool { 63 | // iMessage-Preview doesn't advertise itself. We have a to rely on a hack 64 | // to detect it: it impersonates both facebook and twitter bots. 65 | // See https://medium.com/@siggi/apples-imessage-impersonates-twitter-facebook-bots-when-scraping-cef85b2cbb7d 66 | if !strings.Contains(p.ua, "facebookexternalhit") { 67 | return false 68 | } 69 | if !strings.Contains(p.ua, "Twitterbot") { 70 | return false 71 | } 72 | p.bot = true 73 | p.browser.Name = "iMessage-Preview" 74 | p.browser.Engine = "" 75 | p.browser.EngineVersion = "" 76 | // We don't set the mobile flag because iMessage can be on iOS (mobile) or macOS (not mobile). 77 | return true 78 | } 79 | 80 | // Set the attributes of the receiver as given by the parameters. All the other 81 | // parameters are set to empty. 82 | func (p *UserAgent) setSimple(name, version string, bot bool) { 83 | p.bot = bot 84 | if !bot { 85 | p.mozilla = "" 86 | } 87 | p.browser.Name = name 88 | p.browser.Version = version 89 | p.browser.Engine = "" 90 | p.browser.EngineVersion = "" 91 | p.os = "" 92 | p.localization = "" 93 | } 94 | 95 | // Fix some values for some weird browsers. 96 | func (p *UserAgent) fixOther(sections []section) { 97 | if len(sections) > 0 { 98 | p.browser.Name = sections[0].name 99 | p.browser.Version = sections[0].version 100 | p.mozilla = "" 101 | } 102 | } 103 | 104 | var botRegex = regexp.MustCompile("(?i)(bot|crawler|sp(i|y)der|search|worm|fetch|nutch)") 105 | 106 | // Check if we're dealing with a bot or with some weird browser. If that is the 107 | // case, the receiver will be modified accordingly. 108 | func (p *UserAgent) checkBot(sections []section) { 109 | // If there's only one element, and it's doesn't have the Mozilla string, 110 | // check whether this is a bot or not. 111 | if len(sections) == 1 && sections[0].name != "Mozilla" { 112 | p.mozilla = "" 113 | 114 | // Check whether the name has some suspicious "bot" or "crawler" in his name. 115 | if botRegex.Match([]byte(sections[0].name)) { 116 | p.setSimple(sections[0].name, "", true) 117 | return 118 | } 119 | 120 | // Tough luck, let's try to see if it has a website in his comment. 121 | if name := getFromSite(sections[0].comment); name != "" { 122 | // First of all, this is a bot. Moreover, since it doesn't have the 123 | // Mozilla string, we can assume that the name and the version are 124 | // the ones from the first section. 125 | p.setSimple(sections[0].name, sections[0].version, true) 126 | return 127 | } 128 | 129 | // At this point we are sure that this is not a bot, but some weirdo. 130 | p.setSimple(sections[0].name, sections[0].version, false) 131 | } else { 132 | // Let's iterate over the available comments and check for a website. 133 | for _, v := range sections { 134 | if name := getFromSite(v.comment); name != "" { 135 | // Ok, we've got a bot name. 136 | results := strings.SplitN(name, "/", 2) 137 | version := "" 138 | if len(results) == 2 { 139 | version = results[1] 140 | } 141 | p.setSimple(results[0], version, true) 142 | return 143 | } 144 | } 145 | 146 | // We will assume that this is some other weird browser. 147 | p.fixOther(sections) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /browser.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012-2023 Miquel Sabaté Solà 2 | // This file is licensed under the MIT license. 3 | // See the LICENSE file. 4 | 5 | package user_agent 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | var ie11Regexp = regexp.MustCompile("^rv:(.+)$") 13 | 14 | // Browser is a struct containing all the information that we might be 15 | // interested from the browser. 16 | type Browser struct { 17 | // The name of the browser's engine. 18 | Engine string 19 | 20 | // The version of the browser's engine. 21 | EngineVersion string 22 | 23 | // The name of the browser. 24 | Name string 25 | 26 | // The version of the browser. 27 | Version string 28 | } 29 | 30 | // Extract all the information that we can get from the User-Agent string 31 | // about the browser and update the receiver with this information. 32 | // 33 | // The function receives just one argument "sections", that contains the 34 | // sections from the User-Agent string after being parsed. 35 | func (p *UserAgent) detectBrowser(sections []section) { 36 | slen := len(sections) 37 | 38 | if sections[0].name == "Opera" { 39 | p.browser.Name = "Opera" 40 | p.browser.Version = sections[0].version 41 | p.browser.Engine = "Presto" 42 | if slen > 1 { 43 | p.browser.EngineVersion = sections[1].version 44 | } 45 | } else if sections[0].name == "Dalvik" { 46 | // When Dalvik VM is in use, there is no browser info attached to ua. 47 | // Although browser is still a Mozilla/5.0 compatible. 48 | p.mozilla = "5.0" 49 | } else if slen > 1 { 50 | engine := sections[1] 51 | p.browser.Engine = engine.name 52 | p.browser.EngineVersion = engine.version 53 | if slen > 2 { 54 | sectionIndex := 2 55 | // The version after the engine comment is empty on e.g. Ubuntu 56 | // platforms so if this is the case, let's use the next in line. 57 | if sections[2].version == "" && slen > 3 { 58 | sectionIndex = 3 59 | } 60 | p.browser.Version = sections[sectionIndex].version 61 | if engine.name == "AppleWebKit" { 62 | for _, comment := range engine.comment { 63 | if len(comment) > 5 && 64 | (strings.HasPrefix(comment, "Googlebot") || strings.HasPrefix(comment, "bingbot")) { 65 | p.undecided = true 66 | break 67 | } 68 | } 69 | switch sections[slen-1].name { 70 | case "Edge": 71 | p.browser.Name = "Edge" 72 | p.browser.Version = sections[slen-1].version 73 | p.browser.Engine = "EdgeHTML" 74 | p.browser.EngineVersion = "" 75 | case "Edg": 76 | if !p.undecided { 77 | p.browser.Name = "Edge" 78 | p.browser.Version = sections[slen-1].version 79 | p.browser.Engine = "AppleWebKit" 80 | p.browser.EngineVersion = sections[slen-2].version 81 | } 82 | case "OPR": 83 | p.browser.Name = "Opera" 84 | p.browser.Version = sections[slen-1].version 85 | case "Mobile": 86 | p.browser.Name = "Mobile App" 87 | p.browser.Version = "" 88 | default: 89 | switch sections[slen-3].name { 90 | case "YaBrowser": 91 | p.browser.Name = "YaBrowser" 92 | p.browser.Version = sections[slen-3].version 93 | case "coc_coc_browser": 94 | p.browser.Name = "Coc Coc" 95 | p.browser.Version = sections[slen-3].version 96 | default: 97 | switch sections[slen-2].name { 98 | case "Electron": 99 | p.browser.Name = "Electron" 100 | p.browser.Version = sections[slen-2].version 101 | case "DuckDuckGo": 102 | p.browser.Name = "DuckDuckGo" 103 | p.browser.Version = sections[slen-2].version 104 | case "PhantomJS": 105 | p.browser.Name = "PhantomJS" 106 | p.browser.Version = sections[slen-2].version 107 | default: 108 | switch sections[sectionIndex].name { 109 | case "Chrome", "CriOS": 110 | p.browser.Name = "Chrome" 111 | case "HeadlessChrome": 112 | p.browser.Name = "Headless Chrome" 113 | case "Chromium": 114 | p.browser.Name = "Chromium" 115 | case "GSA": 116 | p.browser.Name = "Google App" 117 | case "FxiOS": 118 | p.browser.Name = "Firefox" 119 | default: 120 | p.browser.Name = "Safari" 121 | } 122 | } 123 | } 124 | // It's possible the google-bot emulates these now 125 | for _, comment := range engine.comment { 126 | if len(comment) > 5 && 127 | (strings.HasPrefix(comment, "Googlebot") || strings.HasPrefix(comment, "bingbot")) { 128 | p.undecided = true 129 | break 130 | } 131 | } 132 | } 133 | } else if engine.name == "Gecko" { 134 | name := sections[2].name 135 | if name == "MRA" && slen > 4 { 136 | name = sections[4].name 137 | p.browser.Version = sections[4].version 138 | } 139 | p.browser.Name = name 140 | } else if engine.name == "like" && sections[2].name == "Gecko" { 141 | // This is the new user agent from Internet Explorer 11. 142 | p.browser.Engine = "Trident" 143 | p.browser.Name = "Internet Explorer" 144 | for _, c := range sections[0].comment { 145 | version := ie11Regexp.FindStringSubmatch(c) 146 | if len(version) > 0 { 147 | p.browser.Version = version[1] 148 | return 149 | } 150 | } 151 | p.browser.Version = "" 152 | } 153 | } 154 | } else if slen == 1 && len(sections[0].comment) > 1 { 155 | comment := sections[0].comment 156 | if comment[0] == "compatible" && strings.HasPrefix(comment[1], "MSIE") { 157 | p.browser.Engine = "Trident" 158 | p.browser.Name = "Internet Explorer" 159 | // The MSIE version may be reported as the compatibility version. 160 | // For IE 8 through 10, the Trident token is more accurate. 161 | // http://msdn.microsoft.com/en-us/library/ie/ms537503(v=vs.85).aspx#VerToken 162 | for _, v := range comment { 163 | if strings.HasPrefix(v, "Trident/") { 164 | switch v[8:] { 165 | case "4.0": 166 | p.browser.Version = "8.0" 167 | case "5.0": 168 | p.browser.Version = "9.0" 169 | case "6.0": 170 | p.browser.Version = "10.0" 171 | } 172 | break 173 | } 174 | } 175 | // If the Trident token is not provided, fall back to MSIE token. 176 | if p.browser.Version == "" { 177 | p.browser.Version = strings.TrimSpace(comment[1][4:]) 178 | } 179 | } 180 | } 181 | } 182 | 183 | // Engine returns two strings. The first string is the name of the engine and the 184 | // second one is the version of the engine. 185 | func (p *UserAgent) Engine() (string, string) { 186 | return p.browser.Engine, p.browser.EngineVersion 187 | } 188 | 189 | // Browser returns two strings. The first string is the name of the browser and the 190 | // second one is the version of the browser. 191 | func (p *UserAgent) Browser() (string, string) { 192 | return p.browser.Name, p.browser.Version 193 | } 194 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mssola/user_agent 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package user_agent 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // detectModel some properties of the model from the given section. 8 | func (p *UserAgent) detectModel(s section) { 9 | if !p.mobile { 10 | return 11 | } 12 | if p.platform == "iPhone" || p.platform == "iPad" { 13 | p.model = p.platform 14 | return 15 | } 16 | // Android model 17 | if s.name == "Mozilla" && p.platform == "Linux" && len(s.comment) > 2 { 18 | mostAndroidModel := s.comment[2] 19 | if strings.Contains(mostAndroidModel, "Android") || strings.Contains(mostAndroidModel, "Linux") { 20 | mostAndroidModel = s.comment[len(s.comment)-1] 21 | } 22 | tmp := strings.Split(mostAndroidModel, "Build") 23 | if len(tmp) > 0 { 24 | p.model = strings.Trim(tmp[0], " ") 25 | return 26 | } 27 | } 28 | // traverse all item 29 | for _, v := range s.comment { 30 | if strings.Contains(v, "Build") { 31 | tmp := strings.Split(v, "Build") 32 | p.model = strings.Trim(tmp[0], " ") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /operating_systems.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012-2023 Miquel Sabaté Solà 2 | // This file is licensed under the MIT license. 3 | // See the LICENSE file. 4 | 5 | package user_agent 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | // OSInfo represents full information on the operating system extracted from the 12 | // user agent. 13 | type OSInfo struct { 14 | // Full name of the operating system. This is identical to the output of ua.OS() 15 | FullName string 16 | 17 | // Name of the operating system. This is sometimes a shorter version of the 18 | // operating system name, e.g. "Mac OS X" instead of "Intel Mac OS X" 19 | Name string 20 | 21 | // Operating system version, e.g. 7 for Windows 7 or 10.8 for Max OS X Mountain Lion 22 | Version string 23 | } 24 | 25 | // Normalize the name of the operating system. By now, this just 26 | // affects to Windows NT. 27 | // 28 | // Returns a string containing the normalized name for the Operating System. 29 | func normalizeOS(name string) string { 30 | sp := strings.SplitN(name, " ", 3) 31 | if len(sp) != 3 || sp[1] != "NT" { 32 | return name 33 | } 34 | 35 | switch sp[2] { 36 | case "5.0": 37 | return "Windows 2000" 38 | case "5.01": 39 | return "Windows 2000, Service Pack 1 (SP1)" 40 | case "5.1": 41 | return "Windows XP" 42 | case "5.2": 43 | return "Windows XP x64 Edition" 44 | case "6.0": 45 | return "Windows Vista" 46 | case "6.1": 47 | return "Windows 7" 48 | case "6.2": 49 | return "Windows 8" 50 | case "6.3": 51 | return "Windows 8.1" 52 | case "10.0": 53 | return "Windows 10" 54 | } 55 | return name 56 | } 57 | 58 | // Guess the OS, the localization and if this is a mobile device for a 59 | // Webkit-powered browser. 60 | // 61 | // The first argument p is a reference to the current UserAgent and the second 62 | // argument is a slice of strings containing the comment. 63 | func webkit(p *UserAgent, comment []string) { 64 | if p.platform == "webOS" { 65 | p.browser.Name = p.platform 66 | p.os = "Palm" 67 | if len(comment) > 2 { 68 | p.localization = comment[2] 69 | } 70 | p.mobile = true 71 | } else if p.platform == "Symbian" { 72 | p.mobile = true 73 | p.browser.Name = p.platform 74 | p.os = comment[0] 75 | } else if p.platform == "Linux" { 76 | p.mobile = true 77 | if p.browser.Name == "Safari" { 78 | p.browser.Name = "Android" 79 | } 80 | if len(comment) > 1 { 81 | if comment[1] == "U" || comment[1] == "arm_64" { 82 | if len(comment) > 2 { 83 | p.os = comment[2] 84 | } else { 85 | p.mobile = false 86 | p.os = comment[0] 87 | } 88 | } else { 89 | p.os = comment[1] 90 | } 91 | } 92 | if len(comment) > 3 { 93 | p.localization = comment[3] 94 | } else if len(comment) == 3 { 95 | _ = p.googleOrBingBot() 96 | } 97 | } else if len(comment) > 0 { 98 | if len(comment) > 3 { 99 | p.localization = comment[3] 100 | } 101 | if strings.HasPrefix(comment[0], "Windows NT") { 102 | p.os = normalizeOS(comment[0]) 103 | } else if len(comment) < 2 { 104 | p.localization = comment[0] 105 | } else if len(comment) < 3 { 106 | if !p.googleOrBingBot() && !p.iMessagePreview() { 107 | p.os = normalizeOS(comment[1]) 108 | } 109 | } else { 110 | p.os = normalizeOS(comment[2]) 111 | } 112 | if p.platform == "BlackBerry" { 113 | p.browser.Name = p.platform 114 | if p.os == "Touch" { 115 | p.os = p.platform 116 | } 117 | } 118 | } 119 | 120 | // Special case for Firefox on iPad, where the platform is advertised as Macintosh instead of iPad 121 | if p.platform == "Macintosh" && p.browser.Engine == "AppleWebKit" && p.browser.Name == "Firefox" { 122 | p.platform = "iPad" 123 | p.mobile = true 124 | } 125 | } 126 | 127 | // Guess the OS, the localization and if this is a mobile device 128 | // for a Gecko-powered browser. 129 | // 130 | // The first argument p is a reference to the current UserAgent and the second 131 | // argument is a slice of strings containing the comment. 132 | func gecko(p *UserAgent, comment []string) { 133 | if len(comment) > 1 { 134 | if comment[1] == "U" || comment[1] == "arm_64" { 135 | if len(comment) > 2 { 136 | p.os = normalizeOS(comment[2]) 137 | } else { 138 | p.os = normalizeOS(comment[1]) 139 | } 140 | } else { 141 | if strings.Contains(p.platform, "Android") { 142 | p.mobile = true 143 | p.platform, p.os = normalizeOS(comment[1]), p.platform 144 | } else if comment[0] == "Mobile" || comment[0] == "Tablet" { 145 | p.mobile = true 146 | p.os = "FirefoxOS" 147 | } else { 148 | if p.os == "" { 149 | p.os = normalizeOS(comment[1]) 150 | } 151 | } 152 | } 153 | // Only parse 4th comment as localization if it doesn't start with rv:. 154 | // For example Firefox on Ubuntu contains "rv:XX.X" in this field. 155 | if len(comment) > 3 && !strings.HasPrefix(comment[3], "rv:") { 156 | p.localization = comment[3] 157 | } 158 | } 159 | } 160 | 161 | // Guess the OS, the localization and if this is a mobile device 162 | // for Internet Explorer. 163 | // 164 | // The first argument p is a reference to the current UserAgent and the second 165 | // argument is a slice of strings containing the comment. 166 | func trident(p *UserAgent, comment []string) { 167 | // Internet Explorer only runs on Windows. 168 | p.platform = "Windows" 169 | 170 | // The OS can be set before to handle a new case in IE11. 171 | if p.os == "" { 172 | if len(comment) > 2 { 173 | p.os = normalizeOS(comment[2]) 174 | } else { 175 | p.os = "Windows NT 4.0" 176 | } 177 | } 178 | 179 | // Last but not least, let's detect if it comes from a mobile device. 180 | for _, v := range comment { 181 | if strings.HasPrefix(v, "IEMobile") { 182 | p.mobile = true 183 | return 184 | } 185 | } 186 | } 187 | 188 | // Guess the OS, the localization and if this is a mobile device 189 | // for Opera. 190 | // 191 | // The first argument p is a reference to the current UserAgent and the second 192 | // argument is a slice of strings containing the comment. 193 | func opera(p *UserAgent, comment []string) { 194 | slen := len(comment) 195 | 196 | if strings.HasPrefix(comment[0], "Windows") { 197 | p.platform = "Windows" 198 | p.os = normalizeOS(comment[0]) 199 | if slen > 2 { 200 | if slen > 3 && strings.HasPrefix(comment[2], "MRA") { 201 | p.localization = comment[3] 202 | } else { 203 | p.localization = comment[2] 204 | } 205 | } 206 | } else { 207 | if strings.HasPrefix(comment[0], "Android") { 208 | p.mobile = true 209 | } 210 | p.platform = comment[0] 211 | if slen > 1 { 212 | p.os = comment[1] 213 | if slen > 3 { 214 | p.localization = comment[3] 215 | } 216 | } else { 217 | p.os = comment[0] 218 | } 219 | } 220 | } 221 | 222 | // Guess the OS. Android browsers send Dalvik as the user agent in the 223 | // request header. 224 | // 225 | // The first argument p is a reference to the current UserAgent and the second 226 | // argument is a slice of strings containing the comment. 227 | func dalvik(p *UserAgent, comment []string) { 228 | slen := len(comment) 229 | 230 | if strings.HasPrefix(comment[0], "Linux") { 231 | p.platform = comment[0] 232 | if slen > 2 { 233 | p.os = comment[2] 234 | } 235 | p.mobile = true 236 | } 237 | } 238 | 239 | // Given the comment of the first section of the UserAgent string, 240 | // get the platform. 241 | func getPlatform(comment []string) string { 242 | if len(comment) > 0 { 243 | if comment[0] != "compatible" { 244 | if strings.HasPrefix(comment[0], "Windows") { 245 | return "Windows" 246 | } else if strings.HasPrefix(comment[0], "Symbian") { 247 | return "Symbian" 248 | } else if strings.HasPrefix(comment[0], "webOS") { 249 | return "webOS" 250 | } else if comment[0] == "BB10" { 251 | return "BlackBerry" 252 | } 253 | return comment[0] 254 | } 255 | } 256 | return "" 257 | } 258 | 259 | // Detect some properties of the OS from the given section. 260 | func (p *UserAgent) detectOS(s section) { 261 | if s.name == "Mozilla" { 262 | // Get the platform here. Be aware that IE11 provides a new format 263 | // that is not backwards-compatible with previous versions of IE. 264 | p.platform = getPlatform(s.comment) 265 | if p.platform == "Windows" && len(s.comment) > 0 { 266 | p.os = normalizeOS(s.comment[0]) 267 | } 268 | 269 | // And finally get the OS depending on the engine. 270 | switch p.browser.Engine { 271 | case "": 272 | p.undecided = true 273 | case "Gecko": 274 | gecko(p, s.comment) 275 | case "AppleWebKit": 276 | webkit(p, s.comment) 277 | case "Trident": 278 | trident(p, s.comment) 279 | } 280 | } else if s.name == "Opera" { 281 | if len(s.comment) > 0 { 282 | opera(p, s.comment) 283 | } 284 | } else if s.name == "Dalvik" { 285 | if len(s.comment) > 0 { 286 | dalvik(p, s.comment) 287 | } 288 | } else if s.name == "okhttp" { 289 | p.mobile = true 290 | p.browser.Name = "OkHttp" 291 | p.browser.Version = s.version 292 | } else { 293 | // Check whether this is a bot or just a weird browser. 294 | p.undecided = true 295 | } 296 | } 297 | 298 | // Platform returns a string containing the platform.. 299 | func (p *UserAgent) Platform() string { 300 | return p.platform 301 | } 302 | 303 | // OS returns a string containing the name of the Operating System. 304 | func (p *UserAgent) OS() string { 305 | return p.os 306 | } 307 | 308 | // Localization returns a string containing the localization. 309 | func (p *UserAgent) Localization() string { 310 | return p.localization 311 | } 312 | 313 | // Model returns a string containing the Phone Model like "Nexus 5X" 314 | func (p *UserAgent) Model() string { 315 | return p.model 316 | } 317 | 318 | // Return OS name and version from a slice of strings created from the full name of the OS. 319 | func osName(osSplit []string) (name, version string) { 320 | if len(osSplit) == 1 { 321 | name = osSplit[0] 322 | version = "" 323 | } else { 324 | // Assume version is stored in the last part of the array. 325 | nameSplit := osSplit[:len(osSplit)-1] 326 | version = osSplit[len(osSplit)-1] 327 | 328 | // Nicer looking Mac OS X 329 | if len(nameSplit) >= 2 && nameSplit[0] == "Intel" && nameSplit[1] == "Mac" { 330 | nameSplit = nameSplit[1:] 331 | } 332 | name = strings.Join(nameSplit, " ") 333 | 334 | if strings.Contains(version, "x86") || strings.Contains(version, "i686") { 335 | // x86_64 and i868 are not Linux versions but architectures 336 | version = "" 337 | } else if version == "X" && name == "Mac OS" { 338 | // X is not a version for Mac OS. 339 | name = name + " " + version 340 | version = "" 341 | } 342 | } 343 | return name, version 344 | } 345 | 346 | // OSInfo returns combined information for the operating system. 347 | func (p *UserAgent) OSInfo() OSInfo { 348 | // Special case for iPhone weirdness 349 | os := strings.Replace(p.os, "like Mac OS X", "", 1) 350 | os = strings.Replace(os, "CPU", "", 1) 351 | os = strings.Trim(os, " ") 352 | 353 | osSplit := strings.Split(os, " ") 354 | 355 | // Special case for x64 edition of Windows 356 | if os == "Windows XP x64 Edition" { 357 | osSplit = osSplit[:len(osSplit)-2] 358 | } 359 | 360 | name, version := osName(osSplit) 361 | 362 | // Special case for names that contain a forward slash version separator. 363 | if strings.Contains(name, "/") { 364 | s := strings.Split(name, "/") 365 | name = s[0] 366 | version = s[1] 367 | } 368 | 369 | // Special case for versions that use underscores 370 | version = strings.Replace(version, "_", ".", -1) 371 | 372 | return OSInfo{ 373 | FullName: p.os, 374 | Name: name, 375 | Version: version, 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /user_agent.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012-2023 Miquel Sabaté Solà 2 | // This file is licensed under the MIT license. 3 | // See the LICENSE file. 4 | 5 | // Package user_agent implements an HTTP User Agent string parser. It defines 6 | // the type UserAgent that contains all the information from the parsed string. 7 | // It also implements the Parse function and getters for all the relevant 8 | // information that has been extracted from a parsed User Agent string. 9 | package user_agent 10 | 11 | import "strings" 12 | 13 | // A section contains the name of the product, its version and 14 | // an optional comment. 15 | type section struct { 16 | name string 17 | version string 18 | comment []string 19 | } 20 | 21 | // The UserAgent struct contains all the info that can be extracted 22 | // from the User-Agent string. 23 | type UserAgent struct { 24 | ua string 25 | mozilla string 26 | platform string 27 | os string 28 | localization string 29 | model string 30 | browser Browser 31 | bot bool 32 | mobile bool 33 | undecided bool 34 | } 35 | 36 | // Read from the given string until the given delimiter or the 37 | // end of the string have been reached. 38 | // 39 | // The first argument is the user agent string being parsed. The second 40 | // argument is a reference pointing to the current index of the user agent 41 | // string. The delimiter argument specifies which character is the delimiter 42 | // and the cat argument determines whether nested '(' should be ignored or not. 43 | // 44 | // Returns an array of bytes containing what has been read. 45 | func readUntil(ua string, index *int, delimiter byte, cat bool) []byte { 46 | var buffer []byte 47 | 48 | i := *index 49 | catalan := 0 50 | for ; i < len(ua); i = i + 1 { 51 | if ua[i] == delimiter { 52 | if catalan == 0 { 53 | *index = i + 1 54 | return buffer 55 | } 56 | catalan-- 57 | } else if cat && ua[i] == '(' { 58 | catalan++ 59 | } 60 | buffer = append(buffer, ua[i]) 61 | } 62 | *index = i + 1 63 | return buffer 64 | } 65 | 66 | // Parse the given product, that is, just a name or a string 67 | // formatted as Name/Version. 68 | // 69 | // It returns two strings. The first string is the name of the product and the 70 | // second string contains the version of the product. 71 | func parseProduct(product []byte) (string, string) { 72 | prod := strings.SplitN(string(product), "/", 2) 73 | if len(prod) == 2 { 74 | return prod[0], prod[1] 75 | } 76 | return string(product), "" 77 | } 78 | 79 | // Parse a section. A section is typically formatted as follows 80 | // "Name/Version (comment)". Both, the comment and the version are optional. 81 | // 82 | // The first argument is the user agent string being parsed. The second 83 | // argument is a reference pointing to the current index of the user agent 84 | // string. 85 | // 86 | // Returns a section containing the information that we could extract 87 | // from the last parsed section. 88 | func parseSection(ua string, index *int) (s section) { 89 | var buffer []byte 90 | 91 | // Check for empty products 92 | if *index < len(ua) && ua[*index] != '(' && ua[*index] != '[' { 93 | buffer = readUntil(ua, index, ' ', false) 94 | s.name, s.version = parseProduct(buffer) 95 | } 96 | 97 | if *index < len(ua) && ua[*index] == '(' { 98 | *index++ 99 | buffer = readUntil(ua, index, ')', true) 100 | s.comment = strings.Split(string(buffer), "; ") 101 | *index++ 102 | } 103 | 104 | // Discards any trailing data within square brackets 105 | if *index < len(ua) && ua[*index] == '[' { 106 | *index++ 107 | _ = readUntil(ua, index, ']', true) 108 | *index++ 109 | } 110 | return s 111 | } 112 | 113 | // Initialize the parser. 114 | func (p *UserAgent) initialize() { 115 | p.ua = "" 116 | p.mozilla = "" 117 | p.platform = "" 118 | p.os = "" 119 | p.localization = "" 120 | p.model = "" 121 | p.browser.Engine = "" 122 | p.browser.EngineVersion = "" 123 | p.browser.Name = "" 124 | p.browser.Version = "" 125 | p.bot = false 126 | p.mobile = false 127 | p.undecided = false 128 | } 129 | 130 | // New parses the given User-Agent string and get the resulting UserAgent 131 | // object. 132 | // 133 | // Returns an UserAgent object that has been initialized after parsing 134 | // the given User-Agent string. 135 | func New(ua string) *UserAgent { 136 | o := &UserAgent{} 137 | o.Parse(ua) 138 | return o 139 | } 140 | 141 | // Parse the given User-Agent string. After calling this function, the 142 | // receiver will be setted up with all the information that we've extracted. 143 | func (p *UserAgent) Parse(ua string) { 144 | var sections []section 145 | 146 | p.initialize() 147 | p.ua = ua 148 | for index, limit := 0, len(ua); index < limit; { 149 | s := parseSection(ua, &index) 150 | if !p.mobile && s.name == "Mobile" { 151 | p.mobile = true 152 | } 153 | sections = append(sections, s) 154 | } 155 | 156 | if len(sections) > 0 { 157 | if sections[0].name == "Mozilla" { 158 | p.mozilla = sections[0].version 159 | } 160 | 161 | p.detectBrowser(sections) 162 | p.detectOS(sections[0]) 163 | p.detectModel(sections[0]) 164 | 165 | if p.undecided { 166 | p.checkBot(sections) 167 | } 168 | } 169 | } 170 | 171 | // Mozilla returns the mozilla version (it's how the User Agent string begins: 172 | // "Mozilla/5.0 ...", unless we're dealing with Opera, of course). 173 | func (p *UserAgent) Mozilla() string { 174 | return p.mozilla 175 | } 176 | 177 | // Bot returns true if it's a bot, false otherwise. 178 | func (p *UserAgent) Bot() bool { 179 | return p.bot 180 | } 181 | 182 | // Mobile returns true if it's a mobile device, false otherwise. 183 | func (p *UserAgent) Mobile() bool { 184 | return p.mobile 185 | } 186 | 187 | // UA returns the original given user agent. 188 | func (p *UserAgent) UA() string { 189 | return p.ua 190 | } 191 | --------------------------------------------------------------------------------