ua.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. package fp
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. ua "github.com/avct/uasurfer"
  7. )
  8. // User agent signature and fingerprint strings have the format
  9. // <br-name>:<br-vers>:<os-plat>:<os-name>:<os-vers>:<dev-type>:<quirk>
  10. //
  11. // For fingerprints the parts have the formats
  12. // <br-name>, <os-plat>, <os-name>, <dev-type>:
  13. // <int>
  14. // <browser-vers>, <os-vers>:
  15. // <major>[.<minor>[.<patch>]]
  16. // <quirk>:
  17. // <str-list>
  18. // where <int> is a decimal-encoded int using constants defined in uasurfer, and
  19. // <str-list> is a comma-separated list of strings.
  20. //
  21. // and for signatures the parts have the formats
  22. // <br-name>, <os-plat>, <os-name>, <dev-type>:
  23. // <int>
  24. // <browser-vers>, <os-vers>:
  25. // [<major>[.<minor>[.<patch>]]][-[<major>[.<minor>[.<patch>]]]]
  26. // <quirk>:
  27. // same as in request.go
  28. // where items in enclosed in square brackets are optional,
  29. const anyVersion int = -1
  30. const (
  31. uaFieldCount int = 7
  32. uaFieldSep string = ":"
  33. uaVersionFieldSep string = "."
  34. uaVersionRangeSep string = "-"
  35. )
  36. // UAFingerprint is a fingerprint for a user agent
  37. type UAFingerprint struct {
  38. BrowserName int
  39. BrowserVersion UAVersion
  40. OSPlatform int
  41. OSName int
  42. OSVersion UAVersion
  43. DeviceType int
  44. Quirk StringList
  45. }
  46. // NewUAFingerprint returns a new user agent fingerprint parsed from a string
  47. func NewUAFingerprint(s string) (UAFingerprint, error) {
  48. var a UAFingerprint
  49. err := a.Parse(s)
  50. return a, err
  51. }
  52. // Parse a user agent fingerprint from a string and return an error on failure
  53. func (a *UAFingerprint) Parse(s string) error {
  54. var err error
  55. fields := strings.Split(s, uaFieldSep)
  56. if len(fields) != uaFieldCount {
  57. return fmt.Errorf("bad ua field count '%s': exp %d, got %d", s, uaFieldCount, len(fields))
  58. }
  59. fieldIdx := 0
  60. if a.BrowserName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  61. return err
  62. }
  63. fieldIdx++
  64. if err = a.BrowserVersion.Parse(fields[fieldIdx]); err != nil {
  65. return err
  66. }
  67. fieldIdx++
  68. if a.OSPlatform, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  69. return err
  70. }
  71. fieldIdx++
  72. if a.OSName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  73. return err
  74. }
  75. fieldIdx++
  76. if err = a.OSVersion.Parse(fields[fieldIdx]); err != nil {
  77. return err
  78. }
  79. fieldIdx++
  80. if a.DeviceType, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  81. return err
  82. }
  83. fieldIdx++
  84. if err = a.Quirk.Parse(fields[fieldIdx]); err != nil {
  85. return err
  86. }
  87. return nil
  88. }
  89. // String returns a string representation of a fingerprint
  90. func (a UAFingerprint) String() string {
  91. return strings.Join([]string{strconv.Itoa(a.BrowserName), a.BrowserVersion.String(), strconv.Itoa(a.OSPlatform), strconv.Itoa(a.OSName), a.OSVersion.String(), strconv.Itoa(a.DeviceType), a.Quirk.String()}, uaFieldSep)
  92. }
  93. // UAVersion represents a user agent browser or OS version.
  94. type UAVersion ua.Version
  95. // Parse a user agent version from a string and return an error on failure.
  96. func (a *UAVersion) Parse(s string) error {
  97. var i int
  98. var err error
  99. a.Major = anyVersion
  100. a.Minor = anyVersion
  101. a.Patch = anyVersion
  102. if len(s) == 0 {
  103. return nil
  104. }
  105. fields := strings.Split(s, uaVersionFieldSep)
  106. switch len(fields) {
  107. case 3:
  108. if len(fields[2]) > 0 {
  109. i, err = strconv.Atoi(fields[2])
  110. if err != nil {
  111. return err
  112. }
  113. a.Patch = i
  114. }
  115. fallthrough
  116. case 2:
  117. if len(fields[1]) > 0 {
  118. i, err := strconv.Atoi(fields[1])
  119. if err != nil {
  120. return err
  121. }
  122. a.Minor = i
  123. }
  124. fallthrough
  125. case 1:
  126. if len(fields[0]) > 0 {
  127. i, err := strconv.Atoi(fields[0])
  128. if err != nil {
  129. return err
  130. }
  131. a.Major = i
  132. }
  133. return nil
  134. default:
  135. return fmt.Errorf("invalid user agent version format: '%s'", s)
  136. }
  137. }
  138. func (a UAVersion) String() string {
  139. var fields []string
  140. if a.Major != anyVersion {
  141. fields = append(fields, strconv.Itoa(a.Major))
  142. if a.Minor != anyVersion {
  143. fields = append(fields, strconv.Itoa(a.Minor))
  144. if a.Patch != anyVersion {
  145. fields = append(fields, strconv.Itoa(a.Patch))
  146. }
  147. }
  148. }
  149. return strings.Join(fields, uaVersionFieldSep)
  150. }
  151. // A UAVersionSignature matches a range of possible user agent versions
  152. type UAVersionSignature struct {
  153. Min UAVersion
  154. Max UAVersion
  155. }
  156. func (a UAVersionSignature) String() string {
  157. if a.Min == a.Max {
  158. return a.Min.String()
  159. }
  160. return strings.Join([]string{a.Min.String(), a.Max.String()}, uaVersionRangeSep)
  161. }
  162. // Parse a user agent version signature from a string and return an error on failure.
  163. func (a *UAVersionSignature) Parse(s string) error {
  164. fields := strings.SplitN(s, uaVersionRangeSep, 2)
  165. if err := a.Min.Parse(fields[0]); err != nil {
  166. return err
  167. }
  168. switch len(fields) {
  169. case 2:
  170. if err := a.Max.Parse(fields[1]); err != nil {
  171. return err
  172. }
  173. case 1:
  174. a.Max = a.Min
  175. }
  176. return nil
  177. }
  178. // minMatch returns true if fingerprint matches the min value
  179. func (a UAVersion) minMatch(fingerprint UAVersion) bool {
  180. if a.Major == anyVersion || a.Major <= fingerprint.Major {
  181. return true
  182. }
  183. if a.Major > fingerprint.Major {
  184. return false
  185. }
  186. if a.Minor == anyVersion || a.Minor <= fingerprint.Minor {
  187. return true
  188. }
  189. if a.Minor > fingerprint.Minor {
  190. return false
  191. }
  192. if a.Patch == anyVersion || a.Patch <= fingerprint.Patch {
  193. return true
  194. }
  195. if a.Patch > fingerprint.Patch {
  196. return false
  197. }
  198. return true
  199. }
  200. // maxMatch returns true if fingerprint matches the max value
  201. func (a UAVersion) maxMatch(fingerprint UAVersion) bool {
  202. if a.Major == anyVersion || a.Major >= fingerprint.Major {
  203. return true
  204. }
  205. if a.Major < fingerprint.Major {
  206. return false
  207. }
  208. if a.Minor == anyVersion || a.Minor >= fingerprint.Minor {
  209. return true
  210. }
  211. if a.Minor < fingerprint.Minor {
  212. return false
  213. }
  214. if a.Patch == anyVersion || a.Patch >= fingerprint.Patch {
  215. return true
  216. }
  217. if a.Patch < fingerprint.Patch {
  218. return false
  219. }
  220. return true
  221. }
  222. // Match a user agent fingerprint against the signature.
  223. // Returns MatchImpossible if no match is possible, MatchUnlikely if the match
  224. // is possible with an unlikely configuration, and MatchPossible otherwise.
  225. func (a UAVersionSignature) Match(fingerprint UAVersion) Match {
  226. if a.Min.minMatch(fingerprint) && a.Max.maxMatch(fingerprint) {
  227. return MatchPossible
  228. }
  229. return MatchImpossible
  230. }
  231. // minMerge returns the min value of two versions.
  232. func (a UAVersion) minMerge(b UAVersion) UAVersion {
  233. if a.Major == anyVersion || b.Major == anyVersion {
  234. return UAVersion{anyVersion, anyVersion, anyVersion}
  235. }
  236. if a.Major < b.Major {
  237. return a
  238. }
  239. if a.Major > b.Major {
  240. return b
  241. }
  242. if a.Minor == anyVersion || b.Minor == anyVersion {
  243. return UAVersion{a.Major, anyVersion, anyVersion}
  244. }
  245. if a.Minor < b.Minor {
  246. return a
  247. }
  248. if a.Minor > b.Minor {
  249. return b
  250. }
  251. if a.Patch == anyVersion || b.Patch == anyVersion {
  252. return UAVersion{a.Major, a.Minor, anyVersion}
  253. }
  254. if a.Patch < b.Patch {
  255. return a
  256. }
  257. if a.Patch > b.Patch {
  258. return b
  259. }
  260. return a
  261. }
  262. // maxMerge returns the max value of two versions.
  263. func (a UAVersion) maxMerge(b UAVersion) UAVersion {
  264. if a.Major == anyVersion || b.Major == anyVersion {
  265. return UAVersion{anyVersion, anyVersion, anyVersion}
  266. }
  267. if a.Major > b.Major {
  268. return a
  269. }
  270. if a.Major < b.Major {
  271. return b
  272. }
  273. if a.Minor == anyVersion || b.Minor == anyVersion {
  274. return UAVersion{a.Major, anyVersion, anyVersion}
  275. }
  276. if a.Minor > b.Minor {
  277. return a
  278. }
  279. if a.Minor < b.Minor {
  280. return b
  281. }
  282. if a.Patch == anyVersion || b.Patch == anyVersion {
  283. return UAVersion{a.Major, a.Minor, anyVersion}
  284. }
  285. if a.Patch > b.Patch {
  286. return a
  287. }
  288. if a.Patch < b.Patch {
  289. return b
  290. }
  291. return a
  292. }
  293. // Merge signatures a and b to match fingerprints from both.
  294. func (a UAVersionSignature) Merge(b UAVersionSignature) UAVersionSignature {
  295. return UAVersionSignature{Min: a.Min.minMerge(b.Min), Max: a.Max.maxMerge(b.Max)}
  296. }
  297. // A UASignature represents a set of user agents
  298. type UASignature struct {
  299. BrowserName int
  300. BrowserVersion UAVersionSignature
  301. OSPlatform int
  302. OSName int
  303. OSVersion UAVersionSignature
  304. DeviceType int
  305. Quirk StringSignature
  306. }
  307. // Parse a user agent signature from a string and return an error on failure
  308. func (a *UASignature) Parse(s string) error {
  309. var err error
  310. fields := strings.Split(s, uaFieldSep)
  311. if len(fields) != uaFieldCount {
  312. return fmt.Errorf("bad ua field count '%s': exp %d, got %d", s, uaFieldCount, len(fields))
  313. }
  314. fieldIdx := 0
  315. if a.BrowserName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  316. return err
  317. }
  318. fieldIdx++
  319. if err = a.BrowserVersion.Parse(fields[fieldIdx]); err != nil {
  320. return err
  321. }
  322. fieldIdx++
  323. if a.OSPlatform, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  324. return err
  325. }
  326. fieldIdx++
  327. if a.OSName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  328. return err
  329. }
  330. fieldIdx++
  331. if err = a.OSVersion.Parse(fields[fieldIdx]); err != nil {
  332. return err
  333. }
  334. fieldIdx++
  335. if a.DeviceType, err = strconv.Atoi(fields[fieldIdx]); err != nil {
  336. return err
  337. }
  338. fieldIdx++
  339. if err = a.Quirk.Parse(fields[fieldIdx]); err != nil {
  340. return err
  341. }
  342. return nil
  343. }
  344. // NewUASignature returns a new user agent signature parsed from a string
  345. func NewUASignature(s string) (UASignature, error) {
  346. var a UASignature
  347. err := a.Parse(s)
  348. return a, err
  349. }
  350. // String returns a string representation of a signature
  351. func (a UASignature) String() string {
  352. return strings.Join([]string{strconv.Itoa(a.BrowserName), a.BrowserVersion.String(), strconv.Itoa(a.OSPlatform), strconv.Itoa(a.OSName), a.OSVersion.String(), strconv.Itoa(a.DeviceType), a.Quirk.String()}, uaFieldSep)
  353. }
  354. // Merge user agent signatures a and b to match fingerprints from both.
  355. func (a UASignature) Merge(b UASignature) UASignature {
  356. var merged UASignature
  357. if a.BrowserName != b.BrowserName {
  358. merged.BrowserName = 0
  359. merged.BrowserVersion.Min = UAVersion{anyVersion, anyVersion, anyVersion}
  360. merged.BrowserVersion.Max = UAVersion{anyVersion, anyVersion, anyVersion}
  361. } else {
  362. merged.BrowserName = a.BrowserName
  363. merged.BrowserVersion = a.BrowserVersion.Merge(b.BrowserVersion)
  364. }
  365. if a.OSPlatform != b.OSPlatform {
  366. merged.OSPlatform = 0
  367. } else {
  368. merged.OSPlatform = a.OSPlatform
  369. }
  370. if a.OSName != b.OSName {
  371. merged.OSName = 0
  372. merged.OSVersion.Min = UAVersion{anyVersion, anyVersion, anyVersion}
  373. merged.OSVersion.Max = UAVersion{anyVersion, anyVersion, anyVersion}
  374. } else {
  375. merged.OSName = a.OSName
  376. merged.OSVersion = a.OSVersion.Merge(b.OSVersion)
  377. }
  378. if a.DeviceType != b.DeviceType {
  379. merged.DeviceType = 0
  380. } else {
  381. merged.DeviceType = a.DeviceType
  382. }
  383. merged.Quirk = a.Quirk.Merge(b.Quirk)
  384. return merged
  385. }
  386. // Match a user agent against the user agent signature.
  387. // Returns MatchImpossible if no match is possible, MatchUnlikely if the match
  388. // is possible with an unlikely configuration, and MatchPossible otherwise.
  389. func (a UASignature) Match(fingerprint UAFingerprint) Match {
  390. if a.BrowserName != 0 && a.BrowserName != fingerprint.BrowserName {
  391. return MatchImpossible
  392. }
  393. if a.OSPlatform != 0 && a.OSPlatform != fingerprint.OSPlatform {
  394. return MatchImpossible
  395. }
  396. if a.OSName != 0 && a.OSName != fingerprint.OSName {
  397. return MatchImpossible
  398. }
  399. if a.DeviceType != 0 && a.DeviceType != fingerprint.DeviceType {
  400. return MatchImpossible
  401. }
  402. matchBrowserVersion := a.BrowserVersion.Match(fingerprint.BrowserVersion)
  403. matchOSVersion := a.OSVersion.Match(fingerprint.OSVersion)
  404. matchQuirk := a.Quirk.Match(fingerprint.Quirk)
  405. if matchBrowserVersion == MatchImpossible || matchOSVersion == MatchImpossible || matchQuirk == MatchImpossible {
  406. return MatchImpossible
  407. }
  408. if matchBrowserVersion == MatchUnlikely || matchOSVersion == MatchUnlikely || matchQuirk == MatchUnlikely {
  409. return MatchUnlikely
  410. }
  411. return MatchPossible
  412. }