123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- package fp
- import (
- "fmt"
- "strconv"
- "strings"
- ua "github.com/avct/uasurfer"
- )
- // User agent signature and fingerprint strings have the format
- // <br-name>:<br-vers>:<os-plat>:<os-name>:<os-vers>:<dev-type>:<quirk>
- //
- // For fingerprints the parts have the formats
- // <br-name>, <os-plat>, <os-name>, <dev-type>:
- // <int>
- // <browser-vers>, <os-vers>:
- // <major>[.<minor>[.<patch>]]
- // <quirk>:
- // <str-list>
- // where <int> is a decimal-encoded int using constants defined in uasurfer, and
- // <str-list> is a comma-separated list of strings.
- //
- // and for signatures the parts have the formats
- // <br-name>, <os-plat>, <os-name>, <dev-type>:
- // <int>
- // <browser-vers>, <os-vers>:
- // [<major>[.<minor>[.<patch>]]][-[<major>[.<minor>[.<patch>]]]]
- // <quirk>:
- // same as in request.go
- // where items in enclosed in square brackets are optional,
- const anyVersion int = -1
- const (
- uaFieldCount int = 7
- uaFieldSep string = ":"
- uaVersionFieldSep string = "."
- uaVersionRangeSep string = "-"
- )
- // UAFingerprint is a fingerprint for a user agent
- type UAFingerprint struct {
- BrowserName int
- BrowserVersion UAVersion
- OSPlatform int
- OSName int
- OSVersion UAVersion
- DeviceType int
- Quirk StringList
- }
- // NewUAFingerprint returns a new user agent fingerprint parsed from a string
- func NewUAFingerprint(s string) (UAFingerprint, error) {
- var a UAFingerprint
- err := a.Parse(s)
- return a, err
- }
- // Parse a user agent fingerprint from a string and return an error on failure
- func (a *UAFingerprint) Parse(s string) error {
- var err error
- fields := strings.Split(s, uaFieldSep)
- if len(fields) != uaFieldCount {
- return fmt.Errorf("bad ua field count '%s': exp %d, got %d", s, uaFieldCount, len(fields))
- }
- fieldIdx := 0
- if a.BrowserName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.BrowserVersion.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.OSPlatform, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.OSName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.OSVersion.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.DeviceType, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.Quirk.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- return nil
- }
- // String returns a string representation of a fingerprint
- func (a UAFingerprint) String() string {
- 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)
- }
- // UAVersion represents a user agent browser or OS version.
- type UAVersion ua.Version
- // Parse a user agent version from a string and return an error on failure.
- func (a *UAVersion) Parse(s string) error {
- var i int
- var err error
- a.Major = anyVersion
- a.Minor = anyVersion
- a.Patch = anyVersion
- if len(s) == 0 {
- return nil
- }
- fields := strings.Split(s, uaVersionFieldSep)
- switch len(fields) {
- case 3:
- if len(fields[2]) > 0 {
- i, err = strconv.Atoi(fields[2])
- if err != nil {
- return err
- }
- a.Patch = i
- }
- fallthrough
- case 2:
- if len(fields[1]) > 0 {
- i, err := strconv.Atoi(fields[1])
- if err != nil {
- return err
- }
- a.Minor = i
- }
- fallthrough
- case 1:
- if len(fields[0]) > 0 {
- i, err := strconv.Atoi(fields[0])
- if err != nil {
- return err
- }
- a.Major = i
- }
- return nil
- default:
- return fmt.Errorf("invalid user agent version format: '%s'", s)
- }
- }
- func (a UAVersion) String() string {
- var fields []string
- if a.Major != anyVersion {
- fields = append(fields, strconv.Itoa(a.Major))
- if a.Minor != anyVersion {
- fields = append(fields, strconv.Itoa(a.Minor))
- if a.Patch != anyVersion {
- fields = append(fields, strconv.Itoa(a.Patch))
- }
- }
- }
- return strings.Join(fields, uaVersionFieldSep)
- }
- // A UAVersionSignature matches a range of possible user agent versions
- type UAVersionSignature struct {
- Min UAVersion
- Max UAVersion
- }
- func (a UAVersionSignature) String() string {
- if a.Min == a.Max {
- return a.Min.String()
- }
- return strings.Join([]string{a.Min.String(), a.Max.String()}, uaVersionRangeSep)
- }
- // Parse a user agent version signature from a string and return an error on failure.
- func (a *UAVersionSignature) Parse(s string) error {
- fields := strings.SplitN(s, uaVersionRangeSep, 2)
- if err := a.Min.Parse(fields[0]); err != nil {
- return err
- }
- switch len(fields) {
- case 2:
- if err := a.Max.Parse(fields[1]); err != nil {
- return err
- }
- case 1:
- a.Max = a.Min
- }
- return nil
- }
- // minMatch returns true if fingerprint matches the min value
- func (a UAVersion) minMatch(fingerprint UAVersion) bool {
- if a.Major == anyVersion || a.Major <= fingerprint.Major {
- return true
- }
- if a.Major > fingerprint.Major {
- return false
- }
- if a.Minor == anyVersion || a.Minor <= fingerprint.Minor {
- return true
- }
- if a.Minor > fingerprint.Minor {
- return false
- }
- if a.Patch == anyVersion || a.Patch <= fingerprint.Patch {
- return true
- }
- if a.Patch > fingerprint.Patch {
- return false
- }
- return true
- }
- // maxMatch returns true if fingerprint matches the max value
- func (a UAVersion) maxMatch(fingerprint UAVersion) bool {
- if a.Major == anyVersion || a.Major >= fingerprint.Major {
- return true
- }
- if a.Major < fingerprint.Major {
- return false
- }
- if a.Minor == anyVersion || a.Minor >= fingerprint.Minor {
- return true
- }
- if a.Minor < fingerprint.Minor {
- return false
- }
- if a.Patch == anyVersion || a.Patch >= fingerprint.Patch {
- return true
- }
- if a.Patch < fingerprint.Patch {
- return false
- }
- return true
- }
- // Match a user agent fingerprint against the signature.
- // Returns MatchImpossible if no match is possible, MatchUnlikely if the match
- // is possible with an unlikely configuration, and MatchPossible otherwise.
- func (a UAVersionSignature) Match(fingerprint UAVersion) Match {
- if a.Min.minMatch(fingerprint) && a.Max.maxMatch(fingerprint) {
- return MatchPossible
- }
- return MatchImpossible
- }
- // minMerge returns the min value of two versions.
- func (a UAVersion) minMerge(b UAVersion) UAVersion {
- if a.Major == anyVersion || b.Major == anyVersion {
- return UAVersion{anyVersion, anyVersion, anyVersion}
- }
- if a.Major < b.Major {
- return a
- }
- if a.Major > b.Major {
- return b
- }
- if a.Minor == anyVersion || b.Minor == anyVersion {
- return UAVersion{a.Major, anyVersion, anyVersion}
- }
- if a.Minor < b.Minor {
- return a
- }
- if a.Minor > b.Minor {
- return b
- }
- if a.Patch == anyVersion || b.Patch == anyVersion {
- return UAVersion{a.Major, a.Minor, anyVersion}
- }
- if a.Patch < b.Patch {
- return a
- }
- if a.Patch > b.Patch {
- return b
- }
- return a
- }
- // maxMerge returns the max value of two versions.
- func (a UAVersion) maxMerge(b UAVersion) UAVersion {
- if a.Major == anyVersion || b.Major == anyVersion {
- return UAVersion{anyVersion, anyVersion, anyVersion}
- }
- if a.Major > b.Major {
- return a
- }
- if a.Major < b.Major {
- return b
- }
- if a.Minor == anyVersion || b.Minor == anyVersion {
- return UAVersion{a.Major, anyVersion, anyVersion}
- }
- if a.Minor > b.Minor {
- return a
- }
- if a.Minor < b.Minor {
- return b
- }
- if a.Patch == anyVersion || b.Patch == anyVersion {
- return UAVersion{a.Major, a.Minor, anyVersion}
- }
- if a.Patch > b.Patch {
- return a
- }
- if a.Patch < b.Patch {
- return b
- }
- return a
- }
- // Merge signatures a and b to match fingerprints from both.
- func (a UAVersionSignature) Merge(b UAVersionSignature) UAVersionSignature {
- return UAVersionSignature{Min: a.Min.minMerge(b.Min), Max: a.Max.maxMerge(b.Max)}
- }
- // A UASignature represents a set of user agents
- type UASignature struct {
- BrowserName int
- BrowserVersion UAVersionSignature
- OSPlatform int
- OSName int
- OSVersion UAVersionSignature
- DeviceType int
- Quirk StringSignature
- }
- // Parse a user agent signature from a string and return an error on failure
- func (a *UASignature) Parse(s string) error {
- var err error
- fields := strings.Split(s, uaFieldSep)
- if len(fields) != uaFieldCount {
- return fmt.Errorf("bad ua field count '%s': exp %d, got %d", s, uaFieldCount, len(fields))
- }
- fieldIdx := 0
- if a.BrowserName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.BrowserVersion.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.OSPlatform, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.OSName, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.OSVersion.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if a.DeviceType, err = strconv.Atoi(fields[fieldIdx]); err != nil {
- return err
- }
- fieldIdx++
- if err = a.Quirk.Parse(fields[fieldIdx]); err != nil {
- return err
- }
- return nil
- }
- // NewUASignature returns a new user agent signature parsed from a string
- func NewUASignature(s string) (UASignature, error) {
- var a UASignature
- err := a.Parse(s)
- return a, err
- }
- // String returns a string representation of a signature
- func (a UASignature) String() string {
- 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)
- }
- // Merge user agent signatures a and b to match fingerprints from both.
- func (a UASignature) Merge(b UASignature) UASignature {
- var merged UASignature
- if a.BrowserName != b.BrowserName {
- merged.BrowserName = 0
- merged.BrowserVersion.Min = UAVersion{anyVersion, anyVersion, anyVersion}
- merged.BrowserVersion.Max = UAVersion{anyVersion, anyVersion, anyVersion}
- } else {
- merged.BrowserName = a.BrowserName
- merged.BrowserVersion = a.BrowserVersion.Merge(b.BrowserVersion)
- }
- if a.OSPlatform != b.OSPlatform {
- merged.OSPlatform = 0
- } else {
- merged.OSPlatform = a.OSPlatform
- }
- if a.OSName != b.OSName {
- merged.OSName = 0
- merged.OSVersion.Min = UAVersion{anyVersion, anyVersion, anyVersion}
- merged.OSVersion.Max = UAVersion{anyVersion, anyVersion, anyVersion}
- } else {
- merged.OSName = a.OSName
- merged.OSVersion = a.OSVersion.Merge(b.OSVersion)
- }
- if a.DeviceType != b.DeviceType {
- merged.DeviceType = 0
- } else {
- merged.DeviceType = a.DeviceType
- }
- merged.Quirk = a.Quirk.Merge(b.Quirk)
- return merged
- }
- // Match a user agent against the user agent signature.
- // Returns MatchImpossible if no match is possible, MatchUnlikely if the match
- // is possible with an unlikely configuration, and MatchPossible otherwise.
- func (a UASignature) Match(fingerprint UAFingerprint) Match {
- if a.BrowserName != 0 && a.BrowserName != fingerprint.BrowserName {
- return MatchImpossible
- }
- if a.OSPlatform != 0 && a.OSPlatform != fingerprint.OSPlatform {
- return MatchImpossible
- }
- if a.OSName != 0 && a.OSName != fingerprint.OSName {
- return MatchImpossible
- }
- if a.DeviceType != 0 && a.DeviceType != fingerprint.DeviceType {
- return MatchImpossible
- }
- matchBrowserVersion := a.BrowserVersion.Match(fingerprint.BrowserVersion)
- matchOSVersion := a.OSVersion.Match(fingerprint.OSVersion)
- matchQuirk := a.Quirk.Match(fingerprint.Quirk)
- if matchBrowserVersion == MatchImpossible || matchOSVersion == MatchImpossible || matchQuirk == MatchImpossible {
- return MatchImpossible
- }
- if matchBrowserVersion == MatchUnlikely || matchOSVersion == MatchUnlikely || matchQuirk == MatchUnlikely {
- return MatchUnlikely
- }
- return MatchPossible
- }
|