diff --git a/.gitignore b/.gitignore index b579b1fc..d3c2ff12 100755 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ compose.yml config certs config*/ +!schemas/** certs*/ bin/ error_pages/ @@ -25,4 +26,4 @@ todo.md .aider* mtrace.json .env -test.Dockerfile +test.Dockerfile \ No newline at end of file diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json index 02f732c4..628682bc 100644 --- a/.vscode/settings.example.json +++ b/.vscode/settings.example.json @@ -1,10 +1,10 @@ { "yaml.schemas": { - "https://github.com/yusing/go-proxy/raw/v0.8/schema/config.schema.json": [ + "https://github.com/yusing/go-proxy/raw/v0.8/schemas/config.schema.json": [ "config.example.yml", "config.yml" ], - "https://github.com/yusing/go-proxy/raw/v0.8/schema/providers.schema.json": [ + "https://github.com/yusing/go-proxy/raw/v0.8/schemas/routes.schema.json": [ "providers.example.yml" ] } diff --git a/Dockerfile b/Dockerfile index 864e79ff..de9677b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Builder -FROM golang:1.23.4-alpine AS builder +FROM golang:1.23.5-alpine AS builder HEALTHCHECK NONE # package version does not matter @@ -51,7 +51,7 @@ COPY config.example.yml /app/config/config.yml COPY --from=builder /etc/ssl/certs /etc/ssl/certs # copy schema -COPY schema /app/schema +COPY schemas/config.schema.json schemas/routes.schema.json schemas/middleware_compose.schema.json /app/schemas/ ENV DOCKER_HOST=unix:///var/run/docker.sock ENV GODOXY_DEBUG=0 diff --git a/Makefile b/Makefile index 94d0ce9f..83b4e26f 100755 --- a/Makefile +++ b/Makefile @@ -70,4 +70,28 @@ push-docker-io: build-docker: docker build -t godoxy-nightly \ - --build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" . \ No newline at end of file + --build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" . + +gen-schema-single: + typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o schemas/${OUT} schemas/${IN} ${CLASS} + +gen-schema: + make IN=config/config.ts \ + CLASS=Config \ + OUT=config.schema.json \ + gen-schema-single + make IN=providers/routes.ts \ + CLASS=Routes \ + OUT=routes.schema.json \ + gen-schema-single + make IN=middlewares/middleware_compose.ts \ + CLASS=MiddlewareCompose \ + OUT=middleware_compose.schema.json \ + gen-schema-single + make IN=docker.ts \ + CLASS=DockerRoutes \ + OUT=docker_routes.schema.json \ + gen-schema-single + +push-github: + git push origin $(shell git rev-parse --abbrev-ref HEAD) \ No newline at end of file diff --git a/go.mod b/go.mod index c821eb1b..6bfd2037 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module github.com/yusing/go-proxy -go 1.23.4 +go 1.23.5 require ( github.com/PuerkitoBio/goquery v1.10.1 github.com/coder/websocket v1.8.12 github.com/coreos/go-oidc/v3 v3.12.0 - github.com/docker/cli v27.4.1+incompatible - github.com/docker/docker v27.4.1+incompatible + github.com/docker/cli v27.5.0+incompatible + github.com/docker/docker v27.5.0+incompatible github.com/fsnotify/fsnotify v1.8.0 github.com/go-acme/lego/v4 v4.21.0 - github.com/go-playground/validator/v10 v10.23.0 + github.com/go-playground/validator/v10 v10.24.0 github.com/gobwas/glob v0.2.3 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/gotify/server/v2 v2.6.1 @@ -32,7 +32,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/cloudflare-go v0.113.0 // indirect + github.com/cloudflare/cloudflare-go v0.114.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -61,21 +61,21 @@ require ( github.com/ovh/go-ovh v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/tools v0.29.0 // indirect - google.golang.org/protobuf v1.36.2 // indirect + google.golang.org/protobuf v1.36.3 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index 684d562e..760e067e 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/cloudflare/cloudflare-go v0.113.0 h1:qnOXmA6RbgZ4rg5gNBK5QGk0Pzbv8pnUYV3C4+8CU6w= -github.com/cloudflare/cloudflare-go v0.113.0/go.mod h1:Dlm4BAnycHc0i8yLxQZb9b+OlMwYOAoDJsUOEFgpVvo= +github.com/cloudflare/cloudflare-go v0.114.0 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA= +github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -27,10 +27,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= -github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= +github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -56,8 +56,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= +github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= @@ -126,8 +126,8 @@ github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+ github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= @@ -150,20 +150,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= -go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= -go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= -go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= -go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= -go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -271,8 +271,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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= diff --git a/internal/api/v1/schema.go b/internal/api/v1/schema.go index 86b732d8..cf5bffe3 100644 --- a/internal/api/v1/schema.go +++ b/internal/api/v1/schema.go @@ -14,7 +14,7 @@ func GetSchemaFile(w http.ResponseWriter, r *http.Request) { if filename == "" { U.RespondError(w, U.ErrMissingKey("filename"), http.StatusBadRequest) } - content, err := os.ReadFile(path.Join(common.SchemaBasePath, filename)) + content, err := os.ReadFile(path.Join(common.SchemasBasePath, filename)) if err != nil { U.HandleErr(w, r, err) return diff --git a/internal/common/constants.go b/internal/common/constants.go index c637a177..d00964a7 100644 --- a/internal/common/constants.go +++ b/internal/common/constants.go @@ -25,9 +25,9 @@ const ( MiddlewareComposeBasePath = ConfigBasePath + "/middlewares" - SchemaBasePath = "schema" - ConfigSchemaPath = SchemaBasePath + "/config.schema.json" - FileProviderSchemaPath = SchemaBasePath + "/providers.schema.json" + SchemasBasePath = "schemas" + ConfigSchemaPath = SchemasBasePath + "/config.schema.json" + FileProviderSchemaPath = SchemasBasePath + "/providers.schema.json" ComposeFileName = "compose.yml" ComposeExampleFileName = "compose.example.yml" @@ -37,7 +37,7 @@ const ( var RequiredDirectories = []string{ ConfigBasePath, - SchemaBasePath, + SchemasBasePath, ErrorPagesBasePath, MiddlewareComposeBasePath, } @@ -49,7 +49,7 @@ const ( HealthCheckTimeoutDefault = 5 * time.Second WakeTimeoutDefault = "30s" - StopTimeoutDefault = "10s" + StopTimeoutDefault = "30s" StopMethodDefault = "stop" ) diff --git a/internal/docker/container_test.go b/internal/docker/container_test.go new file mode 100644 index 00000000..4ecd475e --- /dev/null +++ b/internal/docker/container_test.go @@ -0,0 +1,43 @@ +package docker + +import ( + "testing" + + "github.com/docker/docker/api/types" + . "github.com/yusing/go-proxy/internal/utils/testing" +) + +func TestContainerExplicit(t *testing.T) { + tests := []struct { + name string + labels map[string]string + isExplicit bool + }{ + { + name: "explicit", + labels: map[string]string{ + "proxy.aliases": "foo", + }, + isExplicit: true, + }, + { + name: "explicit2", + labels: map[string]string{ + "proxy.idle_timeout": "1s", + }, + isExplicit: true, + }, + { + name: "not explicit", + labels: map[string]string{}, + isExplicit: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := FromDocker(&types.Container{Names: []string{"test"}, State: "test", Labels: tt.labels}, "") + ExpectEqual(t, c.IsExplicit, tt.isExplicit) + }) + } +} diff --git a/internal/docker/idlewatcher/watcher.go b/internal/docker/idlewatcher/watcher.go index 8451880f..878e4c91 100644 --- a/internal/docker/idlewatcher/watcher.go +++ b/internal/docker/idlewatcher/watcher.go @@ -294,6 +294,9 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) { case errors.Is(err, context.Canceled): continue case err != nil: + if errors.Is(err, context.DeadlineExceeded) { + err = errors.New("timeout waiting for container to stop, please set a higher value for `stop_timeout`") + } w.Err(err).Msgf("container stop with method %q failed", w.StopMethod) default: w.LogReason("container stopped", "idle timeout") diff --git a/internal/net/http/reverseproxy/reverse_proxy_mod.go b/internal/net/http/reverseproxy/reverse_proxy_mod.go index 0bb0d4bb..7d823d67 100644 --- a/internal/net/http/reverseproxy/reverse_proxy_mod.go +++ b/internal/net/http/reverseproxy/reverse_proxy_mod.go @@ -12,6 +12,7 @@ package reverseproxy import ( "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -207,13 +208,25 @@ func copyHeader(dst, src http.Header) { } func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) { + reqURL := r.Host + r.RequestURI switch { case errors.Is(err, context.Canceled), - errors.Is(err, io.EOF): - logger.Debug().Err(err).Str("url", r.URL.String()).Msg("http proxy error") + errors.Is(err, io.EOF), + errors.Is(err, context.DeadlineExceeded): + logger.Debug().Err(err).Str("url", reqURL).Msg("http proxy error") default: - logger.Err(err).Str("url", r.URL.String()).Msg("http proxy error") + var recordErr tls.RecordHeaderError + if errors.As(err, &recordErr) { + logger.Error(). + Str("url", reqURL). + Msgf(`scheme was likely misconfigured as https, + try setting "proxy.%s.scheme" back to "http"`, p.TargetName) + logging.Err(err).Msg("underlying error") + } else { + logger.Err(err).Str("url", reqURL).Msg("http proxy error") + } } + if writeHeader { rw.WriteHeader(http.StatusInternalServerError) } diff --git a/internal/route/provider/all_fields.yaml b/internal/route/provider/all_fields.yaml index 1d812b49..7af53fbb 100644 --- a/internal/route/provider/all_fields.yaml +++ b/internal/route/provider/all_fields.yaml @@ -2,6 +2,7 @@ example: # matching `example.y.z` scheme: http host: 10.0.0.254 port: 80 + no_tls_verify: true path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax - GET / # accept any GET request - POST /auth # for /auth and /auth/* accept only POST diff --git a/schema/access_log.json b/schema/access_log.json deleted file mode 100644 index e19ec13d..00000000 --- a/schema/access_log.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$id": "https://github.com/yusing/go-proxy/raw/v0.8/schema/access_log.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Access log configuration", - "type": "object", - "additionalProperties": false, - "properties": { - "path": { - "title": "Access log path", - "type": "string" - }, - "format": { - "title": "Access log format", - "type": "string", - "enum": [ - "common", - "combined", - "json" - ] - }, - "buffer_size": { - "title": "Access log buffer size in bytes", - "type": "integer", - "minimum": 1 - }, - "filters": { - "title": "Access log filters", - "type": "object", - "additionalProperties": false, - "properties": { - "cidr": { - "title": "CIDR filter", - "$ref": "#/$defs/access_log_filters" - }, - "status_codes": { - "title": "Status code filter", - "$ref": "#/$defs/access_log_filters" - }, - "method": { - "title": "Method filter", - "$ref": "#/$defs/access_log_filters" - }, - "headers": { - "title": "Header filter", - "$ref": "#/$defs/access_log_filters" - }, - "host": { - "title": "Host filter", - "$ref": "#/$defs/access_log_filters" - } - } - }, - "fields": { - "title": "Access log fields", - "type": "object", - "additionalProperties": false, - "properties": { - "headers": { - "title": "Headers field", - "$ref": "#/$defs/access_log_fields" - }, - "query": { - "title": "Query field", - "$ref": "#/$defs/access_log_fields" - }, - "cookies": { - "title": "Cookies field", - "$ref": "#/$defs/access_log_fields" - } - } - } - }, - "$defs": { - "access_log_filters": { - "type": "object", - "additionalProperties": false, - "properties": { - "negative": { - "type": "boolean" - }, - "values": { - "type": "array" - } - } - }, - "access_log_fields": { - "type": "object", - "additionalProperties": false, - "properties": { - "default": { - "enum": [ - "keep", - "redact", - "drop" - ] - }, - "config": { - "type": "object" - } - } - } - } -} \ No newline at end of file diff --git a/schema/config.schema.json b/schema/config.schema.json deleted file mode 100644 index c01a938f..00000000 --- a/schema/config.schema.json +++ /dev/null @@ -1,464 +0,0 @@ -{ - "$id": "https://github.com/yusing/go-proxy/raw/v0.8/schema/config.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "GoDoxy config file", - "properties": { - "autocert": { - "title": "Autocert configuration", - "type": "object", - "properties": { - "email": { - "title": "ACME Email", - "type": "string", - "format": "email" - }, - "domains": { - "title": "Cert Domains", - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "cert_path": { - "title": "path of cert file to load/store", - "default": "certs/cert.crt", - "markdownDescription": "default: `certs/cert.crt`,", - "type": "string" - }, - "key_path": { - "title": "path of key file to load/store", - "default": "certs/priv.key", - "markdownDescription": "default: `certs/priv.key`", - "type": "string" - }, - "acme_key_path": { - "title": "path of acme key file to load/store", - "default": "certs/acme.key", - "markdownDescription": "default: `certs/acme.key`", - "type": "string" - }, - "provider": { - "title": "DNS Challenge Provider", - "default": "local", - "type": "string", - "enum": [ - "local", - "cloudflare", - "clouddns", - "duckdns", - "ovh" - ] - }, - "options": { - "title": "Provider specific options", - "type": "object" - } - }, - "allOf": [ - { - "if": { - "not": { - "properties": { - "provider": { - "const": "local" - } - } - } - }, - "then": { - "required": [ - "email", - "domains", - "provider", - "options" - ] - } - }, - { - "if": { - "properties": { - "provider": { - "const": "cloudflare" - } - } - }, - "then": { - "properties": { - "options": { - "required": [ - "auth_token" - ], - "additionalProperties": false, - "properties": { - "auth_token": { - "description": "Cloudflare API Token with Zone Scope", - "type": "string" - } - } - } - } - } - }, - { - "if": { - "properties": { - "provider": { - "const": "clouddns" - } - } - }, - "then": { - "properties": { - "options": { - "required": [ - "client_id", - "email", - "password" - ], - "additionalProperties": false, - "properties": { - "client_id": { - "description": "CloudDNS Client ID", - "type": "string" - }, - "email": { - "description": "CloudDNS Email", - "type": "string" - }, - "password": { - "description": "CloudDNS Password", - "type": "string" - } - } - } - } - } - }, - { - "if": { - "properties": { - "provider": { - "const": "duckdns" - } - } - }, - "then": { - "properties": { - "options": { - "required": [ - "token" - ], - "additionalProperties": false, - "properties": { - "token": { - "description": "DuckDNS Token", - "type": "string" - } - } - } - } - } - }, - { - "if": { - "properties": { - "provider": { - "const": "ovh" - } - } - }, - "then": { - "properties": { - "options": { - "required": [ - "application_secret", - "consumer_key" - ], - "additionalProperties": false, - "oneOf": [ - { - "required": [ - "application_key" - ] - }, - { - "required": [ - "oauth2_config" - ] - } - ], - "properties": { - "api_endpoint": { - "description": "OVH API endpoint", - "default": "ovh-eu", - "anyOf": [ - { - "enum": [ - "ovh-eu", - "ovh-ca", - "ovh-us", - "kimsufi-eu", - "kimsufi-ca", - "soyoustart-eu", - "soyoustart-ca" - ] - }, - { - "type": "string", - "format": "uri" - } - ] - }, - "application_secret": { - "description": "OVH Application Secret", - "type": "string" - }, - "consumer_key": { - "description": "OVH Consumer Key", - "type": "string" - }, - "application_key": { - "description": "OVH Application Key", - "type": "string" - }, - "oauth2_config": { - "description": "OVH OAuth2 config", - "type": "object", - "additionalProperties": false, - "properties": { - "client_id": { - "description": "OVH Client ID", - "type": "string" - }, - "client_secret": { - "description": "OVH Client Secret", - "type": "string" - } - }, - "required": [ - "client_id", - "client_secret" - ] - } - } - } - } - } - } - ] - }, - "providers": { - "title": "Proxy providers configuration", - "type": "object", - "additionalProperties": false, - "properties": { - "include": { - "title": "Proxy providers configuration files", - "description": "relative path to 'config'", - "type": "array", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+\\.(yml|yaml)$", - "patternErrorMessage": "Invalid file name" - } - }, - "docker": { - "title": "Docker provider configuration", - "description": "docker clients (name-address pairs)", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9-_]+$": { - "type": "string", - "examples": [ - "unix:///var/run/docker.sock", - "tcp://127.0.0.1:2375", - "ssh://user@host:port" - ], - "oneOf": [ - { - "const": "$DOCKER_HOST", - "description": "Use DOCKER_HOST environment variable" - }, - { - "pattern": "^unix://.+$", - "description": "A Unix socket for local Docker communication." - }, - { - "pattern": "^ssh://.+$", - "description": "An SSH connection to a remote Docker host." - }, - { - "pattern": "^fd://.+$", - "description": "A file descriptor for Docker communication." - }, - { - "pattern": "^tcp://.+$", - "description": "A TCP connection to a remote Docker host." - } - ] - } - } - }, - "notification": { - "description": "Notification provider configuration", - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "provider" - ], - "properties": { - "name": { - "type": "string", - "description": "Notifier name" - }, - "provider": { - "description": "Notifier provider", - "type": "string", - "enum": [ - "gotify", - "webhook" - ] - } - }, - "oneOf": [ - { - "description": "Gotify configuration", - "additionalProperties": false, - "properties": { - "name": {}, - "provider": { - "const": "gotify" - }, - "url": { - "description": "Gotify URL", - "type": "string" - }, - "token": { - "description": "Gotify token", - "type": "string" - } - }, - "required": [ - "url", - "token" - ] - }, - { - "description": "Webhook configuration", - "additionalProperties": false, - "properties": { - "name": {}, - "provider": { - "const": "webhook" - }, - "url": { - "description": "Webhook URL", - "type": "string" - }, - "token": { - "description": "Webhook bearer token", - "type": "string" - }, - "template": { - "description": "Webhook template", - "type": "string", - "enum": [ - "discord" - ] - }, - "payload": { - "description": "Webhook payload", - "type": "string", - "format": "json" - }, - "method": { - "description": "Webhook request method", - "type": "string", - "enum": [ - "GET", - "POST", - "PUT" - ] - }, - "mime_type": { - "description": "Webhook NIME type", - "type": "string" - }, - "color_mode": { - "description": "Webhook color mode", - "type": "string", - "enum": [ - "hex", - "dec" - ] - } - }, - "required": [ - "url" - ] - } - ] - } - } - } - }, - "match_domains": { - "title": "Domains to match", - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1 - }, - "homepage": { - "title": "Homepage configuration", - "type": "object", - "additionalProperties": false, - "properties": { - "use_default_categories": { - "title": "Use default categories", - "type": "boolean" - } - } - }, - "entrypoint": { - "title": "Entrypoint configuration", - "type": "object", - "additionalProperties": false, - "properties": { - "middlewares": { - "title": "Entrypoint middlewares", - "type": "array", - "items": { - "type": "object", - "required": [ - "use" - ], - "properties": { - "use": { - "type": "string", - "description": "Middleware to use" - } - } - } - }, - "access_log": { - "$ref": "https://github.com/yusing/go-proxy/raw/v0.8/schema/access_log.json" - } - } - }, - "timeout_shutdown": { - "title": "Shutdown timeout (in seconds)", - "type": "integer", - "minimum": 0 - } - }, - "additionalProperties": false, - "required": [ - "providers" - ] -} \ No newline at end of file diff --git a/schema/providers.schema.json b/schema/providers.schema.json deleted file mode 100644 index bf123ab8..00000000 --- a/schema/providers.schema.json +++ /dev/null @@ -1,290 +0,0 @@ -{ - "$id": "https://github.com/yusing/go-proxy/raw/v0.8/schema/providers.schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GoDoxy standalone include file", - "oneOf": [ - { - "type": "object" - }, - { - "type": "null" - } - ], - "patternProperties": { - ".+": { - "title": "Proxy entry", - "type": "object", - "properties": { - "scheme": { - "title": "Proxy scheme", - "oneOf": [ - { - "type": "string", - "enum": [ - "http", - "https", - "tcp", - "udp", - "tcp:tcp", - "udp:udp", - "tcp:udp", - "udp:tcp" - ] - }, - { - "type": "null", - "description": "Auto detect base on port format" - } - ] - }, - "host": { - "default": "localhost", - "anyOf": [ - { - "type": "null", - "title": "localhost (default)" - }, - { - "type": "string", - "format": "ipv4", - "title": "ipv4 address" - }, - { - "type": "string", - "format": "ipv6", - "title": "ipv6 address" - }, - { - "type": "string", - "format": "hostname", - "title": "hostname" - } - ], - "title": "Proxy host (ipv4/6 / hostname)" - }, - "port": {}, - "no_tls_verify": {}, - "path_patterns": {}, - "middlewares": {}, - "homepage": { - "title": "Dashboard config", - "type": "object", - "additionalProperties": false, - "properties": { - "show": { - "title": "Show on dashboard", - "type": "boolean", - "default": true - }, - "name": { - "title": "Display name", - "type": "string" - }, - "icon": { - "title": "Display icon", - "type": "string", - "oneOf": [ - { - "pattern": "^(png|svg|webp)\\/[\\w\\d\\-_]+\\.\\1$", - "title": "Icon from walkxcode/dashboard-icons" - }, - { - "pattern": "^https?://", - "title": "Absolute URI", - "format": "uri" - }, - { - "pattern": "^@target/", - "title": "Relative URI to target" - } - ] - }, - "url": { - "title": "App URL override", - "type": "string", - "format": "uri", - "pattern": "^https?://" - }, - "category": { - "title": "Category", - "type": "string" - }, - "description": { - "title": "Description", - "type": "string" - }, - "widget_config": { - "title": "Widget config", - "type": "object" - } - } - }, - "load_balance": { - "type": "object", - "additionalProperties": false, - "properties": { - "link": { - "type": "string", - "title": "Name and subdomain of load-balancer" - }, - "mode": { - "enum": [ - "round_robin", - "least_conn", - "ip_hash" - ], - "title": "Load-balance mode", - "default": "roundrobin" - }, - "weight": { - "type": "integer", - "title": "Reserved for future use", - "minimum": 0, - "maximum": 100 - }, - "options": { - "type": "object", - "title": "load-balance mode specific options" - } - } - }, - "healthcheck": { - "type": "object", - "additionalProperties": false, - "properties": { - "disable": { - "type": "boolean", - "default": false, - "title": "Disable healthcheck" - }, - "path": { - "type": "string", - "title": "Healthcheck path", - "default": "/", - "format": "uri-reference", - "description": "should start with `/`" - }, - "use_get": { - "type": "boolean", - "title": "Use GET instead of HEAD", - "default": false - }, - "interval": { - "type": "string", - "title": "healthcheck Interval", - "pattern": "^([0-9]+(ms|s|m|h))+$", - "default": "5s", - "description": "e.g. 5s, 1m, 2h, 3m30s" - } - } - }, - "access_log": { - "$ref": "https://github.com/yusing/go-proxy/raw/v0.8/schema/access_log.json" - } - }, - "additionalProperties": false, - "allOf": [ - { - "if": { - "properties": { - "scheme": { - "anyOf": [ - { - "enum": [ - "http", - "https" - ] - }, - { - "type": "null" - } - ] - } - } - }, - "then": { - "properties": { - "port": { - "title": "Proxy port", - "markdownDescription": "From **0** to **65535**", - "oneOf": [ - { - "type": "string", - "pattern": "^\\d{1,5}$", - "patternErrorMessage": "`port` must be a number" - }, - { - "type": "integer", - "minimum": 0, - "maximum": 65535 - } - ] - }, - "path_patterns": { - "title": "Path patterns", - "type": "array", - "markdownDescription": "See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux", - "items": { - "type": "string", - "pattern": "^(?:([A-Z]+) )?(?:([a-zA-Z0-9.-]+)\\/)?(\\/[^\\s]*)$", - "patternErrorMessage": "invalid path pattern" - } - }, - "middlewares": { - "type": "object" - } - } - }, - "else": { - "properties": { - "port": { - "markdownDescription": "`listening port:proxy port` or `listening port:service name`", - "type": "string", - "pattern": "^[0-9]+:[0-9a-z]+$", - "patternErrorMessage": "invalid syntax" - }, - "no_tls_verify": { - "not": true - }, - "path_patterns": { - "not": true - }, - "middlewares": { - "not": true - } - }, - "required": [ - "port" - ] - } - }, - { - "if": { - "properties": { - "scheme": { - "const": "https" - } - } - }, - "then": { - "properties": { - "no_tls_verify": { - "title": "Disable TLS verification for https proxy", - "type": "boolean", - "default": false - } - } - }, - "else": { - "properties": { - "no_tls_verify": { - "not": true - } - } - } - } - ] - } - }, - "additionalProperties": false -} \ No newline at end of file diff --git a/schemas/config.schema.json b/schemas/config.schema.json new file mode 100644 index 00000000..d944ced2 --- /dev/null +++ b/schemas/config.schema.json @@ -0,0 +1,1228 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "AccessLogFieldMode": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "AccessLogFormat": { + "enum": [ + "combined", + "common", + "json" + ], + "type": "string" + }, + "AutocertConfig": { + "anyOf": [ + { + "$ref": "#/definitions/LocalOptions" + }, + { + "$ref": "#/definitions/CloudflareOptions" + }, + { + "$ref": "#/definitions/CloudDNSOptions" + }, + { + "$ref": "#/definitions/DuckDNSOptions" + }, + { + "$ref": "#/definitions/OVHOptionsWithAppKey" + }, + { + "$ref": "#/definitions/OVHOptionsWithOAuth2Config" + } + ] + }, + "CIDR": { + "anyOf": [ + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*:.*:.*:.*:.*:.*:.*$", + "type": "string" + }, + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*/[0-9]*$", + "type": "string" + }, + { + "pattern": "^::[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*::/[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*::/[0-9]*$", + "type": "string" + } + ] + }, + "CloudDNSOptions": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string" + }, + "email": { + "format": "email", + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "client_id", + "email", + "password" + ], + "type": "object" + }, + "provider": { + "const": "clouddns", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "CloudflareOptions": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "auth_token": { + "type": "string" + } + }, + "required": [ + "auth_token" + ], + "type": "object" + }, + "provider": { + "const": "cloudflare", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "DuckDNSOptions": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "token": { + "type": "string" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "provider": { + "const": "duckdns", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "GotifyConfig": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "provider": { + "const": "gotify", + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "provider", + "token", + "url" + ], + "type": "object" + }, + "LocalOptions": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "provider": { + "const": "local", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "provider" + ], + "type": "object" + }, + "MiddlewareComposeMap": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "CustomErrorPage", + "ErrorPage", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "RedirectHTTP", + "redirectHTTP", + "redirect_http" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "SetXForwarded", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "HideXForwarded", + "hideXForwarded", + "hide_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked (alias of status_code)" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + }, + "use": { + "enum": [ + "CIDRWhitelist", + "cidrWhitelist", + "cidr_whitelist" + ], + "type": "string" + } + }, + "required": [ + "allow", + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "cloudflareRealIp", + "cloudflare_real_ip" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + }, + "use": { + "enum": [ + "ModifyRequest", + "Request", + "modifyRequest", + "modify_request", + "request" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + }, + "use": { + "enum": [ + "ModifyResponse", + "Response", + "modifyResponse", + "modify_response", + "response" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "allowed_groups": { + "description": "Allowed groups", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "allowed_users": { + "description": "Allowed users", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "use": { + "enum": [ + "OIDC", + "oidc" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "average": { + "description": "Average number of requests allowed in a period", + "type": "number" + }, + "burst": { + "description": "Maximum number of requests allowed in a period", + "type": "number" + }, + "period": { + "default": "1s", + "description": "Duration of the rate limit", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use": { + "enum": [ + "RateLimit", + "rateLimit", + "rate_limit" + ], + "type": "string" + } + }, + "required": [ + "average", + "burst", + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "string" + } + }, + "required": [ + "from", + "use" + ], + "type": "object" + } + ] + }, + "OVHEndpoint": { + "enum": [ + "kimsufi-ca", + "kimsufi-eu", + "ovh-ca", + "ovh-eu", + "ovh-us", + "soyoustart-ca", + "soyoustart-eu" + ], + "type": "string" + }, + "OVHOptionsWithAppKey": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "api_endpoint": { + "$ref": "#/definitions/OVHEndpoint" + }, + "application_key": { + "type": "string" + }, + "application_secret": { + "type": "string" + }, + "consumer_key": { + "type": "string" + } + }, + "required": [ + "application_key", + "application_secret", + "consumer_key" + ], + "type": "object" + }, + "provider": { + "const": "ovh", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "OVHOptionsWithOAuth2Config": { + "additionalProperties": false, + "properties": { + "cert_path": { + "type": "string" + }, + "domains": { + "items": { + "pattern": "^(\\*\\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "type": "array" + }, + "email": { + "format": "email", + "type": "string" + }, + "key_path": { + "type": "string" + }, + "options": { + "additionalProperties": false, + "properties": { + "api_endpoint": { + "$ref": "#/definitions/OVHEndpoint" + }, + "application_secret": { + "type": "string" + }, + "consumer_key": { + "type": "string" + }, + "oauth2_config": { + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + }, + "required": [ + "client_id", + "client_secret" + ], + "type": "object" + } + }, + "required": [ + "application_secret", + "consumer_key", + "oauth2_config" + ], + "type": "object" + }, + "provider": { + "const": "ovh", + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "StatusCode": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "StatusCodeRange": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "pattern": "^[0-9]*-[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "WebhookColorMode": { + "enum": [ + "dec", + "hex" + ], + "type": "string" + }, + "WebhookConfig": { + "additionalProperties": false, + "properties": { + "color_mode": { + "$ref": "#/definitions/WebhookColorMode", + "default": "hex", + "description": "Webhook color mode" + }, + "method": { + "$ref": "#/definitions/WebhookMethod", + "default": "POST", + "description": "Webhook method" + }, + "mime_type": { + "$ref": "#/definitions/WebhookMimeType", + "default": "application/json", + "description": "Webhook mime type" + }, + "name": { + "type": "string" + }, + "payload": { + "description": "Webhook message (usally JSON),\nrequired when template is not defined", + "type": "string" + }, + "provider": { + "const": "webhook", + "type": "string" + }, + "template": { + "const": "discord", + "default": "discord", + "description": "Webhook template", + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "provider", + "url" + ], + "type": "object" + }, + "WebhookMethod": { + "enum": [ + "GET", + "POST", + "PUT" + ], + "type": "string" + }, + "WebhookMimeType": { + "enum": [ + "application/json", + "application/x-www-form-urlencoded", + "text/plain" + ], + "type": "string" + } + }, + "properties": { + "autocert": { + "$ref": "#/definitions/AutocertConfig", + "description": "Optional autocert configuration", + "examples": [ + { + "provider": "local" + }, + { + "domains": [ + "example.com" + ], + "email": "abc@gmail", + "options": { + "auth_token": "c1234565789-abcdefghijklmnopqrst" + }, + "provider": "cloudflare" + }, + { + "domains": [ + "example.com" + ], + "email": "abc@gmail", + "options": { + "client_id": "c1234565789", + "email": "abc@gmail", + "password": "password" + }, + "provider": "clouddns" + } + ] + }, + "entrypoint": { + "additionalProperties": false, + "properties": { + "access_log": { + "additionalProperties": false, + "description": "Entrypoint access log configuration", + "examples": [ + { + "fields": { + "headers": { + "config": { + "foo": "redact" + }, + "default": "keep" + } + }, + "filters": { + "status_codes": { + "values": [ + "200-299" + ] + } + }, + "format": "combined", + "path": "/var/log/access.log" + } + ], + "properties": { + "buffer_size": { + "default": 65536, + "description": "The size of the buffer.", + "minimum": 0, + "type": "integer" + }, + "fields": { + "additionalProperties": false, + "properties": { + "cookie": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "header": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "additionalProperties": false, + "properties": { + "cidr": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "headers": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "host": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "method": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "status_code": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/StatusCodeRange" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + } + }, + "type": "object" + }, + "format": { + "$ref": "#/definitions/AccessLogFormat", + "default": "combined", + "description": "The format of the access log." + }, + "path": { + "format": "uri-reference", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "middlewares": { + "description": "Entrypoint middleware configuration", + "examples": [ + { + "use": "RedirectHTTP" + }, + { + "allow": [ + "127.0.0.1", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16" + ], + "message": "Forbidden", + "status": 403, + "use": "CIDRWhitelist" + } + ], + "items": { + "$ref": "#/definitions/MiddlewareComposeMap" + }, + "type": "array" + } + }, + "required": [ + "middlewares" + ], + "type": "object" + }, + "homepage": { + "additionalProperties": false, + "properties": { + "use_default_categories": { + "default": true, + "description": "Use default app categories (uses docker image name)", + "type": "boolean" + } + }, + "required": [ + "use_default_categories" + ], + "type": "object" + }, + "match_domains": { + "description": "Optional list of domains to match", + "examples": [ + "example.com", + "*.example.com" + ], + "items": { + "pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$" + }, + "minItems": 1, + "type": "array" + }, + "providers": { + "additionalProperties": false, + "properties": { + "docker": { + "additionalProperties": { + "type": "string" + }, + "description": "Name-value mapping of docker hosts to retrieve routes from", + "examples": [ + { + "local": "$DOCKER_HOST" + }, + { + "remote": "tcp://10.0.2.1:2375" + }, + { + "remote2": "ssh://root:1234@10.0.2.2" + } + ], + "items": { + "pattern": "^((\\w+://)[^\\s]+)|\\$DOCKER_HOST$" + }, + "minProperties": 1, + "type": "object" + }, + "include": { + "description": "List of route definition files to include", + "examples": [ + "file1.yml", + "file2.yml" + ], + "items": { + "pattern": "^[\\w\\d\\-_]+\\.(yaml|yml)$" + }, + "minItems": 1, + "type": "array" + }, + "notification": { + "description": "List of notification providers", + "examples": [ + { + "name": "gotify", + "provider": "gotify", + "token": "abcd", + "url": "https://gotify.domain.tld" + }, + { + "name": "discord", + "provider": "webhook", + "template": "discord", + "url": "https://discord.com/api/webhooks/1234/abcd" + } + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/GotifyConfig" + }, + { + "$ref": "#/definitions/WebhookConfig" + } + ] + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "timeout_shutdown": { + "default": 3, + "description": "Optional timeout before shutdown", + "minimum": 1, + "type": "number" + } + }, + "required": [ + "providers" + ], + "type": "object" +} + diff --git a/schemas/config/access_log.ts b/schemas/config/access_log.ts new file mode 100644 index 00000000..1438afb6 --- /dev/null +++ b/schemas/config/access_log.ts @@ -0,0 +1,66 @@ +import { CIDR, HTTPHeader, HTTPMethod, StatusCodeRange, URI } from "../types"; + +export const ACCESS_LOG_FORMATS = ["combined", "common", "json"] as const; + +export type AccessLogFormat = (typeof ACCESS_LOG_FORMATS)[number]; + +export type AccessLogConfig = { + /** + * The size of the buffer. + * + * @minimum 0 + * @default 65536 + * @TJS-type integer + */ + buffer_size?: number; + /** The format of the access log. + * + * @default "combined" + */ + format?: AccessLogFormat; + /* The path to the access log file. */ + path: URI; + /* The access log filters. */ + filters?: AccessLogFilters; + /* The access log fields. */ + fields?: AccessLogFields; +}; + +export type AccessLogFilter = { + /** Whether the filter is negative. + * + * @default false + */ + negative?: boolean; + /* The values to filter. */ + values: T[]; +}; + +export type AccessLogFilters = { + /* Status code filter. */ + status_code?: AccessLogFilter; + /* Method filter. */ + method?: AccessLogFilter; + /* Host filter. */ + host?: AccessLogFilter; + /* Header filter. */ + headers?: AccessLogFilter; + /* CIDR filter. */ + cidr?: AccessLogFilter; +}; + +export const ACCESS_LOG_FIELD_MODES = ["keep", "drop", "redact"] as const; +export type AccessLogFieldMode = (typeof ACCESS_LOG_FIELD_MODES)[number]; + +export type AccessLogField = { + default?: AccessLogFieldMode; + config: { + [key: string]: AccessLogFieldMode; + }; +}; + +export type AccessLogFields = { + header?: AccessLogField; + query?: AccessLogField; + cookie?: AccessLogField; +}; diff --git a/schemas/config/autocert.ts b/schemas/config/autocert.ts new file mode 100644 index 00000000..dde342fb --- /dev/null +++ b/schemas/config/autocert.ts @@ -0,0 +1,91 @@ +import { DomainOrWildcards as DomainsOrWildcards, Email } from "../types"; + +export const AUTOCERT_PROVIDERS = [ + "local", + "cloudflare", + "clouddns", + "duckdns", + "ovh", +] as const; + +export type AutocertProvider = (typeof AUTOCERT_PROVIDERS)[number]; + +export type AutocertConfig = + | LocalOptions + | CloudflareOptions + | CloudDNSOptions + | DuckDNSOptions + | OVHOptionsWithAppKey + | OVHOptionsWithOAuth2Config; + +export interface AutocertConfigBase { + /* ACME email */ + email: Email; + /* ACME domains */ + domains: DomainsOrWildcards; + /* ACME certificate path */ + cert_path?: string; + /* ACME key path */ + key_path?: string; +} + +export interface LocalOptions extends AutocertConfigBase { + provider: "local"; +} + +export interface CloudflareOptions extends AutocertConfigBase { + provider: "cloudflare"; + options: { auth_token: string }; +} + +export interface CloudDNSOptions extends AutocertConfigBase { + provider: "clouddns"; + options: { + client_id: string; + email: Email; + password: string; + }; +} + + +export interface DuckDNSOptions extends AutocertConfigBase { + provider: "duckdns"; + options: { + token: string; + }; +} + +export const OVH_ENDPOINTS = [ + "ovh-eu", + "ovh-ca", + "ovh-us", + "kimsufi-eu", + "kimsufi-ca", + "soyoustart-eu", + "soyoustart-ca", +] as const; + +export type OVHEndpoint = (typeof OVH_ENDPOINTS)[number]; + +export interface OVHOptionsWithAppKey extends AutocertConfigBase { + provider: "ovh"; + options: { + application_secret: string; + consumer_key: string; + api_endpoint?: OVHEndpoint; + application_key: string; + }; +} + +export interface OVHOptionsWithOAuth2Config extends AutocertConfigBase { + provider: "ovh"; + options: { + application_secret: string; + consumer_key: string; + api_endpoint?: OVHEndpoint; + oauth2_config: { + client_id: string; + client_secret: string; + }; + }; +} diff --git a/schemas/config/config.ts b/schemas/config/config.ts new file mode 100644 index 00000000..9b539220 --- /dev/null +++ b/schemas/config/config.ts @@ -0,0 +1,52 @@ +import { DomainNames } from "../types"; +import { AutocertConfig } from "./autocert"; +import { EntrypointConfig } from "./entrypoint"; +import { HomepageConfig } from "./homepage"; +import { Providers } from "./providers"; + +export type Config = { + /** Optional autocert configuration + * + * @examples require(".").autocertExamples + */ + autocert?: AutocertConfig; + /* Optional entrypoint configuration */ + entrypoint?: EntrypointConfig; + /* Providers configuration (include file, docker, notification) */ + providers: Providers; + /** Optional list of domains to match + * + * @minItems 1 + * @examples require(".").matchDomainsExamples + */ + match_domains?: DomainNames; + /* Optional homepage configuration */ + homepage?: HomepageConfig; + /** + * Optional timeout before shutdown + * @default 3 + * @minimum 1 + */ + timeout_shutdown?: number; +}; + +export const autocertExamples = [ + { provider: "local" }, + { + provider: "cloudflare", + email: "abc@gmail", + domains: ["example.com"], + options: { auth_token: "c1234565789-abcdefghijklmnopqrst" }, + }, + { + provider: "clouddns", + email: "abc@gmail", + domains: ["example.com"], + options: { + client_id: "c1234565789", + email: "abc@gmail", + password: "password", + }, + }, +]; +export const matchDomainsExamples = ["example.com", "*.example.com"] as const; diff --git a/schemas/config/entrypoint.ts b/schemas/config/entrypoint.ts new file mode 100644 index 00000000..a5121a83 --- /dev/null +++ b/schemas/config/entrypoint.ts @@ -0,0 +1,47 @@ +import { MiddlewareCompose } from "../middlewares/middleware_compose"; +import { AccessLogConfig } from "./access_log"; + +export type EntrypointConfig = { + /** Entrypoint middleware configuration + * + * @examples require(".").middlewaresExamples + */ + middlewares: MiddlewareCompose; + /** Entrypoint access log configuration + * + * @examples require(".").accessLogExamples + */ + access_log?: AccessLogConfig; +}; + +export const accessLogExamples = [ + { + path: "/var/log/access.log", + format: "combined", + filters: { + status_codes: { + values: ["200-299"], + }, + }, + fields: { + headers: { + default: "keep", + config: { + foo: "redact", + }, + }, + }, + }, +] as const; + +export const middlewaresExamples = [ + { + use: "RedirectHTTP", + }, + { + use: "CIDRWhitelist", + allow: ["127.0.0.1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], + status: 403, + message: "Forbidden", + }, +] as const; diff --git a/schemas/config/homepage.ts b/schemas/config/homepage.ts new file mode 100644 index 00000000..31f783b9 --- /dev/null +++ b/schemas/config/homepage.ts @@ -0,0 +1,7 @@ +export type HomepageConfig = { + /** + * Use default app categories (uses docker image name) + * @default true + */ + use_default_categories: boolean; +}; diff --git a/schemas/config/notification.ts b/schemas/config/notification.ts new file mode 100644 index 00000000..aa90ffca --- /dev/null +++ b/schemas/config/notification.ts @@ -0,0 +1,67 @@ +import { URL } from "../types"; + +export const NOTIFICATION_PROVIDERS = ["webhook", "gotify"] as const; + +export type NotificationProvider = (typeof NOTIFICATION_PROVIDERS)[number]; + +export type NotificationConfig = { + /* Name of the notification provider */ + name: string; + /* URL of the notification provider */ + url: URL; +}; + +export interface GotifyConfig extends NotificationConfig { + provider: "gotify"; + /* Gotify token */ + token: string; +} + +export const WEBHOOK_TEMPLATES = ["discord"] as const; +export const WEBHOOK_METHODS = ["POST", "GET", "PUT"] as const; +export const WEBHOOK_MIME_TYPES = [ + "application/json", + "application/x-www-form-urlencoded", + "text/plain", +] as const; +export const WEBHOOK_COLOR_MODES = ["hex", "dec"] as const; + +export type WebhookTemplate = (typeof WEBHOOK_TEMPLATES)[number]; +export type WebhookMethod = (typeof WEBHOOK_METHODS)[number]; +export type WebhookMimeType = (typeof WEBHOOK_MIME_TYPES)[number]; +export type WebhookColorMode = (typeof WEBHOOK_COLOR_MODES)[number]; + +export interface WebhookConfig extends NotificationConfig { + provider: "webhook"; + /** + * Webhook template + * + * @default "discord" + */ + template?: WebhookTemplate; + /* Webhook token */ + token?: string; + /** + * Webhook message (usally JSON), + * required when template is not defined + */ + payload?: string; + /** + * Webhook method + * + * @default "POST" + */ + method?: WebhookMethod; + /** + * Webhook mime type + * + * @default "application/json" + */ + mime_type?: WebhookMimeType; + /** + * Webhook color mode + * + * @default "hex" + */ + color_mode?: WebhookColorMode; +} diff --git a/schemas/config/providers.ts b/schemas/config/providers.ts new file mode 100644 index 00000000..e049927c --- /dev/null +++ b/schemas/config/providers.ts @@ -0,0 +1,46 @@ +import { URI, URL } from "../types"; +import { GotifyConfig, WebhookConfig } from "./notification"; + +export type Providers = { + /** List of route definition files to include + * + * @minItems 1 + * @examples require(".").includeExamples + * @items.pattern ^[\w\d\-_]+\.(yaml|yml)$ + */ + include?: URI[]; + /** Name-value mapping of docker hosts to retrieve routes from + * + * @minProperties 1 + * @examples require(".").dockerExamples + * @items.pattern ^((\w+://)[^\s]+)|\$DOCKER_HOST$ + */ + docker?: { [name: string]: URL }; + /** List of notification providers + * + * @minItems 1 + * @examples require(".").notificationExamples + */ + notification?: (WebhookConfig | GotifyConfig)[]; +}; + +export const includeExamples = ["file1.yml", "file2.yml"] as const; +export const dockerExamples = [ + { local: "$DOCKER_HOST" }, + { remote: "tcp://10.0.2.1:2375" }, + { remote2: "ssh://root:1234@10.0.2.2" }, +] as const; +export const notificationExamples = [ + { + name: "gotify", + provider: "gotify", + url: "https://gotify.domain.tld", + token: "abcd", + }, + { + name: "discord", + provider: "webhook", + template: "discord", + url: "https://discord.com/api/webhooks/1234/abcd", + }, +] as const; diff --git a/schemas/docker.ts b/schemas/docker.ts new file mode 100644 index 00000000..80684401 --- /dev/null +++ b/schemas/docker.ts @@ -0,0 +1,7 @@ +import { IdleWatcherConfig } from "./providers/idlewatcher"; +import { Route } from "./providers/routes"; + +//FIXME: fix this +export type DockerRoutes = { + [key: string]: Route & IdleWatcherConfig; +}; diff --git a/schemas/docker_routes.schema.json b/schemas/docker_routes.schema.json new file mode 100644 index 00000000..dd1f1abe --- /dev/null +++ b/schemas/docker_routes.schema.json @@ -0,0 +1,1198 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "access_log": { + "additionalProperties": false, + "description": "Access log config", + "examples": [ + { + "fields": { + "headers": { + "config": { + "foo": "redact" + }, + "default": "keep" + } + }, + "filters": { + "status_codes": { + "values": [ + "200-299" + ] + } + }, + "format": "combined", + "path": "/var/log/access.log" + } + ], + "properties": { + "buffer_size": { + "default": 65536, + "description": "The size of the buffer.", + "minimum": 0, + "type": "integer" + }, + "fields": { + "additionalProperties": false, + "properties": { + "cookie": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "header": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "additionalProperties": false, + "properties": { + "cidr": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "headers": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "host": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "method": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "status_code": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/StatusCodeRange" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + } + }, + "type": "object" + }, + "format": { + "$ref": "#/definitions/AccessLogFormat", + "default": "combined", + "description": "The format of the access log." + }, + "path": { + "format": "uri-reference", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "alias": { + "description": "Alias (subdomain or FDN)", + "minLength": 1, + "type": "string" + }, + "healthcheck": { + "additionalProperties": false, + "description": "Healthcheck config", + "properties": { + "disable": { + "default": false, + "description": "Disable healthcheck", + "type": "boolean" + }, + "interval": { + "default": "5s", + "description": "Healthcheck interval", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "path": { + "default": "/", + "description": "Healthcheck path", + "format": "uri-reference", + "type": "string" + }, + "timeout": { + "default": "5s", + "description": "Healthcheck timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use_get": { + "default": false, + "description": "Use GET instead of HEAD", + "type": "boolean" + } + }, + "type": "object" + }, + "homepage": { + "additionalProperties": false, + "description": "Homepage config", + "examples": [ + { + "category": "Arr suite", + "icon": "png/sonarr.png", + "name": "Sonarr" + }, + { + "icon": "@target/favicon.ico", + "name": "App" + } + ], + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "show": { + "default": true, + "description": "Whether show in dashboard", + "type": "boolean" + }, + "url": { + "format": "uri", + "type": "string" + }, + "widget_config": { + "additionalProperties": {}, + "type": "object" + } + }, + "type": "object" + }, + "host": { + "default": "localhost", + "description": "Proxy host", + "type": "string" + }, + "idle_timeout": { + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "load_balance": { + "$ref": "#/definitions/LoadBalanceConfig", + "description": "Load balance config" + }, + "middlewares": { + "$ref": "#/definitions/MiddlewaresMap", + "description": "Middlewares" + }, + "no_tls_verify": { + "default": false, + "description": "Skip TLS verification", + "type": "boolean" + }, + "path_patterns": { + "description": "Path patterns (only patterns that match will be proxied).\n\nSee https://pkg.go.dev/net/http#hdr-Patterns-ServeMux", + "items": { + "type": "string" + }, + "type": "array" + }, + "port": { + "default": 80, + "description": "Proxy port", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "scheme": { + "$ref": "#/definitions/ProxyScheme", + "default": "http", + "description": "Proxy scheme" + }, + "start_endpoint": { + "format": "uri-reference", + "type": "string" + }, + "stop_method": { + "$ref": "#/definitions/StopMethod", + "default": "stop", + "description": "Stop method" + }, + "stop_signal": { + "$ref": "#/definitions/Signal" + }, + "stop_timeout": { + "default": "10s", + "description": "Stop timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "wake_timeout": { + "default": "30s", + "description": "Wake timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "description": "Alias (subdomain or FDN)", + "minLength": 1, + "type": "string" + }, + "healthcheck": { + "additionalProperties": false, + "description": "Healthcheck config", + "properties": { + "disable": { + "default": false, + "description": "Disable healthcheck", + "type": "boolean" + }, + "interval": { + "default": "5s", + "description": "Healthcheck interval", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "path": { + "default": "/", + "description": "Healthcheck path", + "format": "uri-reference", + "type": "string" + }, + "timeout": { + "default": "5s", + "description": "Healthcheck timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use_get": { + "default": false, + "description": "Use GET instead of HEAD", + "type": "boolean" + } + }, + "type": "object" + }, + "host": { + "default": "localhost", + "description": "Stream host", + "type": "string" + }, + "idle_timeout": { + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "port": { + "pattern": "^\\d+:\\d+$", + "type": "string" + }, + "scheme": { + "$ref": "#/definitions/StreamScheme", + "default": "tcp", + "description": "Stream scheme" + }, + "start_endpoint": { + "format": "uri-reference", + "type": "string" + }, + "stop_method": { + "$ref": "#/definitions/StopMethod", + "default": "stop", + "description": "Stop method" + }, + "stop_signal": { + "$ref": "#/definitions/Signal" + }, + "stop_timeout": { + "default": "10s", + "description": "Stop timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "wake_timeout": { + "default": "30s", + "description": "Wake timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + } + }, + "required": [ + "port", + "scheme" + ], + "type": "object" + } + ] + }, + "definitions": { + "AccessLogFieldMode": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "AccessLogFormat": { + "enum": [ + "combined", + "common", + "json" + ], + "type": "string" + }, + "CIDR": { + "anyOf": [ + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*:.*:.*:.*:.*:.*:.*$", + "type": "string" + }, + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*/[0-9]*$", + "type": "string" + }, + { + "pattern": "^::[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*::/[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*::/[0-9]*$", + "type": "string" + } + ] + }, + "LoadBalanceConfig": { + "additionalProperties": false, + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "round_robin", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link", + "mode" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "least_conn", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link", + "mode" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": false, + "description": "Real IP config, header to get client IP from", + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "string" + } + }, + "required": [ + "from", + "use" + ], + "type": "object" + }, + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "ip_hash", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "config", + "link", + "mode" + ], + "type": "object" + } + ] + }, + "MiddlewaresMap": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "use": { + "pattern": "^.*@file$", + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/{error_page:Omit;errorPage:Omit;ErrorPage:Omit;custom_error_page:Omit;customErrorPage:Omit;CustomErrorPage:Omit;}" + }, + { + "$ref": "#/definitions/{redirect_http:Omit;redirectHTTP:Omit;RedirectHTTP:Omit;}" + }, + { + "$ref": "#/definitions/{set_x_forwarded:Omit;setXForwarded:Omit;SetXForwarded:Omit;}" + }, + { + "$ref": "#/definitions/{hide_x_forwarded:Omit;hideXForwarded:Omit;HideXForwarded:Omit;}" + }, + { + "$ref": "#/definitions/{cidr_whitelist:Omit;cidrWhitelist:Omit;CIDRWhitelist:Omit;}" + }, + { + "$ref": "#/definitions/{cloudflare_real_ip:Omit;cloudflareRealIp:Omit;}" + }, + { + "$ref": "#/definitions/{request:Omit;Request:Omit;modify_request:Omit;modifyRequest:Omit;ModifyRequest:Omit;}" + }, + { + "$ref": "#/definitions/{response:Omit;Response:Omit;modify_response:Omit;modifyResponse:Omit;ModifyResponse:Omit;}" + }, + { + "$ref": "#/definitions/{oidc:Omit;OIDC:Omit;}" + }, + { + "$ref": "#/definitions/{rate_limit:Omit;rateLimit:Omit;RateLimit:Omit;}" + }, + { + "$ref": "#/definitions/{real_ip:Omit;realIP:Omit;RealIP:Omit;}" + }, + { + "$ref": "#/definitions/{[x:`${string}@file`]:NullOrEmptyMap;}" + } + ] + }, + "Omit": { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked (alias of status_code)" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "required": [ + "allow" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "allowed_groups": { + "description": "Allowed groups", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "allowed_users": { + "description": "Allowed users", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "average": { + "description": "Average number of requests allowed in a period", + "type": "number" + }, + "burst": { + "description": "Maximum number of requests allowed in a period", + "type": "number" + }, + "period": { + "default": "1s", + "description": "Duration of the rate limit", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + } + }, + "required": [ + "average", + "burst" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + } + }, + "required": [ + "from" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "ProxyScheme": { + "enum": [ + "http", + "https" + ], + "type": "string" + }, + "Signal": { + "enum": [ + "", + "HUP", + "INT", + "QUIT", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGTERM", + "TERM" + ], + "type": "string" + }, + "StatusCode": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "StatusCodeRange": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "pattern": "^[0-9]*-[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "StopMethod": { + "enum": [ + "kill", + "pause", + "stop" + ], + "type": "string" + }, + "StreamScheme": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + }, + "{[x:`${string}@file`]:NullOrEmptyMap;}": { + "additionalProperties": false, + "type": "object" + }, + "{cidr_whitelist:Omit;cidrWhitelist:Omit;CIDRWhitelist:Omit;}": { + "additionalProperties": false, + "properties": { + "CIDRWhitelist": { + "$ref": "#/definitions/Omit" + }, + "cidrWhitelist": { + "$ref": "#/definitions/Omit" + }, + "cidr_whitelist": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "CIDRWhitelist", + "cidrWhitelist", + "cidr_whitelist" + ], + "type": "object" + }, + "{cloudflare_real_ip:Omit;cloudflareRealIp:Omit;}": { + "additionalProperties": false, + "properties": { + "cloudflareRealIp": { + "$ref": "#/definitions/Omit" + }, + "cloudflare_real_ip": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "cloudflareRealIp", + "cloudflare_real_ip" + ], + "type": "object" + }, + "{error_page:Omit;errorPage:Omit;ErrorPage:Omit;custom_error_page:Omit;customErrorPage:Omit;CustomErrorPage:Omit;}": { + "additionalProperties": false, + "properties": { + "CustomErrorPage": { + "$ref": "#/definitions/Omit" + }, + "ErrorPage": { + "$ref": "#/definitions/Omit" + }, + "customErrorPage": { + "$ref": "#/definitions/Omit" + }, + "custom_error_page": { + "$ref": "#/definitions/Omit" + }, + "errorPage": { + "$ref": "#/definitions/Omit" + }, + "error_page": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "CustomErrorPage", + "ErrorPage", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page" + ], + "type": "object" + }, + "{hide_x_forwarded:Omit;hideXForwarded:Omit;HideXForwarded:Omit;}": { + "additionalProperties": false, + "properties": { + "HideXForwarded": { + "$ref": "#/definitions/Omit" + }, + "hideXForwarded": { + "$ref": "#/definitions/Omit" + }, + "hide_x_forwarded": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "HideXForwarded", + "hideXForwarded", + "hide_x_forwarded" + ], + "type": "object" + }, + "{oidc:Omit;OIDC:Omit;}": { + "additionalProperties": false, + "properties": { + "OIDC": { + "$ref": "#/definitions/Omit" + }, + "oidc": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "OIDC", + "oidc" + ], + "type": "object" + }, + "{rate_limit:Omit;rateLimit:Omit;RateLimit:Omit;}": { + "additionalProperties": false, + "properties": { + "RateLimit": { + "$ref": "#/definitions/Omit" + }, + "rateLimit": { + "$ref": "#/definitions/Omit" + }, + "rate_limit": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RateLimit", + "rateLimit", + "rate_limit" + ], + "type": "object" + }, + "{real_ip:Omit;realIP:Omit;RealIP:Omit;}": { + "additionalProperties": false, + "properties": { + "RealIP": { + "$ref": "#/definitions/Omit" + }, + "realIP": { + "$ref": "#/definitions/Omit" + }, + "real_ip": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "object" + }, + "{redirect_http:Omit;redirectHTTP:Omit;RedirectHTTP:Omit;}": { + "additionalProperties": false, + "properties": { + "RedirectHTTP": { + "$ref": "#/definitions/Omit" + }, + "redirectHTTP": { + "$ref": "#/definitions/Omit" + }, + "redirect_http": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RedirectHTTP", + "redirectHTTP", + "redirect_http" + ], + "type": "object" + }, + "{request:Omit;Request:Omit;modify_request:Omit;modifyRequest:Omit;ModifyRequest:Omit;}": { + "additionalProperties": false, + "properties": { + "ModifyRequest": { + "$ref": "#/definitions/Omit" + }, + "Request": { + "$ref": "#/definitions/Omit" + }, + "modifyRequest": { + "$ref": "#/definitions/Omit" + }, + "modify_request": { + "$ref": "#/definitions/Omit" + }, + "request": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "ModifyRequest", + "Request", + "modifyRequest", + "modify_request", + "request" + ], + "type": "object" + }, + "{response:Omit;Response:Omit;modify_response:Omit;modifyResponse:Omit;ModifyResponse:Omit;}": { + "additionalProperties": false, + "properties": { + "ModifyResponse": { + "$ref": "#/definitions/Omit" + }, + "Response": { + "$ref": "#/definitions/Omit" + }, + "modifyResponse": { + "$ref": "#/definitions/Omit" + }, + "modify_response": { + "$ref": "#/definitions/Omit" + }, + "response": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "ModifyResponse", + "Response", + "modifyResponse", + "modify_response", + "response" + ], + "type": "object" + }, + "{set_x_forwarded:Omit;setXForwarded:Omit;SetXForwarded:Omit;}": { + "additionalProperties": false, + "properties": { + "SetXForwarded": { + "$ref": "#/definitions/Omit" + }, + "setXForwarded": { + "$ref": "#/definitions/Omit" + }, + "set_x_forwarded": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "SetXForwarded", + "setXForwarded", + "set_x_forwarded" + ], + "type": "object" + } + }, + "type": "object" +} + diff --git a/schemas/middleware_compose.schema.json b/schemas/middleware_compose.schema.json new file mode 100644 index 00000000..557f7a8a --- /dev/null +++ b/schemas/middleware_compose.schema.json @@ -0,0 +1,364 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CIDR": { + "anyOf": [ + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*:.*:.*:.*:.*:.*:.*$", + "type": "string" + }, + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*/[0-9]*$", + "type": "string" + }, + { + "pattern": "^::[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*::/[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*::/[0-9]*$", + "type": "string" + } + ] + }, + "MiddlewareComposeMap": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "CustomErrorPage", + "ErrorPage", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "RedirectHTTP", + "redirectHTTP", + "redirect_http" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "SetXForwarded", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "use": { + "enum": [ + "HideXForwarded", + "hideXForwarded", + "hide_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked (alias of status_code)" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + }, + "use": { + "enum": [ + "CIDRWhitelist", + "cidrWhitelist", + "cidr_whitelist" + ], + "type": "string" + } + }, + "required": [ + "allow", + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "cloudflareRealIp", + "cloudflare_real_ip" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + }, + "use": { + "enum": [ + "ModifyRequest", + "Request", + "modifyRequest", + "modify_request", + "request" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + }, + "use": { + "enum": [ + "ModifyResponse", + "Response", + "modifyResponse", + "modify_response", + "response" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "allowed_groups": { + "description": "Allowed groups", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "allowed_users": { + "description": "Allowed users", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "use": { + "enum": [ + "OIDC", + "oidc" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "average": { + "description": "Average number of requests allowed in a period", + "type": "number" + }, + "burst": { + "description": "Maximum number of requests allowed in a period", + "type": "number" + }, + "period": { + "default": "1s", + "description": "Duration of the rate limit", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use": { + "enum": [ + "RateLimit", + "rateLimit", + "rate_limit" + ], + "type": "string" + } + }, + "required": [ + "average", + "burst", + "use" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "string" + } + }, + "required": [ + "from", + "use" + ], + "type": "object" + } + ] + }, + "StatusCode": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "items": { + "$ref": "#/definitions/MiddlewareComposeMap" + }, + "type": "array" +} + diff --git a/schemas/middlewares/middleware_compose.ts b/schemas/middlewares/middleware_compose.ts new file mode 100644 index 00000000..0bfae2d3 --- /dev/null +++ b/schemas/middlewares/middleware_compose.ts @@ -0,0 +1,3 @@ +import { MiddlewareComposeMap } from "./middlewares"; + +export type MiddlewareCompose = MiddlewareComposeMap[]; \ No newline at end of file diff --git a/schemas/middlewares/middlewares.ts b/schemas/middlewares/middlewares.ts new file mode 100644 index 00000000..483fde2d --- /dev/null +++ b/schemas/middlewares/middlewares.ts @@ -0,0 +1,149 @@ +import * as types from "../types"; + +export type MiddlewareComposeObjectRef = `${string}@file`; + +export type KeyOptMapping = { + [key in T["use"]]: Omit; +} | { use: MiddlewareComposeObjectRef }; + +export type MiddlewaresMap = ( + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | KeyOptMapping + | { [key in MiddlewareComposeObjectRef]: types.NullOrEmptyMap } +); + +export type MiddlewareComposeMap = ( + | CustomErrorPage + | RedirectHTTP + | SetXForwarded + | HideXForwarded + | CIDRWhitelist + | CloudflareRealIP + | ModifyRequest + | ModifyResponse + | OIDC + | RateLimit + | RealIP +); + +export type CustomErrorPage = { + use: "error_page" | "errorPage" | "ErrorPage" | "custom_error_page" | "customErrorPage" | "CustomErrorPage"; +}; + +export type RedirectHTTP = { + use: "redirect_http" | "redirectHTTP" | "RedirectHTTP"; +}; + +export type SetXForwarded = { + use: "set_x_forwarded" | "setXForwarded" | "SetXForwarded"; +}; +export type HideXForwarded = { + use: "hide_x_forwarded" | "hideXForwarded" | "HideXForwarded"; +}; + +export type CIDRWhitelist = { + use: "cidr_whitelist" | "cidrWhitelist" | "CIDRWhitelist"; + /* Allowed CIDRs/IPs */ + allow: types.CIDR[]; + /** HTTP status code when blocked + * + * @default 403 + */ + status_code?: types.StatusCode; + /** HTTP status code when blocked (alias of status_code) + * + * @default 403 + */ + status?: types.StatusCode; + /** Error message when blocked + * + * @default "IP not allowed" + */ + message?: string; +}; + +export type CloudflareRealIP = { + use: "cloudflare_real_ip" | "cloudflareRealIp" | "cloudflare_real_ip"; + /** Recursively resolve the IP + * + * @default false + */ + recursive?: boolean; +}; + +export type ModifyRequest = { + use: "request" | "Request" | "modify_request" | "modifyRequest" | "ModifyRequest"; + /** Set HTTP headers */ + set_headers?: { [key: types.HTTPHeader]: string }; + /** Add HTTP headers */ + add_headers?: { [key: types.HTTPHeader]: string }; + /** Hide HTTP headers */ + hide_headers?: types.HTTPHeader[]; +}; + +export type ModifyResponse = { + use: "response" | "Response" | "modify_response" | "modifyResponse" | "ModifyResponse"; + /** Set HTTP headers */ + set_headers?: { [key: types.HTTPHeader]: string }; + /** Add HTTP headers */ + add_headers?: { [key: types.HTTPHeader]: string }; + /** Hide HTTP headers */ + hide_headers?: types.HTTPHeader[]; +}; + +export type OIDC = { + use: "oidc" | "OIDC"; + /** Allowed users + * + * @minItems 1 + */ + allowed_users?: string[]; + /** Allowed groups + * + * @minItems 1 + */ + allowed_groups?: string[]; +}; + +export type RateLimit = { + use: "rate_limit" | "rateLimit" | "RateLimit"; + /** Average number of requests allowed in a period + * + * @min 1 + */ + average: number; + /** Maximum number of requests allowed in a period + * + * @min 1 + */ + burst: number; + /** Duration of the rate limit + * + * @default 1s + */ + period?: types.Duration; +}; + +export type RealIP = { + use: "real_ip" | "realIP" | "RealIP"; + /** Header to get the client IP from + * + * @default "X-Real-IP" + */ + header?: types.HTTPHeader; + from: types.CIDR[]; + /** Recursive resolve the IP + * + * @default false + */ + recursive?: boolean; +}; diff --git a/schemas/providers/healthcheck.ts b/schemas/providers/healthcheck.ts new file mode 100644 index 00000000..fc2853ed --- /dev/null +++ b/schemas/providers/healthcheck.ts @@ -0,0 +1,33 @@ +import { Duration, URI } from "../types"; + +/** + * @additionalProperties false + */ +export type HealthcheckConfig = { + /** Disable healthcheck + * + * @default false + */ + disable?: boolean; + /** Healthcheck path + * + * @default / + */ + path?: URI; + /** + * Use GET instead of HEAD + * + * @default false + */ + use_get?: boolean; + /** Healthcheck interval + * + * @default 5s + */ + interval?: Duration; + /** Healthcheck timeout + * + * @default 5s + */ + timeout?: Duration; +}; diff --git a/schemas/providers/homepage.ts b/schemas/providers/homepage.ts new file mode 100644 index 00000000..20e499c3 --- /dev/null +++ b/schemas/providers/homepage.ts @@ -0,0 +1,36 @@ +import { URL } from "../types"; + +/** + * @additionalProperties false + */ +export type HomepageConfig = { + /** Whether show in dashboard + * + * @default true + */ + show?: boolean; + /* Display name on dashboard */ + name?: string; + /* Display icon on dashboard */ + icon?: URL | WalkxcodeIcon | TargetRelativeIconPath; + /* App description */ + description?: string; + /* Override url */ + url?: URL; + /* App category */ + category?: string; + /* Widget config */ + widget_config?: { + [key: string]: any; + }; +}; + +/** + * @pattern ^(png|svg|webp)\\/[\\w\\d\\-_]+\\.\\1$ + */ +export type WalkxcodeIcon = string; + +/** + * @pattern ^@target/.+$ + */ +export type TargetRelativeIconPath = string; diff --git a/schemas/providers/idlewatcher.ts b/schemas/providers/idlewatcher.ts new file mode 100644 index 00000000..df24bbfa --- /dev/null +++ b/schemas/providers/idlewatcher.ts @@ -0,0 +1,41 @@ +import { Duration, URI } from "../types"; + +export const STOP_METHODS = ["pause", "stop", "kill"] as const; +export type StopMethod = (typeof STOP_METHODS)[number]; + +export const STOP_SIGNALS = [ + "", + "SIGINT", + "SIGTERM", + "SIGHUP", + "SIGQUIT", + "INT", + "TERM", + "HUP", + "QUIT", +] as const; +export type Signal = (typeof STOP_SIGNALS)[number]; + +export type IdleWatcherConfig = { + /* Idle timeout */ + idle_timeout?: Duration; + /** Wake timeout + * + * @default 30s + */ + wake_timeout?: Duration; + /** Stop timeout + * + * @default 10s + */ + stop_timeout?: Duration; + /** Stop method + * + * @default stop + */ + stop_method?: StopMethod; + /* Stop signal */ + stop_signal?: Signal; + /* Start endpoint (any path can wake the container if not specified) */ + start_endpoint?: URI; +}; diff --git a/schemas/providers/loadbalance.ts b/schemas/providers/loadbalance.ts new file mode 100644 index 00000000..4d29fdf3 --- /dev/null +++ b/schemas/providers/loadbalance.ts @@ -0,0 +1,44 @@ +import { RealIP } from "../middlewares/middlewares"; + +export const LOAD_BALANCE_MODES = [ + "round_robin", + "least_conn", + "ip_hash", +] as const; +export type LoadBalanceMode = (typeof LOAD_BALANCE_MODES)[number]; + +export type LoadBalanceConfigBase = { + /** Alias (subdomain or FDN) of load-balancer + * + * @minLength 1 + */ + link: string; + /** Load-balance weight (reserved for future use) + * + * @minimum 0 + * @maximum 100 + */ + weight?: number; +}; + +export type LoadBalanceConfig = LoadBalanceConfigBase & + ( + | {} // linking other routes + | RoundRobinLoadBalanceConfig + | LeastConnLoadBalanceConfig + | IPHashLoadBalanceConfig + ); + +export type IPHashLoadBalanceConfig = { + mode: "ip_hash"; + /** Real IP config, header to get client IP from */ + config: RealIP; +}; + +export type LeastConnLoadBalanceConfig = { + mode: "least_conn"; +}; + +export type RoundRobinLoadBalanceConfig = { + mode: "round_robin"; +}; diff --git a/schemas/providers/routes.ts b/schemas/providers/routes.ts new file mode 100644 index 00000000..ac5d969d --- /dev/null +++ b/schemas/providers/routes.ts @@ -0,0 +1,114 @@ +import { AccessLogConfig } from "../config/access_log"; +import { accessLogExamples } from "../config/entrypoint"; +import { MiddlewaresMap } from "../middlewares/middlewares"; +import { Hostname, IPv4, IPv6, PathPattern, Port, StreamPort } from "../types"; +import { HealthcheckConfig } from "./healthcheck"; +import { HomepageConfig } from "./homepage"; +import { LoadBalanceConfig } from "./loadbalance"; +export const PROXY_SCHEMES = ["http", "https"] as const; +export const STREAM_SCHEMES = ["tcp", "udp"] as const; + +export type ProxyScheme = (typeof PROXY_SCHEMES)[number]; +export type StreamScheme = (typeof STREAM_SCHEMES)[number]; + +export type Route = ReverseProxyRoute | StreamRoute; +export type Routes = { + [key: string]: Route; +}; + +export type ReverseProxyRoute = { + /** Alias (subdomain or FDN) + * @minLength 1 + */ + alias?: string; + /** Proxy scheme + * + * @default http + */ + scheme?: ProxyScheme; + /** Proxy host + * + * @default localhost + */ + host?: Hostname | IPv4 | IPv6; + /** Proxy port + * + * @default 80 + */ + port?: Port; + /** Skip TLS verification + * + * @default false + */ + no_tls_verify?: boolean; + /** Path patterns (only patterns that match will be proxied). + * + * See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux + */ + path_patterns?: PathPattern[]; + /** Healthcheck config */ + healthcheck?: HealthcheckConfig; + /** Load balance config */ + load_balance?: LoadBalanceConfig; + /** Middlewares */ + middlewares?: MiddlewaresMap; + /** Homepage config + * + * @examples require(".").homepageExamples + */ + homepage?: HomepageConfig; + /** Access log config + * + * @examples require(".").accessLogExamples + */ + access_log?: AccessLogConfig; +}; + +export type StreamRoute = { + /** Alias (subdomain or FDN) + * @minLength 1 + */ + alias?: string; + /** Stream scheme + * + * @default tcp + */ + scheme: StreamScheme; + /** Stream host + * + * @default localhost + */ + host?: Hostname | IPv4 | IPv6; + /* Stream port */ + port: StreamPort; + /** Healthcheck config */ + healthcheck?: HealthcheckConfig; +}; + +export const homepageExamples = [ + { + name: "Sonarr", + icon: "png/sonarr.png", + category: "Arr suite", + }, + { + name: "App", + icon: "@target/favicon.ico", + }, +]; + +export const loadBalanceExamples = [ + { + link: "flaresolverr", + mode: "round_robin", + }, + { + link: "service.domain.com", + mode: "ip_hash", + config: { + header: "X-Real-IP", + }, + }, +]; + +export { accessLogExamples }; diff --git a/schemas/routes.schema.json b/schemas/routes.schema.json new file mode 100644 index 00000000..e533ef26 --- /dev/null +++ b/schemas/routes.schema.json @@ -0,0 +1,1123 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": { + "$ref": "#/definitions/Route" + }, + "definitions": { + "AccessLogFieldMode": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "AccessLogFormat": { + "enum": [ + "combined", + "common", + "json" + ], + "type": "string" + }, + "CIDR": { + "anyOf": [ + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*:.*:.*:.*:.*:.*:.*$", + "type": "string" + }, + { + "pattern": "^[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*/[0-9]*$", + "type": "string" + }, + { + "pattern": "^::[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*::/[0-9]*$", + "type": "string" + }, + { + "pattern": "^.*:.*::/[0-9]*$", + "type": "string" + } + ] + }, + "LoadBalanceConfig": { + "additionalProperties": false, + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "round_robin", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link", + "mode" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "least_conn", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link", + "mode" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": false, + "description": "Real IP config, header to get client IP from", + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + }, + "use": { + "enum": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "string" + } + }, + "required": [ + "from", + "use" + ], + "type": "object" + }, + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "mode": { + "const": "ip_hash", + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "config", + "link", + "mode" + ], + "type": "object" + } + ] + }, + "MiddlewaresMap": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "use": { + "pattern": "^.*@file$", + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/{error_page:Omit;errorPage:Omit;ErrorPage:Omit;custom_error_page:Omit;customErrorPage:Omit;CustomErrorPage:Omit;}" + }, + { + "$ref": "#/definitions/{redirect_http:Omit;redirectHTTP:Omit;RedirectHTTP:Omit;}" + }, + { + "$ref": "#/definitions/{set_x_forwarded:Omit;setXForwarded:Omit;SetXForwarded:Omit;}" + }, + { + "$ref": "#/definitions/{hide_x_forwarded:Omit;hideXForwarded:Omit;HideXForwarded:Omit;}" + }, + { + "$ref": "#/definitions/{cidr_whitelist:Omit;cidrWhitelist:Omit;CIDRWhitelist:Omit;}" + }, + { + "$ref": "#/definitions/{cloudflare_real_ip:Omit;cloudflareRealIp:Omit;}" + }, + { + "$ref": "#/definitions/{request:Omit;Request:Omit;modify_request:Omit;modifyRequest:Omit;ModifyRequest:Omit;}" + }, + { + "$ref": "#/definitions/{response:Omit;Response:Omit;modify_response:Omit;modifyResponse:Omit;ModifyResponse:Omit;}" + }, + { + "$ref": "#/definitions/{oidc:Omit;OIDC:Omit;}" + }, + { + "$ref": "#/definitions/{rate_limit:Omit;rateLimit:Omit;RateLimit:Omit;}" + }, + { + "$ref": "#/definitions/{real_ip:Omit;realIP:Omit;RealIP:Omit;}" + }, + { + "$ref": "#/definitions/{[x:`${string}@file`]:NullOrEmptyMap;}" + } + ] + }, + "Omit": { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked (alias of status_code)" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "required": [ + "allow" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "add_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Add HTTP headers", + "type": "object" + }, + "hide_headers": { + "description": "Hide HTTP headers", + "items": { + "type": "string" + }, + "type": "array" + }, + "set_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Set HTTP headers", + "type": "object" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "allowed_groups": { + "description": "Allowed groups", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "allowed_users": { + "description": "Allowed users", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "average": { + "description": "Average number of requests allowed in a period", + "type": "number" + }, + "burst": { + "description": "Maximum number of requests allowed in a period", + "type": "number" + }, + "period": { + "default": "1s", + "description": "Duration of the rate limit", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + } + }, + "required": [ + "average", + "burst" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "default": "X-Real-IP", + "description": "Header to get the client IP from", + "pattern": "^[a-zA-Z0-9\\-]+$", + "type": "string" + }, + "recursive": { + "default": false, + "description": "Recursive resolve the IP", + "type": "boolean" + } + }, + "required": [ + "from" + ], + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "Omit": { + "additionalProperties": false, + "type": "object" + }, + "ProxyScheme": { + "enum": [ + "http", + "https" + ], + "type": "string" + }, + "Route": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "access_log": { + "additionalProperties": false, + "description": "Access log config", + "examples": [ + { + "fields": { + "headers": { + "config": { + "foo": "redact" + }, + "default": "keep" + } + }, + "filters": { + "status_codes": { + "values": [ + "200-299" + ] + } + }, + "format": "combined", + "path": "/var/log/access.log" + } + ], + "properties": { + "buffer_size": { + "default": 65536, + "description": "The size of the buffer.", + "minimum": 0, + "type": "integer" + }, + "fields": { + "additionalProperties": false, + "properties": { + "cookie": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "header": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "additionalProperties": false, + "properties": { + "cidr": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "headers": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "host": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "method": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "status_code": { + "additionalProperties": false, + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/StatusCodeRange" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + } + }, + "type": "object" + }, + "format": { + "$ref": "#/definitions/AccessLogFormat", + "default": "combined", + "description": "The format of the access log." + }, + "path": { + "format": "uri-reference", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "alias": { + "description": "Alias (subdomain or FDN)", + "minLength": 1, + "type": "string" + }, + "healthcheck": { + "additionalProperties": false, + "description": "Healthcheck config", + "properties": { + "disable": { + "default": false, + "description": "Disable healthcheck", + "type": "boolean" + }, + "interval": { + "default": "5s", + "description": "Healthcheck interval", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "path": { + "default": "/", + "description": "Healthcheck path", + "format": "uri-reference", + "type": "string" + }, + "timeout": { + "default": "5s", + "description": "Healthcheck timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use_get": { + "default": false, + "description": "Use GET instead of HEAD", + "type": "boolean" + } + }, + "type": "object" + }, + "homepage": { + "additionalProperties": false, + "description": "Homepage config", + "examples": [ + { + "category": "Arr suite", + "icon": "png/sonarr.png", + "name": "Sonarr" + }, + { + "icon": "@target/favicon.ico", + "name": "App" + } + ], + "properties": { + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "show": { + "default": true, + "description": "Whether show in dashboard", + "type": "boolean" + }, + "url": { + "format": "uri", + "type": "string" + }, + "widget_config": { + "additionalProperties": {}, + "type": "object" + } + }, + "type": "object" + }, + "host": { + "default": "localhost", + "description": "Proxy host", + "type": "string" + }, + "load_balance": { + "$ref": "#/definitions/LoadBalanceConfig", + "description": "Load balance config" + }, + "middlewares": { + "$ref": "#/definitions/MiddlewaresMap", + "description": "Middlewares" + }, + "no_tls_verify": { + "default": false, + "description": "Skip TLS verification", + "type": "boolean" + }, + "path_patterns": { + "description": "Path patterns (only patterns that match will be proxied).\n\nSee https://pkg.go.dev/net/http#hdr-Patterns-ServeMux", + "items": { + "type": "string" + }, + "type": "array" + }, + "port": { + "default": 80, + "description": "Proxy port", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "scheme": { + "$ref": "#/definitions/ProxyScheme", + "default": "http", + "description": "Proxy scheme" + } + }, + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "description": "Alias (subdomain or FDN)", + "minLength": 1, + "type": "string" + }, + "healthcheck": { + "additionalProperties": false, + "description": "Healthcheck config", + "properties": { + "disable": { + "default": false, + "description": "Disable healthcheck", + "type": "boolean" + }, + "interval": { + "default": "5s", + "description": "Healthcheck interval", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "path": { + "default": "/", + "description": "Healthcheck path", + "format": "uri-reference", + "type": "string" + }, + "timeout": { + "default": "5s", + "description": "Healthcheck timeout", + "pattern": "^([0-9]+(ms|s|m|h))+$", + "type": "string" + }, + "use_get": { + "default": false, + "description": "Use GET instead of HEAD", + "type": "boolean" + } + }, + "type": "object" + }, + "host": { + "default": "localhost", + "description": "Stream host", + "type": "string" + }, + "port": { + "pattern": "^\\d+:\\d+$", + "type": "string" + }, + "scheme": { + "$ref": "#/definitions/StreamScheme", + "default": "tcp", + "description": "Stream scheme" + } + }, + "required": [ + "port", + "scheme" + ], + "type": "object" + } + ] + }, + "StatusCode": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "StatusCodeRange": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "pattern": "^[0-9]*-[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + }, + "StreamScheme": { + "enum": [ + "tcp", + "udp" + ], + "type": "string" + }, + "{[x:`${string}@file`]:NullOrEmptyMap;}": { + "additionalProperties": false, + "type": "object" + }, + "{cidr_whitelist:Omit;cidrWhitelist:Omit;CIDRWhitelist:Omit;}": { + "additionalProperties": false, + "properties": { + "CIDRWhitelist": { + "$ref": "#/definitions/Omit" + }, + "cidrWhitelist": { + "$ref": "#/definitions/Omit" + }, + "cidr_whitelist": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "CIDRWhitelist", + "cidrWhitelist", + "cidr_whitelist" + ], + "type": "object" + }, + "{cloudflare_real_ip:Omit;cloudflareRealIp:Omit;}": { + "additionalProperties": false, + "properties": { + "cloudflareRealIp": { + "$ref": "#/definitions/Omit" + }, + "cloudflare_real_ip": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "cloudflareRealIp", + "cloudflare_real_ip" + ], + "type": "object" + }, + "{error_page:Omit;errorPage:Omit;ErrorPage:Omit;custom_error_page:Omit;customErrorPage:Omit;CustomErrorPage:Omit;}": { + "additionalProperties": false, + "properties": { + "CustomErrorPage": { + "$ref": "#/definitions/Omit" + }, + "ErrorPage": { + "$ref": "#/definitions/Omit" + }, + "customErrorPage": { + "$ref": "#/definitions/Omit" + }, + "custom_error_page": { + "$ref": "#/definitions/Omit" + }, + "errorPage": { + "$ref": "#/definitions/Omit" + }, + "error_page": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "CustomErrorPage", + "ErrorPage", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page" + ], + "type": "object" + }, + "{hide_x_forwarded:Omit;hideXForwarded:Omit;HideXForwarded:Omit;}": { + "additionalProperties": false, + "properties": { + "HideXForwarded": { + "$ref": "#/definitions/Omit" + }, + "hideXForwarded": { + "$ref": "#/definitions/Omit" + }, + "hide_x_forwarded": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "HideXForwarded", + "hideXForwarded", + "hide_x_forwarded" + ], + "type": "object" + }, + "{oidc:Omit;OIDC:Omit;}": { + "additionalProperties": false, + "properties": { + "OIDC": { + "$ref": "#/definitions/Omit" + }, + "oidc": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "OIDC", + "oidc" + ], + "type": "object" + }, + "{rate_limit:Omit;rateLimit:Omit;RateLimit:Omit;}": { + "additionalProperties": false, + "properties": { + "RateLimit": { + "$ref": "#/definitions/Omit" + }, + "rateLimit": { + "$ref": "#/definitions/Omit" + }, + "rate_limit": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RateLimit", + "rateLimit", + "rate_limit" + ], + "type": "object" + }, + "{real_ip:Omit;realIP:Omit;RealIP:Omit;}": { + "additionalProperties": false, + "properties": { + "RealIP": { + "$ref": "#/definitions/Omit" + }, + "realIP": { + "$ref": "#/definitions/Omit" + }, + "real_ip": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RealIP", + "realIP", + "real_ip" + ], + "type": "object" + }, + "{redirect_http:Omit;redirectHTTP:Omit;RedirectHTTP:Omit;}": { + "additionalProperties": false, + "properties": { + "RedirectHTTP": { + "$ref": "#/definitions/Omit" + }, + "redirectHTTP": { + "$ref": "#/definitions/Omit" + }, + "redirect_http": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "RedirectHTTP", + "redirectHTTP", + "redirect_http" + ], + "type": "object" + }, + "{request:Omit;Request:Omit;modify_request:Omit;modifyRequest:Omit;ModifyRequest:Omit;}": { + "additionalProperties": false, + "properties": { + "ModifyRequest": { + "$ref": "#/definitions/Omit" + }, + "Request": { + "$ref": "#/definitions/Omit" + }, + "modifyRequest": { + "$ref": "#/definitions/Omit" + }, + "modify_request": { + "$ref": "#/definitions/Omit" + }, + "request": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "ModifyRequest", + "Request", + "modifyRequest", + "modify_request", + "request" + ], + "type": "object" + }, + "{response:Omit;Response:Omit;modify_response:Omit;modifyResponse:Omit;ModifyResponse:Omit;}": { + "additionalProperties": false, + "properties": { + "ModifyResponse": { + "$ref": "#/definitions/Omit" + }, + "Response": { + "$ref": "#/definitions/Omit" + }, + "modifyResponse": { + "$ref": "#/definitions/Omit" + }, + "modify_response": { + "$ref": "#/definitions/Omit" + }, + "response": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "ModifyResponse", + "Response", + "modifyResponse", + "modify_response", + "response" + ], + "type": "object" + }, + "{set_x_forwarded:Omit;setXForwarded:Omit;SetXForwarded:Omit;}": { + "additionalProperties": false, + "properties": { + "SetXForwarded": { + "$ref": "#/definitions/Omit" + }, + "setXForwarded": { + "$ref": "#/definitions/Omit" + }, + "set_x_forwarded": { + "$ref": "#/definitions/Omit" + } + }, + "required": [ + "SetXForwarded", + "setXForwarded", + "set_x_forwarded" + ], + "type": "object" + } + }, + "type": "object" +} + diff --git a/schemas/types.ts b/schemas/types.ts new file mode 100644 index 00000000..2ed18d57 --- /dev/null +++ b/schemas/types.ts @@ -0,0 +1,111 @@ +/** + * @type "null" + */ +export interface Null {} +export type Nullable = T | Null; +export type NullOrEmptyMap = {} | Null; + +export const HTTP_METHODS = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE", +] as const; + +export type HTTPMethod = (typeof HTTP_METHODS)[number]; +/** + * HTTP Header + * @pattern ^[a-zA-Z0-9\-]+$ + */ +export type HTTPHeader = string; + +/** + * HTTP Query + * @pattern ^[a-zA-Z0-9\-_]+$ + */ +export type HTTPQuery = string; +/** + * HTTP Cookie + * @pattern ^[a-zA-Z0-9\-_]+$ + */ +export type HTTPCookie = string; + +export type StatusCode = number | `${number}`; +export type StatusCodeRange = number | `${number}` | `${number}-${number}`; + +/** + * @items.pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + */ +export type DomainNames = string[]; +/** + * @items.pattern ^(\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$ + */ +export type DomainOrWildcards = string[]; +/** + * @format hostname + */ +export type Hostname = string; +/** + * @format ipv4 + */ +export type IPv4 = string; +/** + * @format ipv6 + */ +export type IPv6 = string; + +/* CIDR / IPv4 / IPv6 */ +export type CIDR = + | `${number}.${number}.${number}.${number}` + | `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}` + | `${number}.${number}.${number}.${number}/${number}` + | `::${number}` + | `${string}::/${number}` + | `${string}:${string}::/${number}`; + +/** + * @type integer + * @minimum 0 + * @maximum 65535 + */ +export type Port = number; + +/** + * @pattern ^\d+:\d+$ + */ +export type StreamPort = string; + +/** + * @format email + */ +export type Email = string; + +/** + * @format uri + */ +export type URL = string; + +/** + * @format uri-reference + */ +export type URI = string; + +/** + * @pattern ^(?:([A-Z]+) )?(?:([a-zA-Z0-9.-]+)\\/)?(\\/[^\\s]*)$ + */ +export type PathPattern = string; + +/** + * @pattern ^([0-9]+(ms|s|m|h))+$ + */ +export type Duration = string; + +/** + * @format date-time + */ +export type DateTime = string;