diff --git a/proxy/config.example.json b/proxy/config.example.json index 40ad7ca2b..744abd534 100644 --- a/proxy/config.example.json +++ b/proxy/config.example.json @@ -1,9 +1,27 @@ { - "listen_address": ":8080", - "target_url": "http://localhost:3000", + "listen_address": ":443", "read_timeout": "30s", "write_timeout": "30s", "idle_timeout": "60s", "shutdown_timeout": "10s", - "log_level": "info" + "log_level": "info", + "grpc_listen_address": ":50051", + "proxy_id": "proxy-1", + "enable_grpc": true, + "http_listen_address": ":80", + "enable_https": false, + "tls_email": "your-email@example.com", + "cert_cache_dir": "./certs", + "oidc_config": { + "provider_url": "https://your-oidc-provider.com", + "client_id": "your-client-id", + "client_secret": "your-client-secret-if-needed", + "redirect_url": "http://localhost:80/auth/callback", + "scopes": ["openid", "profile", "email"], + "jwt_keys_location": "https://your-oidc-provider.com/.well-known/jwks.json", + "jwt_issuer": "https://your-oidc-provider.com/", + "jwt_audience": ["your-api-identifier-or-client-id"], + "jwt_idp_signkey_refresh_enabled": false, + "session_cookie_name": "auth_session" + } } \ No newline at end of file diff --git a/proxy/go.mod b/proxy/go.mod index b5e8a442a..566874927 100644 --- a/proxy/go.mod +++ b/proxy/go.mod @@ -4,132 +4,19 @@ go 1.25 require ( github.com/caarlos0/env/v11 v11.3.1 - github.com/caddyserver/caddy/v2 v2.10.2 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.2 - go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.44.0 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 ) require ( - cel.dev/expr v0.24.0 // indirect - cloud.google.com/go/auth v0.16.2 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - dario.cat/mergo v1.0.1 // indirect - filippo.io/edwards25519 v1.1.0 // indirect - github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect - github.com/KimMachineGun/automemlimit v0.7.4 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.6.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/caddyserver/certmagic v0.24.0 // indirect - github.com/caddyserver/zerossl v0.1.3 // indirect - github.com/ccoveille/go-safecast v1.6.1 // indirect - github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chzyer/readline v1.5.1 // indirect - github.com/cloudflare/circl v1.6.1 // indirect - github.com/coreos/go-oidc/v3 v3.14.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/dgraph-io/badger v1.6.2 // indirect - github.com/dgraph-io/badger/v2 v2.2007.4 // indirect - github.com/dgraph-io/ristretto v0.2.0 // indirect - github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/cel-go v0.26.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.2 // indirect - github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/libdns/libdns v1.1.0 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/mholt/acmez/v3 v3.1.2 // indirect - github.com/miekg/dns v1.1.63 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pires/go-proxyproto v0.8.1 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.23.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/rs/xid v1.6.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/slackhq/nebula v1.9.5 // indirect - github.com/smallstep/certificates v0.28.4 // indirect - github.com/smallstep/cli-utils v0.12.1 // indirect - github.com/smallstep/linkedca v0.23.0 // indirect - github.com/smallstep/nosql v0.7.0 // indirect - github.com/smallstep/pkcs7 v0.2.1 // indirect - github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect - github.com/smallstep/truststore v0.13.0 // indirect - github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect - github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect - github.com/urfave/cli v1.22.17 // indirect - github.com/zeebo/blake3 v0.2.4 // indirect - go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.step.sm/crypto v0.67.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/mock v0.5.2 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.3.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect - golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/mod v0.29.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/api v0.240.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - howett.net/plist v1.0.0 // indirect ) diff --git a/proxy/go.sum b/proxy/go.sum index 20bec14c3..9a541f396 100644 --- a/proxy/go.sum +++ b/proxy/go.sum @@ -1,428 +1,36 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= -cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= -cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= -cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= -github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= -github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= -github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= -github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= -github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= -github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= -github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= -github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8= -github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0= -github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0= -github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE= -github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= -github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= -github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= -github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= -github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= -github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= -github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= -github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= -github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= -github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= -github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= -github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= -github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= -github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= -github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= -github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= -github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= -github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= -github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= -github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= -github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= -github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= -github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= -github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= -github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= -github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= -github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA= -github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= -github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= -github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= -github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= -github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= -github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= -github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= -github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= -github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc= -github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= -github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= -github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4= -github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y= -github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= -github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= -github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= -github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= -github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= -github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= -github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -433,209 +41,25 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= -go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= -go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= -golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= -google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= -google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= -howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/proxy/internal/health/health.go b/proxy/internal/health/health.go deleted file mode 100644 index 18357c693..000000000 --- a/proxy/internal/health/health.go +++ /dev/null @@ -1,109 +0,0 @@ -package health - -import ( - "encoding/json" - "net/http" - "sync" - "time" -) - -// Status represents the health status of the application -type Status string - -const ( - StatusHealthy Status = "healthy" - StatusUnhealthy Status = "unhealthy" - StatusDegraded Status = "degraded" -) - -// Check represents a health check -type Check struct { - Name string `json:"name"` - Status Status `json:"status"` - Error string `json:"error,omitempty"` -} - -// Response represents the health check response -type Response struct { - Status Status `json:"status"` - Timestamp time.Time `json:"timestamp"` - Uptime time.Duration `json:"uptime_seconds"` - Checks map[string]Check `json:"checks,omitempty"` -} - -// Checker is the interface for health checks -type Checker interface { - Check() Check -} - -// Handler manages health checks -type Handler struct { - mu sync.RWMutex - checkers map[string]Checker - startTime time.Time -} - -// NewHandler creates a new health check handler -func NewHandler() *Handler { - return &Handler{ - checkers: make(map[string]Checker), - startTime: time.Now(), - } -} - -// RegisterChecker registers a health checker -func (h *Handler) RegisterChecker(name string, checker Checker) { - h.mu.Lock() - defer h.mu.Unlock() - h.checkers[name] = checker -} - -// ServeHTTP handles health check requests -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.mu.RLock() - defer h.mu.RUnlock() - - response := Response{ - Status: StatusHealthy, - Timestamp: time.Now(), - Uptime: time.Since(h.startTime), - Checks: make(map[string]Check), - } - - // Run all health checks - for name, checker := range h.checkers { - check := checker.Check() - response.Checks[name] = check - - // Update overall status - if check.Status == StatusUnhealthy { - response.Status = StatusUnhealthy - } else if check.Status == StatusDegraded && response.Status != StatusUnhealthy { - response.Status = StatusDegraded - } - } - - // Set HTTP status code based on health - statusCode := http.StatusOK - if response.Status == StatusUnhealthy { - statusCode = http.StatusServiceUnavailable - } else if response.Status == StatusDegraded { - statusCode = http.StatusOK // Still return 200 for degraded - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(response) -} - -// ReadinessHandler returns a simple readiness probe handler -func ReadinessHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("ready")) -} - -// LivenessHandler returns a simple liveness probe handler -func LivenessHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("alive")) -} diff --git a/proxy/internal/middleware/chain.go b/proxy/internal/middleware/chain.go deleted file mode 100644 index 41f54e5e0..000000000 --- a/proxy/internal/middleware/chain.go +++ /dev/null @@ -1,12 +0,0 @@ -package middleware - -import "net/http" - -// Chain creates a middleware chain -func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { - // Apply middlewares in reverse order so they execute in the order provided - for i := len(middlewares) - 1; i >= 0; i-- { - handler = middlewares[i](handler) - } - return handler -} diff --git a/proxy/internal/middleware/logging.go b/proxy/internal/middleware/logging.go deleted file mode 100644 index df83515a4..000000000 --- a/proxy/internal/middleware/logging.go +++ /dev/null @@ -1,55 +0,0 @@ -package middleware - -import ( - "net/http" - "time" - - log "github.com/sirupsen/logrus" -) - -// responseWriter wraps http.ResponseWriter to capture status code -type responseWriter struct { - http.ResponseWriter - statusCode int - written int64 -} - -func (rw *responseWriter) WriteHeader(code int) { - rw.statusCode = code - rw.ResponseWriter.WriteHeader(code) -} - -func (rw *responseWriter) Write(b []byte) (int, error) { - n, err := rw.ResponseWriter.Write(b) - rw.written += int64(n) - return n, err -} - -// Logging middleware logs HTTP requests with details -func Logging(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - // Wrap the response writer - wrapped := &responseWriter{ - ResponseWriter: w, - statusCode: http.StatusOK, - } - - // Call the next handler - next.ServeHTTP(wrapped, r) - - // Log request details - duration := time.Since(start) - - log.WithFields(log.Fields{ - "method": r.Method, - "path": r.URL.Path, - "status": wrapped.statusCode, - "duration_ms": duration.Milliseconds(), - "bytes": wrapped.written, - "remote_addr": r.RemoteAddr, - "user_agent": r.UserAgent(), - }).Info("HTTP request") - }) -} diff --git a/proxy/internal/middleware/recovery.go b/proxy/internal/middleware/recovery.go deleted file mode 100644 index 97e1785c0..000000000 --- a/proxy/internal/middleware/recovery.go +++ /dev/null @@ -1,33 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - "runtime/debug" - - log "github.com/sirupsen/logrus" -) - -// Recovery middleware recovers from panics and logs the error -func Recovery(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - // Log the panic with stack trace - log.WithFields(log.Fields{ - "error": err, - "method": r.Method, - "path": r.URL.Path, - "stack": string(debug.Stack()), - "remote_addr": r.RemoteAddr, - }).Error("Panic recovered") - - // Return 500 Internal Server Error - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "Internal Server Error") - } - }() - - next.ServeHTTP(w, r) - }) -} diff --git a/proxy/internal/reverseproxy/auth.go b/proxy/internal/reverseproxy/auth.go new file mode 100644 index 000000000..08e5ae7f7 --- /dev/null +++ b/proxy/internal/reverseproxy/auth.go @@ -0,0 +1,613 @@ +package reverseproxy + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/shared/auth/jwt" +) + +const ( + // Default values for authentication + defaultSessionCookieName = "auth_session" + defaultPINHeader = "X-PIN" + + // OIDC state expiration time + oidcStateExpiration = 10 * time.Minute + + // Error messages + errInternalServer = "Internal Server Error" +) + +// Global state store for OIDC flow (state -> original URL) +var ( + oidcStateStore = &stateStore{ + states: make(map[string]*oidcState), + } +) + +type stateStore struct { + mu sync.RWMutex + states map[string]*oidcState +} + +type oidcState struct { + originalURL string + createdAt time.Time + routeID string +} + +func (s *stateStore) Store(state, originalURL, routeID string) { + s.mu.Lock() + defer s.mu.Unlock() + s.states[state] = &oidcState{ + originalURL: originalURL, + createdAt: time.Now(), + routeID: routeID, + } + + // Clean up expired states + cutoff := time.Now().Add(-oidcStateExpiration) + for k, v := range s.states { + if v.createdAt.Before(cutoff) { + delete(s.states, k) + } + } +} + +func (s *stateStore) Get(state string) (*oidcState, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + st, ok := s.states[state] + return st, ok +} + +func (s *stateStore) Delete(state string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.states, state) +} + +// AuthConfig holds the authentication configuration for a route +// Only ONE auth method should be configured per route +type AuthConfig struct { + // HTTP Basic authentication (username/password) + BasicAuth *BasicAuthConfig + + // PIN authentication + PIN *PINConfig + + // Bearer token with JWT validation and OAuth/OIDC flow + // When enabled, uses the global OIDCConfig from proxy Config + Bearer *BearerConfig +} + +// BasicAuthConfig holds HTTP Basic authentication settings +type BasicAuthConfig struct { + Username string + Password string +} + +// PINConfig holds PIN authentication settings +type PINConfig struct { + PIN string + Header string // Header name (default: "X-PIN") +} + +// BearerConfig holds JWT/OAuth/OIDC bearer token authentication settings +// The actual OIDC/JWT configuration comes from the global proxy Config.OIDCConfig +// This just enables Bearer auth for a specific route +type BearerConfig struct { + // Enable bearer token authentication for this route + // Uses the global OIDC configuration from proxy Config + Enabled bool +} + +// IsEmpty returns true if no auth methods are configured +func (c *AuthConfig) IsEmpty() bool { + if c == nil { + return true + } + return c.BasicAuth == nil && c.PIN == nil && c.Bearer == nil +} + +// authMiddlewareHandler is a static middleware that checks AuthConfig +type authMiddlewareHandler struct { + next http.Handler + authConfig *AuthConfig + routeID string + rejectResponse func(w http.ResponseWriter, r *http.Request) + oidcConfig *OIDCConfig // Global OIDC configuration from proxy + jwtValidator *jwt.Validator // JWT validator instance (lazily initialized) + validatorMu sync.Mutex // Mutex for thread-safe validator initialization +} + +func (h *authMiddlewareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // If no auth configured, allow request + if h.authConfig.IsEmpty() { + log.WithFields(log.Fields{ + "route_id": h.routeID, + "auth_method": "none", + "path": r.URL.Path, + }).Debug("No authentication configured, allowing request") + r.Header.Set("X-Auth-Method", "none") + h.next.ServeHTTP(w, r) + return + } + + var authMethod string + var userID string + authenticated := false + + // 1. Check Basic Auth + if h.authConfig.BasicAuth != nil { + if auth := r.Header.Get("Authorization"); auth != "" && strings.HasPrefix(auth, "Basic ") { + encoded := strings.TrimPrefix(auth, "Basic ") + if decoded, err := base64.StdEncoding.DecodeString(encoded); err == nil { + credentials := string(decoded) + parts := strings.SplitN(credentials, ":", 2) + if len(parts) == 2 { + username, password := parts[0], parts[1] + if username == h.authConfig.BasicAuth.Username && password == h.authConfig.BasicAuth.Password { + authenticated = true + authMethod = "basic" + userID = username + } + } + } + } + } + + // 2. Check PIN (if not already authenticated) + if !authenticated && h.authConfig.PIN != nil { + headerName := h.authConfig.PIN.Header + if headerName == "" { + headerName = defaultPINHeader + } + if pin := r.Header.Get(headerName); pin != "" { + if pin == h.authConfig.PIN.PIN { + authenticated = true + authMethod = "pin" + userID = "pin_user" // PIN doesn't have a specific user ID + } + } + } + + // 3. Check Bearer Token with JWT validation (if not already authenticated) + if !authenticated && h.authConfig.Bearer != nil && h.oidcConfig != nil { + cookieName := h.oidcConfig.SessionCookieName + if cookieName == "" { + cookieName = defaultSessionCookieName + } + + // First, check if there's an _auth_token query parameter (from callback redirect) + // This allows us to set the cookie for the current domain + if authToken := r.URL.Query().Get("_auth_token"); authToken != "" { + log.WithFields(log.Fields{ + "route_id": h.routeID, + "host": r.Host, + }).Info("Found auth token in query parameter, setting cookie and redirecting") + + // Validate the token before setting cookie + if h.validateJWT(authToken) { + // Set cookie for current domain + cookie := &http.Cookie{ + Name: cookieName, + Value: authToken, + Path: "/", + MaxAge: 3600, // 1 hour + HttpOnly: true, + Secure: false, // Set to false for HTTP testing, true for HTTPS in production + SameSite: http.SameSiteLaxMode, + } + http.SetCookie(w, cookie) + + // Redirect to same URL without the token parameter + cleanURL := *r.URL + q := cleanURL.Query() + q.Del("_auth_token") + cleanURL.RawQuery = q.Encode() + + // Build full URL with scheme and host + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + redirectURL := fmt.Sprintf("%s://%s%s", scheme, r.Host, cleanURL.String()) + + log.WithFields(log.Fields{ + "route_id": h.routeID, + "redirect_url": redirectURL, + }).Debug("Redirecting to clean URL after setting cookie") + + http.Redirect(w, r, redirectURL, http.StatusFound) + return + } else { + log.WithFields(log.Fields{ + "route_id": h.routeID, + }).Warn("Invalid token in query parameter") + } + } + + // Check if we have an existing session cookie (from OIDC flow) + log.WithFields(log.Fields{ + "route_id": h.routeID, + "cookie_name": cookieName, + "host": r.Host, + "path": r.URL.Path, + }).Debug("Checking for session cookie") + + if cookie, err := r.Cookie(cookieName); err == nil && cookie.Value != "" { + log.WithFields(log.Fields{ + "route_id": h.routeID, + "cookie_name": cookieName, + }).Debug("Session cookie found, validating JWT") + + // Validate the JWT token from the session cookie + if h.validateJWT(cookie.Value) { + authenticated = true + authMethod = "bearer_session" + userID = h.extractUserIDFromJWT(cookie.Value) + } else { + log.WithFields(log.Fields{ + "route_id": h.routeID, + }).Debug("JWT validation failed for session cookie") + } + } else { + log.WithFields(log.Fields{ + "route_id": h.routeID, + "error": err, + }).Debug("No session cookie found") + } + + // If no session cookie or validation failed, check Authorization header + if !authenticated { + if auth := r.Header.Get("Authorization"); auth != "" && strings.HasPrefix(auth, "Bearer ") { + token := strings.TrimPrefix(auth, "Bearer ") + // Validate JWT token from Authorization header + if h.validateJWT(token) { + authenticated = true + authMethod = "bearer" + userID = h.extractUserIDFromJWT(token) + } + } else { + // No bearer token and no valid session - redirect to OIDC provider + if h.oidcConfig.ProviderURL != "" { + // Initiate OAuth/OIDC flow + h.redirectToOIDC(w, r) + return + } + } + } + } + + // Reject if authentication failed + if !authenticated { + log.WithFields(log.Fields{ + "route_id": h.routeID, + "path": r.URL.Path, + "source_ip": extractSourceIP(r), + }).Warn("Authentication failed") + + // Call custom reject response or use default + if h.rejectResponse != nil { + h.rejectResponse(w, r) + } else { + // Default: return 401 with WWW-Authenticate header + w.Header().Set("WWW-Authenticate", `Bearer realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + return + } + + log.WithFields(log.Fields{ + "route_id": h.routeID, + "auth_method": authMethod, + "user_id": userID, + "path": r.URL.Path, + }).Debug("Authentication successful") + + // Store auth info in headers for logging + r.Header.Set("X-Auth-Method", authMethod) + r.Header.Set("X-Auth-User-ID", userID) + + // Continue to next handler + h.next.ServeHTTP(w, r) +} + +// redirectToOIDC initiates the OAuth/OIDC authentication flow +func (h *authMiddlewareHandler) redirectToOIDC(w http.ResponseWriter, r *http.Request) { + // Generate random state for CSRF protection + state, err := generateRandomString(32) + if err != nil { + log.WithError(err).Error("Failed to generate OIDC state") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Store state with original URL for redirect after auth + // Include the full URL with scheme and host + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + originalURL := fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.String()) + oidcStateStore.Store(state, originalURL, h.routeID) + + // Default scopes if not configured + scopes := h.oidcConfig.Scopes + if len(scopes) == 0 { + scopes = []string{"openid", "profile", "email"} + } + + // Build authorization URL + authURL, err := url.Parse(h.oidcConfig.ProviderURL) + if err != nil { + log.WithError(err).Error("Invalid OIDC provider URL") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Append /authorize if it doesn't exist (common OIDC endpoint) + if !strings.HasSuffix(authURL.Path, "/authorize") && !strings.HasSuffix(authURL.Path, "/auth") { + authURL.Path = strings.TrimSuffix(authURL.Path, "/") + "/authorize" + } + + // Build query parameters + params := url.Values{} + params.Set("client_id", h.oidcConfig.ClientID) + params.Set("redirect_uri", h.oidcConfig.RedirectURL) + params.Set("response_type", "code") + params.Set("scope", strings.Join(scopes, " ")) + params.Set("state", state) + + // Add audience parameter to get an access token for the API + // This ensures we get a proper JWT for the API audience, not just an ID token + if len(h.oidcConfig.JWTAudience) > 0 && h.oidcConfig.JWTAudience[0] != h.oidcConfig.ClientID { + params.Set("audience", h.oidcConfig.JWTAudience[0]) + } + + authURL.RawQuery = params.Encode() + + log.WithFields(log.Fields{ + "route_id": h.routeID, + "provider_url": authURL.String(), + "redirect_url": h.oidcConfig.RedirectURL, + "state": state, + }).Info("Redirecting to OIDC provider for authentication") + + // Redirect user to identity provider login page + http.Redirect(w, r, authURL.String(), http.StatusFound) +} + +// generateRandomString generates a cryptographically secure random string +func generateRandomString(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(bytes)[:length], nil +} + +// HandleOIDCCallback handles the callback from the OIDC provider +// This should be registered as a route handler for the callback URL +func HandleOIDCCallback(oidcConfig *OIDCConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get authorization code and state from query parameters + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + if code == "" || state == "" { + log.Error("Missing code or state in OIDC callback") + http.Error(w, "Invalid callback parameters", http.StatusBadRequest) + return + } + + // Verify state to prevent CSRF + oidcSt, ok := oidcStateStore.Get(state) + if !ok { + log.Error("Invalid or expired OIDC state") + http.Error(w, "Invalid or expired state parameter", http.StatusBadRequest) + return + } + + // Delete state to prevent reuse + oidcStateStore.Delete(state) + + // Exchange authorization code for token + token, err := exchangeCodeForToken(code, oidcConfig) + if err != nil { + log.WithError(err).Error("Failed to exchange code for token") + http.Error(w, "Authentication failed", http.StatusUnauthorized) + return + } + + // Parse the original URL to add the token as a query parameter + origURL, err := url.Parse(oidcSt.originalURL) + if err != nil { + log.WithError(err).Error("Failed to parse original URL") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Add token as query parameter so the original domain can set its own cookie + // We use a special parameter name that the auth middleware will look for + q := origURL.Query() + q.Set("_auth_token", token) + origURL.RawQuery = q.Encode() + + log.WithFields(log.Fields{ + "route_id": oidcSt.routeID, + "original_url": oidcSt.originalURL, + "redirect_url": origURL.String(), + "callback_host": r.Host, + }).Info("OIDC authentication successful, redirecting with token parameter") + + // Redirect back to original URL with token parameter + http.Redirect(w, r, origURL.String(), http.StatusFound) + } +} + +// exchangeCodeForToken exchanges an authorization code for an access token +func exchangeCodeForToken(code string, config *OIDCConfig) (string, error) { + // Build token endpoint URL + tokenURL, err := url.Parse(config.ProviderURL) + if err != nil { + return "", fmt.Errorf("invalid OIDC provider URL: %w", err) + } + + // Auth0 uses /oauth/token, standard OIDC uses /token + // Check if path already contains token endpoint + if !strings.Contains(tokenURL.Path, "/token") { + tokenURL.Path = strings.TrimSuffix(tokenURL.Path, "/") + "/oauth/token" + } + + // Build request body + data := url.Values{} + data.Set("grant_type", "authorization_code") + data.Set("code", code) + data.Set("redirect_uri", config.RedirectURL) + data.Set("client_id", config.ClientID) + + // Only include client_secret if it's provided (not needed for public/SPA clients) + if config.ClientSecret != "" { + data.Set("client_secret", config.ClientSecret) + } + + // Make token exchange request + resp, err := http.PostForm(tokenURL.String(), data) + if err != nil { + return "", fmt.Errorf("token exchange request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, string(body)) + } + + // Parse response + var tokenResp struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + IDToken string `json:"id_token"` + } + + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return "", fmt.Errorf("failed to decode token response: %w", err) + } + + if tokenResp.AccessToken == "" { + return "", fmt.Errorf("no access token in response") + } + + // Return the ID token if available (contains user claims), otherwise access token + if tokenResp.IDToken != "" { + return tokenResp.IDToken, nil + } + + return tokenResp.AccessToken, nil +} + +// getOrInitValidator lazily initializes and returns the JWT validator +func (h *authMiddlewareHandler) getOrInitValidator() *jwt.Validator { + h.validatorMu.Lock() + defer h.validatorMu.Unlock() + + if h.jwtValidator == nil { + h.jwtValidator = jwt.NewValidator( + h.oidcConfig.JWTIssuer, + h.oidcConfig.JWTAudience, + h.oidcConfig.JWTKeysLocation, + h.oidcConfig.JWTIdpSignkeyRefreshEnabled, + ) + } + + return h.jwtValidator +} + +// validateJWT validates a JWT token using the handler's JWT validator +func (h *authMiddlewareHandler) validateJWT(tokenString string) bool { + if h.oidcConfig == nil || h.oidcConfig.JWTKeysLocation == "" { + log.Error("JWT validation failed: OIDC config or JWTKeysLocation is missing") + return false + } + + // Get or initialize validator + validator := h.getOrInitValidator() + + // Validate the token + ctx := context.Background() + parsedToken, err := validator.ValidateAndParse(ctx, tokenString) + if err != nil { + log.WithError(err).Error("JWT validation failed") + // Try to parse token without validation to see what's in it + parts := strings.Split(tokenString, ".") + if len(parts) == 3 { + // Decode payload (middle part) + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err == nil { + log.WithFields(log.Fields{ + "payload": string(payload), + }).Debug("Token payload for debugging") + } + } + return false + } + + // Token is valid if parsedToken is not nil and Valid is true + return parsedToken != nil && parsedToken.Valid +} + +// extractUserIDFromJWT extracts the user ID from a JWT token +func (h *authMiddlewareHandler) extractUserIDFromJWT(tokenString string) string { + if h.jwtValidator == nil { + return "" + } + + // Parse the token + ctx := context.Background() + parsedToken, err := h.jwtValidator.ValidateAndParse(ctx, tokenString) + if err != nil { + return "" + } + + // parsedToken is already *jwtgo.Token from ValidateAndParse + // Create extractor to get user auth info + extractor := jwt.NewClaimsExtractor() + userAuth, err := extractor.ToUserAuth(parsedToken) + if err != nil { + log.WithError(err).Debug("Failed to extract user ID from JWT") + return "" + } + + return userAuth.UserId +} + +// wrapWithAuth wraps a handler with the static authentication middleware +// This ALWAYS runs (even when authConfig is nil or empty) +func wrapWithAuth(next http.Handler, authConfig *AuthConfig, routeID string, rejectResponse func(w http.ResponseWriter, r *http.Request), oidcConfig *OIDCConfig) http.Handler { + if authConfig == nil { + authConfig = &AuthConfig{} // Empty config = no auth + } + + return &authMiddlewareHandler{ + next: next, + authConfig: authConfig, + routeID: routeID, + rejectResponse: rejectResponse, + oidcConfig: oidcConfig, + } +} diff --git a/proxy/internal/reverseproxy/caddy.go b/proxy/internal/reverseproxy/caddy.go deleted file mode 100644 index 88966fb11..000000000 --- a/proxy/internal/reverseproxy/caddy.go +++ /dev/null @@ -1,626 +0,0 @@ -package reverseproxy - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "sort" - "sync" - "time" - - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" - "github.com/caddyserver/caddy/v2/modules/logging" - log "github.com/sirupsen/logrus" -) - -// CaddyProxy wraps Caddy's reverse proxy functionality -type CaddyProxy struct { - config Config - mu sync.RWMutex - isRunning bool - routes map[string]*RouteConfig // key is route ID - requestCallback RequestDataCallback - // customHandlers stores handlers with custom transports that can't be JSON-serialized - // key is "routeID:path" to uniquely identify each handler - customHandlers map[string]*reverseproxy.Handler -} - -// Config holds the reverse proxy configuration -type Config struct { - // ListenAddress is the address to listen on - ListenAddress string - - // EnableHTTPS enables automatic HTTPS with Let's Encrypt - EnableHTTPS bool - - // TLSEmail is the email for Let's Encrypt registration - TLSEmail string - - // RequestDataCallback is called for each proxied request with metrics - RequestDataCallback RequestDataCallback -} - -// RouteConfig defines a routing configuration -type RouteConfig struct { - // ID is a unique identifier for this route - ID string - - // Domain is the domain to listen on (e.g., "example.com" or "*" for all) - Domain string - - // PathMappings defines paths that should be forwarded to specific ports - // Key is the path prefix (e.g., "/", "/api", "/admin") - // Value is the target IP:port (e.g., "192.168.1.100:3000") - // Must have at least one entry. Use "/" or "" for the default/catch-all route. - PathMappings map[string]string - - // Conn is an optional existing network connection to use for this route - // This allows routing through specific tunnels (e.g., WireGuard) per route - // If set, this connection will be reused for all requests to this route - Conn net.Conn - - // CustomDialer is an optional custom dialer for this specific route - // This is used if Conn is not set. It allows using different network connections per route - CustomDialer func(ctx context.Context, network, address string) (net.Conn, error) -} - -// New creates a new Caddy-based reverse proxy -func New(config Config) (*CaddyProxy, error) { - // Default to port 443 if not specified - if config.ListenAddress == "" { - config.ListenAddress = ":443" - } - - cp := &CaddyProxy{ - config: config, - isRunning: false, - routes: make(map[string]*RouteConfig), - requestCallback: config.RequestDataCallback, - customHandlers: make(map[string]*reverseproxy.Handler), - } - - return cp, nil -} - -// Start starts the Caddy reverse proxy server -func (cp *CaddyProxy) Start() error { - cp.mu.Lock() - if cp.isRunning { - cp.mu.Unlock() - return fmt.Errorf("reverse proxy already running") - } - cp.isRunning = true - cp.mu.Unlock() - - // Build Caddy configuration - cfg, err := cp.buildCaddyConfig() - if err != nil { - cp.mu.Lock() - cp.isRunning = false - cp.mu.Unlock() - return fmt.Errorf("failed to build Caddy config: %w", err) - } - - // Run Caddy with the configuration - err = caddy.Run(cfg) - if err != nil { - cp.mu.Lock() - cp.isRunning = false - cp.mu.Unlock() - return fmt.Errorf("failed to run Caddy: %w", err) - } - - log.Infof("Caddy reverse proxy started on %s", cp.config.ListenAddress) - log.Infof("Configured %d route(s)", len(cp.routes)) - - return nil -} - -// Stop gracefully stops the Caddy reverse proxy -func (cp *CaddyProxy) Stop(ctx context.Context) error { - cp.mu.Lock() - if !cp.isRunning { - cp.mu.Unlock() - return fmt.Errorf("reverse proxy not running") - } - cp.mu.Unlock() - - log.Info("Stopping Caddy reverse proxy...") - - // Stop Caddy - if err := caddy.Stop(); err != nil { - return fmt.Errorf("failed to stop Caddy: %w", err) - } - - cp.mu.Lock() - cp.isRunning = false - cp.mu.Unlock() - - log.Info("Caddy reverse proxy stopped") - return nil -} - -// buildCaddyConfig builds the Caddy configuration -func (cp *CaddyProxy) buildCaddyConfig() (*caddy.Config, error) { - cp.mu.RLock() - defer cp.mu.RUnlock() - - if len(cp.routes) == 0 { - // Create a default empty server that returns 404 - httpServer := &caddyhttp.Server{ - Listen: []string{cp.config.ListenAddress}, - Routes: caddyhttp.RouteList{}, - } - - httpApp := &caddyhttp.App{ - Servers: map[string]*caddyhttp.Server{ - "proxy": httpServer, - }, - } - - cfg := &caddy.Config{ - Admin: &caddy.AdminConfig{ - Disabled: true, - }, - AppsRaw: caddy.ModuleMap{ - "http": caddyconfig.JSON(httpApp, nil), - }, - } - - return cfg, nil - } - - // Build routes grouped by domain - domainRoutes := make(map[string][]caddyhttp.Route) - // Track unique service IDs for logger configuration - serviceIDs := make(map[string]bool) - - for _, routeConfig := range cp.routes { - domain := routeConfig.Domain - if domain == "" { - domain = "*" // wildcard for all domains - } - - // Register callback for this service ID - if cp.requestCallback != nil { - RegisterCallback(routeConfig.ID, cp.requestCallback) - serviceIDs[routeConfig.ID] = true - } - - // Sort path mappings by path length (longest first) for proper matching - // This ensures more specific paths match before catch-all paths - paths := make([]string, 0, len(routeConfig.PathMappings)) - for path := range routeConfig.PathMappings { - paths = append(paths, path) - } - sort.Slice(paths, func(i, j int) bool { - // Sort by length descending, but put empty string last (catch-all) - if paths[i] == "" || paths[i] == "/" { - return false - } - if paths[j] == "" || paths[j] == "/" { - return true - } - return len(paths[i]) > len(paths[j]) - }) - - // Create routes for each path mapping - for _, path := range paths { - target := routeConfig.PathMappings[path] - route := cp.createRoute(routeConfig, path, target) - domainRoutes[domain] = append(domainRoutes[domain], route) - } - } - - // Build Caddy routes - var caddyRoutes caddyhttp.RouteList - for domain, routes := range domainRoutes { - if domain != "*" { - // Add host matcher for specific domains - for i := range routes { - routes[i].MatcherSetsRaw = []caddy.ModuleMap{ - { - "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), - }, - } - } - } - caddyRoutes = append(caddyRoutes, routes...) - } - - // Create HTTP server with access logging if callback is set - httpServer := &caddyhttp.Server{ - Listen: []string{cp.config.ListenAddress}, - Routes: caddyRoutes, - } - - // Configure server logging if callback is set - if cp.requestCallback != nil { - httpServer.Logs = &caddyhttp.ServerLogConfig{ - // Use our custom logger for access logs - LoggerNames: map[string]caddyhttp.StringArray{ - "http.log.access": {"http_access"}, - }, - // Disable default access logging (only use custom logger) - ShouldLogCredentials: false, - } - } - - // Disable automatic HTTPS if not enabled - if !cp.config.EnableHTTPS { - // Explicitly disable automatic HTTPS for the server - httpServer.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{ - Disabled: true, - } - } - - // Build HTTP app - httpApp := &caddyhttp.App{ - Servers: map[string]*caddyhttp.Server{ - "proxy": httpServer, - }, - } - - // Provision the HTTP app to set up handlers from JSON - ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) - defer cancel() - - if err := httpApp.Provision(ctx); err != nil { - return nil, fmt.Errorf("failed to provision HTTP app: %w", err) - } - - // After provisioning, inject custom transports into handlers - // This is done post-provisioning so the Transport field is preserved - if err := cp.injectCustomTransports(httpApp); err != nil { - return nil, fmt.Errorf("failed to inject custom transports: %w", err) - } - - // Create Caddy config with the provisioned app - // IMPORTANT: We pass the already-provisioned app, not JSON - // This preserves the Transport fields we set - cfg := &caddy.Config{ - Admin: &caddy.AdminConfig{ - Disabled: true, - }, - // Apps field takes already-provisioned apps - Apps: map[string]caddy.App{ - "http": httpApp, - }, - } - - // Configure logging if callback is set - if cp.requestCallback != nil { - // Register the callback for the proxy service ID - RegisterCallback("proxy", cp.requestCallback) - - // Build logging config with proper module names - cfg.Logging = &caddy.Logging{ - Logs: map[string]*caddy.CustomLog{ - "http_access": { - BaseLog: caddy.BaseLog{ - WriterRaw: caddyconfig.JSONModuleObject(&CallbackWriter{ServiceID: "proxy"}, "output", "callback", nil), - EncoderRaw: caddyconfig.JSONModuleObject(&logging.JSONEncoder{}, "format", "json", nil), - Level: "INFO", - }, - Include: []string{"http.log.access"}, - }, - }, - } - - log.Infof("Configured custom logging with callback writer for service: proxy") - } - - return cfg, nil -} - -// createRoute creates a Caddy route for a path and target with service ID tracking -func (cp *CaddyProxy) createRoute(routeConfig *RouteConfig, path, target string) caddyhttp.Route { - // Check if this route needs a custom transport - hasCustomTransport := routeConfig.Conn != nil || routeConfig.CustomDialer != nil - - if hasCustomTransport { - // For routes with custom transports, store them separately - // and configure the upstream to use a special dial address that we'll intercept - handlerKey := fmt.Sprintf("%s:%s", routeConfig.ID, path) - - // Create upstream with custom dial configuration - upstream := &reverseproxy.Upstream{ - Dial: target, - } - - // Create the reverse proxy handler with custom transport - handler := &reverseproxy.Handler{ - Upstreams: reverseproxy.UpstreamPool{upstream}, - } - - // Configure the custom transport - if routeConfig.Conn != nil { - // Use the provided connection directly - transport := &http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - log.Debugf("Reusing existing connection for route %s to %s", routeConfig.ID, address) - return routeConfig.Conn, nil - }, - MaxIdleConns: 1, - MaxIdleConnsPerHost: 1, - IdleConnTimeout: 0, - DisableKeepAlives: false, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - handler.Transport = transport - log.Infof("Configured net.Conn transport for route %s (path: %s)", routeConfig.ID, path) - } else if routeConfig.CustomDialer != nil { - // Use the custom dialer function - transport := &http.Transport{ - DialContext: routeConfig.CustomDialer, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - handler.Transport = transport - log.Infof("Configured custom dialer transport for route %s (path: %s)", routeConfig.ID, path) - } - - // Store the handler for later injection - cp.customHandlers[handlerKey] = handler - - // Create route using HandlersRaw with a placeholder that will be replaced - // We'll use JSON serialization here, but inject the real handler after Caddy loads - route := caddyhttp.Route{ - HandlersRaw: []json.RawMessage{ - caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil), - }, - } - - if path != "" { - route.MatcherSetsRaw = []caddy.ModuleMap{ - { - "path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil), - }, - } - } - - return route - } - - // Standard route without custom transport - upstream := &reverseproxy.Upstream{ - Dial: target, - } - - handler := &reverseproxy.Handler{ - Upstreams: reverseproxy.UpstreamPool{upstream}, - } - - route := caddyhttp.Route{ - HandlersRaw: []json.RawMessage{ - caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil), - }, - } - - if path != "" { - route.MatcherSetsRaw = []caddy.ModuleMap{ - { - "path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil), - }, - } - } - - return route -} - -// IsRunning returns whether the proxy is running -func (cp *CaddyProxy) IsRunning() bool { - cp.mu.RLock() - defer cp.mu.RUnlock() - return cp.isRunning -} - -// GetConfig returns the proxy configuration -func (cp *CaddyProxy) GetConfig() Config { - return cp.config -} - -// AddRoute adds a new route configuration to the proxy -// If the proxy is running, it will reload the configuration -func (cp *CaddyProxy) AddRoute(route *RouteConfig) error { - if route == nil { - return fmt.Errorf("route cannot be nil") - } - if route.ID == "" { - return fmt.Errorf("route ID is required") - } - if len(route.PathMappings) == 0 { - return fmt.Errorf("route must have at least one path mapping") - } - - cp.mu.Lock() - // Check if route already exists - if _, exists := cp.routes[route.ID]; exists { - cp.mu.Unlock() - return fmt.Errorf("route with ID %s already exists", route.ID) - } - - // Add new route - cp.routes[route.ID] = route - isRunning := cp.isRunning - cp.mu.Unlock() - - log.WithFields(log.Fields{ - "route_id": route.ID, - "domain": route.Domain, - "paths": len(route.PathMappings), - }).Info("Added route") - - // Reload configuration if proxy is running - if isRunning { - if err := cp.reloadConfig(); err != nil { - // Rollback: remove the route - cp.mu.Lock() - delete(cp.routes, route.ID) - cp.mu.Unlock() - return fmt.Errorf("failed to reload config after adding route: %w", err) - } - } - - return nil -} - -// RemoveRoute removes a route from the proxy -// If the proxy is running, it will reload the configuration -func (cp *CaddyProxy) RemoveRoute(routeID string) error { - cp.mu.Lock() - // Check if route exists - route, exists := cp.routes[routeID] - if !exists { - cp.mu.Unlock() - return fmt.Errorf("route %s not found", routeID) - } - - // Remove route - delete(cp.routes, routeID) - isRunning := cp.isRunning - cp.mu.Unlock() - - log.Infof("Removed route: %s", routeID) - - // Reload configuration if proxy is running - if isRunning { - if err := cp.reloadConfig(); err != nil { - // Rollback: add the route back - cp.mu.Lock() - cp.routes[routeID] = route - cp.mu.Unlock() - return fmt.Errorf("failed to reload config after removing route: %w", err) - } - } - - return nil -} - -// UpdateRoute updates an existing route configuration -// If the proxy is running, it will reload the configuration -func (cp *CaddyProxy) UpdateRoute(route *RouteConfig) error { - if route == nil { - return fmt.Errorf("route cannot be nil") - } - if route.ID == "" { - return fmt.Errorf("route ID is required") - } - - cp.mu.Lock() - // Check if route exists - oldRoute, exists := cp.routes[route.ID] - if !exists { - cp.mu.Unlock() - return fmt.Errorf("route %s not found", route.ID) - } - - // Update route - cp.routes[route.ID] = route - isRunning := cp.isRunning - cp.mu.Unlock() - - log.WithFields(log.Fields{ - "route_id": route.ID, - "domain": route.Domain, - "paths": len(route.PathMappings), - }).Info("Updated route") - - // Reload configuration if proxy is running - if isRunning { - if err := cp.reloadConfig(); err != nil { - // Rollback: restore old route - cp.mu.Lock() - cp.routes[route.ID] = oldRoute - cp.mu.Unlock() - return fmt.Errorf("failed to reload config after updating route: %w", err) - } - } - - return nil -} - -// ListRoutes returns a list of all configured route IDs -func (cp *CaddyProxy) ListRoutes() []string { - cp.mu.RLock() - defer cp.mu.RUnlock() - - routes := make([]string, 0, len(cp.routes)) - for id := range cp.routes { - routes = append(routes, id) - } - return routes -} - -// GetRoute returns a route configuration by ID -func (cp *CaddyProxy) GetRoute(routeID string) (*RouteConfig, error) { - cp.mu.RLock() - defer cp.mu.RUnlock() - - route, exists := cp.routes[routeID] - if !exists { - return nil, fmt.Errorf("route %s not found", routeID) - } - - return route, nil -} - -// injectCustomTransports injects custom transports into provisioned handlers -// This must be called after httpApp.Provision() but before passing to Caddy.Run() -func (cp *CaddyProxy) injectCustomTransports(httpApp *caddyhttp.App) error { - // Iterate through all servers - for serverName, server := range httpApp.Servers { - log.Debugf("Injecting custom transports for server: %s", serverName) - - // Iterate through all routes - for routeIdx, route := range server.Routes { - // Iterate through all handlers in the route - for handlerIdx, handler := range route.Handlers { - // Check if this is a reverse proxy handler - if rpHandler, ok := handler.(*reverseproxy.Handler); ok { - // Try to find a matching custom handler for this route - // We need to match by handler configuration since we don't have route metadata here - for handlerKey, customHandler := range cp.customHandlers { - // Check if the upstream configuration matches - if len(rpHandler.Upstreams) > 0 && len(customHandler.Upstreams) > 0 { - if rpHandler.Upstreams[0].Dial == customHandler.Upstreams[0].Dial { - // Match found! Inject the custom transport - rpHandler.Transport = customHandler.Transport - log.Infof("Injected custom transport for route %d, handler %d (key: %s)", routeIdx, handlerIdx, handlerKey) - break - } - } - } - } - } - } - } - - return nil -} - -// reloadConfig rebuilds and reloads the Caddy configuration -// Must be called without holding the lock -func (cp *CaddyProxy) reloadConfig() error { - log.Info("Reloading Caddy configuration...") - - cfg, err := cp.buildCaddyConfig() - if err != nil { - return fmt.Errorf("failed to build config: %w", err) - } - - if err := caddy.Run(cfg); err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - log.Info("Caddy configuration reloaded successfully") - return nil -} diff --git a/proxy/internal/reverseproxy/logwriter.go b/proxy/internal/reverseproxy/logwriter.go deleted file mode 100644 index c8532f81f..000000000 --- a/proxy/internal/reverseproxy/logwriter.go +++ /dev/null @@ -1,225 +0,0 @@ -package reverseproxy - -import ( - "encoding/json" - "fmt" - "io" - "strings" - "sync" - - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - log "github.com/sirupsen/logrus" -) - -var ( - // Global map to store callbacks per service ID - callbackRegistry = make(map[string]RequestDataCallback) - callbackMu sync.RWMutex -) - -// RegisterCallback registers a callback for a specific service ID -func RegisterCallback(serviceID string, callback RequestDataCallback) { - callbackMu.Lock() - defer callbackMu.Unlock() - callbackRegistry[serviceID] = callback -} - -// UnregisterCallback removes a callback for a specific service ID -func UnregisterCallback(serviceID string) { - callbackMu.Lock() - defer callbackMu.Unlock() - delete(callbackRegistry, serviceID) -} - -// getCallback retrieves the callback for a service ID -func getCallback(serviceID string) RequestDataCallback { - callbackMu.RLock() - defer callbackMu.RUnlock() - return callbackRegistry[serviceID] -} - -func init() { - caddy.RegisterModule(CallbackWriter{}) -} - -// CallbackWriter is a Caddy log writer module that sends request data via callback -type CallbackWriter struct { - ServiceID string `json:"service_id,omitempty"` -} - -// CaddyModule returns the Caddy module information -func (CallbackWriter) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "caddy.logging.writers.callback", - New: func() caddy.Module { return new(CallbackWriter) }, - } -} - -// Provision sets up the callback writer -func (cw *CallbackWriter) Provision(ctx caddy.Context) error { - log.Infof("CallbackWriter.Provision called for service_id: %s", cw.ServiceID) - return nil -} - -// String returns a human-readable representation of the writer -func (cw *CallbackWriter) String() string { - return fmt.Sprintf("callback writer for service %s", cw.ServiceID) -} - -// WriterKey returns a unique key for this writer configuration -func (cw *CallbackWriter) WriterKey() string { - return "callback_" + cw.ServiceID -} - -// OpenWriter opens the writer -func (cw *CallbackWriter) OpenWriter() (io.WriteCloser, error) { - log.Infof("CallbackWriter.OpenWriter called for service_id: %s", cw.ServiceID) - writer := &LogWriter{ - serviceID: cw.ServiceID, - } - log.Infof("Created LogWriter instance: %p for service_id: %s", writer, cw.ServiceID) - return writer, nil -} - -// UnmarshalCaddyfile implements caddyfile.Unmarshaler -func (cw *CallbackWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - cw.ServiceID = d.Val() - } - return nil -} - -// Ensure CallbackWriter implements the required interfaces -var ( - _ caddy.Provisioner = (*CallbackWriter)(nil) - _ caddy.WriterOpener = (*CallbackWriter)(nil) - _ caddyfile.Unmarshaler = (*CallbackWriter)(nil) -) - -// LogWriter is a custom io.Writer that parses Caddy's structured JSON logs -// and extracts request metrics to send via callback -type LogWriter struct { - serviceID string -} - -// NewLogWriter creates a new log writer with the given service ID -func NewLogWriter(serviceID string) *LogWriter { - return &LogWriter{ - serviceID: serviceID, - } -} - -// Write implements io.Writer -func (lw *LogWriter) Write(p []byte) (n int, err error) { - // DEBUG: Log that we received data - log.Infof("LogWriter.Write called with %d bytes for service_id: %s", len(p), lw.serviceID) - log.Debugf("LogWriter content: %s", string(p)) - - // Caddy writes one JSON object per line - // Parse the JSON to extract request metrics - var logEntry map[string]interface{} - if err := json.Unmarshal(p, &logEntry); err != nil { - // Not JSON or malformed, skip - log.Debugf("Failed to unmarshal JSON: %v", err) - return len(p), nil - } - - // Caddy access logs have a nested "request" object - // Check if this is an access log entry by looking for "request" field - requestObj, hasRequest := logEntry["request"] - if !hasRequest { - log.Debugf("Not an access log entry (no 'request' field)") - return len(p), nil - } - - request, ok := requestObj.(map[string]interface{}) - if !ok { - log.Debugf("'request' field is not a map") - return len(p), nil - } - - // Extract fields - data := &RequestData{ - ServiceID: lw.serviceID, - } - - // Extract method from request object - if method, ok := request["method"].(string); ok { - data.Method = method - } - - // Extract host from request object and strip port - if host, ok := request["host"].(string); ok { - // Strip port from host (e.g., "test.netbird.io:54321" -> "test.netbird.io") - if idx := strings.LastIndex(host, ":"); idx != -1 { - data.Host = host[:idx] - } else { - data.Host = host - } - } - - // Extract path (uri field) from request object - if uri, ok := request["uri"].(string); ok { - data.Path = uri - } - - // Extract status code from top-level - if status, ok := logEntry["status"].(float64); ok { - data.ResponseCode = int32(status) - } - - // Extract duration (in seconds, convert to milliseconds) from top-level - if duration, ok := logEntry["duration"].(float64); ok { - data.DurationMs = int64(duration * 1000) - } - - // Extract source IP from request object - try multiple fields - if clientIP, ok := request["client_ip"].(string); ok { - data.SourceIP = clientIP - } else if remoteIP, ok := request["remote_ip"].(string); ok { - data.SourceIP = remoteIP - } else if remoteAddr, ok := request["remote_addr"].(string); ok { - // remote_addr is in "IP:port" format - if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { - data.SourceIP = remoteAddr[:idx] - } else { - data.SourceIP = remoteAddr - } - } - - // Call callback if set and we have valid data - callback := getCallback(lw.serviceID) - if callback != nil && data.Method != "" { - log.Infof("Calling callback for request: %s %s", data.Method, data.Path) - go func() { - // Run in goroutine to avoid blocking log writes - callback(data) - }() - } else { - log.Warnf("No callback registered for service_id: %s", lw.serviceID) - } - - log.WithFields(log.Fields{ - "service_id": data.ServiceID, - "method": data.Method, - "host": data.Host, - "path": data.Path, - "status": data.ResponseCode, - "duration_ms": data.DurationMs, - "source_ip": data.SourceIP, - }).Info("Request logged via callback writer") - - return len(p), nil -} - -// Close implements io.Closer (no-op for our use case) -func (lw *LogWriter) Close() error { - return nil -} - -// Ensure LogWriter implements io.WriteCloser -var _ io.WriteCloser = (*LogWriter)(nil) diff --git a/proxy/internal/reverseproxy/logwriter_test.go b/proxy/internal/reverseproxy/logwriter_test.go deleted file mode 100644 index 3bd8e1a7f..000000000 --- a/proxy/internal/reverseproxy/logwriter_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package reverseproxy - -import ( - "encoding/json" - "sync" - "testing" - "time" -) - -func TestLogWriter_Write(t *testing.T) { - // Create a channel to receive callback data - callbackChan := make(chan *RequestData, 1) - var callbackMu sync.Mutex - var callbackCalled bool - - // Register a test callback - testServiceID := "test-service" - RegisterCallback(testServiceID, func(data *RequestData) { - callbackMu.Lock() - callbackCalled = true - callbackMu.Unlock() - callbackChan <- data - }) - defer UnregisterCallback(testServiceID) - - // Create a log writer - writer := NewLogWriter(testServiceID) - - // Create a sample Caddy access log entry (matching the structure from your logs) - logEntry := map[string]interface{}{ - "level": "info", - "ts": 1768352053.7900746, - "logger": "http.log.access", - "msg": "handled request", - "request": map[string]interface{}{ - "remote_ip": "::1", - "remote_port": "51972", - "client_ip": "::1", - "proto": "HTTP/1.1", - "method": "GET", - "host": "test.netbird.io:54321", - "uri": "/test/path", - }, - "bytes_read": 0, - "user_id": "", - "duration": 0.004779453, - "size": 615, - "status": 200, - } - - // Marshal to JSON - logJSON, err := json.Marshal(logEntry) - if err != nil { - t.Fatalf("Failed to marshal log entry: %v", err) - } - - // Write to the log writer - n, err := writer.Write(logJSON) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - - if n != len(logJSON) { - t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n) - } - - // Wait for callback to be called (with timeout) - select { - case data := <-callbackChan: - // Verify the extracted data - if data.ServiceID != testServiceID { - t.Errorf("Expected service_id %s, got %s", testServiceID, data.ServiceID) - } - if data.Method != "GET" { - t.Errorf("Expected method GET, got %s", data.Method) - } - if data.Host != "test.netbird.io" { - t.Errorf("Expected host test.netbird.io, got %s", data.Host) - } - if data.Path != "/test/path" { - t.Errorf("Expected path /test/path, got %s", data.Path) - } - if data.ResponseCode != 200 { - t.Errorf("Expected status 200, got %d", data.ResponseCode) - } - if data.SourceIP != "::1" { - t.Errorf("Expected source_ip ::1, got %s", data.SourceIP) - } - // Duration should be ~4.78ms (0.004779453 * 1000) - if data.DurationMs < 4 || data.DurationMs > 5 { - t.Errorf("Expected duration ~4-5ms, got %dms", data.DurationMs) - } - case <-time.After(1 * time.Second): - t.Fatal("Callback was not called within timeout") - } - - // Verify callback was called - callbackMu.Lock() - defer callbackMu.Unlock() - if !callbackCalled { - t.Error("Callback was never called") - } -} - -func TestLogWriter_Write_NonAccessLog(t *testing.T) { - // Create a channel to receive callback data - callbackChan := make(chan *RequestData, 1) - - // Register a test callback - testServiceID := "test-service-2" - RegisterCallback(testServiceID, func(data *RequestData) { - callbackChan <- data - }) - defer UnregisterCallback(testServiceID) - - // Create a log writer - writer := NewLogWriter(testServiceID) - - // Create a non-access log entry (e.g., a TLS log) - logEntry := map[string]interface{}{ - "level": "info", - "ts": 1768352032.12347, - "logger": "tls", - "msg": "storage cleaning happened too recently", - } - - // Marshal to JSON - logJSON, err := json.Marshal(logEntry) - if err != nil { - t.Fatalf("Failed to marshal log entry: %v", err) - } - - // Write to the log writer - n, err := writer.Write(logJSON) - if err != nil { - t.Fatalf("Write failed: %v", err) - } - - if n != len(logJSON) { - t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n) - } - - // Callback should NOT be called for non-access logs - select { - case data := <-callbackChan: - t.Errorf("Callback should not be called for non-access log, but got: %+v", data) - case <-time.After(100 * time.Millisecond): - // Expected - callback not called - } -} - -func TestLogWriter_Write_MalformedJSON(t *testing.T) { - // Create a log writer - writer := NewLogWriter("test-service-3") - - // Write malformed JSON - malformedJSON := []byte("{this is not valid json") - - // Should not fail, just skip the entry - n, err := writer.Write(malformedJSON) - if err != nil { - t.Fatalf("Write should not fail on malformed JSON: %v", err) - } - - if n != len(malformedJSON) { - t.Errorf("Expected to write %d bytes, wrote %d", len(malformedJSON), n) - } -} - -func TestCallbackRegistry(t *testing.T) { - serviceID := "test-registry" - var called bool - - // Test registering a callback - callback := func(data *RequestData) { - called = true - } - RegisterCallback(serviceID, callback) - - // Test retrieving the callback - retrievedCallback := getCallback(serviceID) - if retrievedCallback == nil { - t.Fatal("Expected to retrieve callback, got nil") - } - - // Call the retrieved callback to verify it works - retrievedCallback(&RequestData{}) - if !called { - t.Error("Callback was not called") - } - - // Test unregistering - UnregisterCallback(serviceID) - retrievedCallback = getCallback(serviceID) - if retrievedCallback != nil { - t.Error("Expected nil after unregistering, got a callback") - } -} - -func TestCallbackWriter_Module(t *testing.T) { - // Test that the module is properly configured - cw := CallbackWriter{ServiceID: "test"} - - moduleInfo := cw.CaddyModule() - if moduleInfo.ID != "caddy.logging.writers.callback" { - t.Errorf("Expected module ID 'caddy.logging.writers.callback', got '%s'", moduleInfo.ID) - } - - if moduleInfo.New == nil { - t.Error("Expected New function to be set") - } - - // Test creating a new instance via the New function - newModule := moduleInfo.New() - if newModule == nil { - t.Error("Expected New() to return a module instance") - } - - _, ok := newModule.(*CallbackWriter) - if !ok { - t.Error("Expected New() to return a *CallbackWriter") - } -} - -func TestCallbackWriter_WriterKey(t *testing.T) { - cw := &CallbackWriter{ServiceID: "my-service"} - - expectedKey := "callback_my-service" - if cw.WriterKey() != expectedKey { - t.Errorf("Expected writer key '%s', got '%s'", expectedKey, cw.WriterKey()) - } -} - -func TestCallbackWriter_String(t *testing.T) { - cw := &CallbackWriter{ServiceID: "my-service"} - - str := cw.String() - if str != "callback writer for service my-service" { - t.Errorf("Unexpected string representation: %s", str) - } -} - -func TestLogWriter_Close(t *testing.T) { - writer := NewLogWriter("test") - - // Close should not fail - err := writer.Close() - if err != nil { - t.Errorf("Close should not fail: %v", err) - } -} diff --git a/proxy/internal/reverseproxy/middleware.go b/proxy/internal/reverseproxy/middleware.go index f0e24d9e9..0dd3e0976 100644 --- a/proxy/internal/reverseproxy/middleware.go +++ b/proxy/internal/reverseproxy/middleware.go @@ -1,16 +1,7 @@ package reverseproxy -import ( - "net/http" - "strings" - "time" - - "github.com/caddyserver/caddy/v2/modules/caddyhttp" - log "github.com/sirupsen/logrus" -) - // RequestDataCallback is called for each request that passes through the proxy -type RequestDataCallback func(data *RequestData) +type RequestDataCallback func(data RequestData) // RequestData contains metadata about a proxied request type RequestData struct { @@ -21,111 +12,8 @@ type RequestData struct { Method string ResponseCode int32 SourceIP string -} - -// MetricsMiddleware wraps a handler to capture request metrics -type MetricsMiddleware struct { - Next caddyhttp.Handler - ServiceID string - Callback RequestDataCallback -} - -// ServeHTTP implements caddyhttp.MiddlewareHandler -func (m *MetricsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { - // Record start time - startTime := time.Now() - - // Wrap the response writer to capture status code - wrappedWriter := &responseWriterWrapper{ - ResponseWriter: w, - statusCode: http.StatusOK, // Default to 200 - } - - // Call the next handler (Caddy's reverse proxy) - err := next.ServeHTTP(wrappedWriter, r) - - // Calculate duration - duration := time.Since(startTime) - - // Extract source IP (handle X-Forwarded-For or direct connection) - sourceIP := extractSourceIP(r) - - // Create request data - data := &RequestData{ - ServiceID: m.ServiceID, - Path: r.URL.Path, - DurationMs: duration.Milliseconds(), - Method: r.Method, - ResponseCode: int32(wrappedWriter.statusCode), - SourceIP: sourceIP, - } - - // Call callback if set - if m.Callback != nil { - go func() { - // Run callback in goroutine to avoid blocking response - m.Callback(data) - }() - } - - log.WithFields(log.Fields{ - "service_id": data.ServiceID, - "method": data.Method, - "path": data.Path, - "status": data.ResponseCode, - "duration_ms": data.DurationMs, - "source_ip": data.SourceIP, - }).Debug("Request proxied") - - return err -} - -// responseWriterWrapper wraps http.ResponseWriter to capture status code -type responseWriterWrapper struct { - http.ResponseWriter - statusCode int - written bool -} - -// WriteHeader captures the status code -func (w *responseWriterWrapper) WriteHeader(statusCode int) { - if !w.written { - w.statusCode = statusCode - w.written = true - } - w.ResponseWriter.WriteHeader(statusCode) -} - -// Write ensures we capture status if WriteHeader wasn't called explicitly -func (w *responseWriterWrapper) Write(b []byte) (int, error) { - if !w.written { - w.written = true - // Status code defaults to 200 if not explicitly set - } - return w.ResponseWriter.Write(b) -} - -// extractSourceIP extracts the real client IP from the request -func extractSourceIP(r *http.Request) string { - // Check X-Forwarded-For header first (if behind a proxy) - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - // X-Forwarded-For can be a comma-separated list, take the first one - parts := strings.Split(xff, ",") - if len(parts) > 0 { - return strings.TrimSpace(parts[0]) - } - } - - // Check X-Real-IP header - if xri := r.Header.Get("X-Real-IP"); xri != "" { - return xri - } - - // Fall back to RemoteAddr - // RemoteAddr is in format "IP:port", so we need to strip the port - if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { - return r.RemoteAddr[:idx] - } - - return r.RemoteAddr + + AuthMechanism string + UserID string + AuthSuccess bool } diff --git a/proxy/internal/reverseproxy/proxy.go b/proxy/internal/reverseproxy/proxy.go new file mode 100644 index 000000000..91e9457c4 --- /dev/null +++ b/proxy/internal/reverseproxy/proxy.go @@ -0,0 +1,817 @@ +package reverseproxy + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "sort" + "strings" + "sync" + "time" + + "github.com/gorilla/mux" + "golang.org/x/crypto/acme/autocert" + + log "github.com/sirupsen/logrus" +) + +// Proxy wraps a reverse proxy with dynamic routing +type Proxy struct { + config Config + mu sync.RWMutex + routes map[string]*RouteConfig // key is host/domain (for fast O(1) lookup) + server *http.Server + httpServer *http.Server + autocertManager *autocert.Manager + isRunning bool + requestCallback RequestDataCallback +} + +// Config holds the reverse proxy configuration +type Config struct { + // ListenAddress is the address to listen on for HTTPS (default ":443") + ListenAddress string + + // HTTPListenAddress is the address for HTTP (default ":80") + // Used for ACME challenges when HTTPS is enabled, or as main listener when HTTPS is disabled + HTTPListenAddress string + + // EnableHTTPS enables automatic HTTPS with Let's Encrypt + EnableHTTPS bool + + // TLSEmail is the email for Let's Encrypt registration + TLSEmail string + + // CertCacheDir is the directory to cache certificates (default "./certs") + CertCacheDir string + + // RequestDataCallback is called for each proxied request with metrics + RequestDataCallback RequestDataCallback + + // OIDCConfig is the global OIDC/OAuth configuration for authentication + // This is shared across all routes that use Bearer authentication + // If nil, routes with Bearer auth will fail to initialize + OIDCConfig *OIDCConfig +} + +// OIDCConfig holds the global OIDC/OAuth configuration +type OIDCConfig struct { + // OIDC Provider settings + ProviderURL string `env:"NB_OIDC_PROVIDER_URL" json:"provider_url"` // Identity provider URL (e.g., "https://accounts.google.com") + ClientID string `env:"NB_OIDC_CLIENT_ID" json:"client_id"` // OAuth client ID + ClientSecret string `env:"NB_OIDC_CLIENT_SECRET" json:"client_secret"` // OAuth client secret (empty for public clients) + RedirectURL string `env:"NB_OIDC_REDIRECT_URL" json:"redirect_url"` // Redirect URL after auth (e.g., "http://localhost:54321/auth/callback") + Scopes []string `env:"NB_OIDC_SCOPES" json:"scopes"` // Requested scopes (default: ["openid", "profile", "email"]) + + // JWT Validation settings + JWTKeysLocation string `env:"NB_OIDC_JWT_KEYS_LOCATION" json:"jwt_keys_location"` // JWKS URL for fetching public keys + JWTIssuer string `env:"NB_OIDC_JWT_ISSUER" json:"jwt_issuer"` // Expected issuer claim + JWTAudience []string `env:"NB_OIDC_JWT_AUDIENCE" json:"jwt_audience"` // Expected audience claims + JWTIdpSignkeyRefreshEnabled bool `env:"NB_OIDC_JWT_IDP_SIGNKEY_REFRESH_ENABLED" json:"jwt_idp_signkey_refresh_enabled"` // Enable automatic refresh of signing keys + + // Session settings + SessionCookieName string `env:"NB_OIDC_SESSION_COOKIE_NAME" json:"session_cookie_name"` // Cookie name for storing session (default: "auth_session") +} + +// RouteConfig defines a routing configuration +type RouteConfig struct { + // ID is a unique identifier for this route + ID string + + // Domain is the domain to listen on (e.g., "example.com" or "*" for all) + Domain string + + // PathMappings defines paths that should be forwarded to specific ports + // Key is the path prefix (e.g., "/", "/api", "/admin") + // Value is the target IP:port (e.g., "192.168.1.100:3000") + // Must have at least one entry. Use "/" or "" for the default/catch-all route. + PathMappings map[string]string + + // Conn is the network connection to use for this route + // This allows routing through specific tunnels (e.g., WireGuard) per route + // This connection will be reused for all requests to this route + Conn net.Conn + + // AuthConfig is optional authentication configuration for this route + // Configure ONE of: BasicAuth, PIN, or Bearer (JWT/OIDC) + // If nil, requests pass through without authentication + AuthConfig *AuthConfig + + // AuthRejectResponse is an optional custom response for authentication failures + // If nil, returns 401 Unauthorized with WWW-Authenticate header + AuthRejectResponse func(w http.ResponseWriter, r *http.Request) +} + +// routeEntry represents a compiled route with its proxy +type routeEntry struct { + routeConfig *RouteConfig + path string + target string + proxy *httputil.ReverseProxy + handler http.Handler // handler wraps proxy with middleware (auth, logging, etc.) +} + +// New creates a new reverse proxy +func New(config Config) (*Proxy, error) { + // Set defaults + if config.ListenAddress == "" { + config.ListenAddress = ":443" + } + if config.HTTPListenAddress == "" { + config.HTTPListenAddress = ":80" + } + if config.CertCacheDir == "" { + config.CertCacheDir = "./certs" + } + + // Validate HTTPS config + if config.EnableHTTPS { + if config.TLSEmail == "" { + return nil, fmt.Errorf("TLSEmail is required when EnableHTTPS is true") + } + } + + // Set default OIDC session cookie name if not provided + if config.OIDCConfig != nil && config.OIDCConfig.SessionCookieName == "" { + config.OIDCConfig.SessionCookieName = "auth_session" + } + + p := &Proxy{ + config: config, + routes: make(map[string]*RouteConfig), + isRunning: false, + requestCallback: config.RequestDataCallback, + } + + return p, nil +} + +// Start starts the reverse proxy server +func (p *Proxy) Start() error { + p.mu.Lock() + if p.isRunning { + p.mu.Unlock() + return fmt.Errorf("reverse proxy already running") + } + p.isRunning = true + p.mu.Unlock() + + // Build the main HTTP handler + handler := p.buildHandler() + + if p.config.EnableHTTPS { + // Setup autocert manager with dynamic host policy + p.autocertManager = &autocert.Manager{ + Cache: autocert.DirCache(p.config.CertCacheDir), + Prompt: autocert.AcceptTOS, + Email: p.config.TLSEmail, + HostPolicy: p.dynamicHostPolicy, // Use dynamic policy based on routes + } + + // Start HTTP server for ACME challenges + p.httpServer = &http.Server{ + Addr: p.config.HTTPListenAddress, + Handler: p.autocertManager.HTTPHandler(nil), + } + + go func() { + log.Infof("Starting HTTP server on %s for ACME challenges", p.config.HTTPListenAddress) + if err := p.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Errorf("HTTP server error: %v", err) + } + }() + + // Start HTTPS server + p.server = &http.Server{ + Addr: p.config.ListenAddress, + Handler: handler, + TLSConfig: p.autocertManager.TLSConfig(), + } + + go func() { + log.Infof("Starting HTTPS server on %s", p.config.ListenAddress) + if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Errorf("HTTPS server error: %v", err) + p.mu.Lock() + p.isRunning = false + p.mu.Unlock() + } + }() + } else { + // Start HTTP server only + p.server = &http.Server{ + Addr: p.config.HTTPListenAddress, + Handler: handler, + } + + go func() { + log.Infof("Starting HTTP server on %s", p.config.HTTPListenAddress) + if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Errorf("HTTP server error: %v", err) + p.mu.Lock() + p.isRunning = false + p.mu.Unlock() + } + }() + } + + log.Infof("Reverse proxy started with %d route(s)", len(p.routes)) + return nil +} + +// dynamicHostPolicy is a custom host policy that allows certificates for any domain +// that has a configured route +func (p *Proxy) dynamicHostPolicy(ctx context.Context, host string) error { + p.mu.RLock() + defer p.mu.RUnlock() + + // Strip port if present + if idx := strings.LastIndex(host, ":"); idx != -1 { + host = host[:idx] + } + + // O(1) lookup for exact domain match + if _, exists := p.routes[host]; exists { + log.Infof("Allowing certificate for domain: %s", host) + return nil + } + + log.Warnf("Rejecting certificate request for unknown domain: %s", host) + return fmt.Errorf("domain %s not configured in routes", host) +} + +// Stop gracefully stops the reverse proxy +func (p *Proxy) Stop(ctx context.Context) error { + p.mu.Lock() + if !p.isRunning { + p.mu.Unlock() + return fmt.Errorf("reverse proxy not running") + } + p.mu.Unlock() + + log.Info("Stopping reverse proxy...") + + // Stop HTTPS server + if p.server != nil { + if err := p.server.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown HTTPS server: %w", err) + } + } + + // Stop HTTP server (ACME challenge server) + if p.httpServer != nil { + if err := p.httpServer.Shutdown(ctx); err != nil { + return fmt.Errorf("failed to shutdown HTTP server: %w", err) + } + } + + p.mu.Lock() + p.isRunning = false + p.mu.Unlock() + + log.Info("Reverse proxy stopped") + return nil +} + +// buildHandler creates the main HTTP handler with router for static endpoints +func (p *Proxy) buildHandler() http.Handler { + router := mux.NewRouter() + + // Register static endpoints + router.HandleFunc("/auth/callback", p.handleOIDCCallback).Methods("GET") + + // Catch-all handler for dynamic proxy routing + router.PathPrefix("/").HandlerFunc(p.handleProxyRequest) + + return router +} + +// handleProxyRequest handles all dynamic proxy requests +func (p *Proxy) handleProxyRequest(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + routeEntry := p.findRoute(r.Host, r.URL.Path) + if routeEntry == nil { + log.Warnf("No route found for host=%s path=%s", r.Host, r.URL.Path) + http.NotFound(w, r) + return + } + + rw := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + routeEntry.handler.ServeHTTP(rw, r) + + if p.requestCallback != nil { + duration := time.Since(startTime) + + host := r.Host + if idx := strings.LastIndex(host, ":"); idx != -1 { + host = host[:idx] + } + + authMechanism := r.Header.Get("X-Auth-Method") + if authMechanism == "" { + authMechanism = "none" + } + + // Determine auth success based on status code + authSuccess := rw.statusCode != http.StatusUnauthorized && rw.statusCode != http.StatusForbidden + + // Extract user ID (this would need to be enhanced to extract from tokens/headers) + _, userID, _ := extractAuthInfo(r, rw.statusCode) + + data := RequestData{ + ServiceID: routeEntry.routeConfig.ID, + Host: host, + Path: r.URL.Path, + DurationMs: duration.Milliseconds(), + Method: r.Method, + ResponseCode: int32(rw.statusCode), + SourceIP: extractSourceIP(r), + AuthMechanism: authMechanism, + UserID: userID, + AuthSuccess: authSuccess, + } + + p.requestCallback(data) + } +} + +// findRoute finds the matching route for a given host and path +func (p *Proxy) findRoute(host, path string) *routeEntry { + p.mu.RLock() + defer p.mu.RUnlock() + + // Strip port from host + if idx := strings.LastIndex(host, ":"); idx != -1 { + host = host[:idx] + } + + // O(1) lookup by host + routeConfig, exists := p.routes[host] + if !exists { + return nil + } + + // Build list of route entries sorted by path specificity + var entries []*routeEntry + + // Create entries for each path mapping + for routePath, target := range routeConfig.PathMappings { + proxy := p.createProxy(routeConfig, target) + + // ALWAYS wrap proxy with auth middleware (even if no auth configured) + // This ensures consistent auth handling and logging + handler := wrapWithAuth(proxy, routeConfig.AuthConfig, routeConfig.ID, routeConfig.AuthRejectResponse, p.config.OIDCConfig) + + // Log auth configuration + if routeConfig.AuthConfig != nil && !routeConfig.AuthConfig.IsEmpty() { + var authType string + if routeConfig.AuthConfig.BasicAuth != nil { + authType = "basic_auth" + } else if routeConfig.AuthConfig.PIN != nil { + authType = "pin" + } else if routeConfig.AuthConfig.Bearer != nil { + authType = "bearer_jwt" + } + log.WithFields(log.Fields{ + "route_id": routeConfig.ID, + "auth_type": authType, + }).Debug("Auth middleware enabled for route") + } else { + log.WithFields(log.Fields{ + "route_id": routeConfig.ID, + }).Debug("No authentication configured for route") + } + + entries = append(entries, &routeEntry{ + routeConfig: routeConfig, + path: routePath, + target: target, + proxy: proxy, + handler: handler, + }) + } + + // Sort by path specificity (longest first) + sort.Slice(entries, func(i, j int) bool { + pi, pj := entries[i].path, entries[j].path + // Empty string or "/" goes last (catch-all) + if pi == "" || pi == "/" { + return false + } + if pj == "" || pj == "/" { + return true + } + return len(pi) > len(pj) + }) + + // Find first matching entry + for _, entry := range entries { + if entry.path == "" || entry.path == "/" { + // Catch-all route + return entry + } + if strings.HasPrefix(path, entry.path) { + return entry + } + } + + return nil +} + +// createProxy creates a reverse proxy for a target with the route's connection +func (p *Proxy) createProxy(routeConfig *RouteConfig, target string) *httputil.ReverseProxy { + // Parse target URL + targetURL, err := url.Parse("http://" + target) + if err != nil { + log.Errorf("Failed to parse target URL %s: %v", target, err) + // Return a proxy that returns 502 + return &httputil.ReverseProxy{ + Director: func(req *http.Request) {}, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, "Bad Gateway", http.StatusBadGateway) + }, + } + } + + // Create reverse proxy + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + // Check if this is a defaultConn (for testing) + if dc, ok := routeConfig.Conn.(*defaultConn); ok { + // For defaultConn, use its dialer directly + proxy.Transport = &http.Transport{ + DialContext: dc.dialer.DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + log.Infof("Using default network dialer for route %s (testing mode)", routeConfig.ID) + } else { + // Configure transport to use the provided connection (WireGuard, etc.) + proxy.Transport = &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + log.Debugf("Using custom connection for route %s to %s", routeConfig.ID, address) + return routeConfig.Conn, nil + }, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: 0, // Keep alive indefinitely + DisableKeepAlives: false, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + log.Infof("Using custom connection for route %s", routeConfig.ID) + } + + // Custom error handler + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + log.Errorf("Proxy error for %s%s: %v", r.Host, r.URL.Path, err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + } + + return proxy +} + +// AddRoute adds a new route configuration +func (p *Proxy) AddRoute(route *RouteConfig) error { + if route == nil { + return fmt.Errorf("route cannot be nil") + } + if route.ID == "" { + return fmt.Errorf("route ID is required") + } + if route.Domain == "" { + return fmt.Errorf("route Domain is required") + } + if len(route.PathMappings) == 0 { + return fmt.Errorf("route must have at least one path mapping") + } + if route.Conn == nil { + return fmt.Errorf("route connection (Conn) is required") + } + + p.mu.Lock() + defer p.mu.Unlock() + + // Check if route already exists for this domain + if _, exists := p.routes[route.Domain]; exists { + return fmt.Errorf("route for domain %s already exists", route.Domain) + } + + // Add route with domain as key + p.routes[route.Domain] = route + + log.WithFields(log.Fields{ + "route_id": route.ID, + "domain": route.Domain, + "paths": len(route.PathMappings), + }).Info("Added route") + + // Note: With this architecture, we don't need to reload the server + // The handler dynamically looks up routes on each request + // Certificates will be obtained automatically when the domain is first accessed + + return nil +} + +// RemoveRoute removes a route +func (p *Proxy) RemoveRoute(domain string) error { + p.mu.Lock() + defer p.mu.Unlock() + + // Check if route exists + if _, exists := p.routes[domain]; !exists { + return fmt.Errorf("route for domain %s not found", domain) + } + + // Remove route + delete(p.routes, domain) + + log.Infof("Removed route for domain: %s", domain) + return nil +} + +// UpdateRoute updates an existing route +func (p *Proxy) UpdateRoute(route *RouteConfig) error { + if route == nil { + return fmt.Errorf("route cannot be nil") + } + if route.ID == "" { + return fmt.Errorf("route ID is required") + } + if route.Domain == "" { + return fmt.Errorf("route Domain is required") + } + + p.mu.Lock() + defer p.mu.Unlock() + + // Check if route exists for this domain + if _, exists := p.routes[route.Domain]; !exists { + return fmt.Errorf("route for domain %s not found", route.Domain) + } + + // Update route using domain as key + p.routes[route.Domain] = route + + log.WithFields(log.Fields{ + "route_id": route.ID, + "domain": route.Domain, + "paths": len(route.PathMappings), + }).Info("Updated route") + + return nil +} + +// ListRoutes returns a list of all configured domains +func (p *Proxy) ListRoutes() []string { + p.mu.RLock() + defer p.mu.RUnlock() + + domains := make([]string, 0, len(p.routes)) + for domain := range p.routes { + domains = append(domains, domain) + } + return domains +} + +// GetRoute returns a route configuration by domain +func (p *Proxy) GetRoute(domain string) (*RouteConfig, error) { + p.mu.RLock() + defer p.mu.RUnlock() + + route, exists := p.routes[domain] + if !exists { + return nil, fmt.Errorf("route for domain %s not found", domain) + } + + return route, nil +} + +// IsRunning returns whether the proxy is running +func (p *Proxy) IsRunning() bool { + p.mu.RLock() + defer p.mu.RUnlock() + return p.isRunning +} + +// GetConfig returns the proxy configuration +func (p *Proxy) GetConfig() Config { + return p.config +} + +// responseWriter wraps http.ResponseWriter to capture status code +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +// extractSourceIP extracts the source IP from the request +func extractSourceIP(r *http.Request) string { + // Try X-Forwarded-For header first + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + // Take the first IP in the list + if idx := strings.Index(xff, ","); idx != -1 { + return strings.TrimSpace(xff[:idx]) + } + return strings.TrimSpace(xff) + } + + // Try X-Real-IP header + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return strings.TrimSpace(xri) + } + + // Fall back to RemoteAddr + if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr +} + +// extractAuthInfo extracts authentication information from the request +// Returns: authMechanism, userID, authSuccess +func extractAuthInfo(r *http.Request, statusCode int) (string, string, bool) { + // Check if authentication succeeded based on status code + // 401 = Unauthorized, 403 = Forbidden + authSuccess := statusCode != http.StatusUnauthorized && statusCode != http.StatusForbidden + + // Check for Bearer token (JWT, OAuth2, etc.) + if auth := r.Header.Get("Authorization"); auth != "" { + if strings.HasPrefix(auth, "Bearer ") { + // Extract user ID from JWT if possible (you may want to decode the JWT here) + // For now, we'll just indicate it's a bearer token + return "bearer", extractUserIDFromBearer(auth), authSuccess + } + if strings.HasPrefix(auth, "Basic ") { + // Basic authentication + return "basic", extractUserIDFromBasic(auth), authSuccess + } + // Other authorization schemes + return "other", "", authSuccess + } + + // Check for API key in headers + if apiKey := r.Header.Get("X-API-Key"); apiKey != "" { + return "api_key", "", authSuccess + } + if apiKey := r.Header.Get("X-Api-Key"); apiKey != "" { + return "api_key", "", authSuccess + } + + // Check for mutual TLS (client certificate) + if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { + // Extract Common Name from client certificate + cn := r.TLS.PeerCertificates[0].Subject.CommonName + return "mtls", cn, authSuccess + } + + // Check for session cookie (common in web apps) + if cookie, err := r.Cookie("session"); err == nil && cookie.Value != "" { + return "session", "", authSuccess + } + + // No authentication detected + return "none", "", authSuccess +} + +// extractUserIDFromBearer attempts to extract user ID from Bearer token +// Decodes the JWT (without verification) to extract the user ID from standard claims +func extractUserIDFromBearer(auth string) string { + // Remove "Bearer " prefix + tokenString := strings.TrimPrefix(auth, "Bearer ") + if tokenString == "" { + return "" + } + + // JWT format: header.payload.signature + // We only need the payload to extract user ID (no verification needed here) + parts := strings.Split(tokenString, ".") + if len(parts) != 3 { + log.Debug("Invalid JWT format: expected 3 parts") + return "" + } + + // Decode the payload (second part) + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + log.WithError(err).Debug("Failed to decode JWT payload") + return "" + } + + // Parse JSON payload + var claims map[string]interface{} + if err := json.Unmarshal(payload, &claims); err != nil { + log.WithError(err).Debug("Failed to parse JWT claims") + return "" + } + + // Try standard user ID claims in order of preference + // 1. "sub" (standard JWT subject claim) + if sub, ok := claims["sub"].(string); ok && sub != "" { + return sub + } + + // 2. "user_id" (common in some systems) + if userID, ok := claims["user_id"].(string); ok && userID != "" { + return userID + } + + // 3. "email" (fallback) + if email, ok := claims["email"].(string); ok && email != "" { + return email + } + + // 4. "preferred_username" (used by some OIDC providers) + if username, ok := claims["preferred_username"].(string); ok && username != "" { + return username + } + + return "" +} + +// extractUserIDFromBasic extracts username from Basic auth header +func extractUserIDFromBasic(auth string) string { + // Basic auth format: "Basic base64(username:password)" + _ = strings.TrimPrefix(auth, "Basic ") + // Note: We're not decoding it here for security reasons + // The upstream service should handle the actual authentication + // We just note that basic auth was used + return "" +} + +// defaultConn is a lazy connection wrapper that uses the standard network dialer +// This is useful for testing or development when not using WireGuard tunnels +type defaultConn struct { + dialer *net.Dialer + mu sync.Mutex + conns map[string]net.Conn // cache connections by "network:address" +} + +func (dc *defaultConn) Read(b []byte) (n int, err error) { + return 0, fmt.Errorf("Read not supported on defaultConn - use dial via Transport") +} + +func (dc *defaultConn) Write(b []byte) (n int, err error) { + return 0, fmt.Errorf("Write not supported on defaultConn - use dial via Transport") +} + +func (dc *defaultConn) Close() error { + dc.mu.Lock() + defer dc.mu.Unlock() + + for _, conn := range dc.conns { + conn.Close() + } + dc.conns = make(map[string]net.Conn) + return nil +} + +func (dc *defaultConn) LocalAddr() net.Addr { return nil } +func (dc *defaultConn) RemoteAddr() net.Addr { return nil } +func (dc *defaultConn) SetDeadline(t time.Time) error { return nil } +func (dc *defaultConn) SetReadDeadline(t time.Time) error { return nil } +func (dc *defaultConn) SetWriteDeadline(t time.Time) error { return nil } + +// NewDefaultConn creates a connection wrapper that uses the standard network dialer +// This is useful for testing or development when not using WireGuard tunnels +// The actual dialing happens when the HTTP Transport calls DialContext +func NewDefaultConn() net.Conn { + return &defaultConn{ + dialer: &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }, + conns: make(map[string]net.Conn), + } +} + +// handleOIDCCallback handles the global /auth/callback endpoint for all routes +func (p *Proxy) handleOIDCCallback(w http.ResponseWriter, r *http.Request) { + // Check if OIDC is configured globally + if p.config.OIDCConfig == nil { + log.Error("OIDC callback received but no OIDC config found") + http.Error(w, "Authentication not configured", http.StatusInternalServerError) + return + } + + // Use the HandleOIDCCallback function from auth.go with global config + handler := HandleOIDCCallback(p.config.OIDCConfig) + handler(w, r) +} diff --git a/proxy/internal/reverseproxy/transport.go b/proxy/internal/reverseproxy/transport.go deleted file mode 100644 index 87cd21e04..000000000 --- a/proxy/internal/reverseproxy/transport.go +++ /dev/null @@ -1,139 +0,0 @@ -package reverseproxy - -import ( - "context" - "fmt" - "net" - "net/http" - "sync" - "time" - - log "github.com/sirupsen/logrus" -) - -// customTransportRegistry stores custom dialers and connections globally -// This allows them to be accessed after Caddy deserializes the configuration from JSON -var customTransportRegistry = &transportRegistry{ - transports: make(map[string]*customTransport), -} - -// transportRegistry manages custom transports for routes -type transportRegistry struct { - mu sync.RWMutex - transports map[string]*customTransport // key is "routeID:path" -} - -// customTransport wraps either a net.Conn or a custom dialer -type customTransport struct { - routeID string - path string - conn net.Conn - customDialer func(ctx context.Context, network, address string) (net.Conn, error) - defaultDialer *net.Dialer -} - -// Register registers a custom transport for a route -func (r *transportRegistry) Register(routeID, path string, conn net.Conn, dialer func(ctx context.Context, network, address string) (net.Conn, error)) { - r.mu.Lock() - defer r.mu.Unlock() - - key := fmt.Sprintf("%s:%s", routeID, path) - r.transports[key] = &customTransport{ - routeID: routeID, - path: path, - conn: conn, - customDialer: dialer, - defaultDialer: &net.Dialer{Timeout: 30 * time.Second}, - } - - if conn != nil { - log.Infof("Registered net.Conn transport for route %s (path: %s)", routeID, path) - } else if dialer != nil { - log.Infof("Registered custom dialer transport for route %s (path: %s)", routeID, path) - } -} - -// Get retrieves a custom transport for a route -func (r *transportRegistry) Get(routeID, path string) *customTransport { - r.mu.RLock() - defer r.mu.RUnlock() - - key := fmt.Sprintf("%s:%s", routeID, path) - return r.transports[key] -} - -// Unregister removes a custom transport -func (r *transportRegistry) Unregister(routeID, path string) { - r.mu.Lock() - defer r.mu.Unlock() - - key := fmt.Sprintf("%s:%s", routeID, path) - delete(r.transports, key) - log.Infof("Unregistered transport for route %s (path: %s)", routeID, path) -} - -// Clear removes all custom transports -func (r *transportRegistry) Clear() { - r.mu.Lock() - defer r.mu.Unlock() - - r.transports = make(map[string]*customTransport) - log.Info("Cleared all custom transports") -} - -// DialContext implements the DialContext function for custom transports -func (ct *customTransport) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - // If we have a pre-existing connection, return it - if ct.conn != nil { - log.Debugf("Reusing existing connection for route %s (path: %s) to %s", ct.routeID, ct.path, address) - return ct.conn, nil - } - - // If we have a custom dialer, use it - if ct.customDialer != nil { - log.Debugf("Using custom dialer for route %s (path: %s) to %s", ct.routeID, ct.path, address) - return ct.customDialer(ctx, network, address) - } - - // Fallback to default dialer (this shouldn't happen if registered correctly) - log.Warnf("No custom transport found for route %s (path: %s), using default dialer", ct.routeID, ct.path) - return ct.defaultDialer.DialContext(ctx, network, address) -} - -// NewCustomHTTPTransport creates an HTTP transport that uses the custom dialer -func NewCustomHTTPTransport(routeID, path string) *http.Transport { - transport := customTransportRegistry.Get(routeID, path) - if transport == nil { - // No custom transport registered, return standard transport - log.Warnf("No custom transport found for route %s (path: %s), using standard transport", routeID, path) - return &http.Transport{ - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - } - - // Configure transport based on whether we're using a connection or dialer - if transport.conn != nil { - // Using a pre-existing connection - disable pooling - return &http.Transport{ - DialContext: transport.DialContext, - MaxIdleConns: 1, - MaxIdleConnsPerHost: 1, - IdleConnTimeout: 0, // Keep alive indefinitely - DisableKeepAlives: false, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - } - - // Using a custom dialer - use normal pooling - return &http.Transport{ - DialContext: transport.DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } -} diff --git a/proxy/pkg/proxy/config.go b/proxy/pkg/proxy/config.go index c3776307c..ede989203 100644 --- a/proxy/pkg/proxy/config.go +++ b/proxy/pkg/proxy/config.go @@ -5,15 +5,47 @@ import ( "errors" "fmt" "os" + "reflect" "time" "github.com/caarlos0/env/v11" + + "github.com/netbirdio/netbird/proxy/internal/reverseproxy" ) var ( ErrFailedToParseConfig = errors.New("failed to parse config from env") ) +// Duration is a time.Duration that can be unmarshaled from JSON as a string +type Duration time.Duration + +// UnmarshalJSON implements json.Unmarshaler for Duration +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + parsed, err := time.ParseDuration(s) + if err != nil { + return err + } + + *d = Duration(parsed) + return nil +} + +// MarshalJSON implements json.Marshaler for Duration +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).String()) +} + +// ToDuration converts Duration to time.Duration +func (d Duration) ToDuration() time.Duration { + return time.Duration(d) +} + // Config holds the configuration for the reverse proxy server type Config struct { // ListenAddress is the address the proxy server will listen on (e.g., ":443" or "0.0.0.0:443") @@ -42,6 +74,22 @@ type Config struct { // EnableGRPC enables the gRPC control server EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"` + + // Reverse Proxy Configuration + // HTTPListenAddress is the address for HTTP (default ":80") + HTTPListenAddress string `json:"http_listen_address"` + + // EnableHTTPS enables automatic HTTPS with Let's Encrypt + EnableHTTPS bool `json:"enable_https"` + + // TLSEmail is the email for Let's Encrypt registration + TLSEmail string `json:"tls_email"` + + // CertCacheDir is the directory to cache certificates (default "./certs") + CertCacheDir string `json:"cert_cache_dir"` + + // OIDCConfig is the global OIDC/OAuth configuration for authentication + OIDCConfig *reverseproxy.OIDCConfig `json:"oidc_config,omitempty"` } // ParseAndLoad parses configuration from environment variables @@ -104,6 +152,80 @@ func LoadFromFileOrEnv(configPath string) (Config, error) { return cfg, nil } +// UnmarshalJSON implements custom JSON unmarshaling with automatic duration parsing +// Uses reflection to find all time.Duration fields and parse them from string +func (c *Config) UnmarshalJSON(data []byte) error { + // First unmarshal into a map to get raw values + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Get reflection value and type + val := reflect.ValueOf(c).Elem() + typ := val.Type() + + // Iterate through all fields + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // Get JSON tag name + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + + // Parse tag to get field name (handle omitempty, etc.) + jsonFieldName := jsonTag + if idx := len(jsonTag); idx > 0 { + for j, c := range jsonTag { + if c == ',' { + jsonFieldName = jsonTag[:j] + break + } + } + } + + // Get raw value from JSON + rawValue, exists := raw[jsonFieldName] + if !exists { + continue + } + + // Check if this field is a time.Duration + if field.Type() == reflect.TypeOf(time.Duration(0)) { + // Try to parse as string duration + if strValue, ok := rawValue.(string); ok { + duration, err := time.ParseDuration(strValue) + if err != nil { + return fmt.Errorf("invalid duration for field %s: %w", jsonFieldName, err) + } + field.Set(reflect.ValueOf(duration)) + } else { + return fmt.Errorf("field %s must be a duration string", jsonFieldName) + } + } else { + // For non-duration fields, unmarshal normally + fieldData, err := json.Marshal(rawValue) + if err != nil { + return fmt.Errorf("failed to marshal field %s: %w", jsonFieldName, err) + } + + // Create a new instance of the field type + if field.CanSet() { + newVal := reflect.New(field.Type()) + if err := json.Unmarshal(fieldData, newVal.Interface()); err != nil { + return fmt.Errorf("failed to unmarshal field %s: %w", jsonFieldName, err) + } + field.Set(newVal.Elem()) + } + } + } + + return nil +} + // Validate checks if the configuration is valid func (c *Config) Validate() error { if c.ListenAddress == "" { diff --git a/proxy/pkg/proxy/server.go b/proxy/pkg/proxy/server.go index 9cdf3c679..ba741178c 100644 --- a/proxy/pkg/proxy/server.go +++ b/proxy/pkg/proxy/server.go @@ -18,7 +18,7 @@ import ( type Server struct { config Config grpcServer *grpcpkg.Server - caddyProxy *reverseproxy.CaddyProxy + proxy *reverseproxy.Proxy mu sync.RWMutex isRunning bool @@ -84,30 +84,37 @@ func NewServer(config Config) (*Server, error) { exposedServices: make(map[string]*ExposedServiceConfig), } - // Create Caddy reverse proxy with request callback - caddyConfig := reverseproxy.Config{ - ListenAddress: ":54321", // Use port 54321 for local testing - EnableHTTPS: false, // TODO: Add HTTPS support - RequestDataCallback: func(data *reverseproxy.RequestData) { - // This is where access log data arrives - SET BREAKPOINT HERE + // Set defaults for reverse proxy config if not provided + httpListenAddr := config.HTTPListenAddress + if httpListenAddr == "" { + httpListenAddr = ":54321" // Use port 54321 for local testing + } + + // Create reverse proxy with request callback + proxyConfig := reverseproxy.Config{ + HTTPListenAddress: httpListenAddr, + EnableHTTPS: config.EnableHTTPS, + TLSEmail: config.TLSEmail, + CertCacheDir: config.CertCacheDir, + RequestDataCallback: func(data reverseproxy.RequestData) { log.WithFields(log.Fields{ "service_id": data.ServiceID, + "host": data.Host, "method": data.Method, "path": data.Path, "response_code": data.ResponseCode, "duration_ms": data.DurationMs, "source_ip": data.SourceIP, }).Info("Access log received") - - // TODO: Send via gRPC to control service - // This would send pb.ProxyRequestData via the gRPC stream }, + // Use global OIDC configuration from config + OIDCConfig: config.OIDCConfig, } - caddyProxy, err := reverseproxy.New(caddyConfig) + proxy, err := reverseproxy.New(proxyConfig) if err != nil { - return nil, fmt.Errorf("failed to create Caddy proxy: %w", err) + return nil, fmt.Errorf("failed to create reverse proxy: %w", err) } - server.caddyProxy = caddyProxy + server.proxy = proxy // Create gRPC server if enabled if config.EnableGRPC && config.GRPCListenAddress != "" { @@ -131,14 +138,14 @@ func (s *Server) Start() error { s.isRunning = true s.mu.Unlock() - log.Infof("Starting Caddy reverse proxy server on %s", s.config.ListenAddress) + log.Infof("Starting proxy reverse proxy server on %s", s.config.ListenAddress) - // Start Caddy proxy - if err := s.caddyProxy.Start(); err != nil { + // Start reverse proxy + if err := s.proxy.Start(); err != nil { s.mu.Lock() s.isRunning = false s.mu.Unlock() - return fmt.Errorf("failed to start Caddy proxy: %w", err) + return fmt.Errorf("failed to start reverse proxy: %w", err) } // Start gRPC server if configured @@ -162,21 +169,32 @@ func (s *Server) Start() error { s.sendProxyEvent(pb.ProxyEvent_STARTED, "Proxy server started") } - if err := s.caddyProxy.AddRoute( + // Enable Bearer authentication for the test route + // OIDC configuration is set globally in the proxy config above + testAuthConfig := &reverseproxy.AuthConfig{ + Bearer: &reverseproxy.BearerConfig{ + Enabled: true, + }, + } + + // Register main protected route with auth + // The /auth/callback endpoint is automatically handled globally for all routes + if err := s.proxy.AddRoute( &reverseproxy.RouteConfig{ ID: "test", Domain: "test.netbird.io", PathMappings: map[string]string{"/": "localhost:8080"}, + Conn: reverseproxy.NewDefaultConn(), + AuthConfig: testAuthConfig, }); err != nil { log.Warn("Failed to add test route: ", err) } - // Block forever - Caddy runs in background <-s.shutdownCtx.Done() return nil } -// Stop gracefully shuts down both Caddy and gRPC servers +// Stop gracefully shuts down both proxy and gRPC servers func (s *Server) Stop(ctx context.Context) error { s.mu.Lock() if !s.isRunning { @@ -212,9 +230,9 @@ func (s *Server) Stop(ctx context.Context) error { s.mu.Unlock() } - // Shutdown Caddy proxy - if err := s.caddyProxy.Stop(ctx); err != nil { - caddyErr = fmt.Errorf("Caddy proxy shutdown failed: %w", err) + // Shutdown reverse proxy + if err := s.proxy.Stop(ctx); err != nil { + caddyErr = fmt.Errorf("reverse proxy shutdown failed: %w", err) log.Error(caddyErr) } @@ -401,14 +419,14 @@ func (s *Server) handleExposedServiceCreated(serviceID string, peerConfig *PeerC pathMappings[path] = target } - // Add route to Caddy + // Add route to proxy route := &reverseproxy.RouteConfig{ ID: serviceID, Domain: upstreamConfig.Domain, PathMappings: pathMappings, } - if err := s.caddyProxy.AddRoute(route); err != nil { + if err := s.proxy.AddRoute(route); err != nil { return fmt.Errorf("failed to add route: %w", err) } @@ -449,14 +467,14 @@ func (s *Server) handleExposedServiceUpdated(serviceID string, peerConfig *PeerC pathMappings[path] = target } - // Update route in Caddy + // Update route in proxy route := &reverseproxy.RouteConfig{ ID: serviceID, Domain: upstreamConfig.Domain, PathMappings: pathMappings, } - if err := s.caddyProxy.UpdateRoute(route); err != nil { + if err := s.proxy.UpdateRoute(route); err != nil { return fmt.Errorf("failed to update route: %w", err) } @@ -485,8 +503,8 @@ func (s *Server) handleExposedServiceRemoved(serviceID string) error { "service_id": serviceID, }).Info("Removing exposed service") - // Remove route from Caddy - if err := s.caddyProxy.RemoveRoute(serviceID); err != nil { + // Remove route from proxy + if err := s.proxy.RemoveRoute(serviceID); err != nil { return fmt.Errorf("failed to remove route: %w", err) } diff --git a/proxy/proxy b/proxy/proxy deleted file mode 100755 index 12cebcfbb..000000000 Binary files a/proxy/proxy and /dev/null differ