2
0

Merge tag 'tags/v1.3.5'

v.1.3.5
This commit is contained in:
2025-10-15 11:39:07 +03:00
5 changed files with 215 additions and 152 deletions
+4 -1
View File
@@ -1,7 +1,10 @@
.idea/
.vscode/
.DS_Store
old.go old.go
old_test.go old_test.go
file.txt file.txt
user_agents.txt user_agents.txt
ua2.go ua2.go
settings.json settings.json
benchmarks.txt benchmarks.txt
+15
View File
@@ -30,6 +30,11 @@ func (ua UserAgent) IsChromeOS() bool {
return ua.OS == ChromeOS || ua.OS == "CrOS" 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 // IsOpera shorthand function to check if Name == Opera
func (ua UserAgent) IsOpera() bool { func (ua UserAgent) IsOpera() bool {
return ua.Name == Opera return ua.Name == Opera
@@ -65,6 +70,11 @@ func (ua UserAgent) IsEdge() bool {
return ua.Name == Edge 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 // IsGooglebot shorthand function to check if Name == Googlebot
func (ua UserAgent) IsGooglebot() bool { func (ua UserAgent) IsGooglebot() bool {
return ua.Name == Googlebot return ua.Name == Googlebot
@@ -80,6 +90,11 @@ func (ua UserAgent) IsFacebookbot() bool {
return ua.Name == FacebookExternalHit 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. // IsUnknown returns true if the package can't determine the user agent reliably.
// Fields like Name, OS, etc. might still have values. // Fields like Name, OS, etc. might still have values.
func (ua UserAgent) IsUnknown() bool { func (ua UserAgent) IsUnknown() bool {
+166 -136
View File
@@ -25,15 +25,19 @@ type UserAgent struct {
// Constants for browsers and operating systems for easier comparison // Constants for browsers and operating systems for easier comparison
const ( const (
Windows = "Windows" Windows = "Windows"
WindowsPhone = "Windows Phone" WindowsPhone = "Windows Phone"
Android = "Android" WindowsNT = "Windows NT"
MacOS = "macOS" WindowsPhoneOS = "Windows Phone OS"
IOS = "iOS" Android = "Android"
Linux = "Linux" MacOS = "macOS"
FreeBSD = "FreeBSD" IOS = "iOS"
ChromeOS = "ChromeOS" Linux = "Linux"
BlackBerry = "BlackBerry" FreeBSD = "FreeBSD"
ChromeOS = "ChromeOS"
BlackBerry = "BlackBerry"
CrOS = "CrOS"
Harmony = "Harmony"
Opera = "Opera" Opera = "Opera"
OperaMini = "Opera Mini" OperaMini = "Opera Mini"
@@ -45,6 +49,11 @@ const (
Safari = "Safari" Safari = "Safari"
Edge = "Edge" Edge = "Edge"
Vivaldi = "Vivaldi" Vivaldi = "Vivaldi"
MobileSafari = "Mobile Safari"
NetFront = "NetFront"
Mozilla = "Mozilla"
Msie = "MSIE"
SamsungBrowser = "Samsung Browser"
GoogleAdsBot = "Google Ads Bot" GoogleAdsBot = "Google Ads Bot"
Googlebot = "Googlebot" Googlebot = "Googlebot"
@@ -52,10 +61,18 @@ const (
FacebookExternalHit = "facebookexternalhit" FacebookExternalHit = "facebookexternalhit"
Applebot = "Applebot" Applebot = "Applebot"
Bingbot = "Bingbot" Bingbot = "Bingbot"
YandexBot = "YandexBot"
YandexAdNet = "YandexAdNet"
FacebookApp = "Facebook App" FacebookApp = "Facebook App"
InstagramApp = "Instagram App" InstagramApp = "Instagram App"
TiktokApp = "TikTok App" TiktokApp = "TikTok App"
Version = "Version"
Mobile = "Mobile"
Tablet = "Tablet"
tablet = "tablet"
) )
// Parse user agent string returning UserAgent struct // Parse user agent string returning UserAgent struct
@@ -64,26 +81,16 @@ func Parse(userAgent string) UserAgent {
String: userAgent, String: userAgent,
} }
tokens := parse(userAgent) tokens := parse([]byte(userAgent))
ua.URL = tokens.url
// check is there URL
for i, token := range tokens.list {
if strings.HasPrefix(token.Key, "http://") || strings.HasPrefix(token.Key, "https://") {
ua.URL = token.Key
tokens.list = append(tokens.list[:i], tokens.list[i+1:]...)
break
}
}
//fmt.Printf("%+v\n", tokens)
// OS lookup // OS lookup
switch { switch {
case tokens.exists("Android"): case tokens.exists(Android):
ua.OS = Android ua.OS = Android
var osIndex int var osIndex int
osIndex, ua.OSVersion = tokens.getIndexValue(Android) osIndex, ua.OSVersion = tokens.getIndexValue(Android)
ua.Tablet = strings.Contains(strings.ToLower(ua.String), "tablet") ua.Tablet = strings.Contains(strings.ToLower(ua.String), tablet)
ua.Device = tokens.findAndroidDevice(osIndex) ua.Device = tokens.findAndroidDevice(osIndex)
case tokens.exists("iPhone"): case tokens.exists("iPhone"):
@@ -98,14 +105,14 @@ func Parse(userAgent string) UserAgent {
ua.Device = "iPad" ua.Device = "iPad"
ua.Tablet = true ua.Tablet = true
case tokens.exists("Windows NT"): case tokens.exists(WindowsNT):
ua.OS = Windows ua.OS = Windows
ua.OSVersion = tokens.get("Windows NT") ua.OSVersion = tokens.get(WindowsNT)
ua.Desktop = true ua.Desktop = true
case tokens.exists("Windows Phone OS"): case tokens.exists(WindowsPhoneOS):
ua.OS = WindowsPhone ua.OS = WindowsPhone
ua.OSVersion = tokens.get("Windows Phone OS") ua.OSVersion = tokens.get(WindowsPhoneOS)
ua.Mobile = true ua.Mobile = true
case tokens.exists("Macintosh"): case tokens.exists("Macintosh"):
@@ -113,33 +120,38 @@ func Parse(userAgent string) UserAgent {
ua.OSVersion = tokens.findMacOSVersion() ua.OSVersion = tokens.findMacOSVersion()
ua.Desktop = true ua.Desktop = true
case tokens.exists("Linux"): case tokens.exists(Linux):
ua.OS = Linux ua.OS = Linux
ua.OSVersion = tokens.get(Linux) ua.OSVersion = tokens.get(Linux)
ua.Desktop = true ua.Desktop = true
case tokens.exists("FreeBSD"): case tokens.exists(FreeBSD):
ua.OS = FreeBSD ua.OS = FreeBSD
ua.OSVersion = tokens.get(FreeBSD) ua.OSVersion = tokens.get(FreeBSD)
ua.Desktop = true ua.Desktop = true
case tokens.exists("CrOS"): case tokens.exists(CrOS):
ua.OS = ChromeOS ua.OS = ChromeOS
ua.OSVersion = tokens.get("CrOS") ua.OSVersion = tokens.get(CrOS)
ua.Desktop = true ua.Desktop = true
case tokens.exists("BlackBerry"): case tokens.exists(BlackBerry):
ua.OS = BlackBerry ua.OS = BlackBerry
ua.OSVersion = tokens.get("BlackBerry") ua.OSVersion = tokens.get(BlackBerry)
ua.Mobile = true
case tokens.exists("OpenHarmony"):
ua.OS = Harmony
ua.OSVersion = tokens.get("OpenHarmony")
ua.Mobile = true ua.Mobile = true
} }
switch { switch {
case tokens.exists("Googlebot"): case tokens.exists(Googlebot):
ua.Name = Googlebot ua.Name = Googlebot
ua.Version = tokens.get(Googlebot) ua.Version = tokens.get(Googlebot)
ua.Bot = true ua.Bot = true
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.existsAny("GoogleProber", "GoogleProducer"): case tokens.existsAny("GoogleProber", "GoogleProducer"):
if name := tokens.findBestMatch(false); name != "" { if name := tokens.findBestMatch(false); name != "" {
@@ -147,14 +159,19 @@ func Parse(userAgent string) UserAgent {
} }
ua.Bot = true ua.Bot = true
case tokens.exists("Applebot"): case tokens.exists("Bytespider"):
ua.Name = "Bytespider"
ua.Mobile = tokens.exists("Mobile Safari")
ua.Bot = true
case tokens.exists(Applebot):
ua.Name = Applebot ua.Name = Applebot
ua.Version = tokens.get(Applebot) ua.Version = tokens.get(Applebot)
ua.Bot = true ua.Bot = true
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
ua.OS = "" ua.OS = ""
case tokens.get("Opera Mini") != "": case tokens.get(OperaMini) != "":
ua.Name = OperaMini ua.Name = OperaMini
ua.Version = tokens.get(OperaMini) ua.Version = tokens.get(OperaMini)
ua.Mobile = true ua.Mobile = true
@@ -162,84 +179,92 @@ func Parse(userAgent string) UserAgent {
case tokens.get("OPR") != "": case tokens.get("OPR") != "":
ua.Name = Opera ua.Name = Opera
ua.Version = tokens.get("OPR") ua.Version = tokens.get("OPR")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.get("OPT") != "": case tokens.get("OPT") != "":
ua.Name = OperaTouch ua.Name = OperaTouch
ua.Version = tokens.get("OPT") ua.Version = tokens.get("OPT")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
// Opera on iOS // Opera on iOS
case tokens.get("OPiOS") != "": case tokens.get("OPiOS") != "":
ua.Name = Opera ua.Name = Opera
ua.Version = tokens.get("OPiOS") ua.Version = tokens.get("OPiOS")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
// Chrome on iOS // Chrome on iOS
case tokens.get("CriOS") != "": case tokens.get("CriOS") != "":
ua.Name = Chrome ua.Name = Chrome
ua.Version = tokens.get("CriOS") ua.Version = tokens.get("CriOS")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
// Firefox on iOS // Firefox on iOS
case tokens.get("FxiOS") != "": case tokens.get("FxiOS") != "":
ua.Name = Firefox ua.Name = Firefox
ua.Version = tokens.get("FxiOS") 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.Name = Firefox
ua.Version = tokens.get(Firefox) ua.Version = tokens.get(Firefox)
ua.Mobile = tokens.exists("Mobile") ua.Mobile = tokens.exists(Mobile)
ua.Tablet = tokens.exists("Tablet") ua.Tablet = tokens.exists(Tablet)
case tokens.get("Vivaldi") != "": case tokens.get(Vivaldi) != "":
ua.Name = Vivaldi ua.Name = Vivaldi
ua.Version = tokens.get(Vivaldi) ua.Version = tokens.get(Vivaldi)
case tokens.exists("MSIE"): case tokens.exists(Msie):
ua.Name = InternetExplorer ua.Name = InternetExplorer
ua.Version = tokens.get("MSIE") ua.Version = tokens.get(Msie)
case tokens.get("EdgiOS") != "": case tokens.get("EdgiOS") != "":
ua.Name = Edge ua.Name = Edge
ua.Version = tokens.get("EdgiOS") 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.Name = Edge
ua.Version = tokens.get("Edge") ua.Version = tokens.get(Edge)
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.get("Edg") != "": case tokens.get("Edg") != "":
ua.Name = Edge ua.Name = Edge
ua.Version = tokens.get("Edg") ua.Version = tokens.get("Edg")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.get("EdgA") != "": case tokens.get("EdgA") != "":
ua.Name = Edge ua.Name = Edge
ua.Version = tokens.get("EdgA") ua.Version = tokens.get("EdgA")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.get("bingbot") != "": case tokens.get("bingbot") != "":
ua.Name = Bingbot ua.Name = Bingbot
ua.Version = tokens.get("bingbot") ua.Version = tokens.get("bingbot")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.get("YandexBot") != "": case tokens.get(YandexBot) != "":
ua.Name = "YandexBot" ua.Name = YandexBot
ua.Version = tokens.get("YandexBot") ua.Version = tokens.get(YandexBot)
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") 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") != "": case tokens.get("SamsungBrowser") != "":
ua.Name = "Samsung Browser" ua.Name = SamsungBrowser
ua.Version = tokens.get("SamsungBrowser") ua.Version = tokens.get("SamsungBrowser")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
ua.OS = Android
case tokens.get("HeadlessChrome") != "": case tokens.get("HeadlessChrome") != "":
ua.Name = HeadlessChrome ua.Name = HeadlessChrome
ua.Version = tokens.get("HeadlessChrome") ua.Version = tokens.get("HeadlessChrome")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
ua.Bot = true ua.Bot = true
case tokens.existsAny("AdsBot-Google-Mobile", "Mediapartners-Google", "AdsBot-Google"): case tokens.existsAny("AdsBot-Google-Mobile", "Mediapartners-Google", "AdsBot-Google"):
@@ -279,18 +304,18 @@ func Parse(userAgent string) UserAgent {
case tokens.get("HuaweiBrowser") != "": case tokens.get("HuaweiBrowser") != "":
ua.Name = "Huawei Browser" ua.Name = "Huawei Browser"
ua.Version = tokens.get("HuaweiBrowser") ua.Version = tokens.get("HuaweiBrowser")
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.exists("BlackBerry"): case tokens.exists(BlackBerry):
ua.Name = "BlackBerry" ua.Name = BlackBerry
ua.Version = tokens.get("Version") ua.Version = tokens.get(Version)
case tokens.exists("NetFront"): case tokens.exists(NetFront):
ua.Name = "NetFront" ua.Name = NetFront
ua.Version = tokens.get("NetFront") ua.Version = tokens.get(NetFront)
ua.Mobile = true 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): case tokens.exists(Chrome) && tokens.exists(Safari):
name := tokens.findBestMatch(true) name := tokens.findBestMatch(true)
if name != "" { if name != "" {
@@ -300,30 +325,30 @@ func Parse(userAgent string) UserAgent {
} }
fallthrough fallthrough
case tokens.exists("Chrome"): case tokens.exists(Chrome):
ua.Name = Chrome ua.Name = Chrome
ua.Version = tokens.get("Chrome") ua.Version = tokens.get(Chrome)
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
case tokens.exists("Brave Chrome"): case tokens.exists("Brave Chrome"):
ua.Name = Chrome ua.Name = Chrome
ua.Version = tokens.get("Brave 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 ua.Name = Safari
v := tokens.get("Version") v := tokens.get(Version)
if v != "" { if v != "" {
ua.Version = v ua.Version = v
} else { } 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: default:
if ua.OS == "Android" && tokens.get("Version") != "" { if ua.IsAndroid() && tokens.get(Version) != "" {
ua.Name = "Android browser" ua.Name = "Android browser"
ua.Version = tokens.get("Version") ua.Version = tokens.get(Version)
ua.Mobile = true ua.Mobile = true
} else { } else {
if name := tokens.findBestMatch(false); name != "" { if name := tokens.findBestMatch(false); name != "" {
@@ -335,7 +360,7 @@ func Parse(userAgent string) UserAgent {
ua.Bot = strings.Contains(strings.ToLower(ua.Name), "bot") ua.Bot = strings.Contains(strings.ToLower(ua.Name), "bot")
// If mobile flag has already been set, don't override it. // If mobile flag has already been set, don't override it.
if !ua.Mobile { if !ua.Mobile {
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") ua.Mobile = tokens.existsAny(Mobile, MobileSafari)
} }
} }
} }
@@ -349,45 +374,53 @@ func Parse(userAgent string) UserAgent {
ua.Mobile = false 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 != ""
}
if !ua.Bot { if !ua.Bot {
switch ua.Name { switch ua.Name {
case Twitterbot, FacebookExternalHit: case Twitterbot, FacebookExternalHit, "facebookcatalog":
ua.Bot = true ua.Bot = true
default:
ua.Bot = ua.URL != ""
} }
} }
parseVersion(ua.Version, &ua.VersionNo) ua.VersionNo = parseVersion(ua.Version)
parseVersion(ua.OSVersion, &ua.OSVersionNo) ua.OSVersionNo = parseVersion(ua.OSVersion)
return ua return ua
} }
func parse(userAgent string) properties { // var buffPool = sync.Pool{New: func() interface{} {
// return bytes.NewBuffer(make([]byte, 0, 30))
// }}
func parse(userAgent []byte) properties {
clients := properties{ clients := properties{
list: make([]property, 0, 8), list: make([]property, 0, 8),
} }
slash := false slash := false
isURL := false isURL := false
var buff, val bytes.Buffer // buff := buffPool.Get().(*bytes.Buffer)
// val := buffPool.Get().(*bytes.Buffer)
// buff.Reset()
// val.Reset()
buff := bytes.NewBuffer(make([]byte, 0, 30))
val := bytes.NewBuffer(make([]byte, 0, 30))
addToken := func() { addToken := func() {
if buff.Len() != 0 { if buff.Len() != 0 {
s := strings.TrimSpace(buff.String()) s := string(bytes.TrimSpace(buff.Bytes()))
if !ignore(s) { if !ignore(s) {
if isURL { if isURL {
s = strings.TrimPrefix(s, "+") clients.url = strings.TrimPrefix(s, "+")
return
} }
if val.Len() == 0 {
if val.Len() == 0 { // only if value don't exists // if value don't exists, try to get version from the token
var ver string clients.list = append(clients.list, checkVer(s))
s, ver = checkVer(s) // determin version string and split
clients.add(s, ver)
} else { } else {
clients.add(s, strings.TrimSpace(val.String())) clients.list = append(clients.list, property{Key: s, Value: string(bytes.TrimSpace(val.Bytes()))})
} }
} }
} }
@@ -400,10 +433,7 @@ func parse(userAgent string) properties {
parOpen := false parOpen := false
braOpen := false braOpen := false
bua := []byte(userAgent) for i, c := range userAgent {
for i, c := range bua {
//fmt.Println(string(c), c)
switch { switch {
case c == 41: // ) case c == 41: // )
addToken() addToken()
@@ -430,11 +460,11 @@ func parse(userAgent string) properties {
if bytes.HasSuffix(buff.Bytes(), []byte("http")) || bytes.HasSuffix(buff.Bytes(), []byte("https")) { if bytes.HasSuffix(buff.Bytes(), []byte("http")) || bytes.HasSuffix(buff.Bytes(), []byte("https")) {
// If we are part of a URL just write the character. // If we are part of a URL just write the character.
buff.WriteByte(c) buff.WriteByte(c)
} else if i != len(bua)-1 && bua[i+1] != ' ' { } else if i != len(userAgent)-1 && userAgent[i+1] != ' ' {
// If the following character is not a space, change to a space. // If the following character is not a space, change to a space.
buff.WriteByte(' ') 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: case slash && c == 32:
addToken() addToken()
@@ -443,7 +473,7 @@ func parse(userAgent string) properties {
val.WriteByte(c) val.WriteByte(c)
case c == 47 && !isURL: // / 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:"))) { if i != len(userAgent)-1 && userAgent[i+1] == 47 && (bytes.HasSuffix(buff.Bytes(), []byte("http:")) || bytes.HasSuffix(buff.Bytes(), []byte("https:"))) {
buff.WriteByte(c) buff.WriteByte(c)
isURL = true isURL = true
} else { } else {
@@ -460,40 +490,32 @@ func parse(userAgent string) properties {
} }
addToken() addToken()
// buffPool.Put(buff)
// buffPool.Put(val)
return clients return clients
} }
func checkVer(s string) (name, v string) { func checkVer(s string) property {
i := strings.LastIndex(s, " ") i := strings.LastIndex(s, " ")
if i == -1 { if i == -1 {
return s, "" return property{Key: s, Value: ""}
} }
//v = s[i+1:]
switch s[:i] { switch s[:i] {
case "Linux", "Windows NT", "Windows Phone OS", "MSIE", "Android": case Linux, WindowsNT, WindowsPhoneOS, Msie, Android, "OpenHarmony":
return s[:i], s[i+1:] return property{Key: s[:i], Value: s[i+1:]}
case "CrOS x86_64", "CrOS aarch64", "CrOS armv7l": case "CrOS x86_64", "CrOS aarch64", "CrOS armv7l":
j := strings.LastIndex(s[:i], " ") j := strings.LastIndex(s[:i], " ")
return s[:j], s[j+1 : i] return property{Key: s[:j], Value: s[j+1 : i]}
default: default:
return s, "" return property{Key: s, Value: ""}
} }
// 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 { func ignore(s string) bool {
switch s { 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 return true
default: default:
return false return false
@@ -506,10 +528,7 @@ type property struct {
} }
type properties struct { type properties struct {
list []property list []property
} url string
func (p *properties) add(key, value string) {
p.list = append(p.list, property{Key: key, Value: value})
} }
func (p properties) get(key string) string { func (p properties) get(key string) string {
@@ -559,6 +578,17 @@ func (p properties) existsAny(keys ...string) bool {
return false return false
} }
func (p properties) getAny(keys ...string) (key, value string) {
for _, k := range keys {
for _, prop := range p.list {
if prop.Key == k {
return prop.Key, prop.Value
}
}
}
return "", ""
}
func (p properties) findMacOSVersion() string { func (p properties) findMacOSVersion() string {
for _, token := range p.list { for _, token := range p.list {
if strings.Contains(token.Key, "OS") { if strings.Contains(token.Key, "OS") {
@@ -607,9 +637,9 @@ func (p properties) findBestMatch(withVerOnly bool) string {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
for _, prop := range p.list { for _, prop := range p.list {
switch prop.Key { 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, "OpenHarmony":
default: 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 { if len(prop.Key) != 0 && prop.Key[0] >= 48 && prop.Key[0] <= 57 {
break break
} }
@@ -641,15 +671,15 @@ func (p *properties) findAndroidDevice(startIndex int) string {
if len(p.list) > i+1 { if len(p.list) > i+1 {
dev := p.list[i+1].Key dev := p.list[i+1].Key
if len(dev) == 2 || (len(dev) == 5 && dev[2] == '-') { 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 continue
} }
switch dev { switch dev {
case Chrome, Firefox, Safari, "Opera Mini", "Presto", "Version", "Mobile", "Mobile Safari", "Mozilla", "AppleWebKit", "Windows NT", "Windows Phone OS", Android, "Macintosh", Linux, "CrOS": case Chrome, Firefox, Safari, OperaMini, "Presto", Version, Mobile, MobileSafari, Mozilla, "AppleWebKit", WindowsNT, WindowsPhoneOS, Android, "Macintosh", Linux, CrOS:
// ignore this tokens, not device names // ignore these tokens, not device names
default: default:
if strings.Contains(strings.ToLower(dev), "tablet") { if strings.Contains(strings.ToLower(dev), tablet) {
p.list[i+1].Key = "Tablet" // leave Tablet tag for later table detection p.list[i+1].Key = Tablet // leave Tablet tag for later table detection
} else { } else {
p.list = append(p.list[:i+1], p.list[i+2:]...) p.list = append(p.list[:i+1], p.list[i+2:]...)
} }
+18 -5
View File
@@ -54,6 +54,7 @@ var testTable = [][]string{
{"Mozilla/5.0 (Linux; U; Android 11; ru-ru; Redmi Note 10S Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.116 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.13.2-gn", "Miui Browser", "12.13.2-gn", "mobile", ua.Android, "Redmi Note 10S"}, {"Mozilla/5.0 (Linux; U; Android 11; ru-ru; Redmi Note 10S Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.116 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.13.2-gn", "Miui Browser", "12.13.2-gn", "mobile", ua.Android, "Redmi Note 10S"},
{"Mozilla/5.0 (Linux; Android 10; MED-LX9N; HMSCore 6.6.0.311) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 HuaweiBrowser/12.1.0.303 Mobile Safari/537.36", "Huawei Browser", "12.1.0.303", "mobile", "Android"}, {"Mozilla/5.0 (Linux; Android 10; MED-LX9N; HMSCore 6.6.0.311) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.105 HuaweiBrowser/12.1.0.303 Mobile Safari/537.36", "Huawei Browser", "12.1.0.303", "mobile", "Android"},
{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Safari/537.36", ua.SamsungBrowser, "22.0", "mobile", ua.Android},
// useragent, name, version, mobile, os // useragent, name, version, mobile, os
{"Mozilla/5.0 (Linux; Android 9; ONEPLUS A6003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36", ua.Chrome, "71.0.3578.99", "mobile", ua.Android}, {"Mozilla/5.0 (Linux; Android 9; ONEPLUS A6003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36", ua.Chrome, "71.0.3578.99", "mobile", ua.Android},
@@ -90,6 +91,8 @@ 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", ""}, {"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", ""}, {"GoogleProber", "GoogleProber", "", "bot", ""},
{"GoogleProducer; (+http://goo.gl/7y4SX)", "GoogleProducer", "", "bot", ""}, {"GoogleProducer; (+http://goo.gl/7y4SX)", "GoogleProducer", "", "bot", ""},
{"Mozilla/5.0 (compatible; Bytespider; spider-feedback@bytedance.com) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.0.0 Safari/537.36", "Bytespider", "", "bot", ""},
{"Mozilla/5.0 (Linux; Android 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Bytespider; spider-feedback@bytedance.com)", "Bytespider", "", "bot", ua.Android},
// Google ads bots // 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}, {"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},
@@ -153,6 +156,8 @@ var testTable = [][]string{
//{`${jndi:ldap://log4shell-generic-8ZnJfq2XFL3GWyaLyOpT${lower:ten}.w.nessus.org/nessus}`, "", "mobile", ua.Android}, //{`${jndi:ldap://log4shell-generic-8ZnJfq2XFL3GWyaLyOpT${lower:ten}.w.nessus.org/nessus}`, "", "mobile", ua.Android},
// //
{"Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile", "ArkWeb", "4.1.6.1", "mobile", ua.Harmony, ""},
// //
// ${jndi:ldap://log4shell-generic-8ZnJfq2XFL3GWyaLyOpT${lower:ten}.w.nessus.org/nessus} // ${jndi:ldap://log4shell-generic-8ZnJfq2XFL3GWyaLyOpT${lower:ten}.w.nessus.org/nessus}
@@ -195,6 +200,10 @@ func TestParse(t *testing.T) {
t.Error("\n", ua.String, "should be tablet") t.Error("\n", ua.String, "should be tablet")
fmt.Printf("%+v", ua) fmt.Printf("%+v", ua)
} }
if test[3] == "bot" && !ua.Bot {
t.Error("\n", ua.String, "should be bot")
fmt.Printf("%+v", ua)
}
} }
if len(test) > 4 && test[4] != ua.OS { if len(test) > 4 && test[4] != ua.OS {
@@ -210,11 +219,6 @@ func TestParse(t *testing.T) {
} }
} }
func TestSingle(t *testing.T) {
agent := ua.Parse("SonyEricssonK310iv/R4DA Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.1.13.0")
fmt.Printf("\n%+v\n", agent)
}
var testUA ua.UserAgent var testUA ua.UserAgent
func BenchmarkUserAgent(b *testing.B) { func BenchmarkUserAgent(b *testing.B) {
@@ -225,6 +229,12 @@ func BenchmarkUserAgent(b *testing.B) {
} }
} }
func TestSingle(t *testing.T) {
//agent := ua.Parse("SonyEricssonK310iv/R4DA Browser/NetFront/3.3 Profile/MIDP-2.0 Configuration/CLDC-1.1 UP.Link/6.3.1.13.0")
agent := ua.Parse("Mozilla/5.0 (Linux; Android 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Bytespider; spider-feedback@bytedance.com)")
fmt.Printf("\n%+v\n", agent)
}
func ExampleParse() { func ExampleParse() {
userAgents := []string{ userAgents := []string{
// Mac // Mac
@@ -258,6 +268,9 @@ func ExampleParse() {
"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; 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", "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",
// Harmony
"Mozilla/5.0 (Phone; OpenHarmony 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile",
} }
for _, s := range userAgents { for _, s := range userAgents {
+12 -10
View File
@@ -12,24 +12,26 @@ type VersionNo struct {
Patch int Patch int
} }
func parseVersion(ver string, verno *VersionNo) { // parseVersion parse version string into Major.Minor.Patch struct
func parseVersion(ver string) (verno VersionNo) {
var err error var err error
parts := strings.Split(ver, ".") parts := strings.Split(ver, ".")
if len(parts) > 0 { if len(parts) > 0 {
if verno.Major, err = strconv.Atoi(parts[0]); err != nil { if verno.Major, err = strconv.Atoi(parts[0]); err != nil {
return return verno
} }
} if len(parts) > 1 {
if len(parts) > 1 { if verno.Minor, err = strconv.Atoi(parts[1]); err != nil {
if verno.Minor, err = strconv.Atoi(parts[1]); err != nil { return verno
return }
} if len(parts) > 2 {
if len(parts) > 2 { if verno.Patch, err = strconv.Atoi(parts[2]); err != nil {
if verno.Patch, err = strconv.Atoi(parts[2]); err != nil { return verno
return }
} }
} }
} }
return verno
} }
// VersionNoShort return version string in format <Major>.<Minor> // VersionNoShort return version string in format <Major>.<Minor>