mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-11 04:57:42 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b40f81fcc | ||
|
|
afefd925ea | ||
|
|
0850562bf9 | ||
|
|
bc2335a54e | ||
|
|
5a9fc3ad18 | ||
|
|
29f85db022 | ||
|
|
6034908a95 | ||
|
|
ef3dbc217b | ||
|
|
01357617ae | ||
|
|
4775f4ea31 | ||
|
|
ae7b27e1c9 | ||
|
|
70c8c4b4aa |
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
23
internal/api/v1/schema.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,7 +128,7 @@ func (conn *UDPConn) write() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (w *UDPForwarder) getInitConn(conn *UDPConn, key string) (*UDPConn, error) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
320
next-release.md
320
next-release.md
@@ -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
103
schema/access_log.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user