Преглед на файлове

Feature: Parse user agent string in user auth token api response (#16… (#17504)

* Feature: Parse user agent string in user auth token api response (#16222)

* Adding UA Parser Go modules attempt (#16222)

* Bring user agent vals up per req

* fix tests

* doc update

* update to flatten, no maps

* update doc
Shavonn Brown преди 6 години
родител
ревизия
a20309d7d2

+ 10 - 2
docs/sources/http_api/admin.md

@@ -373,7 +373,11 @@ Content-Type: application/json
     "id": 361,
     "isActive": false,
     "clientIp": "127.0.0.1",
-    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+    "browser": "Chrome",
+    "browserVersion": "72.0",
+    "os": "Linux",
+    "osVersion": "",
+    "device": "Other",
     "createdAt": "2019-03-05T21:22:54+01:00",
     "seenAt": "2019-03-06T19:41:06+01:00"
   },
@@ -381,7 +385,11 @@ Content-Type: application/json
     "id": 364,
     "isActive": false,
     "clientIp": "127.0.0.1",
-    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+    "browser": "Mobile Safari",
+    "browserVersion": "11.0",
+    "os": "iOS",
+    "osVersion": "11.0",
+    "device": "iPhone",
     "createdAt": "2019-03-06T19:41:19+01:00",
     "seenAt": "2019-03-06T19:41:21+01:00"
   }

+ 10 - 2
docs/sources/http_api/user.md

@@ -505,7 +505,11 @@ Content-Type: application/json
     "id": 361,
     "isActive": true,
     "clientIp": "127.0.0.1",
-    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
+    "browser": "Chrome",
+    "browserVersion": "72.0",
+    "os": "Linux",
+    "osVersion": "",
+    "device": "Other",
     "createdAt": "2019-03-05T21:22:54+01:00",
     "seenAt": "2019-03-06T19:41:06+01:00"
   },
@@ -513,7 +517,11 @@ Content-Type: application/json
     "id": 364,
     "isActive": false,
     "clientIp": "127.0.0.1",
-    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
+    "browser": "Mobile Safari",
+    "browserVersion": "11.0",
+    "os": "iOS",
+    "osVersion": "11.0",
+    "device": "iPhone",
     "createdAt": "2019-03-06T19:41:19+01:00",
     "seenAt": "2019-03-06T19:41:21+01:00"
   }

+ 1 - 0
go.mod

@@ -58,6 +58,7 @@ require (
 	github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a
 	github.com/stretchr/testify v1.3.0
 	github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
+	github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3
 	github.com/uber-go/atomic v1.3.2 // indirect
 	github.com/uber/jaeger-client-go v2.16.0+incompatible
 	github.com/uber/jaeger-lib v2.0.0+incompatible // indirect

+ 2 - 0
go.sum

@@ -191,6 +191,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA=
 github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
+github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3 h1:E7xa7Zur8hLPvw+03gAeQ9esrglfV389j2PcwhiGf/I=
+github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
 github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
 github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
 github.com/uber/jaeger-client-go v2.16.0+incompatible h1:Q2Pp6v3QYiocMxomCaJuwQGFt7E53bPYqEgug/AoBtY=

+ 10 - 6
pkg/api/dtos/user_token.go

@@ -3,10 +3,14 @@ package dtos
 import "time"
 
 type UserToken struct {
-	Id        int64     `json:"id"`
-	IsActive  bool      `json:"isActive"`
-	ClientIp  string    `json:"clientIp"`
-	UserAgent string    `json:"userAgent"`
-	CreatedAt time.Time `json:"createdAt"`
-	SeenAt    time.Time `json:"seenAt"`
+	Id                     int64     `json:"id"`
+	IsActive               bool      `json:"isActive"`
+	ClientIp               string    `json:"clientIp"`
+	Device                 string    `json:"device"`
+	OperatingSystem        string    `json:"os"`
+	OperatingSystemVersion string    `json:"osVersion"`
+	Browser                string    `json:"browser"`
+	BrowserVersion         string    `json:"browserVersion"`
+	CreatedAt              time.Time `json:"createdAt"`
+	SeenAt                 time.Time `json:"seenAt"`
 }

+ 32 - 6
pkg/api/user_token.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/util"
+	"github.com/ua-parser/uap-go/uaparser"
 )
 
 // GET /api/user/auth-tokens
@@ -62,13 +63,38 @@ func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID
 			isActive = true
 		}
 
+		parser := uaparser.NewFromSaved()
+		client := parser.Parse(token.UserAgent)
+
+		osVersion := ""
+		if client.Os.Major != "" {
+			osVersion = client.Os.Major
+
+			if client.Os.Minor != "" {
+				osVersion = osVersion + "." + client.Os.Minor
+			}
+		}
+
+		browserVersion := ""
+		if client.UserAgent.Major != "" {
+			browserVersion = client.UserAgent.Major
+
+			if client.UserAgent.Minor != "" {
+				browserVersion = browserVersion + "." + client.UserAgent.Minor
+			}
+		}
+
 		result = append(result, &dtos.UserToken{
-			Id:        token.Id,
-			IsActive:  isActive,
-			ClientIp:  token.ClientIp,
-			UserAgent: token.UserAgent,
-			CreatedAt: time.Unix(token.CreatedAt, 0),
-			SeenAt:    time.Unix(token.SeenAt, 0),
+			Id:                     token.Id,
+			IsActive:               isActive,
+			ClientIp:               token.ClientIp,
+			Device:                 client.Device.ToString(),
+			OperatingSystem:        client.Os.Family,
+			OperatingSystemVersion: osVersion,
+			Browser:                client.UserAgent.Family,
+			BrowserVersion:         browserVersion,
+			CreatedAt:              time.Unix(token.CreatedAt, 0),
+			SeenAt:                 time.Unix(token.SeenAt, 0),
 		})
 	}
 

+ 12 - 2
pkg/api/user_token_test.go

@@ -140,17 +140,27 @@ func TestUserTokenApiEndpoint(t *testing.T) {
 			So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id)
 			So(resultOne.Get("isActive").MustBool(), ShouldBeTrue)
 			So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1")
-			So(resultOne.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36")
 			So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339))
 			So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339))
 
+			So(resultOne.Get("device").MustString(), ShouldEqual, "Other")
+			So(resultOne.Get("browser").MustString(), ShouldEqual, "Chrome")
+			So(resultOne.Get("browserVersion").MustString(), ShouldEqual, "72.0")
+			So(resultOne.Get("os").MustString(), ShouldEqual, "Linux")
+			So(resultOne.Get("osVersion").MustString(), ShouldEqual, "")
+
 			resultTwo := result.GetIndex(1)
 			So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id)
 			So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse)
 			So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2")
-			So(resultTwo.Get("userAgent").MustString(), ShouldEqual, "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")
 			So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339))
 			So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].SeenAt, 0).Format(time.RFC3339))
+
+			So(resultTwo.Get("device").MustString(), ShouldEqual, "iPhone")
+			So(resultTwo.Get("browser").MustString(), ShouldEqual, "Mobile Safari")
+			So(resultTwo.Get("browserVersion").MustString(), ShouldEqual, "11.0")
+			So(resultTwo.Get("os").MustString(), ShouldEqual, "iOS")
+			So(resultTwo.Get("osVersion").MustString(), ShouldEqual, "11.0")
 		})
 	})
 }

+ 16 - 0
vendor/github.com/ua-parser/uap-go/LICENSE

@@ -0,0 +1,16 @@
+Apache License, Version 2.0
+===========================
+
+Copyright 2009 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

+ 2 - 0
vendor/github.com/ua-parser/uap-go/uaparser/.gitignore

@@ -0,0 +1,2 @@
+*.out
+*.test

+ 8 - 0
vendor/github.com/ua-parser/uap-go/uaparser/LICENSE.md

@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright (c) 2013 Yihuan Zhou
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 30 - 0
vendor/github.com/ua-parser/uap-go/uaparser/device.go

@@ -0,0 +1,30 @@
+package uaparser
+
+import "strings"
+
+type Device struct {
+	Family string
+	Brand  string
+	Model  string
+}
+
+func (parser *deviceParser) Match(line string, dvc *Device) {
+	matches := parser.Reg.FindStringSubmatchIndex(line)
+
+	if len(matches) == 0 {
+		return
+	}
+
+	dvc.Family = string(parser.Reg.ExpandString(nil, parser.DeviceReplacement, line, matches))
+	dvc.Family = strings.TrimSpace(dvc.Family)
+
+	dvc.Brand = string(parser.Reg.ExpandString(nil, parser.BrandReplacement, line, matches))
+	dvc.Brand = strings.TrimSpace(dvc.Brand)
+
+	dvc.Model = string(parser.Reg.ExpandString(nil, parser.ModelReplacement, line, matches))
+	dvc.Model = strings.TrimSpace(dvc.Model)
+}
+
+func (dvc *Device) ToString() string {
+	return dvc.Family
+}

+ 49 - 0
vendor/github.com/ua-parser/uap-go/uaparser/os.go

@@ -0,0 +1,49 @@
+package uaparser
+
+type Os struct {
+	Family     string
+	Major      string
+	Minor      string
+	Patch      string
+	PatchMinor string `yaml:"patch_minor"`
+}
+
+func (parser *osParser) Match(line string, os *Os) {
+	matches := parser.Reg.FindStringSubmatchIndex(line)
+	if len(matches) > 0 {
+		os.Family = string(parser.Reg.ExpandString(nil, parser.OSReplacement, line, matches))
+		os.Major = string(parser.Reg.ExpandString(nil, parser.V1Replacement, line, matches))
+		os.Minor = string(parser.Reg.ExpandString(nil, parser.V2Replacement, line, matches))
+		os.Patch = string(parser.Reg.ExpandString(nil, parser.V3Replacement, line, matches))
+		os.PatchMinor = string(parser.Reg.ExpandString(nil, parser.V4Replacement, line, matches))
+	}
+}
+
+func (os *Os) ToString() string {
+	var str string
+	if os.Family != "" {
+		str += os.Family
+	}
+	version := os.ToVersionString()
+	if version != "" {
+		str += " " + version
+	}
+	return str
+}
+
+func (os *Os) ToVersionString() string {
+	var version string
+	if os.Major != "" {
+		version += os.Major
+	}
+	if os.Minor != "" {
+		version += "." + os.Minor
+	}
+	if os.Patch != "" {
+		version += "." + os.Patch
+	}
+	if os.PatchMinor != "" {
+		version += "." + os.PatchMinor
+	}
+	return version
+}

+ 355 - 0
vendor/github.com/ua-parser/uap-go/uaparser/parser.go

@@ -0,0 +1,355 @@
+package uaparser
+
+import (
+	"fmt"
+	"io/ioutil"
+	"regexp"
+	"sync"
+	"sync/atomic"
+	"sort"
+	"time"
+
+	"gopkg.in/yaml.v2"
+)
+
+type RegexesDefinitions struct {
+	UA     []*uaParser     `yaml:"user_agent_parsers"`
+	OS     []*osParser     `yaml:"os_parsers"`
+	Device []*deviceParser `yaml:"device_parsers"`
+	sync.RWMutex
+}
+
+type UserAgentSorter []*uaParser
+func (a UserAgentSorter) Len() int           { return len(a) }
+func (a UserAgentSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a UserAgentSorter) Less(i, j int) bool { return atomic.LoadUint64(&a[i].MatchesCount) > atomic.LoadUint64(&a[j].MatchesCount) }
+
+type uaParser struct {
+	Reg               *regexp.Regexp
+	Expr              string `yaml:"regex"`
+	Flags             string `yaml:"regex_flag"`
+	FamilyReplacement string `yaml:"family_replacement"`
+	V1Replacement     string `yaml:"v1_replacement"`
+	V2Replacement     string `yaml:"v2_replacement"`
+	V3Replacement     string `yaml:"v3_replacement"`
+	MatchesCount      uint64
+}
+
+func (ua *uaParser) setDefaults() {
+	if ua.FamilyReplacement == "" {
+		ua.FamilyReplacement = "$1"
+	}
+	if ua.V1Replacement == "" {
+		ua.V1Replacement = "$2"
+	}
+	if ua.V2Replacement == "" {
+		ua.V2Replacement = "$3"
+	}
+	if ua.V3Replacement == "" {
+		ua.V3Replacement = "$4"
+	}
+}
+
+type OsSorter []*osParser
+func (a OsSorter) Len() int           { return len(a) }
+func (a OsSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a OsSorter) Less(i, j int) bool { return atomic.LoadUint64(&a[i].MatchesCount) > atomic.LoadUint64(&a[j].MatchesCount) }
+
+type osParser struct {
+	Reg           *regexp.Regexp
+	Expr          string `yaml:"regex"`
+	Flags         string `yaml:"regex_flag"`
+	OSReplacement string `yaml:"os_replacement"`
+	V1Replacement string `yaml:"os_v1_replacement"`
+	V2Replacement string `yaml:"os_v2_replacement"`
+	V3Replacement string `yaml:"os_v3_replacement"`
+	V4Replacement string `yaml:"os_v4_replacement"`
+	MatchesCount  uint64
+}
+
+func (os *osParser) setDefaults() {
+	if os.OSReplacement == "" {
+		os.OSReplacement = "$1"
+	}
+	if os.V1Replacement == "" {
+		os.V1Replacement = "$2"
+	}
+	if os.V2Replacement == "" {
+		os.V2Replacement = "$3"
+	}
+	if os.V3Replacement == "" {
+		os.V3Replacement = "$4"
+	}
+	if os.V4Replacement == "" {
+		os.V4Replacement = "$5"
+	}
+}
+
+type DeviceSorter []*deviceParser
+func (a DeviceSorter) Len() int           { return len(a) }
+func (a DeviceSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a DeviceSorter) Less(i, j int) bool { return atomic.LoadUint64(&a[i].MatchesCount) > atomic.LoadUint64(&a[j].MatchesCount) }
+
+type deviceParser struct {
+	Reg               *regexp.Regexp
+	Expr              string `yaml:"regex"`
+	Flags             string `yaml:"regex_flag"`
+	DeviceReplacement string `yaml:"device_replacement"`
+	BrandReplacement  string `yaml:"brand_replacement"`
+	ModelReplacement  string `yaml:"model_replacement"`
+	MatchesCount      uint64
+}
+
+func (device *deviceParser) setDefaults() {
+	if device.DeviceReplacement == "" {
+		device.DeviceReplacement = "$1"
+	}
+	if device.ModelReplacement == "" {
+		device.ModelReplacement = "$1"
+	}
+}
+
+type Client struct {
+	UserAgent *UserAgent
+	Os        *Os
+	Device    *Device
+}
+
+type Parser struct {
+	RegexesDefinitions
+	UserAgentMisses   uint64
+	OsMisses          uint64
+	DeviceMisses      uint64
+	Mode              int
+	UseSort           bool
+	debugMode         bool
+}
+
+
+const (
+	EOsLookUpMode		= 1	/* 00000001 */
+	EUserAgentLookUpMode	= 2	/* 00000010 */
+	EDeviceLookUpMode	= 4	/* 00000100 */
+	cMinMissesTreshold	= 100000
+	cDefaultMissesTreshold	= 500000
+	cDefaultMatchIdxNotOk	= 20
+	cDefaultSortOption	= false
+)
+
+var (
+	missesTreshold		= uint64(500000)
+	matchIdxNotOk		= 20
+)
+
+func (parser *Parser) mustCompile() { // until we can use yaml.UnmarshalYAML with embedded pointer struct
+	for _, p := range parser.UA {
+		p.Reg = compileRegex(p.Flags, p.Expr)
+		p.setDefaults()
+	}
+	for _, p := range parser.OS {
+		p.Reg = compileRegex(p.Flags, p.Expr)
+		p.setDefaults()
+	}
+	for _, p := range parser.Device {
+		p.Reg = compileRegex(p.Flags, p.Expr)
+		p.setDefaults()
+	}
+}
+
+func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debugMode bool) (*Parser, error) {
+	data, err := ioutil.ReadFile(regexFile)
+	if nil != err {
+		return nil, err
+	}
+	if topCnt >= 0 {
+		matchIdxNotOk = topCnt
+	}
+	if treshold > cMinMissesTreshold {
+		missesTreshold = uint64(treshold)
+	}
+	parser, err := NewFromBytes(data)
+	if err != nil {
+		return nil, err
+	}
+	parser.Mode = mode
+	parser.UseSort = useSort
+	parser.debugMode = debugMode
+	return parser, nil
+}
+
+func New(regexFile string) (*Parser, error) {
+	data, err := ioutil.ReadFile(regexFile)
+	if nil != err {
+		return nil, err
+	}
+	matchIdxNotOk = cDefaultMatchIdxNotOk
+	missesTreshold = cDefaultMissesTreshold
+	parser, err := NewFromBytes(data)
+	if err != nil {
+		return nil, err
+	}
+	return parser, nil
+}
+
+func NewFromSaved() *Parser {
+	parser, err := NewFromBytes(definitionYaml)
+	if err != nil {
+		// if the YAML is malformed, it's a programmatic error inside what
+		// we've statically-compiled in our binary. Panic!
+		panic(err.Error())
+	}
+	return parser
+}
+
+func NewFromBytes(data []byte) (*Parser, error) {
+	var definitions RegexesDefinitions
+	if err := yaml.Unmarshal(data, &definitions); err != nil {
+		return nil, err
+	}
+
+	parser := &Parser{definitions, 0, 0, 0, (EOsLookUpMode|EUserAgentLookUpMode|EDeviceLookUpMode), false, false}
+	parser.mustCompile()
+
+	return parser, nil
+}
+
+func (parser *Parser) Parse(line string) *Client {
+	cli := new(Client)
+	var wg sync.WaitGroup
+	if EUserAgentLookUpMode & parser.Mode == EUserAgentLookUpMode {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			parser.RLock()
+			cli.UserAgent = parser.ParseUserAgent(line)
+			parser.RUnlock()
+		}()
+	}
+	if EOsLookUpMode & parser.Mode == EOsLookUpMode {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			parser.RLock()
+			cli.Os = parser.ParseOs(line)
+			parser.RUnlock()
+		}()
+	}
+	if EDeviceLookUpMode & parser.Mode == EDeviceLookUpMode {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			parser.RLock()
+			cli.Device = parser.ParseDevice(line)
+			parser.RUnlock()
+		}()
+	}
+	wg.Wait()
+	if parser.UseSort == true {
+		checkAndSort(parser)
+	}
+	return cli
+}
+
+func (parser *Parser) ParseUserAgent(line string) *UserAgent {
+	ua := new(UserAgent)
+	foundIdx := -1
+	found := false
+	for i, uaPattern := range parser.UA {
+		uaPattern.Match(line, ua)
+		if len(ua.Family) > 0 {
+			found = true
+			foundIdx = i
+			atomic.AddUint64(&uaPattern.MatchesCount, 1)
+			break
+		}
+	}
+	if !found {
+		ua.Family = "Other"
+	}
+	if(foundIdx > matchIdxNotOk) {
+		atomic.AddUint64(&parser.UserAgentMisses, 1)
+	}
+	return ua
+}
+
+func (parser *Parser) ParseOs(line string) *Os {
+	os := new(Os)
+	foundIdx := -1
+	found := false
+	for i, osPattern := range parser.OS {
+		osPattern.Match(line, os)
+		if len(os.Family) > 0 {
+			found = true
+			foundIdx = i
+			atomic.AddUint64(&osPattern.MatchesCount, 1)
+			break
+		}
+	}
+	if !found {
+		os.Family = "Other"
+	}
+	if(foundIdx > matchIdxNotOk) {
+		atomic.AddUint64(&parser.OsMisses, 1)
+	}
+	return os
+}
+
+func (parser *Parser) ParseDevice(line string) *Device {
+	dvc := new(Device)
+	foundIdx := -1
+	found := false
+	for i, dvcPattern := range parser.Device {
+		dvcPattern.Match(line, dvc)
+		if len(dvc.Family) > 0 {
+			found = true
+			foundIdx = i
+			atomic.AddUint64(&dvcPattern.MatchesCount, 1)
+			break
+		}
+	}
+	if !found {
+		dvc.Family = "Other"
+	}
+	if(foundIdx > matchIdxNotOk) {
+		atomic.AddUint64(&parser.DeviceMisses, 1)
+	}
+	return dvc
+}
+
+func checkAndSort(parser *Parser) {
+	parser.Lock()
+	if(atomic.LoadUint64(&parser.UserAgentMisses) >= missesTreshold) {
+		if parser.debugMode {
+			fmt.Printf("%s\tSorting UserAgents slice\n", time.Now());
+		}
+		parser.UserAgentMisses = 0
+		sort.Sort(UserAgentSorter(parser.UA));
+	}
+	parser.Unlock()
+	parser.Lock()
+	if(atomic.LoadUint64(&parser.OsMisses) >= missesTreshold) {
+		if parser.debugMode {
+			fmt.Printf("%s\tSorting OS slice\n", time.Now());
+		}
+		parser.OsMisses = 0
+		sort.Sort(OsSorter(parser.OS));
+	}
+	parser.Unlock()
+	parser.Lock()
+	if(atomic.LoadUint64(&parser.DeviceMisses) >= missesTreshold) {
+		if parser.debugMode {
+			fmt.Printf("%s\tSorting Device slice\n", time.Now());
+		}
+		parser.DeviceMisses = 0
+		sort.Sort(DeviceSorter(parser.Device));
+	}
+	parser.Unlock()
+}
+
+func compileRegex(flags, expr string) *regexp.Regexp {
+	if flags == "" {
+		return regexp.MustCompile(expr)
+	} else {
+		return regexp.MustCompile(fmt.Sprintf("(?%s)%s", flags, expr))
+	}
+}

+ 44 - 0
vendor/github.com/ua-parser/uap-go/uaparser/user_agent.go

@@ -0,0 +1,44 @@
+package uaparser
+
+type UserAgent struct {
+	Family string
+	Major  string
+	Minor  string
+	Patch  string
+}
+
+func (parser *uaParser) Match(line string, ua *UserAgent) {
+	matches := parser.Reg.FindStringSubmatchIndex(line)
+	if len(matches) > 0 {
+		ua.Family = string(parser.Reg.ExpandString(nil, parser.FamilyReplacement, line, matches))
+		ua.Major = string(parser.Reg.ExpandString(nil, parser.V1Replacement, line, matches))
+		ua.Minor = string(parser.Reg.ExpandString(nil, parser.V2Replacement, line, matches))
+		ua.Patch = string(parser.Reg.ExpandString(nil, parser.V3Replacement, line, matches))
+	}
+}
+
+func (ua *UserAgent) ToString() string {
+	var str string
+	if ua.Family != "" {
+		str += ua.Family
+	}
+	version := ua.ToVersionString()
+	if version != "" {
+		str += " " + version
+	}
+	return str
+}
+
+func (ua *UserAgent) ToVersionString() string {
+	var version string
+	if ua.Major != "" {
+		version += ua.Major
+	}
+	if ua.Minor != "" {
+		version += "." + ua.Minor
+	}
+	if ua.Patch != "" {
+		version += "." + ua.Patch
+	}
+	return version
+}

Файловите разлики са ограничени, защото са твърде много
+ 54 - 0
vendor/github.com/ua-parser/uap-go/uaparser/yaml.go


+ 2 - 0
vendor/modules.txt

@@ -192,6 +192,8 @@ github.com/smartystreets/goconvey/convey/gotest
 github.com/stretchr/testify/assert
 # github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
 github.com/teris-io/shortid
+# github.com/ua-parser/uap-go v0.0.0-20190303233514-1004ccd816b3
+github.com/ua-parser/uap-go/uaparser
 # github.com/uber/jaeger-client-go v2.16.0+incompatible
 github.com/uber/jaeger-client-go/config
 github.com/uber/jaeger-client-go/zipkin

Някои файлове не бяха показани, защото твърде много файлове са промени