-
Notifications
You must be signed in to change notification settings - Fork 13
/
user_agent.go
190 lines (168 loc) · 4.88 KB
/
user_agent.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Copyright (C) 2012-2023 Miquel Sabaté Solà <mikisabate@gmail.com>
// This file is licensed under the MIT license.
// See the LICENSE file.
// Package useragent implements an HTTP User Agent string parser. It defines
// the type UserAgent that contains all the information from the parsed string.
// It also implements the Parse function and getters for all the relevant
// information that has been extracted from a parsed User Agent string.
package useragent
import "strings"
// A section contains the name of the product, its version and
// an optional comment.
type section struct {
name string
version string
comment []string
}
// The UserAgent struct contains all the info that can be extracted
// from the User-Agent string.
type UserAgent struct {
ua string
mozilla string
platform string
os string
localization string
model string
browser Browser
bot bool
mobile bool
undecided bool
}
// Read from the given string until the given delimiter or the
// end of the string have been reached.
//
// The first argument is the user agent string being parsed. The second
// argument is a reference pointing to the current index of the user agent
// string. The delimiter argument specifies which character is the delimiter
// and the cat argument determines whether nested '(' should be ignored or not.
//
// Returns an array of bytes containing what has been read.
func readUntil(ua string, index *int, delimiter byte, cat bool) []byte {
var buffer []byte
i := *index
catalan := 0
for ; i < len(ua); i = i + 1 {
if ua[i] == delimiter {
if catalan == 0 {
*index = i + 1
return buffer
}
catalan--
} else if cat && ua[i] == '(' {
catalan++
}
buffer = append(buffer, ua[i])
}
*index = i + 1
return buffer
}
// Parse the given product, that is, just a name or a string
// formatted as Name/Version.
//
// It returns two strings. The first string is the name of the product and the
// second string contains the version of the product.
func parseProduct(product []byte) (string, string) {
prod := strings.SplitN(string(product), "/", 2)
if len(prod) == 2 {
return prod[0], prod[1]
}
return string(product), ""
}
// Parse a section. A section is typically formatted as follows
// "Name/Version (comment)". Both, the comment and the version are optional.
//
// The first argument is the user agent string being parsed. The second
// argument is a reference pointing to the current index of the user agent
// string.
//
// Returns a section containing the information that we could extract
// from the last parsed section.
func parseSection(ua string, index *int) (s section) {
var buffer []byte
// Check for empty products
if *index < len(ua) && ua[*index] != '(' && ua[*index] != '[' {
buffer = readUntil(ua, index, ' ', false)
s.name, s.version = parseProduct(buffer)
}
if *index < len(ua) && ua[*index] == '(' {
*index++
buffer = readUntil(ua, index, ')', true)
s.comment = strings.Split(string(buffer), "; ")
*index++
}
// Discards any trailing data within square brackets
if *index < len(ua) && ua[*index] == '[' {
*index++
_ = readUntil(ua, index, ']', true)
*index++
}
return s
}
// Initialize the parser.
func (p *UserAgent) initialize() {
p.ua = ""
p.mozilla = ""
p.platform = ""
p.os = ""
p.localization = ""
p.model = ""
p.browser.Engine = ""
p.browser.EngineVersion = ""
p.browser.Name = ""
p.browser.Version = ""
p.bot = false
p.mobile = false
p.undecided = false
}
// New parses the given User-Agent string and get the resulting UserAgent
// object.
//
// Returns an UserAgent object that has been initialized after parsing
// the given User-Agent string.
func New(ua string) *UserAgent {
o := &UserAgent{}
o.Parse(ua)
return o
}
// Parse the given User-Agent string. After calling this function, the
// receiver will be setted up with all the information that we've extracted.
func (p *UserAgent) Parse(ua string) {
var sections []section
p.initialize()
p.ua = ua
for index, limit := 0, len(ua); index < limit; {
s := parseSection(ua, &index)
if !p.mobile && s.name == "Mobile" {
p.mobile = true
}
sections = append(sections, s)
}
if len(sections) > 0 {
if sections[0].name == "Mozilla" {
p.mozilla = sections[0].version
}
p.detectBrowser(sections)
p.detectOS(sections[0])
p.detectModel(sections[0])
if p.undecided {
p.checkBot(sections)
}
}
}
// Mozilla returns the mozilla version (it's how the User Agent string begins:
// "Mozilla/5.0 ...", unless we're dealing with Opera, of course).
func (p *UserAgent) Mozilla() string {
return p.mozilla
}
// Bot returns true if it's a bot, false otherwise.
func (p *UserAgent) Bot() bool {
return p.bot
}
// Mobile returns true if it's a mobile device, false otherwise.
func (p *UserAgent) Mobile() bool {
return p.mobile
}
// UA returns the original given user agent.
func (p *UserAgent) UA() string {
return p.ua
}