435 lines
9.2 KiB
Go
435 lines
9.2 KiB
Go
|
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"
|
||
|
OperaTouch = "Opera Touch"
|
||
|
Chrome = "Chrome"
|
||
|
Firefox = "Firefox"
|
||
|
InternetExplorer = "Internet Explorer"
|
||
|
Safari = "Safari"
|
||
|
Edge = "Edge"
|
||
|
Vivaldi = "Vivaldi"
|
||
|
|
||
|
Googlebot = "Googlebot"
|
||
|
Twitterbot = "Twitterbot"
|
||
|
FacebookExternalHit = "facebookexternalhit"
|
||
|
Applebot = "Applebot"
|
||
|
)
|
||
|
|
||
|
// 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.exists("Applebot"):
|
||
|
ua.Name = Applebot
|
||
|
ua.Version = tokens[Applebot]
|
||
|
ua.Bot = true
|
||
|
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari")
|
||
|
ua.OS = ""
|
||
|
|
||
|
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")
|
||
|
|
||
|
case tokens["OPT"] != "":
|
||
|
ua.Name = OperaTouch
|
||
|
ua.Version = tokens["OPT"]
|
||
|
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["EdgiOS"] != "":
|
||
|
ua.Name = Edge
|
||
|
ua.Version = tokens["EdgiOS"]
|
||
|
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari")
|
||
|
|
||
|
case tokens["Edge"] != "":
|
||
|
ua.Name = Edge
|
||
|
ua.Version = tokens["Edge"]
|
||
|
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari")
|
||
|
|
||
|
case tokens["Edg"] != "":
|
||
|
ua.Name = Edge
|
||
|
ua.Version = tokens["Edg"]
|
||
|
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari")
|
||
|
|
||
|
case tokens["EdgA"] != "":
|
||
|
ua.Name = Edge
|
||
|
ua.Version = tokens["EdgA"]
|
||
|
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, "GSA":
|
||
|
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 ""
|
||
|
}
|