Compare commits

..

12 Commits
0.8.0 ... 0.8.1

Author SHA1 Message Date
yusing
1b40f81fcc remove next-release.md 2025-01-07 12:56:15 +08:00
yusing
afefd925ea api: updated list/get/set file endpoint 2025-01-07 10:57:53 +08:00
yusing
0850562bf9 fix nil panic on null entry 2025-01-06 04:58:11 +08:00
yusing
bc2335a54e update config example 2025-01-06 04:04:05 +08:00
yusing
5a9fc3ad18 healthcheck: should not include latency when ping failed 2025-01-06 04:03:59 +08:00
yusing
29f85db022 schema update and api /v1/schema 2025-01-06 00:49:29 +08:00
yusing
6034908a95 fix schemas 2025-01-05 15:03:03 +08:00
yusing
ef3dbc217b access log schema 2025-01-05 14:58:57 +08:00
yusing
01357617ae remove api ratelimiter 2025-01-05 12:13:20 +08:00
yusing
4775f4ea31 request/response middleware no longer canonicalize header key 2025-01-05 11:25:56 +08:00
yusing
ae7b27e1c9 fix udp not returning error correctly 2025-01-05 11:20:57 +08:00
yusing
70c8c4b4aa fix edge cases refCounter close channel twice 2025-01-05 09:15:03 +08:00
17 changed files with 338 additions and 590 deletions

View File

@@ -1,10 +1,10 @@
{
"yaml.schemas": {
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
"https://github.com/yusing/go-proxy/raw/v0.8/schema/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
"https://github.com/yusing/go-proxy/raw/v0.8/schema/providers.schema.json": [
"providers.example.yml"
]
}

View File

@@ -50,6 +50,9 @@ COPY config.example.yml /app/config/config.yml
# copy certs
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
# copy schema
COPY schema /app/schema
ENV DOCKER_HOST=unix:///var/run/docker.sock
ENV GODOXY_DEBUG=0

View File

@@ -22,14 +22,22 @@
entrypoint:
middlewares:
# this part blocks all non-LAN HTTP traffic
# remove if you don't want this
- 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"
# end of CIDRWhitelist
# this part redirects HTTP to HTTPS
# remove if you don't want this
- use: RedirectHTTP
# access_log:
# buffer_size: 1024
# path: /var/log/example.log

View File

@@ -8,8 +8,6 @@ import (
"github.com/yusing/go-proxy/internal/api/v1/auth"
. "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/net/http/middleware"
)
type ServeMux struct{ *http.ServeMux }
@@ -19,7 +17,7 @@ func NewServeMux() ServeMux {
}
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(rateLimited(handler)))
mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(handler))
}
func NewHandler() http.Handler {
@@ -33,10 +31,10 @@ func NewHandler() http.Handler {
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(v1.List))
mux.HandleFunc("GET", "/v1/file", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("GET", "/v1/file/{filename...}", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("POST", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("PUT", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.GetFileContent))
mux.HandleFunc("POST", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("PUT", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
mux.HandleFunc("GET", "/v1/schema/{filename...}", v1.GetSchemaFile)
mux.HandleFunc("GET", "/v1/stats", v1.Stats)
mux.HandleFunc("GET", "/v1/stats/ws", v1.StatsWS)
return mux
@@ -58,16 +56,3 @@ func checkHost(f http.HandlerFunc) http.HandlerFunc {
f(w, r)
}
}
func rateLimited(f http.HandlerFunc) http.HandlerFunc {
m, err := middleware.RateLimiter.New(middleware.OptionsRaw{
"average": 10,
"burst": 10,
})
if err != nil {
logging.Fatal().Err(err).Msg("unable to create API rate limiter")
}
return func(w http.ResponseWriter, r *http.Request) {
m.ModifyRequest(f, w, r)
}
}

View File

@@ -15,12 +15,59 @@ import (
"github.com/yusing/go-proxy/internal/route/provider"
)
func GetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
filename = common.ConfigFileName
type FileType string
const (
FileTypeConfig FileType = "config"
FileTypeProvider FileType = "provider"
FileTypeMiddleware FileType = "middleware"
)
func fileType(file string) FileType {
switch {
case strings.HasPrefix(path.Base(file), "config."):
return FileTypeConfig
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
return FileTypeMiddleware
}
content, err := os.ReadFile(path.Join(common.ConfigBasePath, filename))
return FileTypeProvider
}
func (t FileType) IsValid() bool {
switch t {
case FileTypeConfig, FileTypeProvider, FileTypeMiddleware:
return true
}
return false
}
func (t FileType) GetPath(filename string) string {
if t == FileTypeMiddleware {
return path.Join(common.MiddlewareComposeBasePath, filename)
}
return path.Join(common.ConfigBasePath, filename)
}
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
fileType = FileType(r.PathValue("type"))
if !fileType.IsValid() {
err = U.ErrInvalidKey("type")
return
}
filename = r.PathValue("filename")
if filename == "" {
err = U.ErrMissingKey("filename")
}
return
}
func GetFileContent(w http.ResponseWriter, r *http.Request) {
fileType, filename, err := getArgs(r)
if err != nil {
U.RespondError(w, err, http.StatusBadRequest)
return
}
content, err := os.ReadFile(fileType.GetPath(filename))
if err != nil {
U.HandleErr(w, r, err)
return
@@ -29,9 +76,9 @@ func GetFileContent(w http.ResponseWriter, r *http.Request) {
}
func SetFileContent(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
U.HandleErr(w, r, U.ErrMissingKey("filename"), http.StatusBadRequest)
fileType, filename, err := getArgs(r)
if err != nil {
U.RespondError(w, err, http.StatusBadRequest)
return
}
content, err := io.ReadAll(r.Body)
@@ -41,10 +88,10 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
}
var valErr E.Error
switch {
case filename == common.ConfigFileName:
switch fileType {
case FileTypeConfig:
valErr = config.Validate(content)
case strings.HasPrefix(filename, path.Base(common.MiddlewareComposeBasePath)):
case FileTypeMiddleware:
errs := E.NewBuilder("middleware errors")
middleware.BuildMiddlewaresFromYAML(filename, content, errs)
valErr = errs.Error()
@@ -57,7 +104,7 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
return
}
err = os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0o644)
err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
if err != nil {
U.HandleErr(w, r, err)
return

View File

@@ -16,7 +16,7 @@ import (
const (
ListRoute = "route"
ListRoutes = "routes"
ListConfigFiles = "config_files"
ListFiles = "files"
ListMiddlewares = "middlewares"
ListMiddlewareTraces = "middleware_trace"
ListMatchDomains = "match_domains"
@@ -41,8 +41,8 @@ func List(w http.ResponseWriter, r *http.Request) {
}
case ListRoutes:
U.RespondJSON(w, r, config.RoutesByAlias(route.RouteType(r.FormValue("type"))))
case ListConfigFiles:
listConfigFiles(w, r)
case ListFiles:
listFiles(w, r)
case ListMiddlewares:
U.RespondJSON(w, r, middleware.All())
case ListMiddlewareTraces:
@@ -70,14 +70,32 @@ func listRoute(which string) any {
return route
}
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 1)
func listFiles(w http.ResponseWriter, r *http.Request) {
files, err := utils.ListFiles(common.ConfigBasePath, 0)
if err != nil {
U.HandleErr(w, r, err)
return
}
for i := range files {
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
resp := map[FileType][]string{
FileTypeConfig: make([]string, 0),
FileTypeProvider: make([]string, 0),
FileTypeMiddleware: make([]string, 0),
}
U.RespondJSON(w, r, files)
for _, file := range files {
t := fileType(file)
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
resp[t] = append(resp[t], file)
}
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
if err != nil {
U.HandleErr(w, r, err)
return
}
for _, mid := range mids {
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
}
U.RespondJSON(w, r, resp)
}

23
internal/api/v1/schema.go Normal file
View File

@@ -0,0 +1,23 @@
package v1
import (
"net/http"
"os"
"path"
U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
)
func GetSchemaFile(w http.ResponseWriter, r *http.Request) {
filename := r.PathValue("filename")
if filename == "" {
U.RespondError(w, U.ErrMissingKey("filename"), http.StatusBadRequest)
}
content, err := os.ReadFile(path.Join(common.SchemaBasePath, filename))
if err != nil {
U.HandleErr(w, r, err)
return
}
U.WriteBody(w, content)
}

View File

@@ -54,10 +54,10 @@ func (mr *ModifyRequestOpts) modifyHeaders(req *http.Request, resp *http.Respons
req.Host = v
}()
}
headers.Set(k, v)
headers[k] = []string{v}
}
for k, v := range mr.AddHeaders {
headers.Add(k, v)
headers[k] = append(headers[k], v)
}
} else {
for k, v := range mr.SetHeaders {
@@ -66,14 +66,14 @@ func (mr *ModifyRequestOpts) modifyHeaders(req *http.Request, resp *http.Respons
req.Host = varReplace(req, resp, v)
}()
}
headers.Set(k, varReplace(req, resp, v))
headers[k] = []string{varReplace(req, resp, v)}
}
for k, v := range mr.AddHeaders {
headers.Add(k, varReplace(req, resp, v))
headers[k] = append(headers[k], varReplace(req, resp, v))
}
}
for _, k := range mr.HideHeaders {
headers.Del(k)
delete(headers, k)
}
}

View File

@@ -87,6 +87,9 @@ func FromEntries(entries RawEntries) (Routes, E.Error) {
routes := NewRoutes()
entries.RangeAllParallel(func(alias string, en *RawEntry) {
if en == nil {
en = new(RawEntry)
}
en.Alias = alias
if strings.HasPrefix(alias, "x-") { // x properties
return

View File

@@ -128,7 +128,7 @@ func (conn *UDPConn) write() (err error) {
}
}
return nil
return
}
func (w *UDPForwarder) getInitConn(conn *UDPConn, key string) (*UDPConn, error) {

View File

@@ -24,11 +24,31 @@ func (rc *RefCount) Zero() <-chan struct{} {
}
func (rc *RefCount) Add() {
atomic.AddUint32(&rc.refCount, 1)
// We add before checking to ensure proper ordering
newV := atomic.AddUint32(&rc.refCount, 1)
if newV == 1 {
// If it was 0 before we added, that means we're incrementing after a close
// This is a programming error
panic("RefCount.Add() called after count reached zero")
}
}
func (rc *RefCount) Sub() {
if atomic.AddUint32(&rc.refCount, ^uint32(0)) == 0 {
close(rc.zeroCh)
// First read the current value
for {
current := atomic.LoadUint32(&rc.refCount)
if current == 0 {
// Already at zero, channel should be closed
return
}
// Try to decrement, but only if the value hasn't changed
if atomic.CompareAndSwapUint32(&rc.refCount, current, current-1) {
if current == 1 { // Was this the last reference?
close(rc.zeroCh)
}
return
}
// If CAS failed, someone else modified the count, try again
}
}

View File

@@ -4,6 +4,8 @@ import (
"sync"
"testing"
"time"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestRefCounterAddSub(t *testing.T) {
@@ -12,18 +14,16 @@ func TestRefCounterAddSub(t *testing.T) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
rc.Add()
}()
go func() {
defer wg.Done()
rc.Sub()
rc.Sub()
}()
rc.Add()
for range 2 {
go func() {
defer wg.Done()
rc.Sub()
}()
}
wg.Wait()
ExpectEqual(t, int(rc.refCount), 0)
select {
case <-rc.Zero():
@@ -39,7 +39,7 @@ func TestRefCounterMultipleAddSub(t *testing.T) {
var wg sync.WaitGroup
numAdds := 5
numSubs := 5
wg.Add(numAdds + numSubs)
wg.Add(numAdds)
for range numAdds {
go func() {
@@ -47,17 +47,20 @@ func TestRefCounterMultipleAddSub(t *testing.T) {
rc.Add()
}()
}
wg.Wait()
ExpectEqual(t, int(rc.refCount), numAdds+1)
wg.Add(numSubs)
for range numSubs {
go func() {
defer wg.Done()
rc.Sub()
rc.Sub()
}()
}
wg.Wait()
ExpectEqual(t, int(rc.refCount), numAdds+1-numSubs)
rc.Sub()
select {
case <-rc.Zero():
// Expected behavior

View File

@@ -55,19 +55,18 @@ func (mon *HTTPHealthMonitor) CheckHealth() (result *health.HealthCheckResult, e
err = reqErr
return
}
req.Close = true
req.Header.Set("Connection", "close")
req.Header.Set("User-Agent", "GoDoxy/"+pkg.GetVersion())
start := time.Now()
resp, respErr := pinger.Do(req)
if respErr == nil {
resp.Body.Close()
defer resp.Body.Close()
}
result = &health.HealthCheckResult{
Latency: time.Since(start),
}
lat := time.Since(start)
result = &health.HealthCheckResult{}
switch {
case respErr != nil:
@@ -82,6 +81,7 @@ func (mon *HTTPHealthMonitor) CheckHealth() (result *health.HealthCheckResult, e
return
}
result.Latency = lat
result.Healthy = true
return
}

View File

@@ -1,320 +0,0 @@
# GoDoxy v0.8 changes:
## Breaking changes
- **Removed** `redirect_to_https` in `config.yml`, superseded by `redirectHTTP` as an entrypoint middleware
- **New** notification config format, support webhook notification, support multiple notification providers
old
```yaml
providers:
notification:
gotify:
url: ...
token: ...
```
new
```yaml
providers:
notification:
- name: gotify
provider: gotify
url: ...
token: ...
- name: discord
provider: webhook
url: https://discord.com/api/webhooks/...
template: discord
```
Webhook notification fields:
| Field | Description | Required | Allowed values |
| ---------- | ---------------------- | ------------------------------ | ---------------- |
| name | name of the provider | Yes | |
| provider | | Yes | `webhook` |
| url | webhook URL | Yes | Full URL |
| template | webhook template | No | empty, `discord` |
| token | webhook token | No | |
| payload | webhook payload | No **(if `template` is used)** | valid json |
| method | webhook request method | No | `GET POST PUT` |
| mime_type | mime type | No | |
| color_mode | color mode | No | `hex` `dec` |
Available payload variables:
| Variable | Description | Format |
| -------- | --------------------------- | ------------------------------------ |
| $title | message title | json string |
| $message | message in markdown format | json string |
| $fields | extra fields in json format | json object |
| $color | embed color by `color_mode` | `0xff0000` (hex) or `16711680` (dec) |
## Non-breaking changes
- services health notification now in markdown format like `Uptime Kuma` for both webhook and Gotify
- docker services now use docker container health status if possible, fallback to GoDoxy health check on failure / no docker health check, e.g.
```yaml
# docker compose
services:
app:
...
container_name: app
healthcheck:
test: ["CMD-SHELL", "curl --fail http://localhost:8080 || exit 1"]
interval: 5s
```
Health check result will be equivalent to `docker inspect --format='{{json .State.Health}}' app`
- `proxy.<alias>.path_patterns` fully support http.ServeMux patterns `[METHOD ][HOST]/[PATH]` (See https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)
- caching ACME private key in order to reuse ACME account, to prevent from ACME rate limit
- WebUI config editor now validates for middleware compose files
- **New:** fully support string as inline YAML for docker labels
```yaml
services:
app:
...
labels:
# add '|' after colon ':' to treat it as string
proxy.app: |
scheme: http
host: 10.0.0.254
port: 80
path_patterns:
- GET /
- POST /auth
healthcheck:
disabled: false
path: /
interval: 5s
proxy.app1.healthcheck: |
path: /ping
use_get: true
proxy.app1.load_balance: |
link: app
mode: ip_hash
```
- **New:** support entrypoint middlewares (applied to all routes, before route middlewares)
```yaml
entrypoint:
middlewares:
- use: CIDRWhitelist
allow:
- "127.0.0.1"
- "10.0.0.0/8"
- "192.168.0.0/16"
status: 403
message: "Forbidden"
```
- **New:** support exact host matching, i.e.
```yaml
# include file
app1.domain.tld:
host: 10.0.0.1
# docker compose
services:
app1:
...
proxy.aliases: app1.domain.tld
```
will only match exactly `app1.domain.tld`
**`match_domains` in config will be ignored for this route**
- **New:** support host matching without a subdomain, i.e.
```yaml
app1:
host: 10.0.0.1
```
will now also match `app1.tld`
- **New:** support `x-properties` (will be ignored, like in docker compose), useful with YAML anchor e.g.
```yaml
x-proxy: &proxy # this will be ignored in GoDoxy
scheme: https
healthcheck:
disable: true
middlewares:
hideXForwarded:
modifyRequest:
setHeaders:
Host: $req_host
api.openai.com:
<<: *proxy # extends from x-proxy
host: api.openai.com
api.groq.com:
<<: *proxy # extends from x-proxy
host: api.groq.com
```
- new middleware name aliases:
- `modifyRequest` = `request`
- `modifyResponse` = `response`
- **New:** support `$` variables in `request` and `response` middlewares (like nginx config)
- `$req_method`: request http method
- `$req_scheme`: request URL scheme (http/https)
- `$req_host`: request host without port
- `$req_port`: request port
- `$req_addr`: request host with port (if present)
- `$req_path`: request URL path
- `$req_query`: raw query string
- `$req_url`: full request URL
- `$req_uri`: request URI (encoded path?query)
- `$req_content_type`: request Content-Type header
- `$req_content_length`: length of request body (if present)
- `$remote_addr`: client's remote address (may changed by middlewares like `RealIP` and `CloudflareRealIP`)
- `$remote_host`: client's remote ip parse from `$remote_addr`
- `$remote_port`: client's remote port parse from `$remote_addr` (may be empty)
- `$resp_content_type`: response Content-Type header
- `$resp_content_length`: length response body
- `$status_code`: response status code
- `$upstream_name`: upstream server name (alias)
- `$upstream_scheme`: upstream server scheme
- `$upstream_host`: upstream server host
- `$upstream_port`: upstream server port
- `$upstream_addr`: upstream server address with port (if present)
- `$upstream_url`: full upstream server URL
- `$header(name)`: get request header by name
- `$resp_header(name)`: get response header by name
- `$arg(name)`: get URL query parameter by name
- **New:** Access Logging (entrypoint and per route), i.e.
**mount logs directory before setting this**
```yaml
# config.yml
entrypoint:
access_log:
format: json # common, combined, json
path: /app/logs/access.json.log
filters:
cidr:
negative: true # no log for local requests
values:
- 127.0.0.1/32
- 172.0.0.0/8
- 192.168.0.0/16
- 10.0.0.0/16
fields:
headers:
default: drop # drop app headers in log
config: # keep only these
X-Real-Ip: keep
CF-Connecting-Ip: keep
X-Forwarded-For: keep
# include file
# same as above but under route config
app:
access_log:
format: json # common, combined, json
...
# docker labels
labels:
proxy.app.access_log: |
format: json
path: /app/logs/access.json.log
filters:
cidr:
negative: true
values:
- 127.0.0.1/32
- 172.0.0.0/8
- 192.168.0.0/16
- 10.0.0.0/16
```
To integrate with **goaccess**, currently need to use **caddy** as a file web server. Below should work with `combined` log format.
```yaml
# compose.yml
services:
app:
image: reg.6uo.me/yusing/goproxy
...
volumes:
...
- ./logs:/app/logs
caddy:
image: caddy
restart: always
labels:
proxy.goaccess.port: 80
proxy.goaccess.middlewares.request.set_headers.host: goaccess
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./logs:/var/www/goaccess:ro
depends_on:
- goaccess
goaccess:
image: hectorm/goaccess:latest
restart: always
volumes:
- ./logs:/srv/logs
command: > # for combined format
/srv/logs/access.log
-o /srv/logs/report.html
-j 4 # 4 threads
--real-time-html
--ws-url=<your goaccess url>:443 # i.e. goaccess.my.app:443/ws
--log-format='%v %h %^[%d:%t %^] "%r" %s %b "%R" "%u"'
```
Caddyfile
```caddyfile
{
auto_https off
}
goaccess:80 {
@websockets {
header Connection *Upgrade
header Upgrade websocket
}
handle @websockets {
reverse_proxy goaccess:7890
}
root * /var/www/goaccess
file_server
rewrite / /report.html
}
```
## Fixes
- duplicated notification after config reload
- `timeout` was defaulted to `0` in some cases causing health check to fail
- `redirectHTTP` middleware may not work on non standard http port
- various other small bugs
- `realIP` and `cloudflareRealIP` middlewares
- prometheus metrics gone after a single route reload
- WebUI app links now works when `match_domains` is not set
- WebUI config editor now display validation errors properly
- upgraded dependencies to the latest

103
schema/access_log.json Normal file
View File

@@ -0,0 +1,103 @@
{
"$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"
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
{
"$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",
@@ -10,8 +11,7 @@
"email": {
"title": "ACME Email",
"type": "string",
"pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
"patternErrorMessage": "Invalid email"
"format": "email"
},
"domains": {
"title": "Cert Domains",
@@ -24,7 +24,7 @@
"cert_path": {
"title": "path of cert file to load/store",
"default": "certs/cert.crt",
"markdownDescription": "default: `certs/cert.crt`",
"markdownDescription": "default: `certs/cert.crt`,",
"type": "string"
},
"key_path": {
@@ -447,156 +447,7 @@
}
},
"access_log": {
"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",
"type": "object",
"additionalProperties": false,
"properties": {
"negative": {
"type": "boolean"
},
"values": {
"type": "array"
}
}
},
"status_codes": {
"title": "Status code filter",
"type": "object",
"additionalProperties": false,
"properties": {
"negative": {
"type": "boolean"
},
"values": {
"type": "array"
}
}
},
"method": {
"title": "Method filter",
"type": "object",
"additionalProperties": false,
"properties": {
"negative": {
"type": "boolean"
},
"values": {
"type": "array"
}
}
},
"headers": {
"title": "Header filter",
"type": "object",
"additionalProperties": false,
"properties": {
"negative": {
"type": "boolean"
},
"values": {
"type": "array"
}
}
},
"host": {
"title": "Host filter",
"type": "object",
"additionalProperties": false,
"properties": {
"negative": {
"type": "boolean"
},
"values": {
"type": "array"
}
}
}
}
},
"fields": {
"title": "Access log fields",
"type": "object",
"additionalProperties": false,
"properties": {
"headers": {
"type": "object",
"additionalProperties": false,
"properties": {
"default": {
"enum": [
"keep",
"redact",
"drop"
]
},
"config": {
"type": "object"
}
}
},
"query": {
"type": "object",
"additionalProperties": false,
"properties": {
"default": {
"enum": [
"keep",
"redact",
"drop"
]
},
"config": {
"type": "object"
}
}
},
"cookies": {
"type": "object",
"additionalProperties": false,
"properties": {
"default": {
"enum": [
"keep",
"redact",
"drop"
]
},
"config": {
"type": "object"
}
}
}
}
}
}
"$ref": "https://github.com/yusing/go-proxy/raw/v0.8/schema/access_log.json"
}
}
},

View File

@@ -1,4 +1,5 @@
{
"$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": [
@@ -15,7 +16,7 @@
"type": "object",
"properties": {
"scheme": {
"title": "Proxy scheme (http, https, tcp, udp)",
"title": "Proxy scheme",
"oneOf": [
{
"type": "string",
@@ -38,28 +39,28 @@
},
"host": {
"default": "localhost",
"oneOf": [
"anyOf": [
{
"type": "null",
"description": "localhost (default)"
"title": "localhost (default)"
},
{
"type": "string",
"format": "ipv4",
"description": "Proxy to ipv4 address"
"title": "ipv4 address"
},
{
"type": "string",
"format": "ipv6",
"description": "Proxy to ipv6 address"
"title": "ipv6 address"
},
{
"type": "string",
"format": "hostname",
"description": "Proxy to hostname"
"title": "hostname"
}
],
"title": "Proxy host (ipv4 / ipv6 / hostname)"
"title": "Proxy host (ipv4/6 / hostname)"
},
"port": {},
"no_tls_verify": {},
@@ -71,58 +72,60 @@
"additionalProperties": false,
"properties": {
"show": {
"description": "Show on dashboard",
"title": "Show on dashboard",
"type": "boolean",
"default": true
},
"name": {
"description": "Display name",
"title": "Display name",
"type": "string"
},
"icon": {
"description": "Display icon",
"title": "Display icon",
"type": "string",
"oneOf": [
{
"pattern": "^(png|svg)\\/[\\w\\d-_]+\\.(png|svg)$",
"description": "Icon from walkxcode/dashboard-icons",
"errorMessage": "must be png/filename.png or svg/filename.svg"
"pattern": "^(png|svg)\\/[\\w\\d\\-_]+\\.\\1$",
"title": "Icon from walkxcode/dashboard-icons"
},
{
"pattern": "^https?://",
"description": "Absolute URI"
"title": "Absolute URI",
"format": "uri"
},
{
"pattern": "^@target/",
"description": "Relative URI to target"
"title": "Relative URI to target"
}
]
},
"url": {
"description": "App URL override",
"title": "App URL override",
"type": "string",
"format": "uri"
"format": "uri",
"pattern": "^https?://"
},
"category": {
"description": "Category",
"title": "Category",
"type": "string"
},
"description": {
"description": "Description",
"title": "Description",
"type": "string"
},
"widget_config": {
"description": "Widget config",
"title": "Widget config",
"type": "object"
}
}
},
"load_balance": {
"type": "object",
"additionalProperties": false,
"properties": {
"link": {
"type": "string",
"description": "Name and subdomain of load-balancer"
"title": "Name and subdomain of load-balancer"
},
"mode": {
"enum": [
@@ -130,46 +133,53 @@
"least_conn",
"ip_hash"
],
"description": "Load-balance mode",
"title": "Load-balance mode",
"default": "roundrobin"
},
"weight": {
"type": "integer",
"description": "Reserved for future use",
"title": "Reserved for future use",
"minimum": 0,
"maximum": 100
},
"options": {
"type": "object",
"description": "load-balance mode specific options"
"title": "load-balance mode specific options"
}
}
},
"healthcheck": {
"type": "object",
"additionalProperties": false,
"properties": {
"disable": {
"type": "boolean",
"default": false
"default": false,
"title": "Disable healthcheck"
},
"path": {
"type": "string",
"description": "Healthcheck path",
"title": "Healthcheck path",
"default": "/",
"format": "uri"
"format": "uri-reference",
"description": "should start with `/`"
},
"use_get": {
"type": "boolean",
"description": "Use GET instead of HEAD",
"title": "Use GET instead of HEAD",
"default": false
},
"interval": {
"type": "string",
"description": "Interval for healthcheck (e.g. 5s, 1h25m30s)",
"title": "healthcheck Interval",
"pattern": "^([0-9]+(ms|s|m|h))+$",
"default": "5s"
"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,
@@ -195,7 +205,8 @@
"then": {
"properties": {
"port": {
"markdownDescription": "Proxy port from **0** to **65535**",
"title": "Proxy port",
"markdownDescription": "From **0** to **65535**",
"oneOf": [
{
"type": "string",
@@ -210,21 +221,14 @@
]
},
"path_patterns": {
"oneOf": [
{
"type": "array",
"markdownDescription": "A list of [path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)",
"items": {
"type": "string",
"pattern": "^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/(\\w*|{\\w*}|{\\$}))+/?$",
"patternErrorMessage": "invalid path pattern"
}
},
{
"type": "null",
"description": "No proxy path"
}
]
"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"
@@ -236,7 +240,7 @@
"port": {
"markdownDescription": "`listening port:proxy port` or `listening port:service name`",
"type": "string",
"pattern": "^[0-9]+\\:[0-9a-z]+$",
"pattern": "^[0-9]+:[0-9a-z]+$",
"patternErrorMessage": "invalid syntax"
},
"no_tls_verify": {
@@ -265,7 +269,7 @@
"then": {
"properties": {
"no_tls_verify": {
"description": "Disable TLS verification for https proxy",
"title": "Disable TLS verification for https proxy",
"type": "boolean",
"default": false
}