├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── configure.go ├── go.mod ├── go.sum ├── main.go └── services ├── access_history.go ├── bucket.go ├── claims.go ├── clickhouse.go ├── clickhouse_db.go ├── clickhouse_test.go ├── common.go ├── http_proxy.go ├── k8s ├── client.go ├── endpoints.go └── nodes_stat.go ├── resolver.go ├── response_writer_interceptor.go ├── service_location.go ├── services_config.go ├── throttled_request_writer.go ├── url_parser.go ├── url_parser_test.go └── web.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | telepresence.log 3 | torrent-http-proxy 4 | .github 5 | .vscode 6 | .dockerignore 7 | *Dockerfile* 8 | README.md 9 | Makefile 10 | LICENSE 11 | .DS_Store 12 | torrent-http-proxy 13 | __debug_bin -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: pavel_tatarskiy 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: 5 | - 'master' 6 | tags: 7 | - 'v*' 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | docker: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v4 19 | - 20 | name: Docker meta 21 | id: meta 22 | uses: docker/metadata-action@v5 23 | with: 24 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 25 | tags: | 26 | type=ref,event=branch 27 | type=ref,event=pr 28 | type=semver,pattern={{version}} 29 | type=semver,pattern={{major}}.{{minor}} 30 | type=sha 31 | - 32 | name: Login to DockerHub 33 | if: github.event_name != 'pull_request' 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ${{ env.REGISTRY }} 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | - 40 | name: Build and push 41 | uses: docker/build-push-action@v6 42 | with: 43 | context: . 44 | push: ${{ github.event_name != 'pull_request' }} 45 | tags: ${{ steps.meta.outputs.tags }} 46 | labels: ${{ steps.meta.outputs.labels }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | *.db 14 | 15 | tmp 16 | 17 | .DS_Store 18 | 19 | torrent-http-proxy 20 | config.yaml 21 | config-env.yaml 22 | .env 23 | .idea 24 | .vscode -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as certs 2 | 3 | # getting certs 4 | RUN apk update && apk upgrade && apk add --no-cache ca-certificates 5 | 6 | FROM golang:latest as build 7 | 8 | # set work dir 9 | WORKDIR /app 10 | 11 | # copy the source files 12 | COPY . . 13 | 14 | # disable crosscompiling 15 | ENV CGO_ENABLED=0 16 | 17 | # compile linux only 18 | ENV GOOS=linux 19 | 20 | # build the binary with debug information removed 21 | RUN go build -ldflags '-w -s' -a -installsuffix cgo -o server 22 | 23 | FROM alpine:latest 24 | 25 | # copy our static linked library 26 | COPY --from=build /app/server . 27 | 28 | # copy certs 29 | COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 30 | 31 | # tell we are exposing our services 32 | EXPOSE 8080 8081 8082 8083 33 | 34 | # run it! 35 | CMD ["./server"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 webtor.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # torrent-http-proxy 2 | 3 | Special HTTP-proxy that has several features: 4 | 5 | 1. Routes requests to internal kubernetes resources (services/jobs). 6 | 2. Deploys kubernetes job on demand. 7 | 8 | For example if path `/08ada5a7a6183aae1e09d831df6748d566095a10/Sintel%2FSintel.mp4` was called 9 | then a new [torrent-web-seeder](https://github.com/webtor-io/torrent-web-seeder) job will be started with injected environment 10 | variable `INFO_HASH=08ada5a7a6183aae1e09d831df6748d566095a10`. Proxy will wait until pod will be ready and then proxy requst to 11 | it with path `/Sintel%2FSintel.mp4`. All following requests will be proxied to this pod. 12 | 13 | 3. Grants HTTP-access to GRPC-services (including jobs) 14 | 4. Provides Token-authentication 15 | 5. Performs chaining of service calls (matryoshka-style) 16 | 17 | For example `/08ada5a7a6183aae1e09d831df6748d566095a10/Sintel%2FSintel.mp4~hls/index.m3u8` will be processed with following steps: 18 | 19 | 1. Proxy deploys [content-transcoder](https://github.com/webtor-io/content-transcoder) job with injected environment variable `SOURCE_URL=%PROXY_URL%/08ada5a7a6183aae1e09d831df6748d566095a10/Sintel%2FSintel.mp4`. `~hls` is the keyword that indicates what 20 | job or service should be invoked. 21 | 2. [content-transcoder](https://github.com/webtor-io/content-transcoder) requests `SOURCE_URL` for transcoding. 22 | 3. Proxy deploys [torrent-web-seeder](https://github.com/webtor-io/torrent-web-seeder) job with injected environment variable `INFO_HASH=08ada5a7a6183aae1e09d831df6748d566095a10`. 23 | 4. Proxy serves `/index.m3u8` from [content-transcoder](https://github.com/webtor-io/content-transcoder) 24 | 25 | There might be more services in chain. There is no limitation. 26 | 27 | ## Server usage 28 | 29 | ``` 30 | % ./torrent-http-proxy help 31 | NAME: 32 | torrent-http-proxy - Proxies all the things 33 | 34 | USAGE: 35 | torrent-http-proxy [global options] command [command options] [arguments...] 36 | 37 | VERSION: 38 | 0.0.1 39 | 40 | COMMANDS: 41 | help, h Shows a list of commands or help for one command 42 | 43 | GLOBAL OPTIONS: 44 | --host value listening host 45 | --port value http listening port (default: 8080) 46 | --jwt-secret value JWT Secret [$SECRET] 47 | --redis-host value redis host (default: "localhost") [$REDIS_MASTER_SERVICE_HOST, $ REDIS_SERVICE_HOST] 48 | --redis-port value redis port (default: 6379) [$REDIS_MASTER_SERVICE_PORT, $ REDIS_SERVICE_PORT] 49 | --job-node-affinity-key value Node Affinity Key [$JOB_NODE_AFFINITY_KEY] 50 | --job-node-affinity-value value Node Affinity Key [$JOB_NODE_AFFINITY_VALUE] 51 | --job-namespace value Job namespace (default: "webtor") [$JOB_NAMESPACE] 52 | --probe-host value probe listening host 53 | --probe-port value probe listening port (default: 8081) 54 | --help, -h show help 55 | --version, -v print the version 56 | ``` -------------------------------------------------------------------------------- /configure.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "github.com/urfave/cli" 6 | cs "github.com/webtor-io/common-services" 7 | s "github.com/webtor-io/torrent-http-proxy/services" 8 | "github.com/webtor-io/torrent-http-proxy/services/k8s" 9 | "net/http" 10 | ) 11 | 12 | func configure(app *cli.App) { 13 | app.Flags = []cli.Flag{} 14 | app.Flags = cs.RegisterProbeFlags(app.Flags) 15 | app.Flags = cs.RegisterPromFlags(app.Flags) 16 | app.Flags = cs.RegisterPprofFlags(app.Flags) 17 | app.Flags = s.RegisterWebFlags(app.Flags) 18 | app.Flags = s.RegisterClickHouseFlags(app.Flags) 19 | app.Flags = s.RegisterClickHouseDBFlags(app.Flags) 20 | app.Flags = s.RegisterCommonFlags(app.Flags) 21 | app.Flags = k8s.RegisterEndpointsFlags(app.Flags) 22 | app.Flags = k8s.RegisterNodesStatFlags(app.Flags) 23 | app.Flags = s.RegisterAPIFlags(app.Flags) 24 | app.Flags = s.RegisterServicesConfigFlags(app.Flags) 25 | 26 | app.Action = run 27 | } 28 | 29 | func run(c *cli.Context) error { 30 | var servers []cs.Servable 31 | 32 | // Setting Config 33 | config, err := s.LoadServicesConfigFromYAML(c) 34 | 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // Setting URL Parser 40 | urlParser := s.NewURLParser(config) 41 | 42 | // Setting Bucket 43 | bucket := s.NewBucket() 44 | 45 | // Setting Kubernetes client 46 | k8sClient := k8s.NewClient() 47 | 48 | // Setting K8SEndpoints 49 | endpointsPool := k8s.NewEndpoints(c, k8sClient) 50 | 51 | // Setting K8SNodeStats 52 | nodeStatsPool := k8s.NewNodesStat(c, k8sClient) 53 | 54 | // Setting HTTP Client 55 | cl := http.DefaultClient 56 | 57 | // Setting ServiceLocation 58 | svcLocPool := s.NewServiceLocationPool(c, cl, nodeStatsPool, endpointsPool) 59 | 60 | // Setting Resolver 61 | resolver := s.NewResolver(config, svcLocPool) 62 | 63 | // Setting Probe 64 | probe := cs.NewProbe(c) 65 | if probe != nil { 66 | servers = append(servers, probe) 67 | defer probe.Close() 68 | } 69 | 70 | // Setting Prom 71 | prom := cs.NewProm(c) 72 | if prom != nil { 73 | servers = append(servers, prom) 74 | } 75 | 76 | // Setting Pprof 77 | pprof := cs.NewPprof(c) 78 | if pprof != nil { 79 | servers = append(servers, pprof) 80 | defer prom.Close() 81 | } 82 | 83 | // Setting HTTP Proxy Pool 84 | httpProxy := s.NewHTTPProxy(resolver) 85 | 86 | // Setting Claims 87 | claims := s.NewClaims(c) 88 | 89 | var clickHouse *s.ClickHouse 90 | 91 | if c.String(s.ClickhouseDSNFlag) != "" { 92 | // Setting ClickHouse DB 93 | clickHouseDB := s.NewClickHouseDB(c) 94 | defer clickHouseDB.Close() 95 | 96 | // Setting ClickHouse 97 | clickHouse = s.NewClickHouse(c, clickHouseDB) 98 | if clickHouse != nil { 99 | defer clickHouse.Close() 100 | } 101 | } 102 | 103 | // Setting AccessHistory 104 | accessHistory := s.NewAccessHistory() 105 | 106 | // Setting WebService 107 | web := s.NewWeb(c, urlParser, resolver, httpProxy, claims, 108 | bucket, clickHouse, config, accessHistory) 109 | servers = append(servers, web) 110 | defer web.Close() 111 | 112 | // Setting ServeService 113 | serve := cs.NewServe(servers...) 114 | 115 | // And SERVE! 116 | err = serve.Serve() 117 | if err != nil { 118 | log.WithError(err).Error("got serve error") 119 | } 120 | return err 121 | } 122 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webtor-io/torrent-http-proxy 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | code.cloudfoundry.org/bytefmt v0.37.0 7 | github.com/DATA-DOG/go-sqlmock v1.5.0 8 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 9 | github.com/juju/ratelimit v1.0.2 10 | github.com/pkg/errors v0.9.1 11 | github.com/prometheus/client_golang v1.22.0 12 | github.com/prometheus/common v0.63.0 // indirect 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/urfave/cli v1.22.16 15 | github.com/webtor-io/common-services v0.0.0-20250112153432-554128b56bd5 16 | k8s.io/api v0.32.3 17 | k8s.io/apimachinery v0.32.3 18 | k8s.io/client-go v0.32.3 19 | ) 20 | 21 | require ( 22 | github.com/ClickHouse/clickhouse-go/v2 v2.34.0 23 | github.com/webtor-io/lazymap v0.0.0-20250308124910-3a61e0f78108 24 | gopkg.in/yaml.v3 v3.0.1 25 | ) 26 | 27 | require ( 28 | github.com/ClickHouse/ch-go v0.65.1 // indirect 29 | github.com/andybalholm/brotli v1.1.1 // indirect 30 | github.com/aws/aws-sdk-go v1.55.6 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 33 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 36 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 37 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 38 | github.com/go-faster/city v1.0.1 // indirect 39 | github.com/go-faster/errors v0.7.1 // indirect 40 | github.com/go-logr/logr v1.4.2 // indirect 41 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 42 | github.com/go-openapi/jsonreference v0.21.0 // indirect 43 | github.com/go-openapi/swag v0.23.1 // indirect 44 | github.com/go-pg/migrations/v8 v8.1.0 // indirect 45 | github.com/go-pg/pg/v10 v10.14.0 // indirect 46 | github.com/go-pg/zerochecker v0.2.0 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/protobuf v1.5.4 // indirect 49 | github.com/google/gnostic-models v0.6.9 // indirect 50 | github.com/google/go-cmp v0.7.0 // indirect 51 | github.com/google/gofuzz v1.2.0 // indirect 52 | github.com/google/uuid v1.6.0 // indirect 53 | github.com/jinzhu/inflection v1.0.0 // indirect 54 | github.com/jmespath/go-jmespath v0.4.0 // indirect 55 | github.com/josharian/intern v1.0.0 // indirect 56 | github.com/json-iterator/go v1.1.12 // indirect 57 | github.com/klauspost/compress v1.18.0 // indirect 58 | github.com/mailru/easyjson v0.9.0 // indirect 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 60 | github.com/modern-go/reflect2 v1.0.2 // indirect 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 62 | github.com/paulmach/orb v0.11.1 // indirect 63 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 64 | github.com/prometheus/client_model v0.6.2 // indirect 65 | github.com/prometheus/procfs v0.16.1 // indirect 66 | github.com/redis/go-redis/v9 v9.7.3 // indirect 67 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 68 | github.com/segmentio/asm v1.2.0 // indirect 69 | github.com/shopspring/decimal v1.4.0 // indirect 70 | github.com/spf13/pflag v1.0.6 // indirect 71 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 72 | github.com/vmihailenco/bufpool v0.1.11 // indirect 73 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 74 | github.com/vmihailenco/tagparser v0.1.2 // indirect 75 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 76 | github.com/x448/float16 v0.8.4 // indirect 77 | go.opentelemetry.io/otel v1.35.0 // indirect 78 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 79 | golang.org/x/crypto v0.37.0 // indirect 80 | golang.org/x/net v0.39.0 // indirect 81 | golang.org/x/oauth2 v0.29.0 // indirect 82 | golang.org/x/sys v0.32.0 // indirect 83 | golang.org/x/term v0.31.0 // indirect 84 | golang.org/x/text v0.24.0 // indirect 85 | golang.org/x/time v0.11.0 // indirect 86 | google.golang.org/protobuf v1.36.6 // indirect 87 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 88 | gopkg.in/inf.v0 v0.9.1 // indirect 89 | k8s.io/klog/v2 v2.130.1 // indirect 90 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 91 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 92 | mellium.im/sasl v0.3.2 // indirect 93 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 94 | sigs.k8s.io/randfill v1.0.0 // indirect 95 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 96 | sigs.k8s.io/yaml v1.4.0 // indirect 97 | ) 98 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | code.cloudfoundry.org/bytefmt v0.31.0 h1:7Vh8P2Vnz7W89QgtArF1kaS5DS7RnktTCZ7eUKyEzF8= 3 | code.cloudfoundry.org/bytefmt v0.31.0/go.mod h1:HIz0xOci24hA/16xtQ6aeIT9yXP7KxokFlaJ0Orfs/8= 4 | code.cloudfoundry.org/bytefmt v0.37.0 h1:GR5rZgr/6QLV/U2/xgORHUY4lu4EEBgN4/cz7SD3GqM= 5 | code.cloudfoundry.org/bytefmt v0.37.0/go.mod h1:u3/LRyPLBYFtn8h9CnzTeupRMD+76qCovDU0vND81lU= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 8 | github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= 9 | github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4= 10 | github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0= 11 | github.com/ClickHouse/clickhouse-go/v2 v2.33.0 h1:MOvrVAVzINf7uqsuEp3jMToiAGDnQ3NeJRfq6z9u0Dg= 12 | github.com/ClickHouse/clickhouse-go/v2 v2.33.0/go.mod h1:cb1Ss8Sz8PZNdfvEBwkMAdRhoyB6/HiB6o3We5ZIcE4= 13 | github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co= 14 | github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8= 15 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 16 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 17 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 18 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 19 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= 20 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 21 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 22 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 23 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 24 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 25 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 26 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 27 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 28 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 29 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 30 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 31 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 32 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 33 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 39 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 40 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 41 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 42 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 43 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 44 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 45 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 46 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 47 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 48 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 49 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 50 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 51 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 52 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 53 | github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= 54 | github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= 55 | github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= 56 | github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= 57 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 58 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 59 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 60 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 61 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 62 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 63 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 64 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 65 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 66 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 67 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 68 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 69 | github.com/go-pg/migrations/v8 v8.1.0 h1:bc1wQwFoWRKvLdluXCRFRkeaw9xDU4qJ63uCAagh66w= 70 | github.com/go-pg/migrations/v8 v8.1.0/go.mod h1:o+CN1u572XHphEHZyK6tqyg2GDkRvL2bIoLNyGIewus= 71 | github.com/go-pg/pg/v10 v10.4.0/go.mod h1:BfgPoQnD2wXNd986RYEHzikqv9iE875PrFaZ9vXvtNM= 72 | github.com/go-pg/pg/v10 v10.14.0 h1:giXuPsJaWjzwzFJTxy39eBgGE44jpqH1jwv0uI3kBUU= 73 | github.com/go-pg/pg/v10 v10.14.0/go.mod h1:6kizZh54FveJxw9XZdNg07x7DDBWNsQrSiJS04MLwO8= 74 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= 75 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= 76 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 77 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 78 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 79 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 81 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 82 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 84 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 85 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 86 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 87 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 88 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 89 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 90 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 91 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 92 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 93 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 94 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 95 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 96 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 97 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 98 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 99 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 100 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 101 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 102 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 104 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 108 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 109 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 110 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 111 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 112 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 113 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= 114 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 115 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 116 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 117 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 118 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 119 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 120 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 121 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 122 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 123 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 124 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 125 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 126 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 127 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 128 | github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= 129 | github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 130 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 131 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 132 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 133 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 134 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 135 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 136 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 137 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 138 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 139 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 140 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 141 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 142 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 143 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 144 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 145 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 146 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 147 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 148 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 149 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 150 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 151 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 152 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 153 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 154 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 155 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 156 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 157 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 158 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 159 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 160 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 161 | github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= 162 | github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= 163 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 164 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 165 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 166 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 167 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 168 | github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= 169 | github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= 170 | github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= 171 | github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= 172 | github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 173 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 174 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 175 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 176 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 177 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 178 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 179 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 180 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 181 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 182 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 183 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 184 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 185 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 186 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 187 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 188 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 189 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 190 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 191 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 192 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 193 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 194 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 195 | github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= 196 | github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= 197 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 198 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 199 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 200 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 201 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 202 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 203 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 204 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 205 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 206 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 207 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 208 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 209 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 210 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 211 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 212 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 213 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 214 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 215 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 216 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 217 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 218 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 219 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 220 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 221 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 222 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 223 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 224 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 225 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 226 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= 227 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= 228 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 229 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 230 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 231 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 232 | github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 233 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= 234 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 235 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 236 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 237 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 238 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 239 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 240 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 241 | github.com/webtor-io/common-services v0.0.0-20250112153432-554128b56bd5 h1:EGVq16o1t8LO2HR2eGh4YEJjLF9Np2bBgJxHkUr+fb4= 242 | github.com/webtor-io/common-services v0.0.0-20250112153432-554128b56bd5/go.mod h1:6jUeO6R+ytZnEJj7PlcLEQZfWaxw8ovav73BP83MTlI= 243 | github.com/webtor-io/lazymap v0.0.0-20241211155941-e81d935cfa1d h1:Xi9E0LCDgK++QliA7ZNFdSI11Bpg5qe7efN3AMWJ3dY= 244 | github.com/webtor-io/lazymap v0.0.0-20241211155941-e81d935cfa1d/go.mod h1:kioEFK4hk8YfHrhg47tGvMG40xawOJM4gcfRQ4EeX4k= 245 | github.com/webtor-io/lazymap v0.0.0-20250308124910-3a61e0f78108 h1:4rJXuBJFmr4ePOQIIBDOkAzQrjFoMpWns8g1zD95ugM= 246 | github.com/webtor-io/lazymap v0.0.0-20250308124910-3a61e0f78108/go.mod h1:kioEFK4hk8YfHrhg47tGvMG40xawOJM4gcfRQ4EeX4k= 247 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 248 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 249 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 250 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 251 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 252 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 253 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 254 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 255 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 256 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 257 | go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= 258 | go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= 259 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 260 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 261 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 262 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 263 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 264 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 265 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 266 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 267 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 268 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 269 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 270 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 271 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 272 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 273 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 274 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 275 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 276 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 277 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 278 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 279 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 280 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 281 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 282 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 284 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 285 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 286 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 287 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 288 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 289 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 290 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 291 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 292 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 293 | golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 294 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 295 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 296 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 297 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 298 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 299 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 300 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 301 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 302 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 303 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 304 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 305 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 306 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 307 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 312 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 313 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 314 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 315 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 316 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 317 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 318 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 326 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 327 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 328 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 329 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 330 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 331 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 332 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 333 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 334 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 335 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 336 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 337 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 338 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 339 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 340 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 341 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 342 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 343 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 344 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 345 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 346 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 347 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 349 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 350 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 351 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 352 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 353 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 354 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 355 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 356 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 357 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 358 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 359 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 360 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 361 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 362 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 363 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 364 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 365 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 366 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 367 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 368 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 369 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 370 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 371 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 372 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 373 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 374 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 375 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 376 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 377 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 378 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 379 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 380 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 381 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 382 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 383 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 384 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 385 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 386 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 387 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 388 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 389 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 390 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 391 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 392 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 393 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 394 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 395 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 396 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 397 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 398 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 399 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 400 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 401 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 402 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 403 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 404 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 405 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 406 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 407 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 408 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 409 | k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= 410 | k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= 411 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 412 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 413 | k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= 414 | k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 415 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 416 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 417 | k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= 418 | k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= 419 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 420 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 421 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 422 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 423 | k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9 h1:t0huyHnz6HsokckRxAF1bY0cqPFwzINKCL7yltEjZQc= 424 | k8s.io/kube-openapi v0.0.0-20250304201544-e5f78fe3ede9/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 425 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 426 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 427 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 428 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 429 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 430 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 431 | mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= 432 | mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= 433 | mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY= 434 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 435 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 436 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 437 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 438 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 439 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 440 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 441 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 442 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 443 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 444 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 445 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/urfave/cli" 8 | ) 9 | 10 | func main() { 11 | // log.SetFormatter(joonix.NewFormatter()) 12 | log.SetFormatter(&log.TextFormatter{ 13 | FullTimestamp: true, 14 | }) 15 | app := cli.NewApp() 16 | app.Name = "torrent-http-proxy" 17 | app.Usage = "Proxies all the things" 18 | app.Version = "0.0.1" 19 | configure(app) 20 | err := app.Run(os.Args) 21 | if err != nil { 22 | log.WithError(err).Fatal("Failed to serve application") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/access_history.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const ( 11 | accessHistoryLimit = 5 12 | accessHistoryExpire = 3 * time.Hour 13 | ) 14 | 15 | type AccessHistory struct { 16 | mux sync.Mutex 17 | m map[string][]string 18 | limit int 19 | expire time.Duration 20 | } 21 | 22 | func NewAccessHistory() *AccessHistory { 23 | return &AccessHistory{ 24 | m: map[string][]string{}, 25 | limit: accessHistoryLimit, 26 | expire: accessHistoryExpire, 27 | } 28 | } 29 | 30 | func (s *AccessHistory) Store(oip string, oua string, nip string, nua string) (bool, int) { 31 | s.mux.Lock() 32 | defer s.mux.Unlock() 33 | okey := fmt.Sprintf("%x", sha1.Sum([]byte(oip+oua))) 34 | nkey := fmt.Sprintf("%x", sha1.Sum([]byte(nip+nua))) 35 | _, ok := s.m[okey] 36 | if !ok { 37 | s.m[okey] = []string{} 38 | go func(k string) { 39 | <-time.After(s.expire) 40 | s.mux.Lock() 41 | defer s.mux.Unlock() 42 | delete(s.m, k) 43 | }(okey) 44 | } 45 | for _, v := range s.m[okey] { 46 | if v == nkey { 47 | return true, s.limit - len(s.m[okey]) 48 | } 49 | } 50 | if len(s.m[okey]) >= s.limit { 51 | return false, 0 52 | } 53 | s.m[okey] = append(s.m[okey], nkey) 54 | return true, s.limit - len(s.m[okey]) 55 | } 56 | -------------------------------------------------------------------------------- /services/bucket.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/webtor-io/lazymap" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "code.cloudfoundry.org/bytefmt" 10 | 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/juju/ratelimit" 13 | ) 14 | 15 | type Bucket struct { 16 | lazymap.LazyMap[*ratelimit.Bucket] 17 | } 18 | 19 | func NewBucket() *Bucket { 20 | return &Bucket{ 21 | LazyMap: lazymap.New[*ratelimit.Bucket](&lazymap.Config{ 22 | Expire: 5 * 60 * time.Second, 23 | }), 24 | } 25 | } 26 | 27 | func (s *Bucket) Get(mc jwt.MapClaims) (*ratelimit.Bucket, error) { 28 | sessionID, ok := mc["sessionID"].(string) 29 | if !ok { 30 | return nil, nil 31 | } 32 | rate, ok := mc["rate"].(string) 33 | if !ok { 34 | return nil, nil 35 | } 36 | key := sessionID + rate 37 | r, err := bytefmt.ToBytes(rate) 38 | if err != nil { 39 | return nil, errors.Errorf("failed to parse rate %v", rate) 40 | } 41 | return s.LazyMap.Get(key, func() (*ratelimit.Bucket, error) { 42 | return ratelimit.NewBucketWithRate(float64(r)/8, int64(r)), nil 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /services/claims.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "github.com/pkg/errors" 6 | "github.com/urfave/cli" 7 | ) 8 | 9 | const ( 10 | apiKeyFlag = "api-key" 11 | apiSecretFlag = "api-secret" 12 | ) 13 | 14 | func RegisterAPIFlags(flags []cli.Flag) []cli.Flag { 15 | return append(flags, 16 | cli.StringFlag{ 17 | Name: apiKeyFlag, 18 | Usage: "API key for authentication", 19 | EnvVar: "API_KEY", 20 | }, 21 | cli.StringFlag{ 22 | Name: apiSecretFlag, 23 | Usage: "API secret for authentication", 24 | EnvVar: "API_SECRET", 25 | }, 26 | ) 27 | } 28 | 29 | type Claims struct { 30 | apiKey string 31 | apiSecret string 32 | } 33 | type StandardClaims struct { 34 | Grace int `json:"grace"` 35 | Preset string `json:"preset"` 36 | Rate string `json:"rate"` 37 | Role string `json:"role"` 38 | jwt.StandardClaims 39 | } 40 | 41 | func NewClaims(c *cli.Context) *Claims { 42 | return &Claims{ 43 | apiKey: c.String(apiKeyFlag), 44 | apiSecret: c.String(apiSecretFlag), 45 | } 46 | } 47 | 48 | func (s *Claims) Get(tokenString string, apiKey string) (jwt.MapClaims, error) { 49 | 50 | if s.apiKey == "" && s.apiSecret == "" { 51 | return jwt.MapClaims{}, nil 52 | } 53 | 54 | if tokenString == "" { 55 | return nil, errors.Errorf("failed to get token") 56 | } 57 | 58 | if s.apiKey != apiKey { 59 | return nil, errors.New("wrong api key") 60 | } 61 | 62 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 63 | // Don't forget to validate the alg is what you expect: 64 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 65 | return nil, errors.Errorf("Unexpected signing method=%v", token.Header["alg"]) 66 | } 67 | // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") 68 | return []byte(s.apiSecret), nil 69 | }) 70 | if err != nil { 71 | return nil, errors.Wrapf(err, "failed to parse token") 72 | } 73 | claims, ok := token.Claims.(jwt.MapClaims) 74 | if !ok || !token.Valid { 75 | return nil, errors.Wrapf(err, "failed to validate token") 76 | } 77 | return claims, nil 78 | } 79 | -------------------------------------------------------------------------------- /services/clickhouse.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | 13 | "github.com/urfave/cli" 14 | ) 15 | 16 | const ( 17 | clickhouseBatchSizeFlag = "clickhouse-batch-size" 18 | clickhouseReplicatedFlag = "clickhouse-replicated" 19 | clickhouseShardedFlag = "clickhouse-sharded" 20 | ) 21 | 22 | func RegisterClickHouseFlags(f []cli.Flag) []cli.Flag { 23 | return append(f, 24 | cli.IntFlag{ 25 | Name: clickhouseBatchSizeFlag, 26 | Usage: "clickhouse batch size", 27 | Value: 1000, 28 | EnvVar: "CLICKHOUSE_BATCH_SIZE", 29 | }, 30 | cli.BoolFlag{ 31 | Name: clickhouseReplicatedFlag, 32 | Usage: "clickhouse replication enabled", 33 | EnvVar: "CLICKHOUSE_REPLICATED", 34 | }, 35 | cli.BoolFlag{ 36 | Name: clickhouseShardedFlag, 37 | Usage: "clickhouse sharded enabled", 38 | EnvVar: "CLICKHOUSE_SHARDED", 39 | }, 40 | ) 41 | } 42 | 43 | type ClickHouse struct { 44 | db DBProvider 45 | batchSize int 46 | batch []*StatRecord 47 | mux sync.Mutex 48 | storeMux sync.Mutex 49 | init sync.Once 50 | nodeName string 51 | replicated bool 52 | sharded bool 53 | } 54 | 55 | type StatRecord struct { 56 | Timestamp time.Time 57 | ApiKey string 58 | BytesWritten uint64 59 | TTFB uint64 60 | Duration uint64 61 | Path string 62 | InfoHash string 63 | OriginalPath string 64 | SessionID string 65 | Domain string 66 | Status uint64 67 | GroupedStatus uint64 68 | Edge string 69 | Source string 70 | Role string 71 | Ads bool 72 | } 73 | 74 | func NewClickHouse(c *cli.Context, db DBProvider) *ClickHouse { 75 | 76 | return &ClickHouse{ 77 | db: db, 78 | batchSize: c.Int(clickhouseBatchSizeFlag), 79 | batch: make([]*StatRecord, 0, c.Int(clickhouseBatchSizeFlag)), 80 | nodeName: c.String(myNodeNameFlag), 81 | replicated: c.Bool(clickhouseReplicatedFlag), 82 | sharded: c.Bool(clickhouseShardedFlag), 83 | } 84 | } 85 | 86 | func (s *ClickHouse) makeTable(db *sql.DB) error { 87 | table := "proxy_stat" 88 | tableExpr := table 89 | engine := "MergeTree()" 90 | ttl := "3 MONTH" 91 | if s.sharded { 92 | tableExpr += " on cluster '{cluster}'" 93 | } 94 | if s.replicated { 95 | engine = "ReplicatedMergeTree('/clickhouse/{installation}/{cluster}/tables/{shard}/{database}/{table}', '{replica}')" 96 | } 97 | _, err := db.Exec(fmt.Sprintf(strings.TrimSpace(` 98 | CREATE TABLE IF NOT EXISTS %v ( 99 | timestamp DateTime, 100 | api_key String, 101 | bytes_written UInt64, 102 | ttfb UInt32, 103 | duration UInt32, 104 | path String, 105 | infohash String, 106 | original_path String, 107 | session_id String, 108 | domain String, 109 | status UInt16, 110 | grouped_status UInt16, 111 | edge String, 112 | source String, 113 | role String, 114 | ads UInt8, 115 | node String 116 | ) engine = %v 117 | PARTITION BY toYYYYMM(timestamp) 118 | ORDER BY (timestamp) 119 | TTL timestamp + INTERVAL %v 120 | `), tableExpr, engine, ttl)) 121 | if err != nil { 122 | return err 123 | } 124 | if s.sharded { 125 | _, err = db.Exec(fmt.Sprintf(strings.TrimSpace(` 126 | CREATE TABLE IF NOT EXISTS %v_all on cluster '{cluster}' as %v 127 | ENGINE = Distributed('{cluster}', default, %v, rand()) 128 | `), table, table, table)) 129 | } 130 | return err 131 | } 132 | 133 | func (s *ClickHouse) store(sr []*StatRecord) error { 134 | s.storeMux.Lock() 135 | if len(sr) == 0 { 136 | return nil 137 | } 138 | logrus.Infof("storing %v rows to ClickHouse", len(sr)) 139 | defer func() { 140 | logrus.Infof("finish storing %v rows to ClickHouse", len(sr)) 141 | s.storeMux.Unlock() 142 | }() 143 | db, err := s.db.Get() 144 | if err != nil { 145 | return errors.Wrapf(err, "failed to get ClickHouse DB") 146 | } 147 | s.init.Do(func() { 148 | err = s.makeTable(db) 149 | }) 150 | if err != nil { 151 | return errors.Wrapf(err, "failed to create table") 152 | } 153 | err = db.Ping() 154 | if err != nil { 155 | return errors.Wrapf(err, "failed to ping") 156 | } 157 | tx, err := db.Begin() 158 | if err != nil { 159 | return errors.Wrapf(err, "failed to begin") 160 | } 161 | table := "proxy_stat" 162 | if s.replicated { 163 | table += "_all" 164 | } 165 | stmt, err := tx.Prepare(fmt.Sprintf(`INSERT INTO %v (timestamp, api_key, bytes_written, ttfb, 166 | duration, path, infohash, original_path, session_id, domain, status, grouped_status, edge, 167 | source, role, ads, node) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, table)) 168 | if err != nil { 169 | return errors.Wrapf(err, "failed to prepare") 170 | } 171 | defer func(stmt *sql.Stmt) { 172 | _ = stmt.Close() 173 | }(stmt) 174 | for _, r := range sr { 175 | var adsUInt uint8 176 | if r.Ads { 177 | adsUInt = 1 178 | } 179 | _, err = stmt.Exec( 180 | r.Timestamp, r.ApiKey, r.BytesWritten, uint32(r.TTFB), 181 | uint32(r.Duration), r.Path, r.InfoHash, r.OriginalPath, r.SessionID, 182 | r.Domain, uint16(r.Status), uint16(r.GroupedStatus), r.Edge, r.Source, 183 | r.Role, adsUInt, s.nodeName, 184 | ) 185 | if err != nil { 186 | return errors.Wrapf(err, "failed to exec") 187 | } 188 | } 189 | err = tx.Commit() 190 | if err != nil { 191 | return errors.Wrapf(err, "failed to commit") 192 | } 193 | return nil 194 | } 195 | 196 | func (s *ClickHouse) Add(sr *StatRecord) error { 197 | s.mux.Lock() 198 | s.batch = append(s.batch, sr) 199 | s.mux.Unlock() 200 | if len(s.batch) >= s.batchSize { 201 | go func(b []*StatRecord) { 202 | err := s.store(b) 203 | if err != nil { 204 | logrus.WithError(err).Warn("failed to store to ClickHouse") 205 | } 206 | }(s.batch) 207 | s.mux.Lock() 208 | s.batch = make([]*StatRecord, 0, s.batchSize) 209 | s.mux.Unlock() 210 | } 211 | return nil 212 | } 213 | 214 | func (s *ClickHouse) Close() { 215 | _ = s.store(s.batch) 216 | s.batch = []*StatRecord{} 217 | } 218 | -------------------------------------------------------------------------------- /services/clickhouse_db.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "database/sql" 5 | "sync" 6 | 7 | _ "github.com/ClickHouse/clickhouse-go/v2" 8 | _ "github.com/ClickHouse/clickhouse-go/v2/lib/driver" 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | type DBProvider interface { 13 | Get() (*sql.DB, error) 14 | } 15 | 16 | const ( 17 | ClickhouseDSNFlag = "clickhouse-dsn" 18 | ) 19 | 20 | func RegisterClickHouseDBFlags(f []cli.Flag) []cli.Flag { 21 | return append(f, 22 | cli.StringFlag{ 23 | Name: ClickhouseDSNFlag, 24 | Usage: "clickhouse dsn", 25 | Value: "", 26 | EnvVar: "CLICKHOUSE_DSN", 27 | }, 28 | ) 29 | } 30 | 31 | type ClickHouseDB struct { 32 | dsn string 33 | err error 34 | db *sql.DB 35 | once sync.Once 36 | } 37 | 38 | func NewClickHouseDB(c *cli.Context) *ClickHouseDB { 39 | return &ClickHouseDB{ 40 | dsn: c.String(ClickhouseDSNFlag), 41 | } 42 | } 43 | 44 | func (s *ClickHouseDB) Get() (*sql.DB, error) { 45 | s.once.Do(func() { 46 | s.db, s.err = sql.Open("clickhouse", s.dsn) 47 | }) 48 | return s.db, s.err 49 | } 50 | 51 | func (s *ClickHouseDB) Close() { 52 | if s.db != nil { 53 | _ = s.db.Close() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /services/clickhouse_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/DATA-DOG/go-sqlmock" 10 | "github.com/urfave/cli" 11 | ) 12 | 13 | type ClickHouseDBMock struct { 14 | db *sql.DB 15 | } 16 | 17 | func (s *ClickHouseDBMock) Get() (*sql.DB, error) { 18 | return s.db, nil 19 | } 20 | 21 | func TestClickHouse(t *testing.T) { 22 | app := cli.NewApp() 23 | app.Flags = []cli.Flag{} 24 | app.Flags = RegisterClickHouseFlags(app.Flags) 25 | app.Action = func(c *cli.Context) error { 26 | db, mock, err := sqlmock.New() 27 | if err != nil { 28 | return nil 29 | } 30 | r := &StatRecord{} 31 | mock.ExpectExec("CREATE TABLE IF NOT EXISTS").WillReturnResult(sqlmock.NewResult(0, 0)) 32 | mock.ExpectBegin() 33 | stmt := mock.ExpectPrepare("INSERT INTO") 34 | for i := 0; i < 1000; i++ { 35 | stmt.ExpectExec().WithArgs(r.Timestamp, r.ApiKey, r.BytesWritten, r.TTFB, 36 | r.Duration, r.Path, r.InfoHash, r.OriginalPath, r.SessionID, 37 | r.Domain, r.Status, r.GroupedStatus, r.Edge, r.Source, 38 | r.Role, 0, 39 | ).WillReturnResult(sqlmock.NewResult(1, 1)) 40 | } 41 | mock.ExpectCommit() 42 | mock.ExpectBegin() 43 | stmt = mock.ExpectPrepare("INSERT INTO") 44 | for i := 0; i < 1000; i++ { 45 | stmt.ExpectExec().WithArgs(r.Timestamp, r.ApiKey, r.BytesWritten, r.TTFB, 46 | r.Duration, r.Path, r.InfoHash, r.OriginalPath, r.SessionID, 47 | r.Domain, r.Status, r.GroupedStatus, r.Edge, r.Source, 48 | r.Role, 0, 49 | ).WillReturnResult(sqlmock.NewResult(1, 1)) 50 | } 51 | mock.ExpectCommit() 52 | mock.ExpectBegin() 53 | stmt = mock.ExpectPrepare("INSERT INTO") 54 | for i := 0; i < 100; i++ { 55 | stmt.ExpectExec().WithArgs(r.Timestamp, r.ApiKey, r.BytesWritten, r.TTFB, 56 | r.Duration, r.Path, r.InfoHash, r.OriginalPath, r.SessionID, 57 | r.Domain, r.Status, r.GroupedStatus, r.Edge, r.Source, 58 | r.Role, 0, 59 | ).WillReturnResult(sqlmock.NewResult(1, 1)) 60 | } 61 | mock.ExpectCommit() 62 | 63 | clickHouseDB := &ClickHouseDBMock{ 64 | db: db, 65 | } 66 | 67 | clickHouse := NewClickHouse(c, clickHouseDB) 68 | 69 | for i := 0; i < 2100; i++ { 70 | if err = clickHouse.Add(&StatRecord{}); err != nil { 71 | t.Errorf("error while adding stats: %s", err) 72 | } 73 | } 74 | <-time.After(time.Millisecond * 100) 75 | 76 | if len(clickHouse.batch) != 100 { 77 | t.Errorf("expected batch size %v got %v", 100, len(clickHouse.batch)) 78 | } 79 | 80 | clickHouse.Close() 81 | 82 | if len(clickHouse.batch) != 0 { 83 | t.Errorf("expected empty batch but %v records still reamins", len(clickHouse.batch)) 84 | } 85 | 86 | if err := mock.ExpectationsWereMet(); err != nil { 87 | t.Errorf("there were unfulfilled expectations: %s", err) 88 | } 89 | 90 | return nil 91 | } 92 | args := os.Args[0:1] 93 | _ = app.Run(args) 94 | } 95 | -------------------------------------------------------------------------------- /services/common.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/urfave/cli" 4 | 5 | const ( 6 | myNodeNameFlag = "my-node-name" 7 | ) 8 | 9 | func RegisterCommonFlags(f []cli.Flag) []cli.Flag { 10 | return append(f, 11 | cli.StringFlag{ 12 | Name: myNodeNameFlag, 13 | Usage: "My node name", 14 | Value: "", 15 | EnvVar: "MY_NODE_NAME", 16 | }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /services/http_proxy.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/webtor-io/lazymap" 8 | "io" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | type HTTPProxy struct { 19 | lazymap.LazyMap[*httputil.ReverseProxy] 20 | r *Resolver 21 | } 22 | 23 | func NewHTTPProxy(r *Resolver) *HTTPProxy { 24 | return &HTTPProxy{ 25 | r: r, 26 | LazyMap: lazymap.New[*httputil.ReverseProxy](&lazymap.Config{ 27 | Expire: 60 * time.Second, 28 | }), 29 | } 30 | } 31 | 32 | var corsHeaders = []string{ 33 | "Access-Control-Allow-Credentials", 34 | "Access-Control-Allow-Origin", 35 | } 36 | 37 | func delCORSHeaders(header http.Header) { 38 | for _, h := range corsHeaders { 39 | header.Del(h) 40 | } 41 | } 42 | 43 | func modifyResponse(r *http.Response) error { 44 | delCORSHeaders(r.Header) 45 | return nil 46 | } 47 | 48 | type stubTransport struct { 49 | http.RoundTripper 50 | } 51 | 52 | func (t *stubTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 53 | return &http.Response{ 54 | Status: "503 Service Unavailable", 55 | StatusCode: 503, 56 | Proto: "HTTP/1.1", 57 | ProtoMajor: 1, 58 | ProtoMinor: 1, 59 | Body: io.NopCloser(bytes.NewBufferString("")), 60 | ContentLength: int64(0), 61 | Request: req, 62 | Header: make(http.Header), 63 | }, nil 64 | } 65 | 66 | func (s *HTTPProxy) get(loc *Location) (*httputil.ReverseProxy, error) { 67 | u := &url.URL{ 68 | Host: fmt.Sprintf("%s:%d", loc.IP.String(), loc.HTTP), 69 | Scheme: "http", 70 | } 71 | var t http.RoundTripper 72 | if loc.Unavailable { 73 | t = &stubTransport{http.DefaultTransport} 74 | } else { 75 | t = http.DefaultTransport 76 | } 77 | p := httputil.NewSingleHostReverseProxy(u) 78 | p.Transport = t 79 | p.ModifyResponse = modifyResponse 80 | // p.FlushInterval = -1 81 | return p, nil 82 | } 83 | 84 | func (s *HTTPProxy) Get(src *Source, claims jwt.MapClaims, logger *logrus.Entry) (*httputil.ReverseProxy, error) { 85 | loc, err := s.r.Resolve(src, claims, logger) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "failed to get location") 88 | } 89 | return s.LazyMap.Get(fmt.Sprintf("%s:%d", loc.IP.String(), loc.HTTP), func() (*httputil.ReverseProxy, error) { 90 | return s.get(loc) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /services/k8s/client.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sync" 7 | 8 | "github.com/pkg/errors" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/client-go/tools/clientcmd" 14 | ) 15 | 16 | type Client struct { 17 | cl *kubernetes.Clientset 18 | inited bool 19 | err error 20 | mux sync.Mutex 21 | } 22 | 23 | func NewClient() *Client { 24 | return &Client{} 25 | } 26 | 27 | func (s *Client) get() (*kubernetes.Clientset, error) { 28 | kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") 29 | log.Infof("checking local kubeconfig path=%s", kubeconfig) 30 | var config *rest.Config 31 | if _, err := os.Stat(kubeconfig); err == nil { 32 | log.WithField("kubeconfig", kubeconfig).Info("loading config from file (local mode)") 33 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "failed to make config") 36 | } 37 | } else { 38 | log.Info("loading config from cluster (cluster mode)") 39 | config, err = rest.InClusterConfig() 40 | if err != nil { 41 | return nil, errors.Wrap(err, "failed to make config") 42 | } 43 | } 44 | config.Burst = 100 45 | config.QPS = -1 46 | return kubernetes.NewForConfig(config) 47 | } 48 | 49 | func (s *Client) Get() (*kubernetes.Clientset, error) { 50 | s.mux.Lock() 51 | defer s.mux.Unlock() 52 | if s.inited { 53 | return s.cl, s.err 54 | } 55 | s.cl, s.err = s.get() 56 | s.inited = true 57 | return s.cl, s.err 58 | } 59 | -------------------------------------------------------------------------------- /services/k8s/endpoints.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "github.com/pkg/errors" 6 | log "github.com/sirupsen/logrus" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "time" 9 | 10 | "github.com/urfave/cli" 11 | "github.com/webtor-io/lazymap" 12 | corev1 "k8s.io/api/core/v1" 13 | ) 14 | 15 | const ( 16 | endpointsNamespaceFlag = "endpoints-namespace" 17 | ) 18 | 19 | func RegisterEndpointsFlags(f []cli.Flag) []cli.Flag { 20 | return append(f, 21 | cli.StringFlag{ 22 | Name: endpointsNamespaceFlag, 23 | Usage: "K8SEndpoints namespace", 24 | Value: "webtor", 25 | EnvVar: "ENDPOINTS_NAMESPACE", 26 | }, 27 | ) 28 | } 29 | 30 | type Endpoints struct { 31 | lazymap.LazyMap[*corev1.Endpoints] 32 | cl *Client 33 | namespace string 34 | } 35 | 36 | func NewEndpoints(c *cli.Context, cl *Client) *Endpoints { 37 | return &Endpoints{ 38 | cl: cl, 39 | namespace: c.String(endpointsNamespaceFlag), 40 | LazyMap: lazymap.New[*corev1.Endpoints](&lazymap.Config{ 41 | Expire: 60 * time.Second, 42 | }), 43 | } 44 | } 45 | 46 | func (s *Endpoints) Get(name string) (*corev1.Endpoints, error) { 47 | return s.LazyMap.Get(name, func() (*corev1.Endpoints, error) { 48 | log.Infof("getting k8s endpoints for %s", name) 49 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 50 | defer cancel() 51 | cl, err := s.cl.Get() 52 | if err != nil { 53 | return nil, errors.Wrap(err, "failed to get k8s client") 54 | } 55 | endpoints, err := cl.CoreV1().Endpoints(s.namespace).Get(ctx, name, metav1.GetOptions{}) 56 | if err != nil { 57 | return nil, errors.Wrapf(err, "failed to get k8s endpoints for %s", name) 58 | } 59 | return endpoints, nil 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /services/k8s/nodes_stat.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | log "github.com/sirupsen/logrus" 7 | "sort" 8 | "strings" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/urfave/cli" 13 | "github.com/webtor-io/lazymap" 14 | corev1 "k8s.io/api/core/v1" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | ) 17 | 18 | const ( 19 | nodeLabelPrefixFlag = "node-label-prefix" 20 | ) 21 | 22 | func RegisterNodesStatFlags(f []cli.Flag) []cli.Flag { 23 | return append(f, 24 | cli.StringFlag{ 25 | Name: nodeLabelPrefixFlag, 26 | Usage: "node label prefix", 27 | EnvVar: "NODE_LABEL_PREFIX", 28 | Value: "webtor.io/", 29 | }, 30 | ) 31 | } 32 | 33 | type NodeStat struct { 34 | Name string 35 | RolesAllowed []string 36 | RolesDenied []string 37 | } 38 | 39 | func (s *NodeStat) IsAllowed(role string) bool { 40 | if len(s.RolesAllowed) > 0 { 41 | for _, r := range s.RolesAllowed { 42 | if role == r { 43 | return true 44 | } 45 | } 46 | return false 47 | } else if len(s.RolesDenied) > 0 { 48 | allow := true 49 | for _, r := range s.RolesDenied { 50 | if role == r { 51 | allow = false 52 | } 53 | } 54 | return allow 55 | } 56 | return true 57 | } 58 | 59 | type NodesStat struct { 60 | lazymap.LazyMap[[]NodeStat] 61 | kcl *Client 62 | labelPrefix string 63 | } 64 | 65 | func NewNodesStat(c *cli.Context, kcl *Client) *NodesStat { 66 | return &NodesStat{ 67 | LazyMap: lazymap.New[[]NodeStat](&lazymap.Config{ 68 | Expire: 60 * time.Second, 69 | }), 70 | kcl: kcl, 71 | labelPrefix: c.String(nodeLabelPrefixFlag), 72 | } 73 | } 74 | 75 | func (s *NodesStat) Get() ([]NodeStat, error) { 76 | return s.LazyMap.Get("", func() ([]NodeStat, error) { 77 | log.Info("getting k8s nodes") 78 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 79 | defer cancel() 80 | cl, err := s.kcl.Get() 81 | if err != nil { 82 | return nil, errors.Wrap(err, "failed to get k8s client") 83 | } 84 | nodes, err := cl.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) 85 | if err != nil { 86 | return nil, errors.Wrap(err, "failed to get k8s nodes") 87 | } 88 | var res []NodeStat 89 | for _, n := range nodes.Items { 90 | ready := false 91 | for _, c := range n.Status.Conditions { 92 | if c.Status == corev1.ConditionTrue && c.Type == corev1.NodeReady { 93 | ready = true 94 | } 95 | } 96 | if !ready { 97 | continue 98 | } 99 | res = append(res, NodeStat{ 100 | Name: n.Name, 101 | RolesAllowed: s.getLabelList(n, "roles-allowed"), 102 | RolesDenied: s.getLabelList(n, "roles-denied"), 103 | }) 104 | } 105 | sort.Slice(res, func(i, j int) bool { 106 | return res[i].Name < res[j].Name 107 | }) 108 | return res, nil 109 | }) 110 | } 111 | 112 | func (s *NodesStat) getLabelList(n corev1.Node, name string) []string { 113 | var list []string 114 | if v, ok := n.GetLabels()[fmt.Sprintf("%v%v", s.labelPrefix, name)]; ok { 115 | list = strings.Split(v, ",") 116 | for i := range list { 117 | list[i] = strings.TrimSpace(list[i]) 118 | } 119 | } 120 | return list 121 | } 122 | -------------------------------------------------------------------------------- /services/resolver.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/dgrijalva/jwt-go" 5 | "net" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Ports struct { 14 | HTTP int 15 | Probe int 16 | } 17 | 18 | type Location struct { 19 | Ports 20 | IP net.IP 21 | Unavailable bool 22 | } 23 | 24 | type Resolver struct { 25 | cfg *ServicesConfig 26 | svcLoc *ServiceLocation 27 | } 28 | 29 | func NewResolver(cfg *ServicesConfig, svcLoc *ServiceLocation) *Resolver { 30 | return &Resolver{ 31 | cfg: cfg, 32 | svcLoc: svcLoc, 33 | } 34 | } 35 | 36 | func (s *Resolver) Resolve(src *Source, claims jwt.MapClaims, logger *logrus.Entry) (*Location, error) { 37 | start := time.Now() 38 | 39 | l, err := s.svcLoc.Get(s.cfg.GetMod(src.GetEdgeType()), src, claims) 40 | logger = logger.WithField("duration", time.Since(start).Milliseconds()) 41 | if err != nil { 42 | logger.WithError(err).Error("failed to resolve location") 43 | return nil, errors.Wrap(err, "failed to resolve location") 44 | } 45 | logger.WithField("location", l.IP).Info("location resolved") 46 | return l, nil 47 | } 48 | -------------------------------------------------------------------------------- /services/response_writer_interceptor.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type ResponseWriterInterceptor struct { 13 | http.ResponseWriter 14 | statusCode int 15 | bytesWritten int 16 | start time.Time 17 | ttfb time.Duration 18 | } 19 | 20 | func NewResponseWrtierInterceptor(w http.ResponseWriter) *ResponseWriterInterceptor { 21 | return &ResponseWriterInterceptor{ 22 | statusCode: http.StatusOK, 23 | ResponseWriter: w, 24 | start: time.Now(), 25 | } 26 | } 27 | 28 | func (w *ResponseWriterInterceptor) WriteHeader(statusCode int) { 29 | w.statusCode = statusCode 30 | w.ResponseWriter.WriteHeader(statusCode) 31 | } 32 | func (w *ResponseWriterInterceptor) GroupedStatusCode() int { 33 | return w.statusCode / 100 * 100 34 | } 35 | 36 | func (w *ResponseWriterInterceptor) Write(p []byte) (int, error) { 37 | if w.bytesWritten == 0 { 38 | w.ttfb = time.Since(w.start) 39 | } 40 | w.bytesWritten += len(p) 41 | return w.ResponseWriter.Write(p) 42 | } 43 | 44 | func (w *ResponseWriterInterceptor) Hijack() (net.Conn, *bufio.ReadWriter, error) { 45 | h, ok := w.ResponseWriter.(http.Hijacker) 46 | if !ok { 47 | return nil, nil, errors.New("type assertion failed http.ResponseWriter not a http.Hijacker") 48 | } 49 | return h.Hijack() 50 | } 51 | 52 | func (w *ResponseWriterInterceptor) Flush() { 53 | f, ok := w.ResponseWriter.(http.Flusher) 54 | if !ok { 55 | return 56 | } 57 | 58 | f.Flush() 59 | } 60 | 61 | // Check interface implementations. 62 | var ( 63 | _ http.ResponseWriter = &ResponseWriterInterceptor{} 64 | _ http.Hijacker = &ResponseWriterInterceptor{} 65 | _ http.Flusher = &ResponseWriterInterceptor{} 66 | ) 67 | -------------------------------------------------------------------------------- /services/service_location.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/webtor-io/lazymap" 10 | "github.com/webtor-io/torrent-http-proxy/services/k8s" 11 | "io" 12 | corev1 "k8s.io/api/core/v1" 13 | "math/rand" 14 | "net" 15 | "net/http" 16 | "os" 17 | "regexp" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "time" 22 | 23 | "github.com/urfave/cli" 24 | ) 25 | 26 | var sha1R = regexp.MustCompile("^[0-9a-f]{5,40}$") 27 | 28 | type ServiceLocation struct { 29 | lazymap.LazyMap[*Location] 30 | ep *k8s.Endpoints 31 | nodes *k8s.NodesStat 32 | c *cli.Context 33 | nn string 34 | ignore *EndpointIgnoreList 35 | probeChecker *ProbeChecker 36 | } 37 | 38 | type ProbeChecker struct { 39 | lazymap.LazyMap[bool] 40 | cl *http.Client 41 | } 42 | 43 | func (s *ProbeChecker) Get(l *Location) (bool, error) { 44 | return s.LazyMap.Get(l.IP.String(), func() (bool, error) { 45 | probePort := l.Ports.Probe 46 | if probePort == 0 { 47 | probePort = l.Ports.HTTP 48 | } 49 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 50 | defer cancel() 51 | req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://%v:%v", l.IP, probePort), nil) 52 | if err != nil { 53 | return false, err 54 | } 55 | resp, err := s.cl.Do(req) 56 | if err != nil { 57 | return false, err 58 | } 59 | defer func(Body io.ReadCloser) { 60 | _ = Body.Close() 61 | }(resp.Body) 62 | if resp.StatusCode >= 500 { 63 | return false, errors.Errorf("unexpected status code: %d", resp.StatusCode) 64 | } 65 | return true, nil 66 | }) 67 | } 68 | 69 | type EndpointIgnoreList struct { 70 | lazymap.LazyMap[bool] 71 | } 72 | 73 | func (s *EndpointIgnoreList) Ignore(ip string) bool { 74 | res, _ := s.Get(ip, func() (bool, error) { 75 | return true, nil 76 | }) 77 | return res 78 | } 79 | 80 | func (s *EndpointIgnoreList) IsIgnored(ip string) bool { 81 | _, ok := s.Status(ip) 82 | return ok 83 | } 84 | 85 | func NewServiceLocationPool(c *cli.Context, cl *http.Client, nodes *k8s.NodesStat, ep *k8s.Endpoints) *ServiceLocation { 86 | return &ServiceLocation{ 87 | c: c, 88 | ep: ep, 89 | nodes: nodes, 90 | nn: c.String(myNodeNameFlag), 91 | LazyMap: lazymap.New[*Location](&lazymap.Config{ 92 | Expire: 15 * time.Second, 93 | }), 94 | ignore: &EndpointIgnoreList{lazymap.New[bool](&lazymap.Config{ 95 | Expire: 30 * time.Second, 96 | })}, 97 | probeChecker: &ProbeChecker{ 98 | LazyMap: lazymap.New[bool](&lazymap.Config{ 99 | Expire: 30 * time.Second, 100 | StoreErrors: true, 101 | }), 102 | cl: cl, 103 | }, 104 | } 105 | } 106 | 107 | func (s *ServiceLocation) Get(cfg *ServiceConfig, src *Source, claims jwt.MapClaims) (*Location, error) { 108 | key := cfg.Name + src.InfoHash 109 | role, ok := claims["role"].(string) 110 | if ok { 111 | key += role 112 | } 113 | return s.LazyMap.Get(key, func() (*Location, error) { 114 | if cfg.EndpointsProvider == Kubernetes { 115 | return s.getKubernetesWithProbeCheck(cfg, src, claims) 116 | } else if cfg.EndpointsProvider == Environment { 117 | return s.getEnvironment(cfg) 118 | } else { 119 | return nil, errors.Errorf("unknown endpoints provider: %s", cfg.EndpointsProvider) 120 | } 121 | }) 122 | } 123 | 124 | func (s *ServiceLocation) getKubernetesWithProbeCheck(cfg *ServiceConfig, src *Source, claims jwt.MapClaims) (*Location, error) { 125 | i := 0 126 | for { 127 | if i > 2 { 128 | log.Warnf("failed to get location for %v after %v tries", cfg.Name, i) 129 | return &Location{ 130 | Unavailable: true, 131 | }, nil 132 | } 133 | l, err := s.getKubernetes(cfg, src, claims) 134 | if err != nil { 135 | return nil, err 136 | } 137 | if l.Unavailable { 138 | return l, nil 139 | } 140 | _, err = s.probeChecker.Get(l) 141 | if err != nil { 142 | log.WithError(err).Warnf("probe check failed for %v location %+v, add it to ignore", cfg.Name, l) 143 | s.ignore.Ignore(l.IP.String()) 144 | i++ 145 | continue 146 | } 147 | return l, nil 148 | } 149 | } 150 | 151 | func (s *ServiceLocation) getKubernetes(cfg *ServiceConfig, src *Source, claims jwt.MapClaims) (*Location, error) { 152 | endpoints, err := s.ep.Get(cfg.Name) 153 | if err != nil { 154 | return nil, errors.Wrap(err, "failed to get endpoints") 155 | } 156 | subset := endpoints.Subsets[0] 157 | as := subset.Addresses 158 | as = s.filterAddressesByIgnore(as) 159 | if len(as) == 0 { 160 | return &Location{ 161 | Unavailable: true, 162 | }, nil 163 | } 164 | var a *corev1.EndpointAddress 165 | if !sha1R.Match([]byte(src.InfoHash)) { 166 | a = &as[rand.Intn(len(as))] 167 | } else if cfg.Distribution == Hash { 168 | a, err = s.distributeByHash(src, as) 169 | } else if cfg.Distribution == NodeHash { 170 | a, err = s.distributeByNodeHash(src, as, claims) 171 | } 172 | if err != nil { 173 | return nil, errors.Wrap(err, "failed to distribute") 174 | } 175 | if a != nil && s.nn != "" && *a.NodeName != s.nn && cfg.PreferLocalNode { 176 | var las []corev1.EndpointAddress 177 | for _, a := range as { 178 | if *a.NodeName == s.nn { 179 | las = append(las, a) 180 | } 181 | } 182 | if len(las) > 0 { 183 | a, err = s.distributeByHash(src, las) 184 | if err != nil { 185 | return nil, errors.Wrap(err, "failed to distribute locally") 186 | } 187 | } 188 | } 189 | return s.addressToLocation(a, &subset), nil 190 | } 191 | 192 | func (s *ServiceLocation) getPort(sub *corev1.EndpointSubset, name string) int { 193 | for _, p := range sub.Ports { 194 | if p.Name == name { 195 | return int(p.Port) 196 | } 197 | } 198 | return 0 199 | } 200 | 201 | func (s *ServiceLocation) addressToLocation(a *corev1.EndpointAddress, sub *corev1.EndpointSubset) *Location { 202 | if a == nil { 203 | return &Location{ 204 | Unavailable: true, 205 | } 206 | } 207 | return &Location{ 208 | IP: net.ParseIP(a.IP), 209 | Ports: Ports{ 210 | HTTP: s.getPort(sub, "http"), 211 | Probe: s.getPort(sub, "httpprobe"), 212 | }, 213 | Unavailable: false, 214 | } 215 | } 216 | 217 | func (s *ServiceLocation) distributeByHash(src *Source, as []corev1.EndpointAddress) (*corev1.EndpointAddress, error) { 218 | sort.Slice(as, func(i, j int) bool { 219 | return as[i].IP < as[j].IP 220 | }) 221 | hex := src.InfoHash[0:5] 222 | num64, err := strconv.ParseInt(hex, 16, 64) 223 | if err != nil { 224 | return nil, errors.Wrapf(err, "failed to parse hex from infohash=%v", src.InfoHash) 225 | } 226 | num := int(num64 * 1000) 227 | total := 1048575 * 1000 228 | interval := total / len(as) 229 | for i := 0; i < len(as); i++ { 230 | if num < (i+1)*interval { 231 | return &as[i], nil 232 | } 233 | } 234 | return nil, nil 235 | } 236 | 237 | func (s *ServiceLocation) distributeByNodeHash(src *Source, as []corev1.EndpointAddress, claims jwt.MapClaims) (*corev1.EndpointAddress, error) { 238 | sort.Slice(as, func(i, j int) bool { 239 | return as[i].IP < as[j].IP 240 | }) 241 | nodesM := map[string]bool{} 242 | var nodes []string 243 | for _, a := range as { 244 | nodesM[*a.NodeName] = true 245 | } 246 | for n := range nodesM { 247 | nodes = append(nodes, n) 248 | } 249 | sort.Strings(nodes) 250 | nodes, err := s.filterNodesByRole(nodes, claims) 251 | if err != nil { 252 | return nil, errors.Wrap(err, "failed to filter nodes by role") 253 | } 254 | hex := src.InfoHash[0:5] 255 | num64, err := strconv.ParseInt(hex, 16, 64) 256 | if err != nil { 257 | return nil, errors.Wrapf(err, "failed to parse hex from infohash=%v", src.InfoHash) 258 | } 259 | num := int(num64 * 1000) 260 | total := 1048575 * 1000 261 | if len(nodes) == 0 { 262 | return nil, errors.Wrapf(err, "failed to distribute, no nodes found") 263 | } 264 | nodeInterval := total / len(nodes) 265 | for i := 0; i < len(nodes); i++ { 266 | var nas []corev1.EndpointAddress 267 | for _, a := range as { 268 | if *a.NodeName == nodes[i] { 269 | nas = append(nas, a) 270 | } 271 | } 272 | aInterval := nodeInterval / len(nas) 273 | for j := 0; j < len(nas); j++ { 274 | if num < i*nodeInterval+(j+1)*aInterval { 275 | return &nas[j], nil 276 | } 277 | } 278 | } 279 | return nil, nil 280 | } 281 | 282 | func (s *ServiceLocation) filterNodesByRole(nodes []string, claims jwt.MapClaims) ([]string, error) { 283 | if claims == nil { 284 | return nodes, nil 285 | } 286 | role, ok := claims["role"].(string) 287 | if !ok { 288 | return nodes, nil 289 | } 290 | if role == "" { 291 | return nodes, nil 292 | } 293 | ns, err := s.nodes.Get() 294 | if err != nil { 295 | return nil, errors.Wrap(err, "failed to get nodes") 296 | } 297 | var res []string 298 | for _, n := range nodes { 299 | for _, nss := range ns { 300 | if n == nss.Name && nss.IsAllowed(role) { 301 | res = append(res, n) 302 | } 303 | } 304 | } 305 | return res, nil 306 | } 307 | 308 | func (s *ServiceLocation) getEnvironment(cfg *ServiceConfig) (*Location, error) { 309 | name := strings.ReplaceAll(strings.ToUpper(cfg.Name), "-", "_") 310 | portName := name + "_SERVICE_PORT" 311 | hostName := name + "_SERVICE_HOST" 312 | port, err := strconv.Atoi(os.Getenv(portName)) 313 | if err != nil { 314 | return nil, errors.Wrapf(err, "failed to parse environment variable %s with value \"%v\"", portName, os.Getenv(portName)) 315 | } 316 | ip := net.ParseIP(os.Getenv(hostName)) 317 | if ip == nil { 318 | return nil, errors.Errorf("failed to parse environment variable %v with value \"%v\"", hostName, os.Getenv(hostName)) 319 | } 320 | return &Location{ 321 | Ports: Ports{ 322 | HTTP: port, 323 | }, 324 | IP: ip, 325 | }, nil 326 | } 327 | 328 | func (s *ServiceLocation) filterAddressesByIgnore(as []corev1.EndpointAddress) []corev1.EndpointAddress { 329 | var res []corev1.EndpointAddress 330 | for _, a := range as { 331 | if s.ignore.IsIgnored(net.ParseIP(a.IP).String()) { 332 | continue 333 | } 334 | res = append(res, a) 335 | } 336 | return res 337 | } 338 | -------------------------------------------------------------------------------- /services/services_config.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/urfave/cli" 6 | "gopkg.in/yaml.v3" 7 | "os" 8 | ) 9 | 10 | const ( 11 | configFlag = "config" 12 | ) 13 | 14 | type Distribution string 15 | 16 | const ( 17 | Hash Distribution = "Hash" 18 | NodeHash Distribution = "NodeHash" 19 | ) 20 | 21 | type EndpointsProvider string 22 | 23 | const ( 24 | Kubernetes EndpointsProvider = "Kubernetes" 25 | Environment EndpointsProvider = "Environment" 26 | ) 27 | 28 | func RegisterServicesConfigFlags(flags []cli.Flag) []cli.Flag { 29 | return append(flags, &cli.StringFlag{ 30 | Name: configFlag, 31 | Usage: "Path to the services configuration YAML file", 32 | EnvVar: "CONFIG_PATH", 33 | Required: true, 34 | }) 35 | } 36 | 37 | type ServiceConfig struct { 38 | Name string `yaml:"name"` 39 | Distribution Distribution `yaml:"distribution"` 40 | EndpointsProvider EndpointsProvider `yaml:"endpointsProvider"` 41 | PreferLocalNode bool `yaml:"preferLocalNode"` 42 | Headers map[string]string `yaml:"headers"` 43 | } 44 | 45 | type ServicesConfig map[string]*ServiceConfig 46 | 47 | func (s ServicesConfig) GetMods() []string { 48 | var res []string 49 | for k := range map[string]*ServiceConfig(s) { 50 | if k != "default" { 51 | res = append(res, k) 52 | } 53 | } 54 | return res 55 | } 56 | 57 | func (s ServicesConfig) GetMod(name string) *ServiceConfig { 58 | return map[string]*ServiceConfig(s)[name] 59 | } 60 | 61 | func (s ServicesConfig) GetDefault() *ServiceConfig { 62 | for k, v := range map[string]*ServiceConfig(s) { 63 | if k == "default" { 64 | return v 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func LoadServicesConfigFromYAML(c *cli.Context) (*ServicesConfig, error) { 71 | filename := c.String(configFlag) 72 | if filename == "" { 73 | return nil, errors.New("no config file provided") 74 | } 75 | data, err := os.ReadFile(filename) 76 | if err != nil { 77 | return nil, err 78 | } 79 | s := &ServicesConfig{} 80 | if err := yaml.Unmarshal(data, s); err != nil { 81 | return nil, err 82 | } 83 | for _, cfg := range *s { 84 | if cfg.Distribution == "" { 85 | cfg.Distribution = Hash 86 | } 87 | if cfg.EndpointsProvider == "" { 88 | cfg.EndpointsProvider = Kubernetes 89 | } 90 | } 91 | return s, nil 92 | } 93 | -------------------------------------------------------------------------------- /services/throttled_request_writer.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/juju/ratelimit" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type ThrottledResponseWriter struct { 13 | http.ResponseWriter 14 | b *ratelimit.Bucket 15 | } 16 | 17 | func NewThrottledRequestWrtier(w http.ResponseWriter, b *ratelimit.Bucket) *ThrottledResponseWriter { 18 | return &ThrottledResponseWriter{ 19 | ResponseWriter: w, 20 | b: b, 21 | } 22 | } 23 | 24 | func (w *ThrottledResponseWriter) WriteHeader(statusCode int) { 25 | w.ResponseWriter.WriteHeader(statusCode) 26 | } 27 | 28 | func (w *ThrottledResponseWriter) Write(p []byte) (int, error) { 29 | w.b.Wait(int64(len(p))) 30 | return w.ResponseWriter.Write(p) 31 | } 32 | 33 | func (w *ThrottledResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 34 | h, ok := w.ResponseWriter.(http.Hijacker) 35 | if !ok { 36 | return nil, nil, errors.New("type assertion failed http.ResponseWriter not a http.Hijacker") 37 | } 38 | return h.Hijack() 39 | } 40 | 41 | func (w *ThrottledResponseWriter) Flush() { 42 | f, ok := w.ResponseWriter.(http.Flusher) 43 | if !ok { 44 | return 45 | } 46 | 47 | f.Flush() 48 | } 49 | 50 | // Check interface implementations. 51 | var ( 52 | _ http.ResponseWriter = &ThrottledResponseWriter{} 53 | _ http.Hijacker = &ThrottledResponseWriter{} 54 | _ http.Flusher = &ThrottledResponseWriter{} 55 | ) 56 | -------------------------------------------------------------------------------- /services/url_parser.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Mod struct represents modification of source file. 14 | type Mod struct { 15 | Type string `json:"type"` 16 | Path string `json:"path"` 17 | Extra string `json:"extra"` 18 | Name string `json:"name"` 19 | } 20 | 21 | // Source struct represents torrent file source. 22 | // Source may have additional modification. 23 | type Source struct { 24 | Type string `json:"type"` 25 | Name string `json:"name"` 26 | InfoHash string `json:"info_hash"` 27 | Path string `json:"path"` 28 | OriginPath string `json:"origin_path"` 29 | Token string `json:"token"` 30 | ApiKey string `json:"api_key"` 31 | Query string `json:"query"` 32 | Mod *Mod 33 | } 34 | 35 | func (s *Source) GetKey() string { 36 | key := s.InfoHash + s.Type 37 | if s.Mod != nil { 38 | key = key + s.Path + s.Mod.Type + s.Mod.Extra 39 | } 40 | return key 41 | } 42 | 43 | func (s *Source) GetEdgeType() string { 44 | if s.Mod != nil { 45 | return s.Mod.Type 46 | } 47 | return s.Type 48 | } 49 | 50 | func (s *Source) GetEdgeName() string { 51 | if s.Mod != nil { 52 | return s.Mod.Name 53 | } 54 | return s.Name 55 | } 56 | 57 | func checkHash(hash string) bool { 58 | match, _ := regexp.MatchString("[0-9a-f]{5,40}", hash) 59 | return match 60 | } 61 | 62 | func (s *URLParser) extractMod(path string) (string, *Mod, error) { 63 | if !strings.Contains(path, "~") { 64 | return path, nil, nil 65 | } 66 | index := len(path) 67 | exist := false 68 | var p []string 69 | e := "" 70 | name := "" 71 | newPath := "" 72 | t := "" 73 | for { 74 | index = strings.LastIndex(path[:index], "~") 75 | if index == -1 { 76 | break 77 | } 78 | first := path[:index] 79 | last := path[index+1:] 80 | newPath = first 81 | p = strings.SplitN(last, "/", 2) 82 | t = p[0] 83 | ee := strings.SplitN(t, ":", 2) 84 | if len(ee) > 1 { 85 | e = ee[1] 86 | t = ee[0] 87 | } 88 | // if t == "" { 89 | // return "", nil, errors.New("Empty mod name") 90 | // } 91 | for _, v := range s.configs.GetMods() { 92 | if t == v { 93 | exist = true 94 | name = s.configs.GetMod(v).Name 95 | break 96 | } 97 | } 98 | if exist { 99 | break 100 | } 101 | } 102 | if !exist { 103 | return path, nil, nil 104 | } 105 | modPath := "/" 106 | if len(p) > 1 { 107 | modPath += p[1] 108 | } 109 | modPath = filepath.Clean(modPath) 110 | m := &Mod{ 111 | Type: t, 112 | Path: modPath, 113 | Extra: e, 114 | Name: name, 115 | } 116 | return newPath, m, nil 117 | } 118 | 119 | type URLParser struct { 120 | configs *ServicesConfig 121 | } 122 | 123 | func NewURLParser(c *ServicesConfig) *URLParser { 124 | return &URLParser{ 125 | configs: c, 126 | } 127 | } 128 | 129 | // Parse extracts information about source and additional modifiacation of it 130 | func (s *URLParser) Parse(url *url.URL) (*Source, error) { 131 | urlPath := url.Path 132 | if urlPath == "" { 133 | return nil, errors.New("Empty url") 134 | } 135 | p := strings.SplitN(urlPath[1:], "/", 2) 136 | hash := p[0] 137 | if hash == "" { 138 | return nil, errors.New("Empty hash") 139 | } 140 | sourceType := "default" 141 | for _, v := range s.configs.GetMods() { 142 | if hash == v { 143 | sourceType = v 144 | break 145 | } 146 | } 147 | sourceName := s.configs.GetMod(sourceType).Name 148 | if sourceType == "default" && !checkHash(hash) { 149 | return nil, errors.New(fmt.Sprintf("Wrong hash=%s", hash)) 150 | } 151 | path := "/" 152 | if len(p) > 1 { 153 | path += p[1] 154 | } 155 | // path = filepath.Clean(path) 156 | newPath, mod, err := s.extractMod(path) 157 | if err != nil { 158 | return nil, errors.Wrapf(err, "failed to extract mod from path=%s", path) 159 | } 160 | originPath := newPath 161 | var tempMod *Mod 162 | for { 163 | originPath, tempMod, err = s.extractMod(originPath) 164 | if err != nil { 165 | return nil, errors.Wrapf(err, "failed to extract mod from path=%s", path) 166 | } 167 | if tempMod == nil { 168 | break 169 | } 170 | } 171 | ss := &Source{ 172 | InfoHash: hash, 173 | Path: newPath, 174 | OriginPath: originPath, 175 | Token: url.Query().Get("token"), 176 | ApiKey: url.Query().Get("api-key"), 177 | Query: url.RawQuery, 178 | Type: sourceType, 179 | Name: sourceName, 180 | Mod: mod, 181 | } 182 | return ss, nil 183 | } 184 | -------------------------------------------------------------------------------- /services/url_parser_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "testing" 7 | 8 | "github.com/urfave/cli" 9 | s "github.com/webtor-io/torrent-http-proxy/services" 10 | ) 11 | 12 | func TestUrlParse(t *testing.T) { 13 | app := cli.NewApp() 14 | app.Action = func(c *cli.Context) error { 15 | config, err := s.LoadServicesConfigFromYAML(c) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | p := s.NewURLParser(config) 21 | u, _ := url.Parse("https://example.com/935d59df63e6b94305b5e2a32cdfd00488f1b055/%5BErai-raws%5D%20One%20Piece%20-%20401~500%20%5B1080p%5D%5BMultiple%20Subtitle%5D~arch/[Erai-raws]%20One%20Piece%20-%20401~500%20[1080p][Multiple%20Subtitle].zip~dp/[Erai-raws]%20One%20Piece%20-%20401~500%20[1080p][Multiple%20Subtitle].zip?user-id=32d150920bdb5ff511697f28b3437bf9&download-id=87275499d2e74a5257c810c2cb8085c1&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS85MS4wLjQ0NzIuNzcgU2FmYXJpLzUzNy4zNiIsInJlbW90ZUFkZHJlc3MiOiI0Ni4xNjAuMjU1LjE5NyIsImRvbWFpbiI6IndlYnRvci5pbyIsImV4cCI6MTYyMzY4NTg1Nywic2Vzc2lvbklEIjoiU0VjYzcyck5KWFlRcS1UbUJaRkdxWkZjcUpJRlJXMDYiLCJyYXRlIjoiMTBNIiwicm9sZSI6Im5vYm9keSJ9.6UfWJa6vDZrqbwBlfq96_PUV3LZvodpkjhNFnZrE9r0&api-key=8acbcf1e-732c-4574-a3bf-27e6a85b86f1") 22 | src, _ := p.Parse(u) 23 | if src.Mod == nil { 24 | t.Fatalf("Got empty mod") 25 | } 26 | if src.Mod.Name != "download-progress" { 27 | t.Fatalf("Expected %v got %v", "download-progress", src.Mod.Name) 28 | } 29 | u, _ = url.Parse("https://example.com/08ada5a7a6183aae1e09d831df6748d566095a10/~tc/completed_pieces?download-id=812a10f280c6348bdd630f6a38e65fb6&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZ2VudCI6Ik1vemlsbGEvNS4wIChNYWNpbnRvc2g7IEludGVsIE1hYyBPUyBYIDEwXzE1XzcpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS85MS4wLjQ0NzIuNzcgU2FmYXJpLzUzNy4zNiIsInJlbW90ZUFkZHJlc3MiOiI0Ni4xNjAuMjU1LjE5NyIsImRvbWFpbiI6IndlYnRvci5pbyIsImV4cCI6MTYyMzY5MzUyMCwic2Vzc2lvbklEIjoiU0VjYzcyck5KWFlRcS1UbUJaRkdxWkZjcUpJRlJXMDYiLCJyYXRlIjoiMTBNIiwicm9sZSI6Im5vYm9keSJ9.4RakJlhLxFPVTjwYlpcYDxR45s4gFFOYok4n8dA5IqI&api-key=8acbcf1e-732c-4574-a3bf-27e6a85b86f1") 30 | src, _ = p.Parse(u) 31 | if src.Mod == nil { 32 | t.Fatalf("Got empty mod") 33 | } 34 | if src.Mod.Name != "torrent-web-cache" { 35 | t.Fatalf("Expected %v got %v", "torrent-web-cache", src.Mod.Name) 36 | } 37 | return nil 38 | } 39 | args := os.Args[0:1] 40 | _ = app.Run(args) 41 | } 42 | -------------------------------------------------------------------------------- /services/web.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/sirupsen/logrus" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/urfave/cli" 17 | ) 18 | 19 | type SourceType string 20 | 21 | const ( 22 | Internal SourceType = "internal" 23 | External SourceType = "external" 24 | ) 25 | 26 | type Web struct { 27 | host string 28 | port int 29 | ln net.Listener 30 | r *Resolver 31 | pr *HTTPProxy 32 | parser *URLParser 33 | bucket *Bucket 34 | clickHouse *ClickHouse 35 | baseURL string 36 | claims *Claims 37 | cfg *ServicesConfig 38 | ah *AccessHistory 39 | bandwidthLimit bool 40 | } 41 | 42 | const ( 43 | webHostFlag = "host" 44 | webPortFlag = "port" 45 | torrentHTTPProxyHostFlag = "torrent-http-proxy-host" 46 | torrentHTTPProxyPortFlag = "torrent-http-proxy-port" 47 | useBandwidthLimitFlag = "use-bandwidth-limit" 48 | ) 49 | 50 | var ( 51 | promHTTPProxyRequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 52 | Name: "webtor_http_proxy_request_duration_seconds", 53 | Help: "HTTP Proxy request duration in seconds", 54 | }, []string{"source", "role", "name", "status"}) 55 | promHTTPProxyRequestTTFB = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 56 | Name: "webtor_http_proxy_request_ttfb_seconds", 57 | Help: "HTTP Proxy request ttfb in seconds", 58 | }, []string{"source", "role", "name", "status"}) 59 | promHTTPProxyRequestSize = prometheus.NewCounterVec(prometheus.CounterOpts{ 60 | Name: "webtor_http_proxy_request_size_bytes", 61 | Help: "HTTP Proxy request size bytes", 62 | }, []string{"domain", "role", "source", "name", "infohash", "file", "status"}) 63 | promHTTPProxyRequestCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{ 64 | Name: "webtor_http_proxy_request_current", 65 | Help: "HTTP Proxy request current", 66 | }, []string{"source", "role", "name"}) 67 | promHTTPProxyRequestTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ 68 | Name: "webtor_http_proxy_request_total", 69 | Help: "HTTP Proxy dial total", 70 | }, []string{"source", "role", "name", "infohash", "status"}) 71 | ) 72 | 73 | func init() { 74 | prometheus.MustRegister(promHTTPProxyRequestDuration) 75 | prometheus.MustRegister(promHTTPProxyRequestTTFB) 76 | prometheus.MustRegister(promHTTPProxyRequestSize) 77 | prometheus.MustRegister(promHTTPProxyRequestCurrent) 78 | prometheus.MustRegister(promHTTPProxyRequestTotal) 79 | } 80 | 81 | func NewWeb(c *cli.Context, parser *URLParser, r *Resolver, pr *HTTPProxy, claims *Claims, bp *Bucket, ch *ClickHouse, cfg *ServicesConfig, ah *AccessHistory) *Web { 82 | return &Web{ 83 | host: c.String(webHostFlag), 84 | port: c.Int(webPortFlag), 85 | baseURL: fmt.Sprintf("http://%s:%d", c.String(torrentHTTPProxyHostFlag), c.Int(torrentHTTPProxyPortFlag)), 86 | parser: parser, 87 | r: r, 88 | pr: pr, 89 | claims: claims, 90 | bucket: bp, 91 | clickHouse: ch, 92 | cfg: cfg, 93 | ah: ah, 94 | bandwidthLimit: c.Bool(useBandwidthLimitFlag), 95 | } 96 | } 97 | 98 | func RegisterWebFlags(f []cli.Flag) []cli.Flag { 99 | return append(f, 100 | cli.StringFlag{ 101 | Name: webHostFlag, 102 | Usage: "listening host", 103 | Value: "", 104 | EnvVar: "WEB_HOST", 105 | }, 106 | cli.IntFlag{ 107 | Name: webPortFlag, 108 | Usage: "http listening port", 109 | Value: 8080, 110 | EnvVar: "WEB_PORT", 111 | }, 112 | cli.StringFlag{ 113 | Name: torrentHTTPProxyHostFlag, 114 | Usage: "torrent http proxy host", 115 | EnvVar: "TORRENT_HTTP_PROXY_SERVICE_HOST", 116 | }, 117 | cli.IntFlag{ 118 | Name: torrentHTTPProxyPortFlag, 119 | Usage: "torrent http proxy port", 120 | Value: 8080, 121 | EnvVar: "TORRENT_HTTP_PROXY_SERVICE_PORT", 122 | }, 123 | cli.BoolFlag{ 124 | Name: useBandwidthLimitFlag, 125 | Usage: "use bandwidth limit", 126 | EnvVar: "USE_BANDWIDTH_LIMIT", 127 | }, 128 | ) 129 | } 130 | 131 | func (s *Web) getIP(r *http.Request) string { 132 | forwarded := r.Header.Get("X-FORWARDED-FOR") 133 | if forwarded != "" { 134 | return strings.Split(forwarded, ",")[0] 135 | } 136 | return r.RemoteAddr 137 | } 138 | 139 | func (s *Web) proxyHTTP(w http.ResponseWriter, r *http.Request, src *Source, logger *logrus.Entry) { 140 | wi := NewResponseWrtierInterceptor(w) 141 | w = wi 142 | apiKey := r.URL.Query().Get("api-key") 143 | claims, err := s.claims.Get(r.URL.Query().Get("token"), apiKey) 144 | if err != nil { 145 | logger.WithError(err).Warnf("failed to get claims") 146 | w.WriteHeader(http.StatusForbidden) 147 | return 148 | } 149 | 150 | source := Internal 151 | if r.Header.Get("X-FORWARDED-FOR") != "" { 152 | source = External 153 | } 154 | 155 | ads := false 156 | 157 | role := "nobody" 158 | if r, ok := claims["role"].(string); ok { 159 | role = r 160 | } 161 | if r, ok := claims["ads"].(bool); ok { 162 | ads = r 163 | } 164 | domain := "default" 165 | if d, ok := claims["domain"].(string); ok { 166 | domain = d 167 | } 168 | 169 | sessionID := "" 170 | if sid, ok := claims["sessionID"].(string); ok { 171 | sessionID = sid 172 | } 173 | 174 | promHTTPProxyRequestCurrent.WithLabelValues(string(source), role, src.GetEdgeName()).Inc() 175 | defer func() { 176 | if s.clickHouse != nil && wi.bytesWritten > 0 && wi.GroupedStatusCode() == 200 { 177 | err := s.clickHouse.Add(&StatRecord{ 178 | ApiKey: apiKey, 179 | BytesWritten: uint64(wi.bytesWritten), 180 | Domain: domain, 181 | Duration: uint64(time.Since(wi.start).Milliseconds()), 182 | Edge: src.GetEdgeName(), 183 | GroupedStatus: uint64(wi.GroupedStatusCode()), 184 | InfoHash: src.InfoHash, 185 | OriginalPath: src.OriginPath, 186 | Path: src.Path, 187 | Role: role, 188 | SessionID: sessionID, 189 | Source: string(source), 190 | Status: uint64(wi.statusCode), 191 | TTFB: uint64(wi.ttfb.Milliseconds()), 192 | Timestamp: time.Now(), 193 | Ads: ads, 194 | }) 195 | if err != nil { 196 | logger.WithError(err).Warn("failed to store data to ClickHouse") 197 | } 198 | } 199 | promHTTPProxyRequestDuration.WithLabelValues(string(source), role, src.GetEdgeName(), strconv.Itoa(wi.GroupedStatusCode())).Observe(time.Since(wi.start).Seconds()) 200 | if wi.bytesWritten > 0 { 201 | promHTTPProxyRequestTTFB.WithLabelValues(string(source), role, src.GetEdgeName(), strconv.Itoa(wi.GroupedStatusCode())).Observe(wi.ttfb.Seconds()) 202 | } 203 | promHTTPProxyRequestCurrent.WithLabelValues(string(source), role, src.GetEdgeName()).Dec() 204 | promHTTPProxyRequestTotal.WithLabelValues(string(source), role, src.GetEdgeName(), src.InfoHash, strconv.Itoa(wi.GroupedStatusCode())).Inc() 205 | promHTTPProxyRequestSize.WithLabelValues( 206 | domain, 207 | role, 208 | string(source), 209 | src.GetEdgeName(), 210 | src.InfoHash, 211 | src.Path, 212 | strconv.Itoa(wi.GroupedStatusCode()), 213 | ).Add(float64(wi.bytesWritten)) 214 | rate, _ := claims["rate"].(string) 215 | l := logger.WithFields(logrus.Fields{ 216 | "domain": domain, 217 | "role": role, 218 | "source": string(source), 219 | "edge": src.GetEdgeName(), 220 | "infohash": src.InfoHash, 221 | "path": src.Path, 222 | "ttfb": wi.ttfb.Seconds(), 223 | "duration": time.Since(wi.start).Seconds(), 224 | "status": strconv.Itoa(wi.statusCode), 225 | "rate": rate, 226 | "session_id": sessionID, 227 | "referer": r.Referer(), 228 | }) 229 | if wi.GroupedStatusCode() == 500 { 230 | l.Error("failed to serve request") 231 | } else if wi.GroupedStatusCode() == 200 { 232 | l.Info("request served successfully") 233 | } else { 234 | l.Warn("bad request") 235 | } 236 | }() 237 | 238 | headers := map[string]string{ 239 | "X-Source-Url": s.baseURL + "/" + src.InfoHash + src.Path + "?" + src.Query, 240 | "X-Proxy-Url": s.baseURL, 241 | "X-Info-Hash": src.InfoHash, 242 | "X-Path": src.Path, 243 | "X-Origin-Path": src.OriginPath, 244 | "X-Full-Path": "/" + src.InfoHash + "/" + url.PathEscape(strings.TrimPrefix(src.Path, "/")), 245 | "X-Token": src.Token, 246 | "X-Api-Key": apiKey, 247 | "X-Session-ID": sessionID, 248 | } 249 | 250 | rate, ok := claims["rate"].(string) 251 | if ok { 252 | headers["X-Download-Rate"] = rate 253 | } 254 | 255 | cfg := s.cfg.GetMod(src.GetEdgeType()) 256 | 257 | if cfg.Headers != nil { 258 | for k, v := range cfg.Headers { 259 | headers[k] = v 260 | } 261 | } 262 | 263 | if s.bandwidthLimit && source == External { 264 | b, err := s.bucket.Get(claims) 265 | if err != nil { 266 | logger.WithError(err).Errorf("failed to get bucket") 267 | w.WriteHeader(http.StatusInternalServerError) 268 | return 269 | } 270 | if b != nil { 271 | w = NewThrottledRequestWrtier(w, b) 272 | } 273 | } 274 | 275 | for k, v := range headers { 276 | r.Header.Set(k, v) 277 | } 278 | 279 | pr, err := s.pr.Get(src, claims, logger) 280 | 281 | if err != nil { 282 | logger.WithError(err).Errorf("failed to get proxy") 283 | w.WriteHeader(http.StatusInternalServerError) 284 | return 285 | } 286 | if pr == nil { 287 | w.WriteHeader(http.StatusNotImplemented) 288 | return 289 | } 290 | pr.ServeHTTP(w, r) 291 | } 292 | 293 | func (s *Web) Serve() error { 294 | addr := fmt.Sprintf("%s:%d", s.host, s.port) 295 | ln, err := net.Listen("tcp", addr) 296 | if err != nil { 297 | return errors.Wrap(err, "failed to web listen to tcp connection") 298 | } 299 | s.ln = ln 300 | mux := http.NewServeMux() 301 | 302 | var ip net.IP 303 | ifaces, _ := net.Interfaces() 304 | for _, i := range ifaces { 305 | addrs, _ := i.Addrs() 306 | for _, addr := range addrs { 307 | switch v := addr.(type) { 308 | case *net.IPNet: 309 | ip = v.IP 310 | case *net.IPAddr: 311 | ip = v.IP 312 | } 313 | } 314 | } 315 | 316 | mux.HandleFunc("/debug", func(w http.ResponseWriter, r *http.Request) { 317 | _, _ = fmt.Fprintf(w, "Current ip:\t%v\n", ip.String()) 318 | _, _ = fmt.Fprintf(w, "Remote addr:\t%v\n", r.RemoteAddr) 319 | }) 320 | 321 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 322 | if r.URL.Path == "/" || 323 | strings.HasPrefix(r.URL.Path, "/favicon") || 324 | strings.HasPrefix(r.URL.Path, "/ads.txt") || 325 | strings.HasPrefix(r.URL.Path, "/robots.txt") { 326 | w.Header().Set("Access-Control-Allow-Origin", "*") 327 | w.WriteHeader(200) 328 | return 329 | } 330 | logger := logrus.WithFields(logrus.Fields{ 331 | "URL": r.URL.String(), 332 | "Host": r.Host, 333 | }) 334 | 335 | src, err := s.parser.Parse(r.URL) 336 | 337 | if err != nil { 338 | logger.WithError(err).Error("failed to parse url") 339 | w.WriteHeader(500) 340 | return 341 | } 342 | 343 | logger = logger.WithFields(logrus.Fields{ 344 | "InfoHash": src.InfoHash, 345 | "Path": src.Path, 346 | }) 347 | 348 | w.Header().Set("Access-Control-Allow-Origin", "*") 349 | 350 | newPath := "" 351 | 352 | if src.Mod != nil { 353 | newPath = src.Mod.Path 354 | } else { 355 | newPath = src.Path 356 | } 357 | r.URL.Path = newPath 358 | 359 | s.proxyHTTP(w, r, src, logger) 360 | 361 | }) 362 | logrus.Infof("serving Web at %v", addr) 363 | srv := &http.Server{ 364 | Handler: mux, 365 | MaxHeaderBytes: 50 << 20, 366 | } 367 | return srv.Serve(ln) 368 | } 369 | 370 | func (s *Web) Close() { 371 | if s.ln != nil { 372 | _ = s.ln.Close() 373 | } 374 | } 375 | --------------------------------------------------------------------------------