diff --git a/.gitignore b/.gitignore index b579b1f..d3c2ff1 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 02f732c..628682b 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 864e79f..a605cbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 /app/schemas/ ENV DOCKER_HOST=unix:///var/run/docker.sock ENV GODOXY_DEBUG=0 diff --git a/Makefile b/Makefile index 94d0ce9..fe6cef7 100755 --- a/Makefile +++ b/Makefile @@ -70,4 +70,13 @@ 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: + typescript-json-schema --required --constAsEnum --tsNodeRegister=true -o schemas/config.schema.json schemas/config/config.ts Config + typescript-json-schema --required --constAsEnum --tsNodeRegister=true -o schemas/routes.schema.json schemas/providers/routes.ts Routes + typescript-json-schema --required --constAsEnum --tsNodeRegister=true -o schemas/middleware_compose.schema.json schemas/middlewares/middleware_compose.ts MiddlewareComposeConfig + # typescript-json-schema --required --constAsEnum --tsNodeRegister=true -o schemas/docker_routes.schema.json schemas/docker.ts DockerRoutes + +push-github: + git push origin $(shell git rev-parse --abbrev-ref HEAD) \ No newline at end of file diff --git a/internal/api/v1/schema.go b/internal/api/v1/schema.go index 86b732d..cf5bffe 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 c637a17..9a3d167 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, } diff --git a/internal/docker/container_test.go b/internal/docker/container_test.go new file mode 100644 index 0000000..4ecd475 --- /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/route/provider/all_fields.yaml b/internal/route/provider/all_fields.yaml index 1d812b4..7af53fb 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 e19ec13..0000000 --- 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 c01a938..0000000 --- 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 bf123ab..0000000 --- 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 0000000..d671eeb --- /dev/null +++ b/schemas/config.schema.json @@ -0,0 +1,1595 @@ +{ + "$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/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": { + "properties": { + "client_id": { + "type": "string" + }, + "email": { + "format": "email", + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": [ + "client_id", + "email", + "password" + ], + "type": "object" + }, + "provider": { + "enum": [ + "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": { + "properties": { + "auth_token": { + "type": "string" + } + }, + "required": [ + "auth_token" + ], + "type": "object" + }, + "provider": { + "enum": [ + "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": { + "properties": { + "token": { + "type": "string" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "provider": { + "enum": [ + "duckdns" + ], + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "GotifyConfig": { + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the notification provider", + "type": "string" + }, + "provider": { + "enum": [ + "gotify" + ], + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "description": "URL of the notification provider", + "format": "uri", + "type": "string" + } + }, + "required": [ + "name", + "provider", + "token", + "url" + ], + "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": { + "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": { + "enum": [ + "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": { + "properties": { + "api_endpoint": { + "$ref": "#/definitions/OVHEndpoint" + }, + "application_secret": { + "type": "string" + }, + "consumer_key": { + "type": "string" + }, + "oauth2_config": { + "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": { + "enum": [ + "ovh" + ], + "type": "string" + } + }, + "required": [ + "domains", + "email", + "options", + "provider" + ], + "type": "object" + }, + "Partial": { + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "type": "object" + }, + "Partial": { + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "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" + }, + "Partial": { + "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" + }, + "Partial": { + "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" + } + }, + "type": "object" + }, + "Partial": { + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "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" + } + }, + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "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": { + "description": "Name of the notification provider", + "type": "string" + }, + "payload": { + "description": "Webhook message (usally JSON),\nrequired when template is not defined", + "type": "string" + }, + "provider": { + "enum": [ + "webhook" + ], + "type": "string" + }, + "template": { + "default": "discord", + "description": "Webhook template", + "enum": [ + "discord" + ], + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "description": "URL of the notification provider", + "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": { + "properties": { + "cookie": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "header": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "query": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "properties": { + "cidr": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "headers": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "host": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "method": { + "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": { + "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": { + "additionalProperties": false, + "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": { + "anyOf": [ + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + } + ] + }, + "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 0000000..63e3071 --- /dev/null +++ b/schemas/config/access_log.ts @@ -0,0 +1,69 @@ +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]; + +/** + * @additionalProperties false + */ +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 0000000..aed8bfa --- /dev/null +++ b/schemas/config/autocert.ts @@ -0,0 +1,103 @@ +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 = + | 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; +} + +/** + * @additionalProperties false + */ +export interface CloudflareOptions extends AutocertConfigBase { + provider: "cloudflare"; + options: { auth_token: string }; +} + +/** + * @additionalProperties false + */ +export interface CloudDNSOptions extends AutocertConfigBase { + provider: "clouddns"; + options: { + client_id: string; + /** + * @format email + */ + email: Email; + password: string; + }; +} + +/** + * @additionalProperties false + */ +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]; + +/** + * @additionalProperties false + */ +export interface OVHOptionsWithAppKey extends AutocertConfigBase { + provider: "ovh"; + options: { + application_secret: string; + consumer_key: string; + api_endpoint?: OVHEndpoint; + application_key: string; + }; +} + +/** + * @additionalProperties false + */ +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 0000000..b4de625 --- /dev/null +++ b/schemas/config/config.ts @@ -0,0 +1,55 @@ +import { DomainNames } from "../types"; +import { AutocertConfig } from "./autocert"; +import { EntrypointConfig } from "./entrypoint"; +import { HomepageConfig } from "./homepage"; +import { Providers } from "./providers"; + +/** + * @additionalProperties false + */ +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 0000000..f30b796 --- /dev/null +++ b/schemas/config/entrypoint.ts @@ -0,0 +1,50 @@ +import { MiddlewareComposeConfig } from "../middlewares/middleware_compose"; +import { AccessLogConfig } from "./access_log"; + +/** + * @additionalProperties false + */ +export type EntrypointConfig = { + /** Entrypoint middleware configuration + * + * @examples require(".").middlewaresExamples + */ + middlewares: MiddlewareComposeConfig; + /** 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 0000000..40ec2c8 --- /dev/null +++ b/schemas/config/homepage.ts @@ -0,0 +1,10 @@ +/** + * @additionalProperties false + */ +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 0000000..10bc1cf --- /dev/null +++ b/schemas/config/notification.ts @@ -0,0 +1,77 @@ +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; +}; + +/** + * @additionalProperties false + */ +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]; + +/** + * @additionalProperties false + */ +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 0000000..cd3e296 --- /dev/null +++ b/schemas/config/providers.ts @@ -0,0 +1,49 @@ +import { URI, URL } from "../types"; +import { GotifyConfig, WebhookConfig } from "./notification"; + +/** + * @additionalProperties false + */ +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 0000000..8068440 --- /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/middleware_compose.schema.json b/schemas/middleware_compose.schema.json new file mode 100644 index 0000000..1b143e8 --- /dev/null +++ b/schemas/middleware_compose.schema.json @@ -0,0 +1,761 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "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" + } + ] + }, + "Partial": { + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "type": "object" + }, + "Partial": { + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "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" + }, + "Partial": { + "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" + }, + "Partial": { + "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" + } + }, + "type": "object" + }, + "Partial": { + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "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" + } + }, + "type": "object" + }, + "Partial": { + "type": "object" + }, + "Partial": { + "type": "object" + }, + "StatusCode": { + "anyOf": [ + { + "pattern": "^[0-9]*$", + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "items": { + "anyOf": [ + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + }, + { + "allOf": [ + { + "properties": { + "use": { + "enum": [ + "CIDRWhitelist", + "CloudflareRealIP", + "CustomErrorPage", + "HideXForwarded", + "ModifyRequest", + "ModifyResponse", + "OIDC", + "RateLimit", + "RealIP", + "RedirectHTTP", + "Request", + "Response", + "SetXForwarded", + "cidrWhitelist", + "cidr_whitelist", + "cloudflareRealIP", + "cloudflare_real_ip", + "customErrorPage", + "custom_error_page", + "errorPage", + "error_page", + "hideXForwarded", + "hide_x_forwarded", + "modifyRequest", + "modifyResponse", + "modify_request", + "modify_response", + "oidc", + "rateLimit", + "rate_limit", + "realIP", + "real_ip", + "redirectHTTP", + "redirect_http", + "request", + "response", + "setXForwarded", + "set_x_forwarded" + ], + "type": "string" + } + }, + "required": [ + "use" + ], + "type": "object" + }, + { + "$ref": "#/definitions/Partial" + } + ] + } + ] + }, + "type": "array" +} + diff --git a/schemas/middlewares/middleware_compose.ts b/schemas/middlewares/middleware_compose.ts new file mode 100644 index 0000000..9cbe83b --- /dev/null +++ b/schemas/middlewares/middleware_compose.ts @@ -0,0 +1,11 @@ +import { MiddlewaresMap } from "./middlewares"; + +export type MiddlewareComposeConfigBase = { + use: keyof MiddlewaresMap; +}; + +/** + * @additionalProperties false + */ +export type MiddlewareComposeConfig = (MiddlewareComposeConfigBase & + Partial)[]; diff --git a/schemas/middlewares/middlewares.ts b/schemas/middlewares/middlewares.ts new file mode 100644 index 0000000..3530686 --- /dev/null +++ b/schemas/middlewares/middlewares.ts @@ -0,0 +1,161 @@ +import * as types from "../types"; + +export type MiddlewaresMap = { + redirect_http?: RedirectHTTP; + redirectHTTP?: RedirectHTTP; + RedirectHTTP?: RedirectHTTP; + oidc?: OIDC; + OIDC?: OIDC; + request?: ModifyRequest; + Request?: ModifyRequest; + modify_request?: ModifyRequest; + modifyRequest?: ModifyRequest; + ModifyRequest?: ModifyRequest; + response?: ModifyResponse; + Response?: ModifyResponse; + modify_response?: ModifyResponse; + modifyResponse?: ModifyResponse; + ModifyResponse?: ModifyResponse; + set_x_forwarded?: SetXForwarded; + setXForwarded?: SetXForwarded; + SetXForwarded?: SetXForwarded; + hide_x_forwarded?: HideXForwarded; + hideXForwarded?: HideXForwarded; + HideXForwarded?: HideXForwarded; + error_page?: CustomErrorPage; + errorPage?: CustomErrorPage; + custom_error_page?: CustomErrorPage; + customErrorPage?: CustomErrorPage; + CustomErrorPage?: CustomErrorPage; + real_ip?: RealIP; + realIP?: RealIP; + RealIP?: RealIP; + cloudflare_real_ip?: CloudflareRealIP; + cloudflareRealIP?: CloudflareRealIP; + CloudflareRealIP?: CloudflareRealIP; + rate_limit?: RateLimit; + rateLimit?: RateLimit; + RateLimit?: RateLimit; + cidr_whitelist?: CIDRWhitelist; + cidrWhitelist?: CIDRWhitelist; + CIDRWhitelist?: CIDRWhitelist; +}; + +/** + * @additionalProperties false + */ +export type CustomErrorPage = {}; + +/** + * @additionalProperties false + */ +export type CIDRWhitelist = { + /* Allowed CIDRs/IPs */ + allow: types.CIDR[]; + /** HTTP status code when blocked + * + * @default 403 + */ + status_code?: types.StatusCode; + /** Error message when blocked + * + * @default "IP not allowed" + */ + message?: string; +}; + +/** + * @additionalProperties false + */ +export type CloudflareRealIP = { + /** Recursively resolve the IP + * + * @default false + */ + recursive?: boolean; +}; + +/** + * @additionalProperties false + */ +export type 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[]; +}; + +/** + * @additionalProperties false + */ +export type ModifyResponse = ModifyRequest; + +/** + * @additionalProperties false + */ +export type OIDC = { + /** Allowed users + * + * @minItems 1 + */ + allowed_users?: string[]; + /** Allowed groups + * + * @minItems 1 + */ + allowed_groups?: string[]; +}; + +/** + * @additionalProperties false + */ +export type 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; +}; + +/** + * @additionalProperties false + */ +export type RealIP = { + /** Header to get the client IP from + * + */ + header: types.HTTPHeader; + from: types.CIDR[]; + /** Recursive resolve the IP + * + * @default false + */ + recursive: boolean; +}; + +/** + * @additionalProperties false + */ +export type RedirectHTTP = {}; + +/** + * @additionalProperties false + */ +export type SetXForwarded = {}; + +/** + * @additionalProperties false + */ +export type HideXForwarded = {}; diff --git a/schemas/providers/healthcheck.ts b/schemas/providers/healthcheck.ts new file mode 100644 index 0000000..fc2853e --- /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 0000000..288807e --- /dev/null +++ b/schemas/providers/homepage.ts @@ -0,0 +1,31 @@ +import { URL } from "../types"; + +/** + * @additionalProperties false + */ +export type HomepageConfig = { + /* 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 0000000..e002737 --- /dev/null +++ b/schemas/providers/idlewatcher.ts @@ -0,0 +1,45 @@ +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]; + +/** + * @additionalProperties false + */ +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 0000000..9f758bf --- /dev/null +++ b/schemas/providers/loadbalance.ts @@ -0,0 +1,57 @@ +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]; + +/** + * @additionalProperties false + */ +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; +}; + +/** + * @additionalProperties false + */ +export type LoadBalanceConfig = LoadBalanceConfigBase & + ( + | RoundRobinLoadBalanceConfig + | LeastConnLoadBalanceConfig + | IPHashLoadBalanceConfig + ); + +/** + * @additionalProperties false + */ +export type IPHashLoadBalanceConfig = { + mode: "ip_hash"; + /** Real IP config, header to get client IP from */ + config: RealIP; +}; +/** + * @additionalProperties false + */ +export type LeastConnLoadBalanceConfig = { + mode: "least_conn"; +}; +/** + * @additionalProperties false + */ +export type RoundRobinLoadBalanceConfig = { + mode: "round_robin"; +}; diff --git a/schemas/providers/routes.ts b/schemas/providers/routes.ts new file mode 100644 index 0000000..b21882f --- /dev/null +++ b/schemas/providers/routes.ts @@ -0,0 +1,120 @@ +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; +}; + +/** + * @additionalProperties false + */ +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; +}; + +/** + * @additionalProperties false + */ +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 0000000..5409455 --- /dev/null +++ b/schemas/routes.schema.json @@ -0,0 +1,1311 @@ +{ + "$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": [ + { + "allOf": [ + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "mode": { + "enum": [ + "round_robin" + ], + "type": "string" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "mode": { + "enum": [ + "least_conn" + ], + "type": "string" + } + }, + "required": [ + "mode" + ], + "type": "object" + } + ] + }, + { + "allOf": [ + { + "additionalProperties": false, + "properties": { + "link": { + "description": "Alias (subdomain or FDN) of load-balancer", + "minLength": 1, + "type": "string" + }, + "weight": { + "description": "Load-balance weight (reserved for future use)", + "maximum": 100, + "minimum": 0, + "type": "number" + } + }, + "required": [ + "link" + ], + "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": { + "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", + "header", + "recursive" + ], + "type": "object" + }, + "mode": { + "enum": [ + "ip_hash" + ], + "type": "string" + } + }, + "required": [ + "config", + "mode" + ], + "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": { + "properties": { + "cookie": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "header": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + }, + "query": { + "properties": { + "config": { + "additionalProperties": { + "enum": [ + "drop", + "keep", + "redact" + ], + "type": "string" + }, + "type": "object" + }, + "default": { + "$ref": "#/definitions/AccessLogFieldMode" + } + }, + "required": [ + "config" + ], + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "properties": { + "cidr": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "headers": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "host": { + "properties": { + "negative": { + "default": false, + "description": "Whether the filter is negative.", + "type": "boolean" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "values" + ], + "type": "object" + }, + "method": { + "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": { + "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" + }, + "url": { + "format": "uri", + "type": "string" + }, + "widget_config": { + "additionalProperties": {}, + "type": "object" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "host": { + "default": "localhost", + "description": "Proxy host", + "type": "string" + }, + "load_balance": { + "$ref": "#/definitions/LoadBalanceConfig", + "description": "Load balance config" + }, + "middlewares": { + "description": "Middlewares", + "properties": { + "CIDRWhitelist": { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "required": [ + "allow" + ], + "type": "object" + }, + "CloudflareRealIP": { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "CustomErrorPage": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "HideXForwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "ModifyRequest": { + "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" + }, + "ModifyResponse": { + "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" + }, + "OIDC": { + "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" + }, + "RateLimit": { + "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" + }, + "RealIP": { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "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", + "header", + "recursive" + ], + "type": "object" + }, + "RedirectHTTP": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "Request": { + "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" + }, + "Response": { + "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" + }, + "SetXForwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "cidrWhitelist": { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "required": [ + "allow" + ], + "type": "object" + }, + "cidr_whitelist": { + "additionalProperties": false, + "properties": { + "allow": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "message": { + "default": "IP not allowed", + "description": "Error message when blocked", + "type": "string" + }, + "status_code": { + "$ref": "#/definitions/StatusCode", + "default": 403, + "description": "HTTP status code when blocked" + } + }, + "required": [ + "allow" + ], + "type": "object" + }, + "cloudflareRealIP": { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "cloudflare_real_ip": { + "additionalProperties": false, + "properties": { + "recursive": { + "default": false, + "description": "Recursively resolve the IP", + "type": "boolean" + } + }, + "type": "object" + }, + "customErrorPage": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "custom_error_page": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "errorPage": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "error_page": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "hideXForwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "hide_x_forwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "modifyRequest": { + "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" + }, + "modifyResponse": { + "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" + }, + "modify_request": { + "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" + }, + "modify_response": { + "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" + }, + "oidc": { + "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" + }, + "rateLimit": { + "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" + }, + "rate_limit": { + "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" + }, + "realIP": { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "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", + "header", + "recursive" + ], + "type": "object" + }, + "real_ip": { + "additionalProperties": false, + "properties": { + "from": { + "items": { + "$ref": "#/definitions/CIDR" + }, + "type": "array" + }, + "header": { + "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", + "header", + "recursive" + ], + "type": "object" + }, + "redirectHTTP": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "redirect_http": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "request": { + "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" + }, + "response": { + "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" + }, + "setXForwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "set_x_forwarded": { + "additionalProperties": false, + "properties": {}, + "type": "object" + } + }, + "type": "object" + }, + "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" + ], + "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" + } + }, + "type": "object" +} + diff --git a/schemas/types.ts b/schemas/types.ts new file mode 100644 index 0000000..6823f67 --- /dev/null +++ b/schemas/types.ts @@ -0,0 +1,104 @@ +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;