Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
f472f363b6 | |||
6613cfe23e | |||
ac872c1681 | |||
e31652d481 | |||
42a82d3e97 | |||
f345fe3b2c | |||
8b8b818651 | |||
06740b6b86 | |||
6d5c00653b | |||
ead6298a55 | |||
454419abec | |||
b53357aad7 | |||
fee92259b1 | |||
c964c73824 | |||
c2882857a4 | |||
f207f57536 | |||
30fbf1b611 | |||
70900ea2c7 | |||
322e908147 | |||
308f853b81 | |||
d5de8568dd | |||
8b80a73452 | |||
20366ed3e4 | |||
1c52aae429 | |||
c0bd927903 | |||
3ca4448a7e | |||
8c42945e67 | |||
c15121a873 | |||
f954619223 | |||
d010926c07 | |||
7e3f6cf674 | |||
de5626814b | |||
923fdea2a1 | |||
58f77f22a4 | |||
a0d42bbbc3 | |||
7ed0e8fd88 | |||
ede215888a | |||
80306bfe85 | |||
24321190fa |
36
.drone.yml
36
.drone.yml
@ -3,28 +3,20 @@ name: default
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
image: tystuyfzand/goc
|
||||
group: build
|
||||
volumes:
|
||||
- name: build
|
||||
path: /build
|
||||
commands:
|
||||
- mkdir -p /build/i386 /build/amd64 /build/armv7 /build/arm64
|
||||
- GOOS=linux GOARCH=386 go build -o /build/i386/owapi_i386
|
||||
- go build -o /build/amd64/owapi_amd64
|
||||
- GOOS=linux GOARCH=arm GOARM=7 go build -o /build/armv7/owapi_armv7
|
||||
- GOOS=linux GOARCH=arm64 go build -o /build/arm64/owapi_arm64
|
||||
- GOOS=linux,windows GOARCH=386,arm,arm64,amd64 goc -o /build/owapi
|
||||
- name: package
|
||||
image: tystuyfzand/fpm
|
||||
commands:
|
||||
- export VERSION=`grep "Version" main.go | head -n 1 | awk '{print $3}' | sed -e 's/^"//' -e 's/"$//' | tr -d '\n'`
|
||||
- echo "v$VERSION" > /build/version.txt
|
||||
- chmod +x packaging/build-package.sh packaging/package-upload.sh
|
||||
- ARCH=i386 packaging/build-package.sh
|
||||
- ARCH=amd64 packaging/build-package.sh
|
||||
- ARCH=armv7 packaging/build-package.sh
|
||||
- ARCH=arm64 packaging/build-package.sh
|
||||
- packaging/package-upload.sh
|
||||
- chmod +x packaging/build-package.sh
|
||||
- ARCH=386,amd64,arm,arm64 packaging/build-package.sh
|
||||
volumes:
|
||||
- name: build
|
||||
path: /build
|
||||
@ -40,11 +32,27 @@ steps:
|
||||
gitea_server: https://git.meow.tf
|
||||
tag_file: /build/version.txt
|
||||
title_file: /build/version.txt
|
||||
files: [ '/build/*/owapi_*' ]
|
||||
files: [ '/build/owapi_*' ]
|
||||
environment:
|
||||
PLUGIN_API_KEY:
|
||||
from_secret: gitea_token
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: registry.meow.tf/ow-api/ow-api
|
||||
registry: registry.meow.tf
|
||||
tags:
|
||||
- latest
|
||||
volumes:
|
||||
- name: build
|
||||
temp: {}
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
- push
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
vendor/
|
||||
*.iml
|
||||
.idea/
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
ADD . /build
|
||||
|
||||
RUN cd /build && go build -o ow-api
|
||||
|
||||
FROM golang:alpine
|
||||
|
||||
COPY --from=builder /build/ow-api /usr/bin/ow-api
|
||||
|
||||
CMD [ "/usr/bin/ow-api" ]
|
5
LICENSE
Normal file
5
LICENSE
Normal file
@ -0,0 +1,5 @@
|
||||
Copyright 2020 Tyler Stuyfzand <admin@meow.tf>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
34
cache/cache.go
vendored
Normal file
34
cache/cache.go
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Get(key string) ([]byte, error)
|
||||
Set(key string, b []byte, d time.Duration) error
|
||||
}
|
||||
|
||||
func ForURI(uri string) Provider {
|
||||
u, err := url.Parse(uri)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if u.Scheme == "" && u.Path != "" {
|
||||
u.Scheme = u.Path
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "memcached":
|
||||
return NewMemcached(u)
|
||||
case "redis":
|
||||
return NewRedisCache(u)
|
||||
case "gcache":
|
||||
return NewGcache(u)
|
||||
}
|
||||
|
||||
return &NullCache{}
|
||||
}
|
47
cache/gcache.go
vendored
Normal file
47
cache/gcache.go
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/bluele/gcache"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Gcache struct {
|
||||
cache gcache.Cache
|
||||
}
|
||||
|
||||
func NewGcache(u *url.URL) *Gcache {
|
||||
size := 128
|
||||
|
||||
q := u.Query()
|
||||
|
||||
if sizeStr := q.Get("size"); sizeStr != "" {
|
||||
var err error
|
||||
size, err = strconv.Atoi(sizeStr)
|
||||
|
||||
if err != nil {
|
||||
size = 128
|
||||
}
|
||||
}
|
||||
|
||||
gc := gcache.New(size).
|
||||
LRU().
|
||||
Build()
|
||||
|
||||
return &Gcache{cache: gc}
|
||||
}
|
||||
|
||||
func (c *Gcache) Get(key string) ([]byte, error) {
|
||||
res, err := c.cache.Get(key)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.([]byte), err
|
||||
}
|
||||
|
||||
func (c *Gcache) Set(key string, b []byte, d time.Duration) error {
|
||||
return c.cache.SetWithExpire(key, b, d)
|
||||
}
|
32
cache/memcached.go
vendored
Normal file
32
cache/memcached.go
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Memcached struct {
|
||||
client *memcache.Client
|
||||
}
|
||||
|
||||
func NewMemcached(u *url.URL) *Memcached {
|
||||
mc := memcache.New(strings.Split(u.Host, ",")...)
|
||||
|
||||
return &Memcached{client: mc}
|
||||
}
|
||||
|
||||
func (m *Memcached) Get(key string) ([]byte, error) {
|
||||
item, err := m.client.Get(key)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return item.Value, nil
|
||||
}
|
||||
|
||||
func (m *Memcached) Set(key string, b []byte, d time.Duration) error {
|
||||
return m.client.Set(&memcache.Item{Key: key, Value: b, Expiration: int32(d.Seconds())})
|
||||
}
|
14
cache/null.go
vendored
Normal file
14
cache/null.go
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
type NullCache struct {
|
||||
}
|
||||
|
||||
func (n *NullCache) Get(key string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *NullCache) Set(key string, b []byte, d time.Duration) error {
|
||||
return nil
|
||||
}
|
29
cache/redis.go
vendored
Normal file
29
cache/redis.go
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/go-redis/redis"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RedisCache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisCache(uri *url.URL) *RedisCache {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: uri.Host,
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
|
||||
return &RedisCache{client: client}
|
||||
}
|
||||
|
||||
func (c *RedisCache) Get(key string) ([]byte, error) {
|
||||
return c.client.Get(key).Bytes()
|
||||
}
|
||||
|
||||
func (c *RedisCache) Set(key string, b []byte, d time.Duration) error {
|
||||
return c.client.Set(key, b, d).Err()
|
||||
}
|
166
endpoints.go
Normal file
166
endpoints.go
Normal file
@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stats(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
data, err := statsResponse(w, r, ps, nil)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
cacheKey := generateCacheKey(r, ps) + "-profile"
|
||||
|
||||
res, err := cacheProvider.Get(cacheKey)
|
||||
|
||||
if res != nil && err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
// Cache result for profile specifically
|
||||
data, err := statsResponse(w, r, ps, profilePatch)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cacheTime > 0 {
|
||||
cacheProvider.Set(cacheKey, data, cacheTime)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func heroes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
names := strings.Split(ps.ByName("heroes"), ",")
|
||||
|
||||
if len(names) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
writeError(w, errors.New("name list must contain at least one hero"))
|
||||
return
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
cacheKey := generateCacheKey(r, ps) + "-heroes-" + hex.EncodeToString(md5.New().Sum([]byte(strings.Join(names, ","))))
|
||||
|
||||
res, err := cacheProvider.Get(cacheKey)
|
||||
|
||||
if res != nil && err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
nameMap := make(map[string]bool)
|
||||
|
||||
for _, name := range names {
|
||||
nameMap[name] = true
|
||||
}
|
||||
|
||||
ops := make([]patchOperation, 0)
|
||||
|
||||
for _, heroName := range heroNames {
|
||||
if _, exists := nameMap[heroName]; !exists {
|
||||
ops = append(ops, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/quickPlayStats/topHeroes/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/quickPlayStats/careerStats/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/competitiveStats/topHeroes/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/competitiveStats/careerStats/" + heroName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
patch, err := patchFromOperations(ops)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a patch to remove all but specified heroes
|
||||
data, err := statsResponse(w, r, ps, patch)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if cacheTime > 0 {
|
||||
cacheProvider.Set(cacheKey, data, cacheTime)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
type versionObject struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func versionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(&versionObject{Version: Version}); err != nil {
|
||||
writeError(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
type statusObject struct {
|
||||
ResponseCode int `json:"responseCode"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func statusHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
status := &statusObject{}
|
||||
|
||||
res, err := http.DefaultClient.Head("https://playoverwatch.com")
|
||||
|
||||
if err == nil {
|
||||
status.ResponseCode = res.StatusCode
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
w.WriteHeader(res.StatusCode)
|
||||
}
|
||||
} else {
|
||||
status.Error = err.Error()
|
||||
}
|
||||
|
||||
if r.Method != http.MethodHead {
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
writeError(w, err)
|
||||
}
|
||||
}
|
||||
}
|
32
go.mod
32
go.mod
@ -1,14 +1,28 @@
|
||||
module git.meow.tf/ow-api/ow-api
|
||||
|
||||
go 1.12
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis v6.14.2+incompatible
|
||||
github.com/julienschmidt/httprouter v1.2.0
|
||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
||||
github.com/onsi/gomega v1.5.0 // indirect
|
||||
github.com/rs/cors v1.6.0
|
||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080 // indirect
|
||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7
|
||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
github.com/go-redis/redis v6.15.6+incompatible
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/miekg/dns v1.1.26
|
||||
github.com/ow-api/ovrstat v0.0.0-20221214025448-fafff9caacf4
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/stoewer/go-strcase v1.2.0
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.20.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
||||
)
|
||||
|
156
go.sum
156
go.sum
@ -1,61 +1,125 @@
|
||||
github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f h1:cWOyRTtBcTBjB0c+GyaQaXgP3g1HVM1KbvZL/Q5QNAM=
|
||||
github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0=
|
||||
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
|
||||
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/labstack/echo/v4 v4.0.1-0.20190313005416-e3717be4beda/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q=
|
||||
github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
||||
github.com/ow-api/ovrstat v0.0.0-20221214025448-fafff9caacf4 h1:f+vlTFtbl5+Ft4UycjLSYwwVrf9HY0NrOGPbu8xSqu4=
|
||||
github.com/ow-api/ovrstat v0.0.0-20221214025448-fafff9caacf4/go.mod h1:tQqYdSdfFHIW4JYBZV78Bj/lQyWfq28WlZ9KLMr1cyk=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
|
||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080 h1:2MLHNkv6Jl3E58fFOlOeshNo6eqanZV+yGPbJukRQIQ=
|
||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080/go.mod h1:ZaOOtAQcXELCP9jugRrAv1qg3ctLlGnz++lH0KpIZ24=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7 h1:ZHUhYZJiBOfJ9NPDT/YEKHOCc0tTtQiLGpTcW0M2mBM=
|
||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7/go.mod h1:GgVOFCqOnwT77FsVfvySTbw6u3rMW9rqUxvY7nGgpjg=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58 h1:1ioAQKrIwwtxMm4uZySpqj9imNuyCDbL7dk9ccxntmQ=
|
||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58/go.mod h1:ukrDi0b+Kdtd+dI4M0HILhkMGtMZ4v3l2C82meAMzyw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
370
main.go
370
main.go
@ -2,31 +2,37 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.meow.tf/ow-api/ow-api/cache"
|
||||
"git.meow.tf/ow-api/ow-api/json-patch"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/ow-api/ovrstat/ovrstat"
|
||||
"github.com/rs/cors"
|
||||
"github.com/stoewer/go-strcase"
|
||||
"golang.org/x/net/context"
|
||||
"log"
|
||||
"net/http"
|
||||
"s32x.com/ovrstat/ovrstat"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "2.0.8"
|
||||
Version = "2.4.7"
|
||||
|
||||
OpAdd = "add"
|
||||
OpRemove = "remove"
|
||||
)
|
||||
|
||||
type patchOperation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
type ApiVersion int
|
||||
|
||||
const (
|
||||
VersionOne ApiVersion = iota
|
||||
VersionTwo
|
||||
VersionThree
|
||||
)
|
||||
|
||||
type gamesStats struct {
|
||||
Played int64 `json:"played"`
|
||||
@ -43,22 +49,26 @@ type awardsStats struct {
|
||||
|
||||
var (
|
||||
flagBind = flag.String("bind-address", ":8080", "Address to bind to for http requests")
|
||||
flagCache = flag.String("cache", "redis://localhost:6379", "Cache uri or 'none' to disable")
|
||||
flagCacheTime = flag.Int("cacheTime", 300, "Cache time in seconds")
|
||||
|
||||
client *redis.Client
|
||||
cacheProvider cache.Provider
|
||||
|
||||
cacheTime time.Duration
|
||||
|
||||
profilePatch *jsonpatch.Patch
|
||||
|
||||
heroNames []string
|
||||
|
||||
platforms = []string{ovrstat.PlatformPC, ovrstat.PlatformConsole}
|
||||
)
|
||||
|
||||
func main() {
|
||||
loadHeroNames()
|
||||
|
||||
client = redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
cacheProvider = cache.ForURI(*flagCache)
|
||||
|
||||
cacheTime = time.Duration(*flagCacheTime) * time.Second
|
||||
|
||||
var err error
|
||||
|
||||
@ -77,21 +87,12 @@ func main() {
|
||||
|
||||
router := httprouter.New()
|
||||
|
||||
// PC
|
||||
router.GET("/v1/stats/pc/:region/:tag/heroes/:heroes", heroes)
|
||||
router.GET("/v1/stats/pc/:region/:tag/profile", profile)
|
||||
router.GET("/v1/stats/pc/:region/:tag/complete", stats)
|
||||
router.HEAD("/status", statusHandler)
|
||||
router.GET("/status", statusHandler)
|
||||
|
||||
// Console
|
||||
router.GET("/v1/stats/psn/:tag/heroes/:heroes", injectPlatform("psn", heroes))
|
||||
router.GET("/v1/stats/psn/:tag/profile", injectPlatform("psn", profile))
|
||||
router.GET("/v1/stats/psn/:tag/complete", injectPlatform("psn", stats))
|
||||
router.GET("/v1/stats/xbl/:tag/heroes/:heroes", injectPlatform("xbl", heroes))
|
||||
router.GET("/v1/stats/xbl/:tag/profile", injectPlatform("xbl", profile))
|
||||
router.GET("/v1/stats/xbl/:tag/complete", injectPlatform("xbl", stats))
|
||||
registerVersionOne(router)
|
||||
|
||||
// Version
|
||||
router.GET("/v1/version", versionHandler)
|
||||
registerVersionTwo(router)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
@ -106,55 +107,123 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(*flagBind, c.Handler(router)))
|
||||
}
|
||||
|
||||
func registerVersionOne(router *httprouter.Router) {
|
||||
for _, platform := range platforms {
|
||||
router.GET("/v1/stats/"+platform+"/:region/:tag/heroes/:heroes", injectPlatform(platform, heroes))
|
||||
router.GET("/v1/stats/"+platform+"/:region/:tag/profile", injectPlatform(platform, profile))
|
||||
router.GET("/v1/stats/"+platform+"/:region/:tag/complete", injectPlatform(platform, stats))
|
||||
}
|
||||
|
||||
// Version
|
||||
router.GET("/v1/version", versionHandler)
|
||||
|
||||
router.HEAD("/v1/status", statusHandler)
|
||||
router.GET("/v1/status", statusHandler)
|
||||
}
|
||||
|
||||
func registerVersionTwo(router *httprouter.Router) {
|
||||
for _, platform := range platforms {
|
||||
router.GET("/v2/stats/"+platform+"/:tag/heroes/:heroes", injectPlatform(platform, heroes))
|
||||
router.GET("/v2/stats/"+platform+"/:tag/profile", injectPlatform(platform, profile))
|
||||
router.GET("/v2/stats/"+platform+"/:tag/complete", injectPlatform(platform, stats))
|
||||
router.GET("/v3/stats/"+platform+"/:tag/heroes/:heroes", injectPlatform(platform, heroes))
|
||||
router.GET("/v3/stats/"+platform+"/:tag/profile", injectPlatform(platform, profile))
|
||||
router.GET("/v3/stats/"+platform+"/:tag/complete", injectPlatform(platform, stats))
|
||||
}
|
||||
|
||||
// Version
|
||||
router.GET("/v2/version", versionHandler)
|
||||
router.GET("/v3/version", versionHandler)
|
||||
}
|
||||
|
||||
func loadHeroNames() {
|
||||
stats, err := ovrstat.PCStats("cats-11481")
|
||||
res, err := http.Get("https://playoverwatch.com/en-us/heroes/")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m := make(map[string]bool)
|
||||
defer res.Body.Close()
|
||||
|
||||
for k := range stats.QuickPlayStats.TopHeroes {
|
||||
m[k] = true
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for k := range stats.QuickPlayStats.CareerStats {
|
||||
m[k] = true
|
||||
}
|
||||
links := doc.Find(".hero-portrait-detailed")
|
||||
|
||||
heroNames = make([]string, 0)
|
||||
|
||||
for k := range m {
|
||||
heroNames = append(heroNames, k)
|
||||
links.Each(func(_ int, s *goquery.Selection) {
|
||||
val, exists := s.Attr("data-hero-id")
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
heroNames = append(heroNames, strcase.LowerCamelCase(val))
|
||||
})
|
||||
|
||||
log.Println("Loaded heroes", heroNames)
|
||||
}
|
||||
|
||||
var (
|
||||
versionRegexp = regexp.MustCompile("^/(v\\d+)/")
|
||||
)
|
||||
|
||||
func injectPlatform(platform string, handler httprouter.Handle) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
if platform == "psn" || platform == "xbl" || platform == "nintendo-switch" {
|
||||
platform = ovrstat.PlatformConsole
|
||||
}
|
||||
|
||||
ps = append(ps, httprouter.Param{Key: "platform", Value: platform})
|
||||
|
||||
handler(w, r, ps)
|
||||
ctx := context.Background()
|
||||
|
||||
m := versionRegexp.FindStringSubmatch(r.RequestURI)
|
||||
|
||||
if m != nil {
|
||||
version := VersionOne
|
||||
|
||||
switch m[1] {
|
||||
case "v2":
|
||||
version = VersionTwo
|
||||
case "v3":
|
||||
version = VersionThree
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "version", version)
|
||||
}
|
||||
|
||||
handler(w, r.WithContext(ctx), ps)
|
||||
}
|
||||
}
|
||||
|
||||
func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch.Patch) ([]byte, error) {
|
||||
var (
|
||||
tagRegexp = regexp.MustCompile("-(\\d+)$")
|
||||
)
|
||||
|
||||
func statsResponse(w http.ResponseWriter, r *http.Request, ps httprouter.Params, patch *jsonpatch.Patch) ([]byte, error) {
|
||||
var stats *ovrstat.PlayerStats
|
||||
var err error
|
||||
|
||||
version := VersionOne
|
||||
|
||||
if v := r.Context().Value("version"); v != nil {
|
||||
version = v.(ApiVersion)
|
||||
}
|
||||
|
||||
tag := ps.ByName("tag")
|
||||
|
||||
tag = strings.Replace(tag, "#", "-", -1)
|
||||
|
||||
cacheKey := generateCacheKey(ps)
|
||||
cacheKey := generateCacheKey(r, ps)
|
||||
|
||||
if region := ps.ByName("region"); region != "" {
|
||||
stats, err = ovrstat.PCStats(tag)
|
||||
} else if platform := ps.ByName("platform"); platform != "" {
|
||||
stats, err = ovrstat.ConsoleStats(platform, tag)
|
||||
} else {
|
||||
return nil, errors.New("unknown region/platform")
|
||||
}
|
||||
platform := ps.ByName("platform")
|
||||
|
||||
stats, err = ovrstat.Stats(platform, tag)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -162,7 +231,7 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
||||
|
||||
// Caching of full response for modification
|
||||
|
||||
res, err := client.Get(cacheKey).Bytes()
|
||||
res, err := cacheProvider.Get(cacheKey)
|
||||
|
||||
if res != nil && err == nil {
|
||||
if patch != nil {
|
||||
@ -224,6 +293,46 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
||||
})
|
||||
}
|
||||
|
||||
rating := 0
|
||||
var ratingIcon string
|
||||
|
||||
if len(stats.Ratings) > 0 {
|
||||
if version == VersionThree {
|
||||
m := make(map[string]ovrstat.Rating)
|
||||
|
||||
ratingsPatches := make([]patchOperation, len(stats.Ratings))
|
||||
|
||||
for i, rating := range stats.Ratings {
|
||||
m[rating.Role] = rating
|
||||
ratingsPatches[i] = patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/ratings/" + rating.Role + "/role",
|
||||
}
|
||||
}
|
||||
|
||||
extra = append(extra, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/ratings",
|
||||
}, patchOperation{
|
||||
Op: OpAdd,
|
||||
Path: "/ratings",
|
||||
Value: m,
|
||||
})
|
||||
|
||||
extra = append(extra, ratingsPatches...)
|
||||
}
|
||||
}
|
||||
|
||||
extra = append(extra, patchOperation{
|
||||
Op: OpAdd,
|
||||
Path: "/rating",
|
||||
Value: rating,
|
||||
}, patchOperation{
|
||||
Op: OpAdd,
|
||||
Path: "/ratingIcon",
|
||||
Value: ratingIcon,
|
||||
})
|
||||
|
||||
b, err := json.Marshal(stats)
|
||||
|
||||
if err != nil {
|
||||
@ -245,7 +354,9 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
||||
}
|
||||
|
||||
// Cache response
|
||||
client.Set(cacheKey, b, 10*time.Minute)
|
||||
if cacheTime > 0 {
|
||||
cacheProvider.Set(cacheKey, b, cacheTime)
|
||||
}
|
||||
|
||||
if patch != nil {
|
||||
// Apply filter patch
|
||||
@ -255,163 +366,16 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
||||
return b, err
|
||||
}
|
||||
|
||||
func stats(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
data, err := statsResponse(w, ps, nil)
|
||||
func generateCacheKey(r *http.Request, ps httprouter.Params) string {
|
||||
version := VersionOne
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
if v := r.Context().Value("version"); v != nil {
|
||||
version = v.(ApiVersion)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
return versionToString(version) + "-" + ps.ByName("platform") + "-" + ps.ByName("tag")
|
||||
}
|
||||
|
||||
func profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
cacheKey := generateCacheKey(ps) + "-profile"
|
||||
|
||||
// Check cache for -profile to prevent jsonpatch calls
|
||||
res, err := client.Get(cacheKey).Bytes()
|
||||
|
||||
if res != nil && err == nil {
|
||||
w.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
// Cache result for profile specifically
|
||||
data, err := statsResponse(w, ps, profilePatch)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
client.Set(cacheKey, data, 10*time.Minute)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func heroes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
names := strings.Split(ps.ByName("heroes"), ",")
|
||||
|
||||
if len(names) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
writeError(w, "Name list must contain at least one hero")
|
||||
return
|
||||
}
|
||||
|
||||
nameMap := make(map[string]bool)
|
||||
|
||||
for _, name := range names {
|
||||
nameMap[name] = true
|
||||
}
|
||||
|
||||
ops := make([]patchOperation, 0)
|
||||
|
||||
for _, heroName := range heroNames {
|
||||
if _, exists := nameMap[heroName]; !exists {
|
||||
ops = append(ops, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/quickPlayStats/topHeroes/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/quickPlayStats/careerStats/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/competitiveStats/topHeroes/" + heroName,
|
||||
}, patchOperation{
|
||||
Op: OpRemove,
|
||||
Path: "/competitiveStats/careerStats/" + heroName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
patch, err := patchFromOperations(ops)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create a patch to remove all but specified heroes
|
||||
data, err := statsResponse(w, ps, patch)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func valueOrDefault(m map[string]interface{}, key string, d int64) int64 {
|
||||
if v, ok := m[key]; ok {
|
||||
switch v.(type) {
|
||||
case int64:
|
||||
return v.(int64)
|
||||
case int:
|
||||
return int64(v.(int))
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func patchFromOperations(ops []patchOperation) (*jsonpatch.Patch, error) {
|
||||
patchBytes, err := json.Marshal(ops)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patch, err := jsonpatch.DecodePatch(patchBytes)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &patch, nil
|
||||
}
|
||||
|
||||
type versionObject struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func versionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(&versionObject{Version: Version}); err != nil {
|
||||
writeError(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
type errorObject struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, err string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(&errorObject{Error: err}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func generateCacheKey(ps httprouter.Params) string {
|
||||
var cacheKey string
|
||||
|
||||
tag := ps.ByName("tag")
|
||||
|
||||
if region := ps.ByName("region"); region != "" {
|
||||
cacheKey = "pc-" + region + "-" + tag
|
||||
} else if platform := ps.ByName("platform"); platform != "" {
|
||||
cacheKey = platform + "-" + tag
|
||||
}
|
||||
|
||||
return cacheKey
|
||||
func versionToString(version ApiVersion) string {
|
||||
return fmt.Sprintf("v%d", version)
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/tystuyfzand/ovrstat/ovrstat"
|
||||
"github.com/ow-api/ovrstat/ovrstat"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Stats(t *testing.T) {
|
||||
stats, err := ovrstat.PCStats("us", "cats-11481")
|
||||
stats, err := ovrstat.Stats(ovrstat.PlatformPC, "cats-11481")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -1,10 +1,12 @@
|
||||
fpm -s dir -t deb -p /build/$ARCH/owapi_${VERSION}_${ARCH}.deb \
|
||||
-n owapi -v $VERSION \
|
||||
for arch in $(echo $ARCH | sed "s/,/ /g"); do
|
||||
fpm -s dir -t deb -p /build/owapi_${VERSION}_${arch}.deb \
|
||||
-n ow-api -v $VERSION -a $arch \
|
||||
--deb-priority optional --force \
|
||||
--deb-compression bzip2 \
|
||||
--deb-compression gz \
|
||||
--description "Overwatch API Server" \
|
||||
-m "Tyler Stuyfzand <admin@meow.tf>" --vendor "Meow.tf" \
|
||||
--before-install packaging/scripts/preinst.deb \
|
||||
--after-install packaging/scripts/postinst.deb \
|
||||
-a $ARCH /build/$ARCH/owapi_${ARCH}=/usr/bin/owapi \
|
||||
/build/owapi_linux_${arch}=/usr/bin/owapi \
|
||||
packaging/owapi.service=/lib/systemd/system/owapi.service
|
||||
done
|
@ -1 +1,3 @@
|
||||
curl -X POST "$UPLOAD_URL" -F "file_i386=@/build/i386/owapi_${VERSION}_i386.deb" -F "file_amd64=@/build/amd64/owapi_${VERSION}_amd64.deb" -F "file_armv7=@/build/armv7/owapi_${VERSION}_armv7.deb"
|
||||
curl -X POST "$UPLOAD_URL" -F "file_i386=@/build/i386/owapi_${VERSION}_i386.deb" \
|
||||
-F "file_amd64=@/build/amd64/owapi_${VERSION}_amd64.deb" \
|
||||
-F "file_armv7=@/build/armv7/owapi_${VERSION}_armv7.deb"
|
268
pro/main.go
Normal file
268
pro/main.go
Normal file
@ -0,0 +1,268 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/ow-api/ovrstat/ovrstat"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cloudfrontUrl, err := lookupCloudfrontURL()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Unable to find url:", err)
|
||||
}
|
||||
|
||||
log.Println("URL:", cloudfrontUrl)
|
||||
|
||||
ips, err := lookupIpv6Dns(cloudfrontUrl)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Unable to find ips:", err)
|
||||
}
|
||||
|
||||
log.Println("IPs:", ips)
|
||||
|
||||
localIps, err := checkIpv6Local()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Unable to get local ips")
|
||||
}
|
||||
|
||||
log.Println("Local IPs:", localIps)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
localAddr, err := net.ResolveIPAddr("ip", "")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// You also need to do this to make it work and not give you a
|
||||
// "mismatched local address type ip"
|
||||
// This will make the ResolveIPAddr a TCPAddr without needing to
|
||||
// say what SRC port number to use.
|
||||
localTCPAddr := net.TCPAddr{
|
||||
IP: localAddr.IP,
|
||||
}
|
||||
|
||||
defaultDialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: &localTCPAddr,
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
oldNetwork := network
|
||||
oldAddr := addr
|
||||
|
||||
if addr == "playoverwatch.com:443" {
|
||||
network = "tcp6"
|
||||
addr = "[" + ips[r.Intn(len(ips))] + "]:443"
|
||||
}
|
||||
|
||||
log.Println("Dial using", addr)
|
||||
|
||||
c, err := dialer.DialContext(ctx, network, addr)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Fallback")
|
||||
c, err = defaultDialer.DialContext(ctx, oldNetwork, oldAddr)
|
||||
}
|
||||
|
||||
return c, err
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: "playoverwatch.com",
|
||||
},
|
||||
}
|
||||
|
||||
http.DefaultClient = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
stats, err := ovrstat.Stats(ovrstat.PlatformPC, "cats-11481")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Error retrieving:", err)
|
||||
}
|
||||
|
||||
log.Println(stats.Name+" is level", stats.Endorsement)
|
||||
}
|
||||
|
||||
func lookupCloudfrontURL() (string, error) {
|
||||
res, err := http.Get("https://playoverwatch.com/en-us/")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
|
||||
links := doc.Find("link").Map(func(i int, s *goquery.Selection) string {
|
||||
v, exists := s.Attr("href")
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return v
|
||||
})
|
||||
|
||||
for _, link := range links {
|
||||
u, err := url.Parse(link)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(u.Host, "cloudfront.net") {
|
||||
return u.Host, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no cloudfront url")
|
||||
}
|
||||
|
||||
func lookupIpv6Dns(host string) ([]string, error) {
|
||||
c := &dns.Client{
|
||||
Net: "udp",
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.RecursionDesired = true
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}
|
||||
|
||||
in, _, err := c.Exchange(msg, "8.8.8.8:53")
|
||||
|
||||
result := make([]string, 0)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||
return result, errors.New(dns.RcodeToString[in.Rcode])
|
||||
}
|
||||
|
||||
for _, record := range in.Answer {
|
||||
if t, ok := record.(*dns.AAAA); ok {
|
||||
result = append(result, t.AAAA.String())
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func checkIpv6Local() ([]string, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := make([]string, 0)
|
||||
|
||||
for _, i := range ifaces {
|
||||
addrs, err := i.Addrs()
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
log.Println("Check", ip.String())
|
||||
|
||||
if ip == nil || ip.To4() == nil || isPrivateIP(ip) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Checking", ip.String())
|
||||
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
for ip := ipnet.IP.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||
ret = append(ret, ip.String())
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func inc(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var privateIPBlocks []*net.IPNet
|
||||
|
||||
func init() {
|
||||
for _, cidr := range []string{
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"192.168.0.0/16", // RFC1918
|
||||
"169.254.0.0/16", // RFC3927 link-local
|
||||
"::1/128", // IPv6 loopback
|
||||
"fe80::/10", // IPv6 link-local
|
||||
"fc00::/7", // IPv6 unique local addr
|
||||
} {
|
||||
_, block, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
|
||||
}
|
||||
privateIPBlocks = append(privateIPBlocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, block := range privateIPBlocks {
|
||||
if block.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
58
util.go
Normal file
58
util.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
jsonpatch "git.meow.tf/ow-api/ow-api/json-patch"
|
||||
"github.com/ow-api/ovrstat/ovrstat"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func valueOrDefault(m map[string]interface{}, key string, d int64) int64 {
|
||||
if v, ok := m[key]; ok {
|
||||
switch v.(type) {
|
||||
case int64:
|
||||
return v.(int64)
|
||||
case int:
|
||||
return int64(v.(int))
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
type patchOperation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func patchFromOperations(ops []patchOperation) (*jsonpatch.Patch, error) {
|
||||
patchBytes, err := json.Marshal(ops)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patch, err := jsonpatch.DecodePatch(patchBytes)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &patch, nil
|
||||
}
|
||||
|
||||
type errorObject struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err == ovrstat.ErrPlayerNotFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(&errorObject{Error: err.Error()}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
22
validator/error.go
Normal file
22
validator/error.go
Normal file
@ -0,0 +1,22 @@
|
||||
package validator
|
||||
|
||||
func NewSelectorError(message, parent, selector string) *parentSelectorError {
|
||||
return &parentSelectorError{message, parent, selector}
|
||||
}
|
||||
|
||||
// errorString is a trivial implementation of error.
|
||||
type parentSelectorError struct {
|
||||
message, parent, selector string
|
||||
}
|
||||
|
||||
func (e *parentSelectorError) Error() string {
|
||||
str := e.message + ": "
|
||||
|
||||
if e.parent != "" {
|
||||
str += e.parent + ","
|
||||
}
|
||||
|
||||
str += e.selector
|
||||
|
||||
return str
|
||||
}
|
196
validator/validator.go
Normal file
196
validator/validator.go
Normal file
@ -0,0 +1,196 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://playoverwatch.com/en-us/career"
|
||||
|
||||
apiURL = "https://playoverwatch.com/en-us/search/account-by-name/"
|
||||
)
|
||||
|
||||
type Platform struct {
|
||||
Platform string `json:"platform"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URLName string `json:"urlName"`
|
||||
PlayerLevel int `json:"playerLevel"`
|
||||
Portrait string `json:"portrait"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
}
|
||||
|
||||
var (
|
||||
careerInitRegexp = regexp.MustCompile(`window\.app\.career\.init\((\d+)\,`)
|
||||
|
||||
// Response/server errors
|
||||
errInvalidResponse = errors.New("error querying the response")
|
||||
errUnexpectedStatus = errors.New("unexpected status code")
|
||||
errRequestBlocked = errors.New("request blocked")
|
||||
errNoApiResponse = errors.New("unable to contact api endpoint")
|
||||
errApiInvalidJson = errors.New("invalid json response")
|
||||
errApiUnexpectedStatus = errors.New("unexpected status code from api")
|
||||
|
||||
// Invalid/missing elements
|
||||
errNoMasthead = errors.New("unable to find masthead")
|
||||
errNoCareerInit = errors.New("no career init call found")
|
||||
|
||||
// Element definitions
|
||||
mastheadElements = []string{
|
||||
"img.player-portrait",
|
||||
"div.player-level div.u-vertical-center",
|
||||
"div.player-level",
|
||||
"div.player-rank",
|
||||
"div.EndorsementIcon-tooltip div.u-center",
|
||||
"div.EndorsementIcon",
|
||||
"div.masthead p.masthead-detail.h4 span",
|
||||
}
|
||||
)
|
||||
|
||||
func ValidateEndpoint() error {
|
||||
url := baseURL + "/pc/cats-11481"
|
||||
|
||||
res, err := http.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return errInvalidResponse
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusForbidden {
|
||||
return errRequestBlocked
|
||||
} else if res.StatusCode != http.StatusOK {
|
||||
return errUnexpectedStatus
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
pd, err := goquery.NewDocumentFromReader(res.Body)
|
||||
|
||||
if err != nil {
|
||||
return errInvalidResponse
|
||||
}
|
||||
|
||||
masthead := pd.Find("div.masthead")
|
||||
|
||||
if masthead.Length() < 1 {
|
||||
return errNoMasthead
|
||||
}
|
||||
|
||||
// Validate masthead elements
|
||||
if err := validateElementsExist(masthead.First(), "div.masthead", mastheadElements...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate API response
|
||||
if err := validateApi(strings.Replace(url[strings.LastIndex(url, "/")+1:], "-", "%23", -1)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate specific elements
|
||||
if err := validateElementsExist(pd.Selection, "", "div#quickplay", "div#competitive"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateDetailedStats(pd.Find("div#quickplay").First(), "div#quickplay"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateDetailedStats(pd.Find("div#competitive").First(), "div#competitive"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDetailedStats(s *goquery.Selection, parent string) error {
|
||||
if err := validateElementsExist(s, parent, "div.progress-category", "div.js-stats"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Validate hero stats
|
||||
err = validateHeroStats(s.Find("div.progress-category").Parent(), parent)
|
||||
|
||||
if err != nil {
|
||||
log.Println("No hero stats: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return validateCareerStats(s.Find("div.js-stats").Parent(), parent)
|
||||
}
|
||||
|
||||
func validateHeroStats(s *goquery.Selection, parent string) error {
|
||||
selectors := []string{
|
||||
"div.progress-category", // Top level
|
||||
"div.progress-category div.ProgressBar", // ProgressBar
|
||||
"div.progress-category div.ProgressBar div.ProgressBar-title", // ProgressBar Title
|
||||
"div.progress-category div.ProgressBar div.ProgressBar-description", // ProgressBar Description
|
||||
}
|
||||
|
||||
return validateElementsExist(s, parent, selectors...)
|
||||
}
|
||||
|
||||
func validateCareerStats(careerStatsSelector *goquery.Selection, parent string) error {
|
||||
if err := validateElementsExist(careerStatsSelector, parent, "select option"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectors := []string{
|
||||
"div.row div.js-stats", // Top level
|
||||
"div.row div.js-stats div.column", // stat boxes
|
||||
"div.row div.js-stats div.column .stat-title", // stat boxes
|
||||
"div.row div.js-stats div.column table.DataTable", // data table
|
||||
"div.row div.js-stats div.column table.DataTable tbody", // data table tbody
|
||||
"div.row div.js-stats div.column table.DataTable tbody tr", // data table tbody tr
|
||||
"div.row div.js-stats div.column table.DataTable tbody tr td", // data table tbody tr td
|
||||
}
|
||||
|
||||
return validateElementsExist(careerStatsSelector, parent, selectors...)
|
||||
}
|
||||
|
||||
func validateApi(code string) error {
|
||||
var platforms []Platform
|
||||
|
||||
apires, err := http.Get(apiURL + code)
|
||||
|
||||
if err != nil {
|
||||
return errNoApiResponse
|
||||
}
|
||||
|
||||
defer apires.Body.Close()
|
||||
|
||||
if apires.StatusCode != http.StatusOK {
|
||||
return errApiUnexpectedStatus
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(apires.Body).Decode(&platforms); err != nil {
|
||||
return errApiInvalidJson
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateElementsExist(s *goquery.Selection, parent string, elements ...string) error {
|
||||
var e *goquery.Selection
|
||||
|
||||
for _, selector := range elements {
|
||||
e = s.Find(selector)
|
||||
|
||||
if e.Length() < 1 {
|
||||
return errors.New("unable to find element: " + parent + " " + selector)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
11
validator/validator_test.go
Normal file
11
validator/validator_test.go
Normal file
@ -0,0 +1,11 @@
|
||||
package validator
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_Validator(t *testing.T) {
|
||||
err := ValidateEndpoint()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user