useragent.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. package nginx_log
  2. import (
  3. "regexp"
  4. "strings"
  5. )
  6. // SimpleUserAgentParser implements UserAgentParser with regex-based parsing
  7. type SimpleUserAgentParser struct {
  8. browserPatterns map[string]*regexp.Regexp
  9. osPatterns map[string]*regexp.Regexp
  10. devicePatterns map[string]*regexp.Regexp
  11. }
  12. // NewSimpleUserAgentParser creates a new simple user agent parser
  13. func NewSimpleUserAgentParser() *SimpleUserAgentParser {
  14. return &SimpleUserAgentParser{
  15. browserPatterns: initBrowserPatterns(),
  16. osPatterns: initOSPatterns(),
  17. devicePatterns: initDevicePatterns(),
  18. }
  19. }
  20. // Parse parses a user agent string and returns structured information
  21. func (p *SimpleUserAgentParser) Parse(userAgent string) UserAgentInfo {
  22. if userAgent == "" || userAgent == "-" {
  23. return UserAgentInfo{}
  24. }
  25. info := UserAgentInfo{}
  26. // Parse browser information
  27. info.Browser, info.BrowserVer = p.parseBrowser(userAgent)
  28. // Parse OS information
  29. info.OS, info.OSVersion = p.parseOS(userAgent)
  30. // Parse device type
  31. info.DeviceType = p.parseDeviceType(userAgent)
  32. return info
  33. }
  34. // parseBrowser extracts browser name and version
  35. func (p *SimpleUserAgentParser) parseBrowser(userAgent string) (browser, version string) {
  36. // Try each browser pattern
  37. for name, pattern := range p.browserPatterns {
  38. if matches := pattern.FindStringSubmatch(userAgent); len(matches) >= 2 {
  39. browser = name
  40. if len(matches) >= 3 {
  41. // Combine major and minor version: matches[1].matches[2]
  42. version = matches[1] + "." + matches[2]
  43. } else if len(matches) >= 2 {
  44. // Only major version available
  45. version = matches[1]
  46. }
  47. return
  48. }
  49. }
  50. return "Unknown", ""
  51. }
  52. // parseOS extracts operating system name and version
  53. func (p *SimpleUserAgentParser) parseOS(userAgent string) (os, version string) {
  54. // Check specific OS patterns in order of specificity
  55. osOrder := []string{
  56. "iOS", // iOS must come before macOS to avoid false matches
  57. "Android", // Android must come before Linux since Android contains "Linux"
  58. "Windows", "macOS",
  59. "Ubuntu", "CentOS", "Debian", "Red Hat", "Fedora", "SUSE", "Linux",
  60. }
  61. for _, name := range osOrder {
  62. if pattern, exists := p.osPatterns[name]; exists {
  63. if matches := pattern.FindStringSubmatch(userAgent); len(matches) >= 1 {
  64. os = name
  65. if len(matches) >= 3 && matches[2] != "" {
  66. // Two capture groups: major.minor version
  67. version = matches[1] + "." + matches[2]
  68. } else if len(matches) >= 2 {
  69. // One capture group: version
  70. if name == "Android" {
  71. // For Android, add .0 if no minor version
  72. version = matches[1] + ".0"
  73. } else {
  74. version = matches[1]
  75. }
  76. }
  77. return
  78. }
  79. }
  80. }
  81. return "Unknown", ""
  82. }
  83. // parseDeviceType determines the device type
  84. func (p *SimpleUserAgentParser) parseDeviceType(userAgent string) string {
  85. userAgent = strings.ToLower(userAgent)
  86. // Check for specific device types in order of priority
  87. // Bot detection first
  88. if p.devicePatterns["Bot"].MatchString(userAgent) {
  89. return "Bot"
  90. }
  91. // Apple devices (specific models first)
  92. if p.devicePatterns["iPhone"].MatchString(userAgent) {
  93. return "iPhone"
  94. }
  95. if p.devicePatterns["iPad"].MatchString(userAgent) {
  96. return "iPad"
  97. }
  98. if p.devicePatterns["iPod"].MatchString(userAgent) {
  99. return "iPod"
  100. }
  101. // Mobile detection (Android Mobile and other mobile devices)
  102. if p.devicePatterns["Mobile"].MatchString(userAgent) ||
  103. (strings.Contains(userAgent, "android") && strings.Contains(userAgent, "mobile")) {
  104. return "Mobile"
  105. }
  106. // Tablet detection (Android tablets and other tablets)
  107. if p.devicePatterns["Tablet"].MatchString(userAgent) ||
  108. (strings.Contains(userAgent, "android") && !strings.Contains(userAgent, "mobile")) {
  109. return "Tablet"
  110. }
  111. // Check other device types
  112. for deviceType, pattern := range p.devicePatterns {
  113. if deviceType != "Bot" && deviceType != "Mobile" && deviceType != "Tablet" && deviceType != "Desktop" &&
  114. deviceType != "iPhone" && deviceType != "iPad" && deviceType != "iPod" {
  115. if pattern.MatchString(userAgent) {
  116. return deviceType
  117. }
  118. }
  119. }
  120. return "Desktop"
  121. }
  122. // initBrowserPatterns initializes browser detection patterns
  123. func initBrowserPatterns() map[string]*regexp.Regexp {
  124. return map[string]*regexp.Regexp{
  125. "Chrome": regexp.MustCompile(`(?i)chrome[\/\s](\d+)\.(\d+)`),
  126. "Firefox": regexp.MustCompile(`(?i)firefox[\/\s](\d+)\.(\d+)`),
  127. "Safari": regexp.MustCompile(`(?i)version[\/\s](\d+)\.(\d+).*safari`),
  128. "Edge": regexp.MustCompile(`(?i)edg[\/\s](\d+)\.(\d+)`),
  129. "Internet Explorer": regexp.MustCompile(`(?i)msie[\/\s](\d+)\.(\d+)`),
  130. "Opera": regexp.MustCompile(`(?i)opera[\/\s](\d+)\.(\d+)`),
  131. "Brave": regexp.MustCompile(`(?i)brave[\/\s](\d+)\.(\d+)`),
  132. "Vivaldi": regexp.MustCompile(`(?i)vivaldi[\/\s](\d+)\.(\d+)`),
  133. "UC Browser": regexp.MustCompile(`(?i)ucbrowser[\/\s](\d+)\.(\d+)`),
  134. "Samsung Browser": regexp.MustCompile(`(?i)samsungbrowser[\/\s](\d+)\.(\d+)`),
  135. "Yandex": regexp.MustCompile(`(?i)yabrowser[\/\s](\d+)\.(\d+)`),
  136. "QQ Browser": regexp.MustCompile(`(?i)qqbrowser[\/\s](\d+)\.(\d+)`),
  137. "Sogou Explorer": regexp.MustCompile(`(?i)se[\/\s](\d+)\.(\d+)`),
  138. "360 Browser": regexp.MustCompile(`(?i)360se[\/\s](\d+)\.(\d+)`),
  139. "Maxthon": regexp.MustCompile(`(?i)maxthon[\/\s](\d+)\.(\d+)`),
  140. "Baidu Browser": regexp.MustCompile(`(?i)baidubrowser[\/\s](\d+)\.(\d+)`),
  141. "WeChat": regexp.MustCompile(`(?i)micromessenger[\/\s](\d+)\.(\d+)`),
  142. "QQ": regexp.MustCompile(`(?i)qq[\/\s](\d+)\.(\d+)`),
  143. "DingTalk": regexp.MustCompile(`(?i)dingtalk[\/\s](\d+)\.(\d+)`),
  144. "Alipay": regexp.MustCompile(`(?i)alipayclient[\/\s](\d+)\.(\d+)`),
  145. }
  146. }
  147. // initOSPatterns initializes operating system detection patterns
  148. func initOSPatterns() map[string]*regexp.Regexp {
  149. return map[string]*regexp.Regexp{
  150. "Windows": regexp.MustCompile(`(?i)windows`),
  151. "macOS": regexp.MustCompile(`(?i)mac os x|macos|darwin`),
  152. "iOS": regexp.MustCompile(`(?i)(?:iphone|ipad|ipod).*?(?:iphone )?os (\d+)[_\.](\d+)`),
  153. "Android": regexp.MustCompile(`(?i)android (\d+)(?:\.(\d+))?`),
  154. "Ubuntu": regexp.MustCompile(`(?i)ubuntu[\/\s](\d+)\.(\d+)`),
  155. "CentOS": regexp.MustCompile(`(?i)centos[\/\s](\d+)`),
  156. "Debian": regexp.MustCompile(`(?i)debian`),
  157. "Red Hat": regexp.MustCompile(`(?i)red hat`),
  158. "Fedora": regexp.MustCompile(`(?i)fedora[\/\s](\d+)`),
  159. "SUSE": regexp.MustCompile(`(?i)suse`),
  160. "Linux": regexp.MustCompile(`(?i)linux`),
  161. "FreeBSD": regexp.MustCompile(`(?i)freebsd`),
  162. "OpenBSD": regexp.MustCompile(`(?i)openbsd`),
  163. "NetBSD": regexp.MustCompile(`(?i)netbsd`),
  164. "Unix": regexp.MustCompile(`(?i)unix`),
  165. "Chrome OS": regexp.MustCompile(`(?i)cros`),
  166. }
  167. }
  168. // initDevicePatterns initializes device type detection patterns
  169. func initDevicePatterns() map[string]*regexp.Regexp {
  170. return map[string]*regexp.Regexp{
  171. "iPhone": regexp.MustCompile(`(?i)iphone`),
  172. "iPad": regexp.MustCompile(`(?i)ipad`),
  173. "iPod": regexp.MustCompile(`(?i)ipod`),
  174. "Mobile": regexp.MustCompile(`(?i)mobile|phone|blackberry|windows phone|palm|symbian`),
  175. "Tablet": regexp.MustCompile(`(?i)tablet|kindle|silk`),
  176. "TV": regexp.MustCompile(`(?i)smart-?tv|tv|roku|chromecast|apple.?tv|xbox|playstation|nintendo`),
  177. "Bot": regexp.MustCompile(`(?i)bot|crawl|spider|scraper|parser|checker|monitoring|curl|wget|python|java|go-http|okhttp`),
  178. "Smart Speaker": regexp.MustCompile(`(?i)alexa|google.?home|echo`),
  179. "Game Console": regexp.MustCompile(`(?i)xbox|playstation|nintendo|psp|vita`),
  180. "Wearable": regexp.MustCompile(`(?i)watch|wearable`),
  181. "Desktop": regexp.MustCompile(`.*`), // Default fallback
  182. }
  183. }
  184. // MockUserAgentParser is a mock implementation for testing
  185. type MockUserAgentParser struct {
  186. responses map[string]UserAgentInfo
  187. }
  188. // NewMockUserAgentParser creates a new mock user agent parser
  189. func NewMockUserAgentParser() *MockUserAgentParser {
  190. return &MockUserAgentParser{
  191. responses: map[string]UserAgentInfo{
  192. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36": {
  193. Browser: "Chrome",
  194. BrowserVer: "91.0",
  195. OS: "Windows 10",
  196. OSVersion: "10.0",
  197. DeviceType: "Desktop",
  198. },
  199. "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)": {
  200. Browser: "Safari",
  201. BrowserVer: "14.0",
  202. OS: "iOS",
  203. OSVersion: "14.6",
  204. DeviceType: "Mobile",
  205. },
  206. },
  207. }
  208. }
  209. // Parse returns mock user agent information for testing
  210. func (m *MockUserAgentParser) Parse(userAgent string) UserAgentInfo {
  211. if info, exists := m.responses[userAgent]; exists {
  212. return info
  213. }
  214. return UserAgentInfo{
  215. Browser: "Unknown",
  216. OS: "Unknown",
  217. DeviceType: "Desktop",
  218. }
  219. }
  220. // AddResponse adds a mock response for testing
  221. func (m *MockUserAgentParser) AddResponse(userAgent string, info UserAgentInfo) {
  222. m.responses[userAgent] = info
  223. }