├── go.mod
├── Makefile
├── .github
├── workflows
│ └── ci.yml
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE
│ └── custom.md
├── model.go
├── LICENSE
├── CONTRIBUTING.md
├── README.md
├── CHANGELOG.md
├── bot.go
├── user_agent.go
├── browser.go
├── operating_systems.go
└── all_test.go
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/mssola/useragent
2 |
3 | go 1.13
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
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', '1.18', '1.19', '1.20']
15 |
16 | name: Go ${{ matrix.go }}
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Setup go
21 | uses: actions/setup-go@v3
22 | with:
23 | go-version: ${{ matrix.go }}
24 |
25 | - name: Lint
26 | uses: golangci/golangci-lint-action@v3
27 |
28 | - name: Test
29 | run: |
30 | make test
31 |
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/model.go:
--------------------------------------------------------------------------------
1 | package useragent
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to useragent
2 |
3 | ## Check that your changes do not break anything
4 |
5 | You can safely run tests and the linting utilities with the default `make` target:
6 |
7 | ```
8 | $ make
9 | ```
10 |
11 | Note that this target assumes that you have
12 | [golangci-lint](https://github.com/golangci/golangci-lint) and
13 | [git-valitation](https://github.com/vbatts/git-validation) already installed. If
14 | that is not the case, first install them to get a proper execution of the
15 | default `make` target.
16 |
17 | Otherwise, if you want to be more specific, refer to the `Makefile` to check
18 | which target fits your needs. That being said, the default target is usually
19 | what you want.
20 |
21 | ## Issue reporting
22 |
23 | I'm using [Github](https://github.com/mssola/useragent) in order to host the
24 | code. Thus, in order to report issues you can do it on its [issue
25 | tracker](https://github.com/mssola/useragent/issues). A couple of notes on
26 | reports:
27 |
28 | - Check that the issue has not already been reported or fixed in `main`.
29 | - Try to be concise and precise in your description of the problem.
30 | - Provide a step by step guide on how to reproduce this problem.
31 | - Provide the version you are using (the commit SHA, if possible).
32 |
33 | ## Pull requests
34 |
35 | - Write a [good commit message](https://chris.beams.io/posts/git-commit/).
36 | - Make sure that tests are passing on your local machine (it will also be
37 | checked by the CI system whenever you submit the pull request).
38 | - Update the [changelog](./CHANGELOG.md).
39 | - Try to use the same coding conventions as used in this project.
40 | - Open a pull request with *only* one subject and a clear title and
41 | description. Refrain from submitting pull requests with tons of different
42 | unrelated commits.
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ---
8 |
9 | UserAgent is a Go library that parses HTTP User Agents. As an example:
10 |
11 | ```go
12 | package main
13 |
14 | import (
15 | "fmt"
16 |
17 | "github.com/mssola/useragent"
18 | )
19 |
20 | func main() {
21 | // The "New" function will create a new UserAgent object and it will parse
22 | // the given string. If you need to parse more strings, you can re-use
23 | // this object and call: ua.Parse("another string")
24 | ua := useragent.New("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")
25 |
26 | fmt.Printf("%v\n", ua.Mobile()) // => true
27 | fmt.Printf("%v\n", ua.Bot()) // => false
28 | fmt.Printf("%v\n", ua.Mozilla()) // => "5.0"
29 | fmt.Printf("%v\n", ua.Model()) // => "Nexus One"
30 |
31 | fmt.Printf("%v\n", ua.Platform()) // => "Linux"
32 | fmt.Printf("%v\n", ua.OS()) // => "Android 2.3.7"
33 |
34 | name, version := ua.Engine()
35 | fmt.Printf("%v\n", name) // => "AppleWebKit"
36 | fmt.Printf("%v\n", version) // => "533.1"
37 |
38 | name, version = ua.Browser()
39 | fmt.Printf("%v\n", name) // => "Android"
40 | fmt.Printf("%v\n", version) // => "4.0"
41 |
42 | // Let's see an example with a bot.
43 |
44 | ua.Parse("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
45 |
46 | fmt.Printf("%v\n", ua.Bot()) // => true
47 |
48 | name, version = ua.Browser()
49 | fmt.Printf("%v\n", name) // => Googlebot
50 | fmt.Printf("%v\n", version) // => 2.1
51 | }
52 | ```
53 |
54 | If you want to read the full API documentation simply check
55 | [godoc](https://pkg.go.dev/github.com/mssola/useragent).
56 |
57 | ## Installation
58 |
59 | ```
60 | go get -u github.com/mssola/useragent
61 | ```
62 |
63 | ## Contributing
64 |
65 | Do you want to contribute with code, or to report an issue you are facing? Read
66 | the [CONTRIBUTING.md](./CONTRIBUTING.md) file.
67 |
68 | ## [Changelog](https://pbs.twimg.com/media/DJDYCcLXcAA_eIo?format=jpg&name=small)
69 |
70 | Read the [CHANGELOG.md](./CHANGELOG.md) file.
71 |
72 | ## License
73 |
74 | ```
75 | Copyright (c) 2012-2023 Miquel Sabaté Solà
76 |
77 | Permission is hereby granted, free of charge, to any person obtaining
78 | a copy of this software and associated documentation files (the
79 | "Software"), to deal in the Software without restriction, including
80 | without limitation the rights to use, copy, modify, merge, publish,
81 | distribute, sublicense, and/or sell copies of the Software, and to
82 | permit persons to whom the Software is furnished to do so, subject to
83 | the following conditions:
84 |
85 | The above copyright notice and this permission notice shall be
86 | included in all copies or substantial portions of the Software.
87 |
88 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
89 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
90 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
91 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
92 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
93 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
94 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
95 | ```
96 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.0.0
4 |
5 | - Changed package's name to `useragent`. See [b486b63b54cb](https://github.com/mssola/useragent/commit/b486b63b54cbbc69cdf72a38842be4b9e6537e53).
6 | - Renamed default branch to `main`. See [7e944763aee7](https://github.com/mssola/useragent/commit/7e944763aee796efcf4eec461abfbe7ec5fadc18).
7 |
8 | ## 0.6.0
9 |
10 | - Added information on the model of mobile devices. See [746647ad73b5](https://github.com/mssola/useragent/commit/746647ad73b5ad8648175bbd07319c0a8ac559c6).
11 | - Added support for PhantomJS. See [6b5e6f6ebfa8](https://github.com/mssola/useragent/commit/6b5e6f6ebfa87464ccdb42bac5448cbf46ce1ba1).
12 |
13 | ## 0.5.4
14 |
15 | - Add detection of Coc Coc Browser. See [897eb45aec23](https://github.com/mssola/useragent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9).
16 | - Add detection of Headless Chrome. See [897eb45aec23](https://github.com/mssola/useragent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9).
17 | - Add detection of iOS WebViews. See [897eb45aec23](https://github.com/mssola/useragent/commit/897eb45aec2330e7566c48c9e54192aae84bd8e9).
18 |
19 | ## 0.5.3
20 |
21 | - Fix detection of Firefox on iPad. See [42e4a8f39125](https://github.com/mssola/useragent/commit/42e4a8f39125a6680fb5367a4602963f1351e069).
22 | - Fix detection of Linux ARM-based Android. See [3b0e113c8047](https://github.com/mssola/useragent/commit/3b0e113c804708c01de00c27aae07d2acfee40d8).
23 | - Add detection of Chromium Edge on Windows. See [ea81f1e9d61c](https://github.com/mssola/useragent/commit/ea81f1e9d61c094df4156690a8f4d5481b0d6c4a).
24 | - Add detection of OkHttp. See [6b33e248e796](https://github.com/mssola/useragent/commit/6b33e248e7969cf3e76128a34d33be88d4eb0dc8).
25 |
26 | ## 0.5.2
27 |
28 | - Detect Electron. See [commit](https://github.com/mssola/useragent/commit/1a36963d74c0efca7de80dc7518a0958c66b3c4f).
29 | - Add support for both http and https site urls. See [commit](https://github.com/mssola/useragent/commit/d78bf2c5886a0ab7e1cf90b68c808fe3e3ab6f8c).
30 | - Add more support for BingBot. See [commit](https://github.com/mssola/useragent/commit/c6402a7b8aefdc4acfbf1e7f3b43eac0b266e49e).
31 | - Add a test case for Firefox focus on iOS. See [commit](https://github.com/mssola/useragent/commit/a1e9c19d5a6887a17cef1d249118ccbd45cf4c0b).
32 | - Detect iMessage-Preview. See [commit](https://github.com/mssola/useragent/commit/e8f5e19ded9711ee1f4b43218b9d57d00ef5c26a).
33 |
34 | ## 0.5.1
35 |
36 | - add Firefox for iOS. See [commit](https://github.com/mssola/useragent/commit/00a868fa17e7).
37 | - Add go.mod. See [commit](https://github.com/mssola/useragent/commit/8c16c37f4e07).
38 | - Use CodeLingo to Address Further Issues. See [commit](https://github.com/mssola/useragent/commit/7e313fc62553).
39 | - Fix function comments based on best practices from Effective Go. See [commit](https://github.com/mssola/useragent/commit/95b0c164394f).
40 | - test: mobile Yandex Browser. See [commit](https://github.com/mssola/useragent/commit/1df9e04ee4f5).
41 | - Add Yandex browser. See [commit](https://github.com/mssola/useragent/commit/6eb76c60b5e8).
42 | - Updating license notice. See [commit](https://github.com/mssola/useragent/commit/8b3999083770).
43 | - Detect Chrome for iOS correctly. See [commit](https://github.com/mssola/useragent/commit/82f141dea4a8).
44 | - Facebook App Handling. See [commit](https://github.com/mssola/useragent/commit/5723c361ed97).
45 | - Add a new google bot user agent format. See [commit](https://github.com/mssola/useragent/commit/57c32981bd5f).
46 |
47 | ## 0.5.0
48 |
49 | ### Newly supported and improvements
50 |
51 | - Added support for Microsoft Edge. See [commit](https://github.com/mssola/useragent/commit/f659b9863849).
52 | - Precompile regular expressions. See [commit](https://github.com/mssola/useragent/commit/783ec61292ae).
53 | - Added support for Dalvik user agent parsing. See [commit](https://github.com/mssola/useragent/commit/78413629666f).
54 | - Improved bot support (also e25e612b37a4). See [commit](https://github.com/mssola/useragent/commit/0319fcf00bfd).
55 | - Add Chromium support and Ubuntu specific tests. See [commit](https://github.com/mssola/useragent/commit/6e7843e05771).
56 | - Add OSInfo function to user agent (also 7286ca6abc28). See [commit](https://github.com/mssola/useragent/commit/3335cae017e7).
57 | - Detect updated UA for Googlebot. See [commit](https://github.com/mssola/useragent/commit/6fe362d7cd64).
58 | - Adds the Adsense bot (mobile). See [commit](https://github.com/mssola/useragent/commit/1438bfba89d7).
59 |
60 | ### Fixes
61 |
62 | - Fixed bug when extracting windows 10. See [commit](https://github.com/mssola/useragent/commit/8d86c2cf88bf).
63 | - Fixed bug on mobile Firefox browsers running on Android OS versions that report their version number inline.. See [commit](https://github.com/mssola/useragent/commit/9d00ff9e4202).
64 |
65 | ### Other
66 |
67 | - Improved testing infrastructure. See [commit](https://github.com/mssola/useragent/commit/63395b193f8812526305bec75ea7117262a124aa).
68 |
69 | ## Older releases
70 |
71 | See the description on each release
72 | [here](https://github.com/mssola/useragent/releases).
73 |
--------------------------------------------------------------------------------
/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 useragent
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 |
--------------------------------------------------------------------------------
/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 useragent 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 useragent
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 |
--------------------------------------------------------------------------------
/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 useragent
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 |
--------------------------------------------------------------------------------
/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 useragent
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 |
--------------------------------------------------------------------------------
/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 useragent
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 |
--------------------------------------------------------------------------------