mirror of
https://github.com/sot-tech/mochi.git
synced 2026-06-11 23:43:29 -07:00
Merge pull request #17 from sot-tech/jwt_mod
Reimplement JWT middleware
This commit is contained in:
+4
-5
@@ -1,5 +1,8 @@
|
||||
---
|
||||
run:
|
||||
# mochi in not written with generics (a.t.m),
|
||||
# so we can check with 1.17
|
||||
go: "1.17"
|
||||
timeout: "5m"
|
||||
output:
|
||||
sort-results: true
|
||||
@@ -17,7 +20,6 @@ linters:
|
||||
enable:
|
||||
- "bidichk"
|
||||
- "bodyclose"
|
||||
- "deadcode"
|
||||
- "errcheck"
|
||||
- "errname"
|
||||
- "errorlint"
|
||||
@@ -27,7 +29,6 @@ linters:
|
||||
- "gosec"
|
||||
- "gosimple"
|
||||
- "govet"
|
||||
- "ifshort"
|
||||
- "importas"
|
||||
- "ineffassign"
|
||||
- "makezero"
|
||||
@@ -36,13 +37,11 @@ linters:
|
||||
- "revive"
|
||||
- "rowserrcheck"
|
||||
- "staticcheck"
|
||||
- "structcheck"
|
||||
- "stylecheck"
|
||||
- "tenv"
|
||||
- "typecheck"
|
||||
- "unconvert"
|
||||
- "unused"
|
||||
- "varcheck"
|
||||
- "wastedassign"
|
||||
- "whitespace"
|
||||
issues:
|
||||
@@ -51,4 +50,4 @@ issues:
|
||||
- "EXC0012" # Exported should have comment
|
||||
- "EXC0013" # Package comment should be of form
|
||||
- "EXC0014" # Comment on exported should be of form
|
||||
- "EXC0015" # Should have a package comment
|
||||
- "EXC0015" # Should have a package comment
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
|
||||
@@ -180,15 +180,15 @@ func parseQuery(query string) (q *QueryParams, err error) {
|
||||
|
||||
// String returns a string parsed from a query. Every key can be returned as a
|
||||
// string because they are encoded in the URL as strings.
|
||||
func (qp *QueryParams) String(key string) (string, bool) {
|
||||
value, ok := qp.params[key]
|
||||
func (qp QueryParams) String(key string) (string, bool) {
|
||||
value, ok := qp.params[strings.ToLower(key)]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Uint returns an uint parsed from a query. After being called, it is safe to
|
||||
// cast the uint64 to your desired length.
|
||||
func (qp *QueryParams) Uint(key string, bitSize int) (uint64, error) {
|
||||
str, exists := qp.params[key]
|
||||
func (qp QueryParams) Uint(key string, bitSize int) (uint64, error) {
|
||||
str, exists := qp.params[strings.ToLower(key)]
|
||||
if !exists {
|
||||
return 0, ErrKeyNotFound
|
||||
}
|
||||
@@ -197,17 +197,17 @@ func (qp *QueryParams) Uint(key string, bitSize int) (uint64, error) {
|
||||
}
|
||||
|
||||
// InfoHashes returns a list of requested infohashes.
|
||||
func (qp *QueryParams) InfoHashes() []InfoHash {
|
||||
func (qp QueryParams) InfoHashes() []InfoHash {
|
||||
return qp.infoHashes
|
||||
}
|
||||
|
||||
// RawPath returns the raw path from the parsed URL.
|
||||
func (qp *QueryParams) RawPath() string {
|
||||
func (qp QueryParams) RawPath() string {
|
||||
return qp.path
|
||||
}
|
||||
|
||||
// RawQuery returns the raw query from the parsed URL.
|
||||
func (qp *QueryParams) RawQuery() string {
|
||||
func (qp QueryParams) RawQuery() string {
|
||||
return qp.query
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Package main contains End-to-End MoChi check implementation.
|
||||
// not used in production
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package main contains entry point logic of MoChi server
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
Vendored
+41
-37
@@ -1,3 +1,4 @@
|
||||
# @formatter:off
|
||||
mochi:
|
||||
# The interval communicated with BitTorrent clients informing them how
|
||||
# frequently they should announce in between client events.
|
||||
@@ -207,43 +208,43 @@ mochi:
|
||||
# Dial timeout for establishing new connections.
|
||||
#connect_timeout: 15s
|
||||
|
||||
# This block defines configuration used for PostgreSQL storage.
|
||||
# example `mo_peers` table structure:
|
||||
# - info_hash bytea
|
||||
# - peer_id bytea
|
||||
# - address inet or bytea
|
||||
# - port int4
|
||||
# - is_seeder bool
|
||||
# - is_v6 bool
|
||||
# - created timestamp
|
||||
#storage:
|
||||
#name: pg
|
||||
#config:
|
||||
# connection string to pg storage. may be URL (postgres://...) or DSN (host=... port=...)
|
||||
#connection_string: host=127.0.0.1 database=test user=postgres pool_max_conns=50
|
||||
# query and parameters for announce operation
|
||||
#announce:
|
||||
#query: SELECT peer_id, address, port FROM mo_peers WHERE info_hash=$1 AND is_seeder=$2 AND is_v6=$3 LIMIT $4
|
||||
#peer_id_column: peer_id
|
||||
#address_column: address
|
||||
#port_column: port
|
||||
#peer:
|
||||
# expected parameters: 1 - info hash (bytea), 2 - peer id (bytea), 3 - ip address (bytea/inet)
|
||||
# 4 - port (int), 5 - is seeder (bool), 6 - is IPv6 (bool), 7 - create date and time (timestamp)
|
||||
#add_query: INSERT INTO mo_peers VALUES($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (info_hash, peer_id, address, port) DO UPDATE SET created = EXCLUDED.created, is_seeder = EXCLUDED.is_seeder
|
||||
#del_query: DELETE FROM mo_peers WHERE info_hash=$1 AND peer_id=$2 AND address=$3 AND port=$4 AND is_seeder=$5
|
||||
#graduate_query: UPDATE mo_peers SET is_seeder=TRUE WHERE info_hash=$1 AND peer_id=$2 AND address=$3 AND port=$4 AND NOT is_seeder
|
||||
#count_query: SELECT COUNT(1) FILTER (WHERE is_seeder) AS seeders, COUNT(1) FILTER (WHERE NOT is_seeder) AS leechers FROM mo_peers
|
||||
# predicate part of `count_query` for get count of peers by info hash
|
||||
#by_info_hash_clause: WHERE info_hash = $1
|
||||
#count_seeders_column: seeders
|
||||
#count_leechers_column: leechers
|
||||
# queries for KV-store
|
||||
#data:
|
||||
# expected parameters: 1 - context (varchar), 2 - name (bytea), 3 - value (bytea)
|
||||
#add_query: INSERT INTO mo_kv VALUES($1, $2, $3) ON CONFLICT (context, name) DO NOTHING
|
||||
#del_query: DELETE FROM mo_kv WHERE context=$1 AND name=$2
|
||||
#get_query: SELECT value FROM mo_kv WHERE context=$1 AND name=$2
|
||||
# This block defines configuration used for PostgreSQL storage.
|
||||
# example `mo_peers` table structure:
|
||||
# - info_hash bytea
|
||||
# - peer_id bytea
|
||||
# - address inet or bytea
|
||||
# - port int4
|
||||
# - is_seeder bool
|
||||
# - is_v6 bool
|
||||
# - created timestamp
|
||||
#storage:
|
||||
#name: pg
|
||||
#config:
|
||||
# connection string to pg storage. may be URL (postgres://...) or DSN (host=... port=...)
|
||||
#connection_string: host=127.0.0.1 database=test user=postgres pool_max_conns=50
|
||||
# query and parameters for announce operation
|
||||
#announce:
|
||||
#query: SELECT peer_id, address, port FROM mo_peers WHERE info_hash=$1 AND is_seeder=$2 AND is_v6=$3 LIMIT $4
|
||||
#peer_id_column: peer_id
|
||||
#address_column: address
|
||||
#port_column: port
|
||||
#peer:
|
||||
# expected parameters: 1 - info hash (bytea), 2 - peer id (bytea), 3 - ip address (bytea/inet)
|
||||
# 4 - port (int), 5 - is seeder (bool), 6 - is IPv6 (bool), 7 - create date and time (timestamp)
|
||||
#add_query: INSERT INTO mo_peers VALUES($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (info_hash, peer_id, address, port) DO UPDATE SET created = EXCLUDED.created, is_seeder = EXCLUDED.is_seeder
|
||||
#del_query: DELETE FROM mo_peers WHERE info_hash=$1 AND peer_id=$2 AND address=$3 AND port=$4 AND is_seeder=$5
|
||||
#graduate_query: UPDATE mo_peers SET is_seeder=TRUE WHERE info_hash=$1 AND peer_id=$2 AND address=$3 AND port=$4 AND NOT is_seeder
|
||||
#count_query: SELECT COUNT(1) FILTER (WHERE is_seeder) AS seeders, COUNT(1) FILTER (WHERE NOT is_seeder) AS leechers FROM mo_peers
|
||||
# predicate part of `count_query` for get count of peers by info hash
|
||||
#by_info_hash_clause: WHERE info_hash = $1
|
||||
#count_seeders_column: seeders
|
||||
#count_leechers_column: leechers
|
||||
# queries for KV-store
|
||||
#data:
|
||||
# expected parameters: 1 - context (varchar), 2 - name (bytea), 3 - value (bytea)
|
||||
#add_query: INSERT INTO mo_kv VALUES($1, $2, $3) ON CONFLICT (context, name) DO NOTHING
|
||||
#del_query: DELETE FROM mo_kv WHERE context=$1 AND name=$2
|
||||
#get_query: SELECT value FROM mo_kv WHERE context=$1 AND name=$2
|
||||
# query for check if database is alive
|
||||
#ping_query: SELECT 1
|
||||
# query for garbage collection, expected parameter is timestamp
|
||||
@@ -264,10 +265,13 @@ mochi:
|
||||
prehooks:
|
||||
# - name: jwt
|
||||
# options:
|
||||
# header: "authorization"
|
||||
# issuer: "https://issuer.com"
|
||||
# audience: "https://some.issuer.com"
|
||||
# jwk_set_url: "https://issuer.com/keys"
|
||||
# jwk_set_update_interval: 5m
|
||||
# handle_announce: true
|
||||
# handle_scrape: false
|
||||
#
|
||||
# - name: client approval
|
||||
# options:
|
||||
|
||||
@@ -47,7 +47,7 @@ Implementation expects next data types:
|
||||
* value - byte array (`bytea`)
|
||||
|
||||
(*) in KV table `name` present as byte array because of possibility
|
||||
to place hash as _raw_ string, which is not supported by PostgreSQL.
|
||||
to place hash as _raw_ string, which is not supported by PostgreSQL.
|
||||
|
||||
Sample script to create tables:
|
||||
|
||||
|
||||
@@ -3,25 +3,24 @@ module github.com/sot-tech/mochi
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220601181242-ac2da19efd60
|
||||
github.com/SermoDigital/jose v0.9.2-0.20180104203859-803625baeddc
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220830191835-c1b067f33eca
|
||||
github.com/MicahParks/keyfunc v1.2.2
|
||||
github.com/anacrolix/torrent v1.46.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/jackc/pgx/v4 v4.16.1
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/jackc/pgx/v4 v4.17.2
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/libp2p/go-reuseport v0.2.0
|
||||
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103
|
||||
github.com/minio/sha256-simd v1.0.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.12.2
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anacrolix/dht/v2 v2.18.0 // indirect
|
||||
github.com/anacrolix/dht/v2 v2.18.1 // indirect
|
||||
github.com/anacrolix/log v0.13.2-0.20220426014722-7b7d13a55d55 // indirect
|
||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||
github.com/anacrolix/missinggo/v2 v2.7.0 // indirect
|
||||
@@ -34,23 +33,23 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.12.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.11.0 // indirect
|
||||
github.com/jackc/puddle v1.2.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/puddle v1.3.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
)
|
||||
|
||||
@@ -30,19 +30,19 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220601181242-ac2da19efd60 h1:hBycJRDauXKMEAOl90iFjTqrQ4VNGb918x3lLLDXBBU=
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220601181242-ac2da19efd60/go.mod h1:HLP7HKUU1eqMAGMk247yT91tDDi4xxnehkyXh6hGcr0=
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220830191835-c1b067f33eca h1:snlCH49dbxeL78CFFgSWhqeQ8UY3mCp9/utxkOGhKPM=
|
||||
code.cloudfoundry.org/go-diodes v0.0.0-20220830191835-c1b067f33eca/go.mod h1:02VWjyNigcAIscOkao4SSaMv9siy/gQMwsLHuoaKVIQ=
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
|
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/MicahParks/keyfunc v1.2.2 h1:MtA/6bnmhcyDMarVF9QRQmfKg1ssKxH1C+k3Gw8DY1g=
|
||||
github.com/MicahParks/keyfunc v1.2.2/go.mod h1:GWZYIBflWXRPShQPMFRrUg+8buvyD0IWeg3Fi2rcrME=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/SermoDigital/jose v0.9.2-0.20180104203859-803625baeddc h1:MhBvG7RLaLqlyjxMR6of35vt6MVQ+eXMcgn9X/sy0FE=
|
||||
github.com/SermoDigital/jose v0.9.2-0.20180104203859-803625baeddc/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -50,8 +50,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anacrolix/dht/v2 v2.18.0 h1:btjVjzjKqO5nKGbJHJ2UmuwiRx+EgX3e+OCHC9+WRz8=
|
||||
github.com/anacrolix/dht/v2 v2.18.0/go.mod h1:mxrSeP/LIY429SgWMO9o6UdjBjB8ZjBh6HHCmd8Ly1g=
|
||||
github.com/anacrolix/dht/v2 v2.18.1 h1:0y/2HoD1VzhJ4o/XDtcTwKGDc5rotFmpV+bEC48ZAd8=
|
||||
github.com/anacrolix/dht/v2 v2.18.1/go.mod h1:mxrSeP/LIY429SgWMO9o6UdjBjB8ZjBh6HHCmd8Ly1g=
|
||||
github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
|
||||
github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
|
||||
@@ -146,6 +146,9 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -188,7 +191,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -227,8 +230,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=
|
||||
github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=
|
||||
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
|
||||
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
@@ -244,27 +247,27 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y=
|
||||
github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
|
||||
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs=
|
||||
github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
|
||||
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
|
||||
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
|
||||
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
|
||||
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.2.1 h1:gI8os0wpRXFd4FiAY2dWiqRK037tjj3t7rKFeO4X5iw=
|
||||
github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -280,8 +283,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -303,17 +306,17 @@ github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7z
|
||||
github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 h1:Z/i1e+gTZrmcGeZyWckaLfucYG6KYOXLWo4co8pZYNY=
|
||||
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103/go.mod h1:o9YPB5aGP8ob35Vy6+vyq3P3bWe7NQWzf+JLiXCiMaE=
|
||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -332,7 +335,7 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
@@ -350,8 +353,8 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
|
||||
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -373,19 +376,20 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
|
||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
@@ -450,8 +454,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -515,9 +520,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -584,8 +590,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
|
||||
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -652,7 +659,6 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
|
||||
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -731,8 +737,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -748,7 +754,6 @@ 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.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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=
|
||||
|
||||
+216
-162
@@ -1,25 +1,19 @@
|
||||
// Package jwt implements a Hook that fails an Announce if the client's request
|
||||
// Package jwt implements a Hook that fails on Announce or Scrape if the client's request
|
||||
// is missing a valid JSON Web Token.
|
||||
//
|
||||
// JWTs are validated against the standard claims in RFC7519 along with an
|
||||
// extra "infohash" claim that verifies the client has access to the Swarm.
|
||||
// RS256 keys are asychronously rotated from a provided JWK Set HTTP endpoint.
|
||||
// extra "infohash(es)" claim that verifies the client has access to the Swarm.
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jc "github.com/SermoDigital/jose/crypto"
|
||||
"github.com/SermoDigital/jose/jws"
|
||||
"github.com/SermoDigital/jose/jwt"
|
||||
"github.com/mendsley/gojwk"
|
||||
"github.com/MicahParks/keyfunc"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
"github.com/sot-tech/mochi/middleware"
|
||||
@@ -30,7 +24,11 @@ import (
|
||||
)
|
||||
|
||||
// Name is the name by which this middleware is registered with Conf.
|
||||
const Name = "jwt"
|
||||
const (
|
||||
Name = "jwt"
|
||||
authorizationHeader = "authorization"
|
||||
bearerAuthPrefix = "bearer "
|
||||
)
|
||||
|
||||
func init() {
|
||||
middleware.RegisterBuilder(Name, build)
|
||||
@@ -44,192 +42,248 @@ var (
|
||||
// ErrInvalidJWT is returned when a JWT fails to verify.
|
||||
ErrInvalidJWT = bittorrent.ClientError("unapproved request: invalid jwt")
|
||||
|
||||
errInvalidInfoHashClaim = errors.New("claim \"infohash\" is invalid")
|
||||
errJWKsNotSet = errors.New("required parameters not provided: Issuer/Audience/JWKSetURL")
|
||||
|
||||
errInvalidKid = errors.New("invalid kid")
|
||||
|
||||
errUnknownKidSigner = errors.New("signed by unknown kid")
|
||||
hmacAlgorithms = jwt.WithValidMethods([]string{
|
||||
jwt.SigningMethodHS256.Alg(), jwt.SigningMethodHS384.Alg(), jwt.SigningMethodHS512.Alg(),
|
||||
jwt.SigningMethodRS256.Alg(), jwt.SigningMethodRS384.Alg(), jwt.SigningMethodRS512.Alg(),
|
||||
jwt.SigningMethodPS256.Alg(), jwt.SigningMethodPS384.Alg(), jwt.SigningMethodPS512.Alg(),
|
||||
jwt.SigningMethodES256.Alg(), jwt.SigningMethodES384.Alg(), jwt.SigningMethodES512.Alg(),
|
||||
jwt.SigningMethodEdDSA.Alg(),
|
||||
})
|
||||
)
|
||||
|
||||
// Config represents all the values required by this middleware to fetch JWKs
|
||||
// and verify JWTs.
|
||||
type Config struct {
|
||||
Header string
|
||||
Issuer string
|
||||
Audience string
|
||||
JWKSetURL string `cfg:"jwk_set_url"`
|
||||
JWKUpdateInterval time.Duration `cfg:"jwk_set_update_interval"`
|
||||
HandleAnnounce bool `cfg:"handle_announce"`
|
||||
HandleScrape bool `cfg:"handle_scrape"`
|
||||
}
|
||||
|
||||
type hook struct {
|
||||
cfg Config
|
||||
publicKeys map[string]crypto.PublicKey
|
||||
closing chan struct{}
|
||||
cfg Config
|
||||
jwks *keyfunc.JWKS
|
||||
}
|
||||
|
||||
func build(options conf.MapConfig, _ storage.PeerStorage) (middleware.Hook, error) {
|
||||
func build(options conf.MapConfig, _ storage.PeerStorage) (h middleware.Hook, err error) {
|
||||
var cfg Config
|
||||
|
||||
if err := options.Unmarshal(&cfg); err != nil {
|
||||
logger.Debug().Object("options", options).Msg("creating new JWT middleware")
|
||||
|
||||
if err = options.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("middleware %s: %w", Name, err)
|
||||
}
|
||||
|
||||
logger.Debug().Object("options", options).Msg("creating new JWT middleware")
|
||||
h := &hook{
|
||||
cfg: cfg,
|
||||
publicKeys: map[string]crypto.PublicKey{},
|
||||
closing: make(chan struct{}),
|
||||
}
|
||||
if len(cfg.JWKSetURL) > 0 && len(cfg.Issuer) > 0 && len(cfg.Audience) > 0 {
|
||||
if len(cfg.Header) == 0 {
|
||||
cfg.Header = authorizationHeader
|
||||
logger.Warn().
|
||||
Str("name", "Header").
|
||||
Str("default", cfg.Header).
|
||||
Msg("falling back to default configuration")
|
||||
}
|
||||
|
||||
logger.Debug().Msg("performing initial fetch of JWKs")
|
||||
if err := h.updateKeys(); err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch initial JWK Set: %w", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-h.closing:
|
||||
return
|
||||
case <-time.After(cfg.JWKUpdateInterval):
|
||||
logger.Debug().Msg("performing fetch of JWKs")
|
||||
_ = h.updateKeys()
|
||||
var jwks *keyfunc.JWKS
|
||||
if cfg.HandleAnnounce || cfg.HandleScrape {
|
||||
jwks, err = keyfunc.Get(cfg.JWKSetURL, keyfunc.Options{
|
||||
Ctx: context.Background(),
|
||||
RefreshErrorHandler: func(err error) {
|
||||
logger.Error().Err(err).Msg("error occurred while updating JWKs")
|
||||
},
|
||||
RefreshInterval: cfg.JWKUpdateInterval,
|
||||
RefreshUnknownKID: true,
|
||||
})
|
||||
} else {
|
||||
logger.Warn().Msg("both announce and scrape handle disabled")
|
||||
}
|
||||
if err == nil {
|
||||
h = &hook{
|
||||
cfg: cfg,
|
||||
jwks: jwks,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *hook) updateKeys() error {
|
||||
resp, err := http.Get(h.cfg.JWKSetURL)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to fetch JWK Set")
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var parsedJWKs gojwk.Key
|
||||
err = json.NewDecoder(resp.Body).Decode(&parsedJWKs)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to decode JWK JSON")
|
||||
return err
|
||||
} else {
|
||||
err = errJWKsNotSet
|
||||
}
|
||||
|
||||
keys := map[string]crypto.PublicKey{}
|
||||
for _, parsedJWK := range parsedJWKs.Keys {
|
||||
publicKey, err := parsedJWK.DecodePublicKey()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("failed to decode JWK into public key")
|
||||
return err
|
||||
}
|
||||
keys[parsedJWK.Kid] = publicKey
|
||||
}
|
||||
h.publicKeys = keys
|
||||
|
||||
logger.Debug().Msg("successfully fetched JWK Set")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (h *hook) Stop() stop.Result {
|
||||
logger.Debug().Msg("attempting to shutdown JWT middleware")
|
||||
select {
|
||||
case <-h.closing:
|
||||
return stop.AlreadyStopped
|
||||
default:
|
||||
}
|
||||
c := make(stop.Channel)
|
||||
go func() {
|
||||
close(h.closing)
|
||||
c.Done()
|
||||
}()
|
||||
if h.jwks != nil {
|
||||
go h.jwks.EndBackground()
|
||||
}
|
||||
return c.Result()
|
||||
}
|
||||
|
||||
type verifiableClaims interface {
|
||||
jwt.Claims
|
||||
VerifyIssuer(iss string, req bool) bool
|
||||
GetIssuer() string
|
||||
VerifyAudience(aud string, req bool) bool
|
||||
GetAudience() []string
|
||||
}
|
||||
|
||||
type registeredClaimsWrapper struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (rc registeredClaimsWrapper) GetIssuer() string {
|
||||
return rc.Issuer
|
||||
}
|
||||
|
||||
func (rc registeredClaimsWrapper) GetAudience() []string {
|
||||
return rc.Audience
|
||||
}
|
||||
|
||||
type announceClaims struct {
|
||||
registeredClaimsWrapper
|
||||
InfoHash string `json:"infohash,omitempty"`
|
||||
}
|
||||
|
||||
func (h *hook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, _ *bittorrent.AnnounceResponse) (context.Context, error) {
|
||||
if req.Params == nil {
|
||||
return ctx, ErrMissingJWT
|
||||
if !h.cfg.HandleAnnounce {
|
||||
return ctx, nil
|
||||
}
|
||||
var err error
|
||||
|
||||
jwtParam, ok := req.Params.String("jwt")
|
||||
if !ok {
|
||||
return ctx, ErrMissingJWT
|
||||
}
|
||||
|
||||
if err := validateJWT(req.InfoHash, []byte(jwtParam), h.cfg.Issuer, h.cfg.Audience, h.publicKeys); err != nil {
|
||||
return ctx, ErrInvalidJWT
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (h *hook) HandleScrape(ctx context.Context, _ *bittorrent.ScrapeRequest, _ *bittorrent.ScrapeResponse) (context.Context, error) {
|
||||
// Scrapes don't require any protection.
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func validateJWT(ih bittorrent.InfoHash, jwtBytes []byte, cfgIss, cfgAud string, publicKeys map[string]crypto.PublicKey) error {
|
||||
parsedJWT, err := jws.ParseJWT(jwtBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
claims := parsedJWT.Claims()
|
||||
if iss, ok := claims.Issuer(); !ok || iss != cfgIss {
|
||||
logger.Debug().
|
||||
Bool("exists", ok).
|
||||
Str("claim", iss).
|
||||
Str("config", cfgIss).
|
||||
Msg("unequal or missing issuer when validating JWT")
|
||||
return jwt.ErrInvalidISSClaim
|
||||
}
|
||||
|
||||
if auds, ok := claims.Audience(); !ok || !in(cfgAud, auds) {
|
||||
logger.Debug().
|
||||
Bool("exists", ok).
|
||||
Strs("claim", auds).
|
||||
Str("config", cfgAud).
|
||||
Msg("unequal or missing audience when validating JWT")
|
||||
return jwt.ErrInvalidAUDClaim
|
||||
}
|
||||
|
||||
ihHex := hex.EncodeToString([]byte(ih))
|
||||
if ihClaim, ok := claims.Get("infohash").(string); !ok || ihClaim != ihHex {
|
||||
logger.Debug().
|
||||
Bool("exists", ok).
|
||||
Str("claim", ihClaim).
|
||||
Str("request", ihHex).
|
||||
Msg("unequal or missing infohash when validating JWT")
|
||||
return errInvalidInfoHashClaim
|
||||
}
|
||||
|
||||
parsedJWS := parsedJWT.(jws.JWS)
|
||||
kid, ok := parsedJWS.Protected().Get("kid").(string)
|
||||
if !ok {
|
||||
logger.Debug().
|
||||
Bool("exists", ok).
|
||||
Str("claim", kid).
|
||||
Msg("missing kid when validating JWT")
|
||||
return errInvalidKid
|
||||
}
|
||||
publicKey, ok := publicKeys[kid]
|
||||
if !ok {
|
||||
logger.Debug().Str("claim", kid).Msg("missing public key forkid when validating JWT")
|
||||
return errUnknownKidSigner
|
||||
}
|
||||
|
||||
err = parsedJWS.Verify(publicKey, jc.SigningMethodRS256)
|
||||
if err != nil {
|
||||
logger.Debug().Err(err).Msg("failed to verify signature of JWT")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func in(x string, xs []string) bool {
|
||||
for _, y := range xs {
|
||||
if x == y {
|
||||
return true
|
||||
if jwtParam := h.getJWTString(req.Params); len(jwtParam) == 0 {
|
||||
err = ErrMissingJWT
|
||||
} else {
|
||||
claims := new(announceClaims)
|
||||
if errs := h.validateBaseJWT(jwtParam, claims); len(errs) > 0 {
|
||||
logger.Info().
|
||||
Errs("errors", errs).
|
||||
Object("source", req.RequestPeer).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
} else {
|
||||
var claimIH bittorrent.InfoHash
|
||||
if claimIH, err = bittorrent.NewInfoHash(claims.InfoHash); err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Object("source", req.RequestPeer).
|
||||
Msg("'infohash' claim parse failed")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
if req.InfoHash != claimIH {
|
||||
logger.Info().
|
||||
Stringer("claimInfoHash", claimIH).
|
||||
Stringer("requestInfoHash", req.InfoHash).
|
||||
Object("peer", req.RequestPeer).
|
||||
Msg("unequal 'infohash' claim when validating JWT")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
type scrapeClaims struct {
|
||||
registeredClaimsWrapper
|
||||
InfoHashes []string `json:"infohashes,omitempty"`
|
||||
}
|
||||
|
||||
func (h *hook) HandleScrape(ctx context.Context, req *bittorrent.ScrapeRequest, _ *bittorrent.ScrapeResponse) (context.Context, error) {
|
||||
if !h.cfg.HandleScrape {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
if jwtParam := h.getJWTString(req.Params); len(jwtParam) == 0 {
|
||||
err = ErrMissingJWT
|
||||
} else {
|
||||
claims := new(scrapeClaims)
|
||||
if errs := h.validateBaseJWT(jwtParam, claims); len(errs) > 0 {
|
||||
logger.Info().
|
||||
Errs("errors", errs).
|
||||
Array("source", req.RequestAddresses).
|
||||
Msg("JWT validation failed")
|
||||
err = ErrInvalidJWT
|
||||
} else {
|
||||
var claimIHs bittorrent.InfoHashes
|
||||
for _, s := range claims.InfoHashes {
|
||||
if providedIh, err := bittorrent.NewInfoHash(s); err == nil {
|
||||
claimIHs = append(claimIHs, providedIh)
|
||||
} else {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Array("addresses", req.RequestAddresses).
|
||||
Msg("'infohashes' claim parse failed")
|
||||
}
|
||||
}
|
||||
eq := len(req.InfoHashes) == len(claimIHs)
|
||||
if eq {
|
||||
for _, rIH := range req.InfoHashes {
|
||||
found := false
|
||||
for _, cIH := range claimIHs {
|
||||
if rIH == cIH {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
eq = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !eq {
|
||||
logger.Info().
|
||||
Array("claimInfoHashes", claimIHs).
|
||||
Array("requestInfoHashes", req.InfoHashes).
|
||||
Array("addresses", req.RequestAddresses).
|
||||
Msg("unequal 'infohashes' claim when validating JWT")
|
||||
err = ErrInvalidJWT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
func (h *hook) getJWTString(params bittorrent.Params) (jwt string) {
|
||||
if params != nil {
|
||||
var found bool
|
||||
if jwt, found = params.String(h.cfg.Header); found {
|
||||
if strings.HasPrefix(strings.ToLower(jwt), bearerAuthPrefix) {
|
||||
jwt = jwt[len(bearerAuthPrefix):]
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *hook) validateBaseJWT(jwtParam string, claims verifiableClaims) (errs []error) {
|
||||
if _, err := jwt.ParseWithClaims(jwtParam, claims, h.jwks.Keyfunc, hmacAlgorithms); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := claims.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if !claims.VerifyIssuer(h.cfg.Issuer, true) {
|
||||
logger.Debug().
|
||||
Str("provided", claims.GetIssuer()).
|
||||
Str("required", h.cfg.Issuer).
|
||||
Msg("unequal or missing issuer when validating JWT")
|
||||
errs = append(errs, jwt.ErrTokenInvalidIssuer)
|
||||
}
|
||||
if !claims.VerifyAudience(h.cfg.Audience, true) {
|
||||
logger.Debug().
|
||||
Strs("provided", claims.GetAudience()).
|
||||
Str("required", h.cfg.Audience).
|
||||
Msg("unequal or missing audience when validating JWT")
|
||||
errs = append(errs, jwt.ErrTokenInvalidAudience)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/minio/sha256-simd"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
"github.com/sot-tech/mochi/pkg/conf"
|
||||
"github.com/sot-tech/mochi/pkg/log"
|
||||
_ "github.com/sot-tech/mochi/pkg/randseed"
|
||||
)
|
||||
|
||||
const (
|
||||
privKeyPEM = `
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCI7Zc2IUKazCBCK5VY
|
||||
WxxE6lVhGR+exaWgrh0Yq9t4gQ==
|
||||
-----END PRIVATE KEY-----
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
privKey *ecdsa.PrivateKey
|
||||
infoHash bittorrent.InfoHash
|
||||
jwksData JWKSKeys
|
||||
)
|
||||
|
||||
type JWKSKey struct {
|
||||
KeyType string `json:"kty"`
|
||||
Usage string `json:"use"`
|
||||
KeyID string `json:"kid"`
|
||||
Algorithm string `json:"alg"`
|
||||
Curve string `json:"crv"`
|
||||
X string `json:"x"`
|
||||
Y string `json:"y"`
|
||||
}
|
||||
|
||||
type JWKSKeys struct {
|
||||
Keys []JWKSKey `json:"keys"`
|
||||
}
|
||||
|
||||
type params map[string]string
|
||||
|
||||
func (p params) String(key string) (out string, found bool) {
|
||||
out, found = p[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (params) RawPath() (s string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (params) RawQuery() (s string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (params) MarshalZerologObject(*zerolog.Event) {}
|
||||
|
||||
func init() {
|
||||
_ = log.ConfigureLogger("", "info", false, false)
|
||||
privKey, _ = jwt.ParseECPrivateKeyFromPEM([]byte(privKeyPEM))
|
||||
ihBytes := make([]byte, bittorrent.InfoHashV1Len)
|
||||
rand.Read(ihBytes)
|
||||
infoHash, _ = bittorrent.NewInfoHash(ihBytes)
|
||||
s2 := sha256.New()
|
||||
s2.Write(elliptic.Marshal(privKey.PublicKey.Curve, privKey.PublicKey.X, privKey.PublicKey.Y))
|
||||
jwksData = JWKSKeys{Keys: []JWKSKey{
|
||||
{
|
||||
KeyType: "EC",
|
||||
Usage: "sig",
|
||||
KeyID: base64.RawURLEncoding.EncodeToString(s2.Sum(nil)),
|
||||
Algorithm: jwt.SigningMethodES256.Name,
|
||||
Curve: privKey.Curve.Params().Name,
|
||||
X: base64.RawURLEncoding.EncodeToString(privKey.PublicKey.X.Bytes()),
|
||||
Y: base64.RawURLEncoding.EncodeToString(privKey.PublicKey.Y.Bytes()),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestHook_HandleAnnounceValid(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(jwksData)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, announceClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
},
|
||||
InfoHash: infoHash.String(),
|
||||
})
|
||||
|
||||
token.Header["kid"] = jwksData.Keys[0].KeyID
|
||||
tokenString, err := token.SignedString(privKey)
|
||||
require.Nil(t, err)
|
||||
//goland:noinspection HttpUrlsUsage
|
||||
cfg := conf.MapConfig{
|
||||
"handle_announce": true,
|
||||
"issuer": "CN=test",
|
||||
"audience": "test",
|
||||
"jwk_set_url": "http://" + s.Listener.Addr().String(),
|
||||
"jwk_set_update_interval": time.Minute,
|
||||
}
|
||||
h, err := build(cfg, nil)
|
||||
require.Nil(t, err)
|
||||
data := make(params)
|
||||
data[authorizationHeader] = bearerAuthPrefix + tokenString
|
||||
_, err = h.HandleAnnounce(context.Background(), &bittorrent.AnnounceRequest{
|
||||
InfoHash: infoHash,
|
||||
RequestPeer: bittorrent.RequestPeer{},
|
||||
Params: data,
|
||||
}, nil)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestHook_HandleAnnounceInvalid(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(jwksData)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
// now we wll use HMAC-SHA256 with invalid random key
|
||||
// all errors should be nil except announce request
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, announceClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
},
|
||||
InfoHash: infoHash.String(),
|
||||
})
|
||||
|
||||
token.Header["kid"] = jwksData.Keys[0].KeyID
|
||||
k := make([]byte, 20)
|
||||
rand.Read(k)
|
||||
tokenString, err := token.SignedString(k)
|
||||
require.Nil(t, err)
|
||||
//goland:noinspection HttpUrlsUsage
|
||||
cfg := conf.MapConfig{
|
||||
"handle_announce": true,
|
||||
"header": "jwt",
|
||||
"issuer": "CN=test",
|
||||
"audience": "test",
|
||||
"jwk_set_url": "http://" + s.Listener.Addr().String(),
|
||||
"jwk_set_update_interval": time.Minute,
|
||||
}
|
||||
h, err := build(cfg, nil)
|
||||
require.Nil(t, err)
|
||||
data := make(params)
|
||||
data["jwt"] = tokenString
|
||||
_, err = h.HandleAnnounce(context.Background(), &bittorrent.AnnounceRequest{
|
||||
InfoHash: infoHash,
|
||||
RequestPeer: bittorrent.RequestPeer{},
|
||||
Params: data,
|
||||
}, nil)
|
||||
require.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestHook_HandleScrapeValid(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(jwksData)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
ihs := make(bittorrent.InfoHashes, rand.Intn(10)+1)
|
||||
ihss := make([]string, len(ihs))
|
||||
for i := range ihs {
|
||||
bb := []byte(infoHash)
|
||||
bb[i] = byte(i)
|
||||
ihs[i] = bittorrent.InfoHash(bb)
|
||||
ihss[i] = ihs[i].String()
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, scrapeClaims{
|
||||
registeredClaimsWrapper: registeredClaimsWrapper{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "CN=test",
|
||||
Subject: "CN=test",
|
||||
Audience: []string{"test"},
|
||||
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(time.Hour)},
|
||||
NotBefore: &jwt.NumericDate{Time: time.Now().Add(-time.Hour)},
|
||||
ID: strconv.FormatInt(rand.Int63(), 16),
|
||||
},
|
||||
},
|
||||
InfoHashes: ihss,
|
||||
})
|
||||
|
||||
token.Header["kid"] = jwksData.Keys[0].KeyID
|
||||
tokenString, err := token.SignedString(privKey)
|
||||
require.Nil(t, err)
|
||||
//goland:noinspection HttpUrlsUsage
|
||||
cfg := conf.MapConfig{
|
||||
"handle_scrape": true,
|
||||
"issuer": "CN=test",
|
||||
"audience": "test",
|
||||
"jwk_set_url": "http://" + s.Listener.Addr().String(),
|
||||
"jwk_set_update_interval": time.Minute,
|
||||
}
|
||||
h, err := build(cfg, nil)
|
||||
require.Nil(t, err)
|
||||
data := make(params)
|
||||
data[authorizationHeader] = bearerAuthPrefix + tokenString
|
||||
_, err = h.HandleScrape(context.Background(), &bittorrent.ScrapeRequest{
|
||||
InfoHashes: ihs,
|
||||
RequestAddresses: bittorrent.RequestAddresses{},
|
||||
Params: data,
|
||||
}, nil)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package random
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
)
|
||||
|
||||
// DeriveEntropyFromRequest generates 2*64 bits of pseudo random state from an
|
||||
// AnnounceRequest.
|
||||
//
|
||||
// Calling DeriveEntropyFromRequest multiple times yields the same values.
|
||||
func DeriveEntropyFromRequest(req *bittorrent.AnnounceRequest) (v0 uint64, v1 uint64) {
|
||||
if len(req.InfoHash) >= bittorrent.InfoHashV1Len {
|
||||
v0 = binary.BigEndian.Uint64([]byte(req.InfoHash[:8])) + binary.BigEndian.Uint64([]byte(req.InfoHash[8:16]))
|
||||
}
|
||||
v1 = binary.BigEndian.Uint64(req.ID[:8]) + binary.BigEndian.Uint64(req.ID[8:16])
|
||||
return
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Package random implements the XORShift PRNG and a way to derive random state
|
||||
// from an AnnounceRequest.
|
||||
package random
|
||||
|
||||
// GenerateAndAdvance applies XORShift128Plus on s0 and s1, returning
|
||||
// the new states newS0, newS1 and a pseudo-random number v.
|
||||
func GenerateAndAdvance(s0, s1 uint64) (v, newS0, newS1 uint64) {
|
||||
v = s0 + s1
|
||||
newS0 = s1
|
||||
s0 ^= s0 << 23
|
||||
newS1 = s0 ^ s1 ^ (s0 >> 18) ^ (s1 >> 5)
|
||||
return
|
||||
}
|
||||
|
||||
// Intn generates an int k that satisfies k >= 0 && k < n.
|
||||
// n must be > 0.
|
||||
// It returns the generated k and the new state of the generator.
|
||||
func Intn(s0, s1 uint64, n int) (int, uint64, uint64) {
|
||||
if n <= 0 {
|
||||
panic("invalid n <= 0")
|
||||
}
|
||||
v, newS0, newS1 := GenerateAndAdvance(s0, s1)
|
||||
k := int(v)
|
||||
if k < 0 {
|
||||
k = -k
|
||||
}
|
||||
return k % n, newS0, newS1
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/sot-tech/mochi/pkg/randseed"
|
||||
)
|
||||
|
||||
func TestIntn(t *testing.T) {
|
||||
s0, s1 := rand.Uint64(), rand.Uint64()
|
||||
var k int
|
||||
for i := 0; i < 10000; i++ {
|
||||
k, s0, s1 = Intn(s0, s1, 10)
|
||||
require.True(t, k >= 0, "Intn() must be >= 0")
|
||||
require.True(t, k < 10, "Intn(k) must be < k")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAdvanceXORShift128Plus(b *testing.B) {
|
||||
s0, s1 := rand.Uint64(), rand.Uint64()
|
||||
var v uint64
|
||||
for i := 0; i < b.N; i++ {
|
||||
v, s0, s1 = GenerateAndAdvance(s0, s1)
|
||||
}
|
||||
_, _, _ = v, s0, s1
|
||||
}
|
||||
|
||||
func BenchmarkIntn(b *testing.B) {
|
||||
s0, s1 := rand.Uint64(), rand.Uint64()
|
||||
var v int
|
||||
for i := 0; i < b.N; i++ {
|
||||
v, s0, s1 = Intn(s0, s1, 1000)
|
||||
}
|
||||
_, _, _ = v, s0, s1
|
||||
}
|
||||
@@ -3,14 +3,15 @@ package varinterval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
"github.com/sot-tech/mochi/middleware"
|
||||
"github.com/sot-tech/mochi/middleware/pkg/random"
|
||||
"github.com/sot-tech/mochi/pkg/conf"
|
||||
"github.com/sot-tech/mochi/storage"
|
||||
)
|
||||
@@ -79,14 +80,12 @@ type hook struct {
|
||||
}
|
||||
|
||||
func (h *hook) HandleAnnounce(ctx context.Context, req *bittorrent.AnnounceRequest, resp *bittorrent.AnnounceResponse) (context.Context, error) {
|
||||
s0, s1 := random.DeriveEntropyFromRequest(req)
|
||||
// Generate a probability p < 1.0.
|
||||
v, s0, s1 := random.Intn(s0, s1, 1<<24)
|
||||
p := float32(v) / (1 << 24)
|
||||
if h.cfg.ModifyResponseProbability == 1 || p < h.cfg.ModifyResponseProbability {
|
||||
p, s0, s1 := xoroshiro128p(deriveEntropyFromRequest(req))
|
||||
if float32(float64(p)/math.MaxUint64) < h.cfg.ModifyResponseProbability {
|
||||
// Generate the increase delta.
|
||||
v, _, _ = random.Intn(s0, s1, h.cfg.MaxIncreaseDelta)
|
||||
add := time.Duration(v+1) * time.Second
|
||||
v, _, _ := xoroshiro128p(s0, s1)
|
||||
add := time.Duration(v%uint64(h.cfg.MaxIncreaseDelta)+1) * time.Second
|
||||
|
||||
resp.Interval += add
|
||||
|
||||
@@ -102,3 +101,26 @@ func (h *hook) HandleScrape(ctx context.Context, _ *bittorrent.ScrapeRequest, _
|
||||
// Scrapes are not altered.
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// deriveEntropyFromRequest generates 2*64 bits of pseudo random state from an
|
||||
// bittorrent.AnnounceRequest.
|
||||
//
|
||||
// Calling deriveEntropyFromRequest multiple times yields the same values.
|
||||
func deriveEntropyFromRequest(req *bittorrent.AnnounceRequest) (v0 uint64, v1 uint64) {
|
||||
if len(req.InfoHash) >= bittorrent.InfoHashV1Len {
|
||||
v0 = binary.BigEndian.Uint64([]byte(req.InfoHash[:8])) + binary.BigEndian.Uint64([]byte(req.InfoHash[8:16]))
|
||||
}
|
||||
v1 = binary.BigEndian.Uint64(req.ID[:8]) + binary.BigEndian.Uint64(req.ID[8:16])
|
||||
return
|
||||
}
|
||||
|
||||
// xoroshiro128p calculates predictable pseudorandom number
|
||||
// with XOR/rotate/shift/rotate 128+ algorithm.
|
||||
// see https://prng.di.unimi.it/xoroshiro128plus.c
|
||||
func xoroshiro128p(s0, s1 uint64) (result, ns0, ns1 uint64) {
|
||||
result = s0 + s1
|
||||
s1 ^= s0
|
||||
ns0 = ((s0 << 24) | (s0 >> 40)) ^ s1 ^ (s1 << 16) // rotl(s0, 24) ^ s1 ^ (s1 << 16)
|
||||
ns1 = (s1 << 37) | (s1 >> 27) // rotl(s1, 37)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ package varinterval
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sot-tech/mochi/bittorrent"
|
||||
"github.com/sot-tech/mochi/pkg/conf"
|
||||
|
||||
_ "github.com/sot-tech/mochi/pkg/randseed"
|
||||
)
|
||||
|
||||
var configTests = []struct {
|
||||
@@ -61,3 +64,13 @@ func TestHandleAnnounce(t *testing.T) {
|
||||
require.True(t, resp.Interval > 0, "interval should have been increased")
|
||||
require.True(t, resp.MinInterval > 0, "min_interval should have been increased")
|
||||
}
|
||||
|
||||
func BenchmarkXORoShiRo128Plus(b *testing.B) {
|
||||
s0, s1 := rand.Uint64(), rand.Uint64()
|
||||
var v uint64
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v, s0, s1 = xoroshiro128p(s0, s1)
|
||||
}
|
||||
_, _, _ = v, s0, s1
|
||||
}
|
||||
|
||||
+4
-2
@@ -32,7 +32,8 @@ func init() {
|
||||
|
||||
// ConfigureLogger initializes root and all child loggers.
|
||||
// NOTE: this function MUST be called before any child log call
|
||||
// otherwise any goroutine, which uses logger will wait logger initialization
|
||||
//
|
||||
// otherwise any goroutine, which uses logger will wait logger initialization
|
||||
func ConfigureLogger(output, level string, formatted, colored bool) (err error) {
|
||||
lvl := zerolog.WarnLevel
|
||||
output = strings.ToLower(output)
|
||||
@@ -298,7 +299,8 @@ func Close() {
|
||||
|
||||
// NewLogger creates child logger with specified component name
|
||||
// NOTE: root logger MUST be initialized with ConfigureLogger
|
||||
// before any logger call
|
||||
//
|
||||
// before any logger call
|
||||
func NewLogger(component string) *Logger {
|
||||
return &Logger{comp: component}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Package pg implements PostgreSQL-like storage interface.
|
||||
// This implementation does not use ORM and relies on database structure
|
||||
// and queries provided in configuration.
|
||||
package pg
|
||||
|
||||
import (
|
||||
|
||||
+44
-44
@@ -2,21 +2,21 @@
|
||||
// BitTorrent tracker keeping peer data in redis with hash.
|
||||
// There two categories of hash:
|
||||
//
|
||||
// - CHI_{L,S}{4,6}_<HASH> (hash type)
|
||||
// To save peers that hold the infohash, used for fast searching,
|
||||
// deleting, and timeout handling
|
||||
// - CHI_{L,S}{4,6}_<HASH> (hash type)
|
||||
// To save peers that hold the infohash, used for fast searching,
|
||||
// deleting, and timeout handling
|
||||
//
|
||||
// - CHI_I (set type)
|
||||
// To save all the infohashes, used for garbage collection,
|
||||
// metrics aggregation and leecher graduation
|
||||
// - CHI_I (set type)
|
||||
// To save all the infohashes, used for garbage collection,
|
||||
// metrics aggregation and leecher graduation
|
||||
//
|
||||
// Two keys are used to record the count of seeders and leechers.
|
||||
//
|
||||
// - CHI_C_S (key type)
|
||||
// To record the number of seeders.
|
||||
// - CHI_C_S (key type)
|
||||
// To record the number of seeders.
|
||||
//
|
||||
// - CHI_C_L (key type)
|
||||
// To record the number of leechers.
|
||||
// - CHI_C_L (key type)
|
||||
// To record the number of leechers.
|
||||
package redis
|
||||
|
||||
import (
|
||||
@@ -644,45 +644,45 @@ func (ps *Connection) Ping() error {
|
||||
// This function must be able to execute while other methods on this interface
|
||||
// are being executed in parallel.
|
||||
//
|
||||
// - The Delete(Seeder|Leecher) and GraduateLeecher methods never delete an
|
||||
// infohash key from an addressFamily hash. They also never decrement the
|
||||
// infohash counter.
|
||||
// - The Put(Seeder|Leecher) and GraduateLeecher methods only ever add infohash
|
||||
// keys to addressFamily hashes and increment the infohash counter.
|
||||
// - The only method that deletes from the addressFamily hashes is
|
||||
// gc, which also decrements the counters. That means that,
|
||||
// even if a Delete(Seeder|Leecher) call removes the last peer from a swarm,
|
||||
// the infohash counter is not changed and the infohash is left in the
|
||||
// addressFamily hash until it will be cleaned up by gc.
|
||||
// - gc must run regularly.
|
||||
// - A WATCH ... MULTI ... EXEC block fails, if between the WATCH and the 'EXEC'
|
||||
// any of the watched keys have changed. The location of the 'MULTI' doesn't
|
||||
// matter.
|
||||
// - The Delete(Seeder|Leecher) and GraduateLeecher methods never delete an
|
||||
// infohash key from an addressFamily hash. They also never decrement the
|
||||
// infohash counter.
|
||||
// - The Put(Seeder|Leecher) and GraduateLeecher methods only ever add infohash
|
||||
// keys to addressFamily hashes and increment the infohash counter.
|
||||
// - The only method that deletes from the addressFamily hashes is
|
||||
// gc, which also decrements the counters. That means that,
|
||||
// even if a Delete(Seeder|Leecher) call removes the last peer from a swarm,
|
||||
// the infohash counter is not changed and the infohash is left in the
|
||||
// addressFamily hash until it will be cleaned up by gc.
|
||||
// - gc must run regularly.
|
||||
// - A WATCH ... MULTI ... EXEC block fails, if between the WATCH and the 'EXEC'
|
||||
// any of the watched keys have changed. The location of the 'MULTI' doesn't
|
||||
// matter.
|
||||
//
|
||||
// We have to analyze four cases to prove our algorithm works. I'll characterize
|
||||
// them by a tuple (number of peers in a swarm before WATCH, number of peers in
|
||||
// the swarm during the transaction).
|
||||
//
|
||||
// 1. (0,0), the easy case: The swarm is empty, we watch the key, we execute
|
||||
// HLEN and find it empty. We remove it and decrement the counter. It stays
|
||||
// empty the entire time, the transaction goes through.
|
||||
// 2. (1,n > 0): The swarm is not empty, we watch the key, we find it non-empty,
|
||||
// we unwatch the key. All good. No transaction is made, no transaction fails.
|
||||
// 3. (0,1): We have to analyze this in two ways.
|
||||
// - If the change happens before the HLEN call, we will see that the swarm is
|
||||
// not empty and start no transaction.
|
||||
// - If the change happens after the HLEN, we will attempt a transaction and it
|
||||
// will fail. This is okay, the swarm is not empty, we will try cleaning it up
|
||||
// next time gc runs.
|
||||
// 4. (1,0): Again, two ways:
|
||||
// - If the change happens before the HLEN, we will see an empty swarm. This
|
||||
// situation happens if a call to Delete(Seeder|Leecher) removed the last
|
||||
// peer asynchronously. We will attempt a transaction, but the transaction
|
||||
// will fail. This is okay, the infohash key will remain in the addressFamily
|
||||
// hash, we will attempt to clean it up the next time 'gc` runs.
|
||||
// - If the change happens after the HLEN, we will not even attempt to make the
|
||||
// transaction. The infohash key will remain in the addressFamil hash and
|
||||
// we'll attempt to clean it up the next time gc runs.
|
||||
// 1. (0,0), the easy case: The swarm is empty, we watch the key, we execute
|
||||
// HLEN and find it empty. We remove it and decrement the counter. It stays
|
||||
// empty the entire time, the transaction goes through.
|
||||
// 2. (1,n > 0): The swarm is not empty, we watch the key, we find it non-empty,
|
||||
// we unwatch the key. All good. No transaction is made, no transaction fails.
|
||||
// 3. (0,1): We have to analyze this in two ways.
|
||||
// - If the change happens before the HLEN call, we will see that the swarm is
|
||||
// not empty and start no transaction.
|
||||
// - If the change happens after the HLEN, we will attempt a transaction and it
|
||||
// will fail. This is okay, the swarm is not empty, we will try cleaning it up
|
||||
// next time gc runs.
|
||||
// 4. (1,0): Again, two ways:
|
||||
// - If the change happens before the HLEN, we will see an empty swarm. This
|
||||
// situation happens if a call to Delete(Seeder|Leecher) removed the last
|
||||
// peer asynchronously. We will attempt a transaction, but the transaction
|
||||
// will fail. This is okay, the infohash key will remain in the addressFamily
|
||||
// hash, we will attempt to clean it up the next time 'gc` runs.
|
||||
// - If the change happens after the HLEN, we will not even attempt to make the
|
||||
// transaction. The infohash key will remain in the addressFamil hash and
|
||||
// we'll attempt to clean it up the next time gc runs.
|
||||
func (ps *store) gc(cutoff time.Time) {
|
||||
cutoffNanos := cutoff.UnixNano()
|
||||
// list all infoHashKeys in the group
|
||||
|
||||
+2
-2
@@ -124,12 +124,12 @@ type DataStorage interface {
|
||||
// Implementations of the PeerStorage interface must do the following in addition
|
||||
// to implementing the methods of the interface in the way documented:
|
||||
//
|
||||
// - Implement a garbage-collection strategy that ensures stale data is removed.
|
||||
// - Implement a garbage-collection strategy that ensures stale data is removed.
|
||||
// For example, a timestamp on each InfoHash/Peer combination can be used
|
||||
// to track the last activity for that Peer. The entire database can then
|
||||
// be scanned periodically and too old Peers removed. The intervals and
|
||||
// durations involved should be configurable.
|
||||
// - IPv4 and IPv6 swarms may be isolated from each other.
|
||||
// - IPv4 and IPv6 swarms may be isolated from each other.
|
||||
//
|
||||
// Implementations can be tested against this interface using the tests in
|
||||
// storage_test.go and the benchmarks in storage_bench.go.
|
||||
|
||||
Reference in New Issue
Block a user