OAuth2 / JWT / OpenID Connect for mocking auth... which isn't that different from doing it for real, actually. https://mock.pocketid.app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

434 lines
9.2 KiB

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 ""
}