mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-09-17 08:05:12 +00:00
f49525ee9c
- Personal Center page with profile editing and last-login info - TOTP-based two-factor authentication with QR code setup - 30-day trusted-device cookie to skip 2FA on familiar browsers - Friendly browser/OS labels for the trusted device list Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
112 lines
3.1 KiB
Go
112 lines
3.1 KiB
Go
// Package useragent provides a tiny, dependency-free helper that turns a raw
|
|
// HTTP User-Agent header into a short human-readable label such as
|
|
// "Chrome 146 · Windows" or "Safari 17 · iOS 17". It only recognises the
|
|
// browsers / OSes that we actually display in the trusted-device list — for
|
|
// anything unknown it falls back to a truncated copy of the original UA.
|
|
package useragent
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Friendly turns a raw User-Agent header into a short human-readable label.
|
|
// Returns "Unknown" for an empty input.
|
|
func Friendly(ua string) string {
|
|
ua = strings.TrimSpace(ua)
|
|
if ua == "" {
|
|
return "Unknown"
|
|
}
|
|
|
|
browser := parseBrowser(ua)
|
|
os := parseOS(ua)
|
|
|
|
switch {
|
|
case browser != "" && os != "":
|
|
return browser + " · " + os
|
|
case browser != "":
|
|
return browser
|
|
case os != "":
|
|
return os
|
|
}
|
|
|
|
// Fallback: truncated raw UA so we never lose information entirely.
|
|
if len(ua) > 80 {
|
|
return ua[:80] + "…"
|
|
}
|
|
return ua
|
|
}
|
|
|
|
// --- browser detection ----------------------------------------------------
|
|
|
|
// Order matters: Edge / Opera / Brave embed "Chrome" in their UA, so they must
|
|
// be checked first. Likewise Chrome embeds "Safari", so Safari is last.
|
|
var browserPatterns = []struct {
|
|
name string
|
|
re *regexp.Regexp
|
|
}{
|
|
{"Edge", regexp.MustCompile(`Edg(?:e|A|iOS)?/(\d+)`)},
|
|
{"Opera", regexp.MustCompile(`(?:OPR|Opera)/(\d+)`)},
|
|
{"Vivaldi", regexp.MustCompile(`Vivaldi/(\d+)`)},
|
|
{"Firefox", regexp.MustCompile(`Firefox/(\d+)`)},
|
|
{"Chrome", regexp.MustCompile(`(?:Chrome|CriOS)/(\d+)`)},
|
|
{"Safari", regexp.MustCompile(`Version/(\d+)[\d.]*\s+.*Safari/`)},
|
|
}
|
|
|
|
func parseBrowser(ua string) string {
|
|
for _, p := range browserPatterns {
|
|
m := p.re.FindStringSubmatch(ua)
|
|
if len(m) >= 2 {
|
|
return p.name + " " + m[1]
|
|
}
|
|
}
|
|
if strings.Contains(ua, "Safari/") {
|
|
return "Safari"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// --- OS detection ---------------------------------------------------------
|
|
|
|
var (
|
|
reAndroid = regexp.MustCompile(`Android (\d+)`)
|
|
reIOS = regexp.MustCompile(`(?:iPhone OS|CPU OS) (\d+)`)
|
|
reMac = regexp.MustCompile(`Mac OS X (\d+)[._](\d+)`)
|
|
)
|
|
|
|
func parseOS(ua string) string {
|
|
switch {
|
|
case strings.Contains(ua, "Windows NT"):
|
|
// Windows NT 10.0 covers Windows 10 and 11; Microsoft never bumped the
|
|
// NT version, so we can't tell them apart from the UA alone.
|
|
return "Windows"
|
|
case strings.Contains(ua, "Android"):
|
|
if m := reAndroid.FindStringSubmatch(ua); len(m) >= 2 {
|
|
return "Android " + m[1]
|
|
}
|
|
return "Android"
|
|
case strings.Contains(ua, "iPhone") || strings.Contains(ua, "iPad"):
|
|
if m := reIOS.FindStringSubmatch(ua); len(m) >= 2 {
|
|
return "iOS " + m[1]
|
|
}
|
|
return "iOS"
|
|
case strings.Contains(ua, "Mac OS X"):
|
|
if m := reMac.FindStringSubmatch(ua); len(m) >= 3 {
|
|
major := m[1]
|
|
minor := m[2]
|
|
// Mac OS X 10.x is "macOS", 11+ is also "macOS" but with the major
|
|
// number directly.
|
|
if major == "10" {
|
|
return "macOS"
|
|
}
|
|
return "macOS " + major + "." + minor
|
|
}
|
|
return "macOS"
|
|
case strings.Contains(ua, "CrOS"):
|
|
return "ChromeOS"
|
|
case strings.Contains(ua, "Linux"):
|
|
return "Linux"
|
|
}
|
|
return ""
|
|
}
|