diff --git a/.gitignore b/.gitignore index 8078d86..c58450a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.idea/ +.vscode/ +.DS_Store old.go old_test.go file.txt diff --git a/is.go b/is.go index a3bf5b3..3fff5a4 100644 --- a/is.go +++ b/is.go @@ -30,6 +30,11 @@ func (ua UserAgent) IsChromeOS() bool { return ua.OS == ChromeOS || ua.OS == "CrOS" } +// IsBlackberryOS shorthand function to check if OS == BlackBerry +func (ua UserAgent) IsBlackberryOS() bool { + return ua.OS == BlackBerry +} + // IsOpera shorthand function to check if Name == Opera func (ua UserAgent) IsOpera() bool { return ua.Name == Opera @@ -65,6 +70,11 @@ func (ua UserAgent) IsEdge() bool { return ua.Name == Edge } +// IsBlackBerry shorthand function to check if Name == BlackBerry +func (ua UserAgent) IsBlackBerry() bool { + return ua.Name == BlackBerry +} + // IsGooglebot shorthand function to check if Name == Googlebot func (ua UserAgent) IsGooglebot() bool { return ua.Name == Googlebot @@ -80,6 +90,11 @@ func (ua UserAgent) IsFacebookbot() bool { return ua.Name == FacebookExternalHit } +// IsYandexbot shorthand function to check if Name == YandexBot +func (ua UserAgent) IsYandexbot() bool { + return ua.Name == YandexBot +} + // IsUnknown returns true if the package can't determine the user agent reliably. // Fields like Name, OS, etc. might still have values. func (ua UserAgent) IsUnknown() bool { diff --git a/ua.go b/ua.go index a292f9c..0771191 100644 --- a/ua.go +++ b/ua.go @@ -25,15 +25,18 @@ type UserAgent struct { // Constants for browsers and operating systems for easier comparison const ( - Windows = "Windows" - WindowsPhone = "Windows Phone" - Android = "Android" - MacOS = "macOS" - IOS = "iOS" - Linux = "Linux" - FreeBSD = "FreeBSD" - ChromeOS = "ChromeOS" - BlackBerry = "BlackBerry" + Windows = "Windows" + WindowsPhone = "Windows Phone" + WindowsNT = "Windows NT" + WindowsPhoneOS = "Windows Phone OS" + Android = "Android" + MacOS = "macOS" + IOS = "iOS" + Linux = "Linux" + FreeBSD = "FreeBSD" + ChromeOS = "ChromeOS" + BlackBerry = "BlackBerry" + CrOS = "CrOS" Opera = "Opera" OperaMini = "Opera Mini" @@ -45,6 +48,10 @@ const ( Safari = "Safari" Edge = "Edge" Vivaldi = "Vivaldi" + MobileSafari = "Mobile Safari" + NetFront = "NetFront" + Mozilla = "Mozilla" + Msie = "MSIE" GoogleAdsBot = "Google Ads Bot" Googlebot = "Googlebot" @@ -52,10 +59,16 @@ const ( FacebookExternalHit = "facebookexternalhit" Applebot = "Applebot" Bingbot = "Bingbot" + YandexBot = "YandexBot" + YandexAdNet = "YandexAdNet" FacebookApp = "Facebook App" InstagramApp = "Instagram App" TiktokApp = "TikTok App" + + Version = "Version" + Mobile = "Mobile" + Tablet = "Tablet" ) // Parse user agent string returning UserAgent struct @@ -75,15 +88,13 @@ func Parse(userAgent string) UserAgent { } } - //fmt.Printf("%+v\n", tokens) - // OS lookup switch { - case tokens.exists("Android"): + case tokens.exists(Android): ua.OS = Android var osIndex int osIndex, ua.OSVersion = tokens.getIndexValue(Android) - ua.Tablet = strings.Contains(strings.ToLower(ua.String), "tablet") + ua.Tablet = strings.Contains(strings.ToLower(ua.String), strings.ToLower(Tablet)) ua.Device = tokens.findAndroidDevice(osIndex) case tokens.exists("iPhone"): @@ -98,14 +109,14 @@ func Parse(userAgent string) UserAgent { ua.Device = "iPad" ua.Tablet = true - case tokens.exists("Windows NT"): + case tokens.exists(WindowsNT): ua.OS = Windows - ua.OSVersion = tokens.get("Windows NT") + ua.OSVersion = tokens.get(WindowsNT) ua.Desktop = true - case tokens.exists("Windows Phone OS"): + case tokens.exists(WindowsPhoneOS): ua.OS = WindowsPhone - ua.OSVersion = tokens.get("Windows Phone OS") + ua.OSVersion = tokens.get(WindowsPhoneOS) ua.Mobile = true case tokens.exists("Macintosh"): @@ -113,33 +124,33 @@ func Parse(userAgent string) UserAgent { ua.OSVersion = tokens.findMacOSVersion() ua.Desktop = true - case tokens.exists("Linux"): + case tokens.exists(Linux): ua.OS = Linux ua.OSVersion = tokens.get(Linux) ua.Desktop = true - case tokens.exists("FreeBSD"): + case tokens.exists(FreeBSD): ua.OS = FreeBSD ua.OSVersion = tokens.get(FreeBSD) ua.Desktop = true - case tokens.exists("CrOS"): + case tokens.exists(CrOS): ua.OS = ChromeOS - ua.OSVersion = tokens.get("CrOS") + ua.OSVersion = tokens.get(CrOS) ua.Desktop = true - case tokens.exists("BlackBerry"): + case tokens.exists(BlackBerry): ua.OS = BlackBerry - ua.OSVersion = tokens.get("BlackBerry") + ua.OSVersion = tokens.get(BlackBerry) ua.Mobile = true } switch { - case tokens.exists("Googlebot"): + case tokens.exists(Googlebot): ua.Name = Googlebot ua.Version = tokens.get(Googlebot) ua.Bot = true - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.existsAny("GoogleProber", "GoogleProducer"): if name := tokens.findBestMatch(false); name != "" { @@ -147,14 +158,14 @@ func Parse(userAgent string) UserAgent { } ua.Bot = true - case tokens.exists("Applebot"): + case tokens.exists(Applebot): ua.Name = Applebot ua.Version = tokens.get(Applebot) ua.Bot = true - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) ua.OS = "" - case tokens.get("Opera Mini") != "": + case tokens.get(OperaMini) != "": ua.Name = OperaMini ua.Version = tokens.get(OperaMini) ua.Mobile = true @@ -162,84 +173,91 @@ func Parse(userAgent string) UserAgent { case tokens.get("OPR") != "": ua.Name = Opera ua.Version = tokens.get("OPR") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.get("OPT") != "": ua.Name = OperaTouch ua.Version = tokens.get("OPT") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) // Opera on iOS case tokens.get("OPiOS") != "": ua.Name = Opera ua.Version = tokens.get("OPiOS") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) // Chrome on iOS case tokens.get("CriOS") != "": ua.Name = Chrome ua.Version = tokens.get("CriOS") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) // Firefox on iOS case tokens.get("FxiOS") != "": ua.Name = Firefox ua.Version = tokens.get("FxiOS") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) - case tokens.get("Firefox") != "": + case tokens.get(Firefox) != "": ua.Name = Firefox ua.Version = tokens.get(Firefox) - ua.Mobile = tokens.exists("Mobile") - ua.Tablet = tokens.exists("Tablet") + ua.Mobile = tokens.exists(Mobile) + ua.Tablet = tokens.exists(Tablet) - case tokens.get("Vivaldi") != "": + case tokens.get(Vivaldi) != "": ua.Name = Vivaldi ua.Version = tokens.get(Vivaldi) - case tokens.exists("MSIE"): + case tokens.exists(Msie): ua.Name = InternetExplorer - ua.Version = tokens.get("MSIE") + ua.Version = tokens.get(Msie) case tokens.get("EdgiOS") != "": ua.Name = Edge ua.Version = tokens.get("EdgiOS") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) - case tokens.get("Edge") != "": + case tokens.get(Edge) != "": ua.Name = Edge - ua.Version = tokens.get("Edge") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Version = tokens.get(Edge) + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.get("Edg") != "": ua.Name = Edge ua.Version = tokens.get("Edg") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.get("EdgA") != "": ua.Name = Edge ua.Version = tokens.get("EdgA") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.get("bingbot") != "": ua.Name = Bingbot ua.Version = tokens.get("bingbot") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) - case tokens.get("YandexBot") != "": - ua.Name = "YandexBot" - ua.Version = tokens.get("YandexBot") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + case tokens.get(YandexBot) != "": + ua.Name = YandexBot + ua.Version = tokens.get(YandexBot) + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) + ua.Bot = true + + case tokens.get(YandexAdNet) != "": + ua.Name = YandexAdNet + ua.Version = tokens.get(YandexAdNet) + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) + ua.Bot = true case tokens.get("SamsungBrowser") != "": ua.Name = "Samsung Browser" ua.Version = tokens.get("SamsungBrowser") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.get("HeadlessChrome") != "": ua.Name = HeadlessChrome ua.Version = tokens.get("HeadlessChrome") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) ua.Bot = true case tokens.existsAny("AdsBot-Google-Mobile", "Mediapartners-Google", "AdsBot-Google"): @@ -279,18 +297,18 @@ func Parse(userAgent string) UserAgent { case tokens.get("HuaweiBrowser") != "": ua.Name = "Huawei Browser" ua.Version = tokens.get("HuaweiBrowser") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) - case tokens.exists("BlackBerry"): - ua.Name = "BlackBerry" - ua.Version = tokens.get("Version") + case tokens.exists(BlackBerry): + ua.Name = BlackBerry + ua.Version = tokens.get(Version) - case tokens.exists("NetFront"): - ua.Name = "NetFront" - ua.Version = tokens.get("NetFront") + case tokens.exists(NetFront): + ua.Name = NetFront + ua.Version = tokens.get(NetFront) ua.Mobile = true - // if chrome and Safari defined, find any other token sent descr + // if Chrome and Safari defined, find any other token sent descr case tokens.exists(Chrome) && tokens.exists(Safari): name := tokens.findBestMatch(true) if name != "" { @@ -300,30 +318,30 @@ func Parse(userAgent string) UserAgent { } fallthrough - case tokens.exists("Chrome"): + case tokens.exists(Chrome): ua.Name = Chrome - ua.Version = tokens.get("Chrome") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Version = tokens.get(Chrome) + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) case tokens.exists("Brave Chrome"): ua.Name = Chrome ua.Version = tokens.get("Brave Chrome") - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) - case tokens.exists("Safari"): + case tokens.exists(Safari): ua.Name = Safari - v := tokens.get("Version") + v := tokens.get(Version) if v != "" { ua.Version = v } else { - ua.Version = tokens.get("Safari") + ua.Version = tokens.get(Safari) } - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) default: - if ua.OS == "Android" && tokens.get("Version") != "" { + if ua.IsAndroid() && tokens.get(Version) != "" { ua.Name = "Android browser" - ua.Version = tokens.get("Version") + ua.Version = tokens.get(Version) ua.Mobile = true } else { if name := tokens.findBestMatch(false); name != "" { @@ -335,7 +353,7 @@ func Parse(userAgent string) UserAgent { ua.Bot = strings.Contains(strings.ToLower(ua.Name), "bot") // If mobile flag has already been set, don't override it. if !ua.Mobile { - ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") + ua.Mobile = tokens.existsAny(Mobile, MobileSafari) } } } @@ -349,7 +367,7 @@ func Parse(userAgent string) UserAgent { ua.Mobile = false } - // if not already bot, check some popular bots and wether URL is set + // if not already bot, check some popular bots and whether URL is set if !ua.Bot { ua.Bot = ua.URL != "" } @@ -384,7 +402,7 @@ func parse(userAgent string) properties { if val.Len() == 0 { // only if value don't exists var ver string - s, ver = checkVer(s) // determin version string and split + s, ver = checkVer(s) // determine version string and split clients.add(s, ver) } else { clients.add(s, strings.TrimSpace(val.String())) @@ -403,7 +421,6 @@ func parse(userAgent string) properties { bua := []byte(userAgent) for i, c := range bua { - //fmt.Println(string(c), c) switch { case c == 41: // ) addToken() @@ -434,7 +451,7 @@ func parse(userAgent string) properties { // If the following character is not a space, change to a space. buff.WriteByte(' ') } - // Otherwise don't write as its probably a badly formatted key value separator. + // Otherwise don't write as it's probably a badly formatted key value separator. case slash && c == 32: addToken() @@ -469,10 +486,8 @@ func checkVer(s string) (name, v string) { return s, "" } - //v = s[i+1:] - switch s[:i] { - case "Linux", "Windows NT", "Windows Phone OS", "MSIE", "Android": + case Linux, WindowsNT, WindowsPhoneOS, Msie, Android: return s[:i], s[i+1:] case "CrOS x86_64", "CrOS aarch64", "CrOS armv7l": j := strings.LastIndex(s[:i], " ") @@ -480,20 +495,12 @@ func checkVer(s string) (name, v string) { default: return s, "" } - - // for _, c := range v { - // if (c >= 48 && c <= 57) || c == 46 { - // } else { - // return s, "" - // } - // } - // return s[:i], s[i+1:] } -// ignore retursn true if token should be ignored +// ignore returns true if token should be ignored func ignore(s string) bool { switch s { - case "KHTML, like Gecko", "U", "compatible", "Mozilla", "WOW64", "en", "en-us", "en-gb", "ru-ru", "Browser": + case "KHTML, like Gecko", "U", "compatible", Mozilla, "WOW64", "en", "en-us", "en-gb", "ru-ru", "Browser": return true default: return false @@ -607,9 +614,9 @@ func (p properties) findBestMatch(withVerOnly bool) string { for i := 0; i < n; i++ { for _, prop := range p.list { switch prop.Key { - case Chrome, Firefox, Safari, "Version", "Mobile", "Mobile Safari", "Mozilla", "AppleWebKit", "Windows NT", "Windows Phone OS", Android, "Macintosh", Linux, "GSA", "CrOS", "Tablet": + case Chrome, Firefox, Safari, Version, Mobile, MobileSafari, Mozilla, "AppleWebKit", WindowsNT, WindowsPhoneOS, Android, "Macintosh", Linux, "GSA", CrOS, Tablet: default: - // don' pick if starts with number + // don't pick if starts with number if len(prop.Key) != 0 && prop.Key[0] >= 48 && prop.Key[0] <= 57 { break } @@ -641,15 +648,15 @@ func (p *properties) findAndroidDevice(startIndex int) string { if len(p.list) > i+1 { dev := p.list[i+1].Key if len(dev) == 2 || (len(dev) == 5 && dev[2] == '-') { - // probably langage tag (en-us etc..), ignore and continue loop + // probably language tag (en-us etc..), ignore and continue loop continue } switch dev { - case Chrome, Firefox, Safari, "Opera Mini", "Presto", "Version", "Mobile", "Mobile Safari", "Mozilla", "AppleWebKit", "Windows NT", "Windows Phone OS", Android, "Macintosh", Linux, "CrOS": - // ignore this tokens, not device names + case Chrome, Firefox, Safari, OperaMini, "Presto", Version, Mobile, MobileSafari, Mozilla, "AppleWebKit", WindowsNT, WindowsPhoneOS, Android, "Macintosh", Linux, CrOS: + // ignore these tokens, not device names default: - if strings.Contains(strings.ToLower(dev), "tablet") { - p.list[i+1].Key = "Tablet" // leave Tablet tag for later table detection + if strings.Contains(strings.ToLower(dev), strings.ToLower(Tablet)) { + p.list[i+1].Key = Tablet // leave Tablet tag for later table detection } else { p.list = append(p.list[:i+1], p.list[i+2:]...) } diff --git a/ua_test.go b/ua_test.go index fd88446..3a7e50a 100644 --- a/ua_test.go +++ b/ua_test.go @@ -90,6 +90,7 @@ var testTable = [][]string{ {"Mozilla/5.0 (compatible; Yahoo Ad monitoring; https://help.yahoo.com/kb/yahoo-ad-monitoring-SLN24857.html) cnv.aws-prod---sieve.hlfs-rest_client/1681346790-0", "Yahoo Ad monitoring", "", "bot", ""}, {"GoogleProber", "GoogleProber", "", "bot", ""}, {"GoogleProducer; (+http://goo.gl/7y4SX)", "GoogleProducer", "", "bot", ""}, + {"Mozilla/5.0 (compatible; YandexAdNet/1.0; +http://yandex.com/bots) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.268", "YandexAdNet", "1.0", "bot", ""}, // Google ads bots {"Mozilla/5.0 (Linux; Android 4.0.0; Galaxy Nexus Build/IMM76B) AppleWebKit/537.36 (KHTML, like Gecko; Mediapartners-Google) Chrome/104.0.0.0 Mobile Safari/537.36", ua.GoogleAdsBot, "", "bot", ua.Android},