From e1aff7cb1ec2c2dbc3114c09b565a9bb50beaff9 Mon Sep 17 00:00:00 2001 From: mileusna Date: Wed, 16 Aug 2017 18:01:24 +0200 Subject: [PATCH] First commit --- LICENSE | 21 +++ README.md | 74 ++++++++++ is.go | 76 ++++++++++ ua.go | 407 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ua_test.go | 163 +++++++++++++++++++++ 5 files changed, 741 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 is.go create mode 100644 ua.go create mode 100644 ua_test.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2da0046 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Miloš Mileusnić + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dded69 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Go/Golang package for parsing user agent strings [![GoDoc](https://godoc.org/github.com/mileusna/useragent?status.svg)](https://godoc.org/github.com/mileusna/useragent) + +Parse browser's and bot's user agents strings and determin user agent name, version, operating system name etc. + +## Status + +Still need some work on detecting Andorid device name. + +## Installation +``` +go get github.com/mileusna/useragent +``` + +## Example + +```go +package main + +import ( + "fmt" + "strings" + + "github.com/mileusna/useragent" +) + +func main() { + userAgents := []string{ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", + "Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36", + "Mozilla/5.0 (Android 4.3; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0", + "Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36 OPR/42.9.2246.119956", + "Opera/9.80 (Android; Opera Mini/28.0.2254/66.318; U; en) Presto/2.12.423 Version/12.16", + } + + for _, s := range userAgents { + ua := ua.Parse(s) + fmt.Println() + fmt.Println(ua.String) + fmt.Println(strings.Repeat("=", len(ua.String))) + fmt.Println("Name:", ua.Name, "v", ua.Version) + fmt.Println("OS:", ua.OS, "v", ua.OSVersion) + fmt.Println("Device:", ua.Device) + if ua.Mobile { + fmt.Println("(Mobile)") + } + if ua.Tablet { + fmt.Println("(Tablet)") + } + if ua.Desktop { + fmt.Println("(Desktop)") + } + if ua.Bot { + fmt.Println("(Bot)") + } + if ua.URL != "" { + fmt.Println(ua.URL) + } + } +} + + +``` + +## Notice + ++ Opera and Opera Mini are two browsers, since they operate on very differenta way. ++ If Googlebot (or any other bot) is detected and it is using its mobile crawler, both `bot` and `mobile` flags will be set to `true`. + + + diff --git a/is.go b/is.go new file mode 100644 index 0000000..d312b24 --- /dev/null +++ b/is.go @@ -0,0 +1,76 @@ +package ua + +// IsWindows shorthand function to check if OS == Windows +func (ua UserAgent) IsWindows() bool { + return ua.OS == Windows +} + +// IsAndroid shorthand function to check if OS == Android +func (ua UserAgent) IsAndroid() bool { + return ua.OS == Android +} + +// IsMacOS shorthand function to check if OS == MacOS +func (ua UserAgent) IsMacOS() bool { + return ua.OS == MacOS +} + +// IsIOS shorthand function to check if OS == IOS +func (ua UserAgent) IsIOS() bool { + return ua.OS == IOS +} + +// IsLinux shorthand function to check if OS == Linux +func (ua UserAgent) IsLinux() bool { + return ua.OS == Linux +} + +// IsOpera shorthand function to check if Name == Opera +func (ua UserAgent) IsOpera() bool { + return ua.Name == Opera +} + +// IsOperaMini shorthand function to check if Name == Opera Mini +func (ua UserAgent) IsOperaMini() bool { + return ua.Name == OperaMini +} + +// IsChrome shorthand function to check if Name == Chrome +func (ua UserAgent) IsChrome() bool { + return ua.Name == Chrome +} + +// IsFirefox shorthand function to check if Name == Firefox +func (ua UserAgent) IsFirefox() bool { + return ua.Name == Firefox +} + +// IsInternetExplorer shorthand function to check if Name == Internet Explorer +func (ua UserAgent) IsInternetExplorer() bool { + return ua.Name == InternetExplorer +} + +// IsSafari shorthand function to check if Name == Safari +func (ua UserAgent) IsSafari() bool { + return ua.Name == Safari +} + +// IsEdge shorthand function to check if Name == Edge +func (ua UserAgent) IsEdge() bool { + return ua.Name == Edge +} + +// IsGooglebot shorthand function to check if Name == Googlebot +func (ua UserAgent) IsGooglebot() bool { + return ua.Name == Googlebot +} + +// IsTwitterbot shorthand function to check if Name == Twitterbot +func (ua UserAgent) IsTwitterbot() bool { + return ua.Name == Twitterbot +} + +// IsFacebookbot shorthand function to check if Name == FacebookExternalHit +func (ua UserAgent) IsFacebookbot() bool { + return ua.Name == FacebookExternalHit +} diff --git a/ua.go b/ua.go new file mode 100644 index 0000000..436b982 --- /dev/null +++ b/ua.go @@ -0,0 +1,407 @@ +package ua + +import ( + "bytes" + "regexp" + "strings" +) + +// UserAgent struct containg all determined datra from parsed user-agent string +type UserAgent struct { + Name string + Version string + OS string + OSVersion string + Device string + + Mobile bool + Tablet bool + Desktop bool + Bot bool + URL string + + String string +} + +var ignore = map[string]struct{}{ + "KHTML, like Gecko": struct{}{}, + "U": struct{}{}, + "compatible": struct{}{}, + "Mozilla": struct{}{}, + "WOW64": struct{}{}, +} + +// Constants for browsers and operating systems for easier comparation +const ( + Windows = "Windows" + WindowsPhone = "Windows Phone" + Android = "Android" + MacOS = "macOS" + IOS = "iOS" + Linux = "Linux" + + Opera = "Opera" + OperaMini = "Opera Mini" + Chrome = "Chrome" + Firefox = "Firefox" + InternetExplorer = "Internet Explorer" + Safari = "Safari" + Edge = "Edge" + Vivaldi = "Vivaldi" + + Googlebot = "Googlebot" + Twitterbot = "Twitterbot" + FacebookExternalHit = "facebookexternalhit" +) + +// Parse user agent string returning UserAgent struct +func Parse(userAgent string) UserAgent { + ua := UserAgent{ + String: userAgent, + } + + tokens := parse(userAgent) + + // check is there URL + for k := range tokens { + if strings.HasPrefix(k, "http://") || strings.HasPrefix(k, "https://") { + ua.URL = k + delete(tokens, k) + break + } + } + + // OS lookup + switch { + case tokens.exists("Android"): + ua.OS = Android + ua.OSVersion = tokens[Android] + for s := range tokens { + if strings.HasSuffix(s, "Build") { + ua.Device = strings.TrimSpace(s[:len(s)-5]) + ua.Tablet = strings.Contains(strings.ToLower(ua.Device), "tablet") + } + } + + case tokens.exists("iPhone"): + ua.OS = IOS + ua.OSVersion = tokens.findMacOSVersion() + ua.Device = "iPhone" + ua.Mobile = true + + case tokens.exists("iPad"): + ua.OS = IOS + ua.OSVersion = tokens.findMacOSVersion() + ua.Device = "iPad" + ua.Tablet = true + + case tokens.exists("Windows NT"): + ua.OS = Windows + ua.OSVersion = tokens["Windows NT"] + ua.Desktop = true + + case tokens.exists("Windows Phone OS"): + ua.OS = WindowsPhone + ua.OSVersion = tokens["Windows Phone OS"] + ua.Mobile = true + + case tokens.exists("Macintosh"): + ua.OS = MacOS + ua.OSVersion = tokens.findMacOSVersion() + ua.Desktop = true + + case tokens.exists("Linux"): + ua.OS = Linux + ua.OSVersion = tokens[Linux] + ua.Desktop = true + + } + + // for s, val := range sys { + // fmt.Println(s, "--", val) + // } + + switch { + + case tokens.exists("Googlebot"): + ua.Name = Googlebot + ua.Version = tokens[Googlebot] + ua.Bot = true + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + case tokens["Opera Mini"] != "": + ua.Name = OperaMini + ua.Version = tokens[OperaMini] + ua.Mobile = true + + case tokens["OPR"] != "": + ua.Name = Opera + ua.Version = tokens["OPR"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + // Opera on iOS + case tokens["OPiOS"] != "": + ua.Name = Opera + ua.Version = tokens["OPiOS"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + // Chrome on iOS + case tokens["CriOS"] != "": + ua.Name = Chrome + ua.Version = tokens["CriOS"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + // Firefox on iOS + case tokens["FxiOS"] != "": + ua.Name = Firefox + ua.Version = tokens["FxiOS"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + case tokens["Firefox"] != "": + ua.Name = Firefox + ua.Version = tokens[Firefox] + _, ua.Mobile = tokens["Mobile"] + _, ua.Tablet = tokens["Tablet"] + + case tokens["Vivaldi"] != "": + ua.Name = Vivaldi + ua.Version = tokens[Vivaldi] + + case tokens.exists("MSIE"): + ua.Name = InternetExplorer + ua.Version = tokens["MSIE"] + + case tokens["Edge"] != "": + ua.Name = Edge + ua.Version = tokens["Edge"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + case tokens["bingbot"] != "": + ua.Name = "Bingbot" + ua.Version = tokens["bingbot"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + case tokens["SamsungBrowser"] != "": + ua.Name = "Samsung Browser" + ua.Version = tokens["SamsungBrowser"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + // if chrome and Safari defined, find any other tokensent descr + case tokens.exists(Chrome) && tokens.exists(Safari): + name := tokens.findBestMatch(true) + if name != "" { + ua.Name = name + ua.Version = tokens[name] + break + } + fallthrough + + case tokens.exists("Chrome"): + ua.Name = Chrome + ua.Version = tokens["Chrome"] + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + case tokens.exists("Safari"): + ua.Name = Safari + if v, ok := tokens["Version"]; ok { + ua.Version = v + } else { + ua.Version = tokens["Safari"] + } + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + + default: + if ua.OS == "Android" && tokens["Version"] != "" { + ua.Name = "Android browser" + ua.Version = tokens["Version"] + ua.Mobile = true + } else { + if name := tokens.findBestMatch(false); name != "" { + ua.Name = name + ua.Version = tokens[name] + } else { + ua.Name = ua.String + } + ua.Bot = strings.Contains(strings.ToLower(ua.Name), "bot") + ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + } + } + + // if tabler, switch mobile to off + if ua.Tablet { + ua.Mobile = false + } + + // if not already bot, check some popular bots and weather URL is set + if !ua.Bot { + ua.Bot = ua.URL != "" + } + + if !ua.Bot { + switch ua.Name { + case Twitterbot, FacebookExternalHit: + ua.Bot = true + } + } + + return ua +} + +func parse(userAgent string) (clients properties) { + clients = make(map[string]string, 0) + slash := false + isURL := false + var buff, val bytes.Buffer + addToken := func() { + if buff.Len() != 0 { + s := strings.TrimSpace(buff.String()) + if _, ign := ignore[s]; !ign { + if isURL { + s = strings.TrimPrefix(s, "+") + } + + if val.Len() == 0 { // only if value don't exists + var ver string + s, ver = checkVer(s) // determin version string and split + clients[s] = ver + } else { + clients[s] = strings.TrimSpace(val.String()) + } + } + } + buff.Reset() + val.Reset() + slash = false + isURL = false + } + + parOpen := false + + bua := []byte(userAgent) + for i, c := range bua { + + //fmt.Println(string(c), c) + switch { + case c == 41: // ) + addToken() + parOpen = false + + case parOpen && c == 59: // ; + addToken() + + case c == 40: // ( + addToken() + parOpen = true + + case slash && c == 32: + addToken() + + case slash: + val.WriteByte(c) + + case c == 47 && !isURL: // / + if i != len(bua)-1 && bua[i+1] == 47 && (bytes.HasSuffix(buff.Bytes(), []byte("http:")) || bytes.HasSuffix(buff.Bytes(), []byte("https:"))) { + buff.WriteByte(c) + isURL = true + } else { + slash = true + } + + default: + buff.WriteByte(c) + } + } + addToken() + + return clients +} + +func checkVer(s string) (name, v string) { + i := strings.LastIndex(s, " ") + if i == -1 { + return s, "" + } + + //v = s[i+1:] + + switch s[:i] { + case "Linux", "Windows NT", "Windows Phone OS", "MSIE", "Android": + return s[:i], s[i+1:] + default: + return s, "" + } + + // for _, c := range v { + // if (c >= 48 && c <= 57) || c == 46 { + // } else { + // return s, "" + // } + // } + + // return s[:i], s[i+1:] + +} + +type properties map[string]string + +func (p properties) exists(key string) bool { + _, ok := p[key] + return ok +} + +func (p properties) existsAny(keys ...string) bool { + for _, k := range keys { + if _, ok := p[k]; ok { + return true + } + } + return false +} + +func (p properties) findMacOSVersion() string { + for k, v := range p { + if strings.Contains(k, "OS") { + if ver := findVersion(v); ver != "" { + return ver + } else if ver = findVersion(k); ver != "" { + return ver + } + } + } + return "" +} + +// findBestMatch from the rest of the bunch +// in first cycle only return key vith version value +// if withVerValue is false, do another cycle and return any token +func (p properties) findBestMatch(withVerOnly bool) string { + n := 2 + if withVerOnly { + n = 1 + } + for i := 0; i < n; i++ { + for k, v := range p { + switch k { + case Chrome, Firefox, Safari, "Version", "Mobile", "Mobile Safari", "Mozilla", "AppleWebKit", "Windows NT", "Windows Phone OS", Android, "Macintosh", Linux: + default: + if i == 0 { + if v != "" { // in first check, only return keys with value + return k + } + } else { + return k + } + } + } + } + return "" +} + +var rxMacOSVer = regexp.MustCompile("[_\\d\\.]+") + +func findVersion(s string) string { + if ver := rxMacOSVer.FindString(s); ver != "" { + return strings.Replace(ver, "_", ".", -1) + } + return "" +} diff --git a/ua_test.go b/ua_test.go new file mode 100644 index 0000000..67e5810 --- /dev/null +++ b/ua_test.go @@ -0,0 +1,163 @@ +package ua_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/mileusna/useragent" +) + +func TestParse(t *testing.T) { + var testTable = [][]string{ + // Mac + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8", ua.Safari, "10.1.2", "desktop", "macOS"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", ua.Chrome, "60.0.3112.90", "desktop", "macOS"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", ua.Firefox, "54.0", "desktop", "macOS"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36 OPR/46.0.2597.57", ua.Opera, "46.0.2597.57", "desktop", "macOS"}, + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.91 Safari/537.36 Vivaldi/1.92.917.39", "Vivaldi", "1.92.917.39", "desktop", "macOS"}, + + // Windows + {"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", ua.Chrome, "59.0.3071.115", "desktop", "Windows"}, + {"Mozilla/4.0 (compatible; MSIE 8.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.2; GWX:RED)", ua.InternetExplorer, "8.0", "desktop", "Windows"}, + {"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) NS8/0.9.6", ua.InternetExplorer, "6.0", "desktop", "Windows"}, + + // iPhone + {"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", ua.Safari, "10.0", "mobile", "iOS"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) CriOS/60.0.3112.89 Mobile/14F89 Safari/602.1", ua.Chrome, "60.0.3112.89", "mobile", "iOS"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) OPiOS/14.0.0.104835 Mobile/13E233 Safari/9537.53", ua.Opera, "14.0.0.104835", "mobile", "iOS"}, + {"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", ua.Firefox, "8.1.1b4948", "mobile", "iOS"}, + + // iPad + {"Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", ua.Safari, "10.0", "tablet", "iOS"}, + {"Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/58.0.3029.113 Mobile/14F89 Safari/602.1", ua.Chrome, "58.0.3029.113", "tablet", "iOS"}, + {"Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", ua.Firefox, "8.1.1b4948", "tablet", "iOS"}, + + // Andorid + {"Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36", ua.Chrome, "59.0.3071.125", "mobile", "Android"}, + {"Mozilla/5.0 (Android 4.3; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0", ua.Firefox, "54.0", "mobile", "Android"}, + {"Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36 OPR/42.9.2246.119956", ua.Opera, "42.9.2246.119956", "mobile", ua.Android}, + {"Opera/9.80 (Android; Opera Mini/28.0.2254/66.318; U; en) Presto/2.12.423 Version/12.16", ua.OperaMini, "28.0.2254/66.318", "mobile", "Android"}, + {"Mozilla/5.0 (Linux; U; Android 4.3; en-us; GT-I9300 Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", "Android browser", "4.0", "mobile", "Android"}, + + {"Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-A310F/A310FXXU2BQB1 Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/5.4 Chrome/51.0.2704.106 Mobile Safari/537.36", "Samsung Browser", "5.4", "mobile", "Android"}, + + // Windows phone + {"Mozilla/4.0 (compatible; MSIE 7.0; Windows Phone OS 7.0; Trident/3.1; IEMobile/7.0; NOKIA; Lumia 630)", ua.InternetExplorer, "7.0", "mobile", ua.WindowsPhone}, + + // Bots + {"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)", ua.Googlebot, "2.1", "mobile", "Android"}, + {"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", ua.Googlebot, "2.1", "bot", ""}, + {"Twitterbot/1.0", ua.Twitterbot, "1.0", "bot", ""}, + {"facebookexternalhit/1.1", ua.FacebookExternalHit, "1.1", "bot", ""}, + + // other + {"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 Google (+https://developers.google.com/+/web/snippet/)", ua.Chrome, "56.0.2924.87", "bot", ua.Linux}, // Google+ fetch + + // tools + {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.6.0 Chrome/45.0.2454.101 Safari/537.36", "QtWebEngine", "5.6.0", "", "macOS"}, + {"Go-http-client/1.1", "Go-http-client", "1.1", "", ""}, + {"Wget/1.12 (linux-gnu)", "Wget", "1.12", "", ""}, + {"Wget/1.17.1 (darwin15.2.0)", "Wget", "1.17.1", "", ""}, + + // unstandard stuff + {"BUbiNG (+http://law.di.unimi.it/BUbiNG.html)", "BUbiNG", "", "", ""}, + + //GooglePlus "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 Google (+https://developers.google.com/+/web/snippet/)" + //Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/600.2.5 (KHTML, like Gecko) Version/8.0.2 Safari/600.2.5 (Applebot/0.1; +http://www.apple.com/go/applebot) + //Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.6.0 Chrome/45.0.2454.101 Safari/537.36 + } + + for _, test := range testTable { + ua := ua.Parse(test[0]) + if ua.Name != test[1] { + t.Error("\n", test[0], "\nName should be", test[1], "not", ua.Name) + } + if ua.Version != test[2] { + t.Error("\nVersion should be", test[2], "not", ua.Version) + } + + if len(test) > 3 { + if test[3] == "desktop" && ua.Mobile { + t.Error("\n", ua.String, "should be desktop type not mobile") + } + + if test[3] == "mobile" && !ua.Mobile { + t.Error("\n", ua.String, "should be mobile") + } + if test[3] == "tablet" && !ua.Tablet { + t.Error("\n", ua.String, "should be tablet") + } + } + + if len(test) > 4 && test[4] != ua.OS { + t.Error("\n", test[0], "OS should", test[4], "not", ua.OS) + } + //fmt.Println(ua.OS, ua.OSVersion, ua.Device) + + } + +} + +func ExampleParse() { + userAgents := []string{ + // Mac + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36 OPR/46.0.2597.57", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.91 Safari/537.36 Vivaldi/1.92.917.39", + + // Windows + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", + "Mozilla/4.0 (compatible; MSIE 8.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.2; GWX:RED)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) NS8/0.9.6", + + // iPhone + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) CriOS/60.0.3112.89 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) OPiOS/14.0.0.104835 Mobile/13E233 Safari/9537.53", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", + + // iPad + "Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/58.0.3029.113 Mobile/14F89 Safari/602.1", + "Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", + + // Andorid + "Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36", + "Mozilla/5.0 (Android 4.3; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0", + "Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36 OPR/42.9.2246.119956", + "Opera/9.80 (Android; Opera Mini/28.0.2254/66.318; U; en) Presto/2.12.423 Version/12.16", + "Mozilla/5.0 (Linux; U; Android 4.3; en-us; GT-I9300 Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30", + + "Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-A310F/A310FXXU2BQB1 Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/5.4 Chrome/51.0.2704.106 Mobile Safari/537.36", + } + + for _, s := range userAgents { + ua := ua.Parse(s) + fmt.Println() + fmt.Println(ua.String) + fmt.Println(strings.Repeat("=", len(ua.String))) + fmt.Println("Name:", ua.Name, "v", ua.Version) + fmt.Println("OS:", ua.OS, "v", ua.OSVersion) + fmt.Println("Device:", ua.Device) + if ua.Mobile { + fmt.Println("(Mobile)") + } + if ua.Tablet { + fmt.Println("(Tablet)") + } + if ua.Desktop { + fmt.Println("(Desktop)") + } + if ua.Bot { + fmt.Println("(Bot)") + } + if ua.URL != "" { + fmt.Println(ua.URL) + } + + } + +}