mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-22 20:24:03 +02:00
v0.5: (BREAKING) replacing path with path_patterns, improved docker monitoring mechanism, bug fixes
This commit is contained in:
parent
2e7ba51521
commit
7a0478164f
20 changed files with 252 additions and 246 deletions
23
.vscode/settings.example.json
vendored
23
.vscode/settings.example.json
vendored
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||||
"providers.example.yml",
|
"providers.example.yml",
|
||||||
"*.providers.yml"
|
"*.providers.yml",
|
||||||
]
|
"providers.yml"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,9 +13,9 @@ FROM alpine:latest
|
||||||
LABEL maintainer="yusing@6uo.me"
|
LABEL maintainer="yusing@6uo.me"
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
COPY schema/ /app/schema
|
|
||||||
# copy binary
|
# copy binary
|
||||||
COPY --from=builder /src/go-proxy /app/
|
COPY --from=builder /src/go-proxy /app/
|
||||||
|
COPY schema/ /app/schema
|
||||||
|
|
||||||
RUN chmod +x /app/go-proxy
|
RUN chmod +x /app/go-proxy
|
||||||
ENV DOCKER_HOST unix:///var/run/docker.sock
|
ENV DOCKER_HOST unix:///var/run/docker.sock
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
6. Run `docker compose up -d` to start the container
|
6. Run `docker compose up -d` to start the container
|
||||||
|
|
||||||
7. Navigate to Web panel `http://gp.yourdomain.com` and edit proxy config
|
7. Navigate to Web panel `http://gp.yourdomain.com` or use **Visual Studio Code (provides schema check)** to edit proxy config
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
|
@ -93,17 +93,16 @@
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
| Field | Description | Default | Allowed Values / Syntax |
|
| Field | Description | Default | Allowed Values / Syntax |
|
||||||
| --------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||||
| `host` | proxy host | `container_name` | IP address, hostname |
|
| `host` | proxy host | <ul><li>Docker: `container_name`</li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `0 - 65535` |
|
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||||
| `path` | proxy path | empty | **(http/s only)** string |
|
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||||
| `path_mode` | path handling **(http/s only)** | empty | empty, `forward` |
|
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>1</sup>](#1-key-value-mapping-example) |
|
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>2</sup>](#2-list-example) |
|
|
||||||
|
|
||||||
#### Key-value mapping example
|
#### Key-value mapping example
|
||||||
|
|
||||||
|
@ -142,6 +141,9 @@ services:
|
||||||
nginx:
|
nginx:
|
||||||
...
|
...
|
||||||
labels:
|
labels:
|
||||||
|
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||||
|
- GET /
|
||||||
|
- POST /auth
|
||||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
proxy.nginx.hide_headers: | # remember to add the '|'
|
||||||
- X-Custom-Header1
|
- X-Custom-Header1
|
||||||
- X-Custom-Header2
|
- X-Custom-Header2
|
||||||
|
@ -152,6 +154,9 @@ File Provider
|
||||||
```yaml
|
```yaml
|
||||||
service_a:
|
service_a:
|
||||||
host: service_a.internal
|
host: service_a.internal
|
||||||
|
path_patterns:
|
||||||
|
- GET /
|
||||||
|
- POST /auth
|
||||||
hide_headers:
|
hide_headers:
|
||||||
- X-Custom-Header1
|
- X-Custom-Header1
|
||||||
- X-Custom-Header2
|
- X-Custom-Header2
|
||||||
|
|
|
@ -23,16 +23,19 @@
|
||||||
},
|
},
|
||||||
"cert_path": {
|
"cert_path": {
|
||||||
"title": "path of cert file to load/store",
|
"title": "path of cert file to load/store",
|
||||||
"description": "default: certs/cert.crt",
|
"default": "certs/cert.crt",
|
||||||
|
"markdownDescription": "default: `certs/cert.crt`",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"key_path": {
|
"key_path": {
|
||||||
"title": "path of key file to load/store",
|
"title": "path of key file to load/store",
|
||||||
"description": "default: certs/priv.key",
|
"default": "certs/priv.key",
|
||||||
|
"markdownDescription": "default: `certs/priv.key`",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "DNS Challenge Provider",
|
"title": "DNS Challenge Provider",
|
||||||
|
"default": "local",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
||||||
},
|
},
|
||||||
|
@ -44,10 +47,11 @@
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"properties": {
|
"not": {
|
||||||
"provider": {
|
"properties": {
|
||||||
"not": true,
|
"provider": {
|
||||||
"const": "local"
|
"const": "local"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -151,7 +155,7 @@
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"title": "Docker provider configuration",
|
"title": "Docker provider configuration",
|
||||||
"description": "docker clients (name: address)",
|
"description": "docker clients (name-address pairs)",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
"^[a-zA-Z0-9-_]+$": {
|
"^[a-zA-Z0-9-_]+$": {
|
||||||
|
@ -194,7 +198,7 @@
|
||||||
"minimum": 0
|
"minimum": 0
|
||||||
},
|
},
|
||||||
"redirect_to_https": {
|
"redirect_to_https": {
|
||||||
"title": "Redirect to HTTPS",
|
"title": "Redirect to HTTPS on HTTP requests",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,12 +32,17 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "null",
|
"type": "null",
|
||||||
"description": "Auto detect base on port number"
|
"description": "Auto detect base on port format"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"host": {
|
"host": {
|
||||||
|
"default": "localhost",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null",
|
||||||
|
"description": "localhost (default)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "ipv4",
|
"format": "ipv4",
|
||||||
|
@ -56,59 +61,38 @@
|
||||||
],
|
],
|
||||||
"title": "Proxy host (ipv4 / ipv6 / hostname)"
|
"title": "Proxy host (ipv4 / ipv6 / hostname)"
|
||||||
},
|
},
|
||||||
"port": {
|
"port": {},
|
||||||
"title": "Proxy port"
|
"no_tls_verify": {},
|
||||||
},
|
"path_patterns": {},
|
||||||
"path": {
|
|
||||||
"title": "Proxy path pattern (See https://pkg.go.dev/net/http#ServeMux)"
|
|
||||||
},
|
|
||||||
"no_tls_verify": {
|
|
||||||
"description": "Disable TLS verification for https proxy",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"set_headers": {},
|
"set_headers": {},
|
||||||
"hide_headers": {}
|
"hide_headers": {}
|
||||||
},
|
},
|
||||||
"required": ["host"],
|
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"anyOf": [
|
"properties": {
|
||||||
{
|
"scheme": {
|
||||||
"properties": {
|
"anyOf": [
|
||||||
"scheme": {
|
{
|
||||||
"enum": ["http", "https"]
|
"enum": ["http", "https"]
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
},
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"scheme": {
|
|
||||||
"not": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"scheme": {
|
|
||||||
"type": "null"
|
"type": "null"
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
},
|
},
|
||||||
"then": {
|
"then": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"port": {
|
"port": {
|
||||||
|
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[0-9]{1,5}$",
|
"pattern": "^\\d{1,5}$",
|
||||||
"minimum": 1,
|
"patternErrorMessage": "`port` must be a number"
|
||||||
"maximum": 65535,
|
|
||||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
|
||||||
"patternErrorMessage": "'port' must be a number"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
@ -117,11 +101,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"path": {
|
"path_patterns": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Proxy path"
|
"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*)+/?$",
|
||||||
|
"patternErrorMessage": "invalid path pattern"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "null",
|
"type": "null",
|
||||||
|
@ -133,7 +122,6 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Proxy headers to set",
|
"description": "Proxy headers to set",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "array",
|
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -151,12 +139,15 @@
|
||||||
"else": {
|
"else": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"port": {
|
"port": {
|
||||||
"markdownDescription": "`listening port`:`proxy port | service name`",
|
"markdownDescription": "`listening port:proxy port` or `listening port:service name`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
||||||
"patternErrorMessage": "'port' must be in the format of '<listening port>:<proxy port | service name>'"
|
"patternErrorMessage": "invalid syntax"
|
||||||
},
|
},
|
||||||
"path": {
|
"no_tls_verify": {
|
||||||
|
"not": true
|
||||||
|
},
|
||||||
|
"path_patterns": {
|
||||||
"not": true
|
"not": true
|
||||||
},
|
},
|
||||||
"set_headers": {
|
"set_headers": {
|
||||||
|
@ -171,15 +162,22 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
"not": {
|
"properties": {
|
||||||
"properties": {
|
"scheme": {
|
||||||
"scheme": {
|
"const": "https"
|
||||||
"const": "https"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"then": {
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"no_tls_verify": {
|
||||||
|
"description": "Disable TLS verification for https proxy",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"no_tls_verify": {
|
"no_tls_verify": {
|
||||||
"not": true
|
"not": true
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/config"
|
"github.com/yusing/go-proxy/config"
|
||||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
|
||||||
R "github.com/yusing/go-proxy/route"
|
R "github.com/yusing/go-proxy/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,17 +23,7 @@ func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
case *R.HTTPRoute:
|
case *R.HTTPRoute:
|
||||||
path, err := PT.NewPath(r.FormValue("path"))
|
ok = U.IsSiteHealthy(route.TargetURL.String())
|
||||||
if err.IsNotNil() {
|
|
||||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sr, hasSr := route.GetSubroute(path)
|
|
||||||
if !hasSr {
|
|
||||||
U.HandleErr(w, r, U.ErrNotFound("path", string(path)), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = U.IsSiteHealthy(sr.TargetURL.String())
|
|
||||||
case *R.StreamRoute:
|
case *R.StreamRoute:
|
||||||
ok = U.IsStreamHealthy(
|
ok = U.IsStreamHealthy(
|
||||||
string(route.Scheme.ProxyScheme),
|
string(route.Scheme.ProxyScheme),
|
||||||
|
|
|
@ -1,32 +1,37 @@
|
||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/error"
|
E "github.com/yusing/go-proxy/error"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func yamlParser[T any](value string) (any, E.NestedError) {
|
func yamlListParser(value string) (any, E.NestedError) {
|
||||||
var data T
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return []string{}, E.Nil()
|
||||||
|
}
|
||||||
|
var data []string
|
||||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func setHeadersParser(value string) (any, E.NestedError) {
|
func yamlStringMappingParser(value string) (any, E.NestedError) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
lines := strings.Split(value, "\n")
|
lines := strings.Split(value, "\n")
|
||||||
h := make(http.Header)
|
h := make(map[string]string)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
parts := strings.SplitN(line, ":", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return nil, E.Invalid("set header statement", line)
|
return nil, E.Invalid("set header statement", line)
|
||||||
}
|
}
|
||||||
key := strings.TrimSpace(parts[0])
|
key := strings.TrimSpace(parts[0])
|
||||||
vals := strings.Split(parts[1], ",")
|
val := strings.TrimSpace(parts[1])
|
||||||
for i := range vals {
|
if existing, ok := h[key]; ok {
|
||||||
h.Add(key, strings.TrimSpace(vals[i]))
|
h[key] = existing + ", " + val
|
||||||
|
} else {
|
||||||
|
h[key] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return h, E.Nil()
|
return h, E.Nil()
|
||||||
|
@ -56,8 +61,9 @@ const NSProxy = "proxy"
|
||||||
var _ = func() int {
|
var _ = func() int {
|
||||||
RegisterNamespace(NSProxy, ValueParserMap{
|
RegisterNamespace(NSProxy, ValueParserMap{
|
||||||
"aliases": commaSepParser,
|
"aliases": commaSepParser,
|
||||||
"set_headers": setHeadersParser,
|
"path_patterns": yamlListParser,
|
||||||
"hide_headers": yamlParser[[]string],
|
"set_headers": yamlStringMappingParser,
|
||||||
|
"hide_headers": yamlListParser,
|
||||||
"no_tls_verify": boolParser,
|
"no_tls_verify": boolParser,
|
||||||
})
|
})
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -2,7 +2,6 @@ package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -82,26 +81,22 @@ X-Custom-Header1: foo, bar
|
||||||
X-Custom-Header1: baz
|
X-Custom-Header1: baz
|
||||||
X-Custom-Header2: boo`
|
X-Custom-Header2: boo`
|
||||||
v = strings.TrimPrefix(v, "\n")
|
v = strings.TrimPrefix(v, "\n")
|
||||||
h := make(http.Header, 0)
|
h := map[string]string{
|
||||||
h.Set("X-Custom-Header1", "foo")
|
"X-Custom-Header1": "foo, bar, baz",
|
||||||
h.Add("X-Custom-Header1", "bar")
|
"X-Custom-Header2": "boo",
|
||||||
h.Add("X-Custom-Header1", "baz")
|
}
|
||||||
h.Set("X-Custom-Header2", "boo")
|
|
||||||
|
|
||||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||||
if err.IsNotNil() {
|
if err.IsNotNil() {
|
||||||
t.Errorf("expected err=nil, got %s", err.Error())
|
t.Errorf("expected err=nil, got %s", err.Error())
|
||||||
}
|
}
|
||||||
hGot, ok := pl.Value.(http.Header)
|
hGot, ok := pl.Value.(map[string]string)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("value is not http.Header")
|
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for k, vWant := range h {
|
if !reflect.DeepEqual(h, hGot) {
|
||||||
vGot := hGot[k]
|
t.Errorf("expected %v, got %v", h, hGot)
|
||||||
if !reflect.DeepEqual(vGot, vWant) {
|
|
||||||
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import (
|
||||||
R "github.com/yusing/go-proxy/route"
|
R "github.com/yusing/go-proxy/route"
|
||||||
"github.com/yusing/go-proxy/server"
|
"github.com/yusing/go-proxy/server"
|
||||||
F "github.com/yusing/go-proxy/utils/functional"
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
W "github.com/yusing/go-proxy/watcher"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -70,7 +69,6 @@ func main() {
|
||||||
|
|
||||||
onShutdown.Add(func() {
|
onShutdown.Add(func() {
|
||||||
docker.CloseAllClients()
|
docker.CloseAllClients()
|
||||||
W.StopAllFileWatchers()
|
|
||||||
cfg.Dispose()
|
cfg.Dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
F "github.com/yusing/go-proxy/utils/functional"
|
F "github.com/yusing/go-proxy/utils/functional"
|
||||||
|
@ -9,14 +8,14 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ProxyEntry struct {
|
ProxyEntry struct {
|
||||||
Alias string `yaml:"-" json:"-"`
|
Alias string `yaml:"-" json:"-"`
|
||||||
Scheme string `yaml:"scheme" json:"scheme"`
|
Scheme string `yaml:"scheme" json:"scheme"`
|
||||||
Host string `yaml:"host" json:"host"`
|
Host string `yaml:"host" json:"host"`
|
||||||
Port string `yaml:"port" json:"port"`
|
Port string `yaml:"port" json:"port"`
|
||||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
|
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||||
Path string `yaml:"path" json:"path"` // http proxy only
|
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||||
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
|
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
|
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
|
||||||
}
|
}
|
||||||
|
|
||||||
ProxyEntries = *F.Map[string, *ProxyEntry]
|
ProxyEntries = *F.Map[string, *ProxyEntry]
|
||||||
|
@ -37,8 +36,8 @@ func (e *ProxyEntry) SetDefaults() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.Path == "" {
|
if e.Host == "" {
|
||||||
e.Path = "/"
|
e.Host = "localhost"
|
||||||
}
|
}
|
||||||
switch e.Scheme {
|
switch e.Scheme {
|
||||||
case "http":
|
case "http":
|
||||||
|
|
|
@ -12,15 +12,15 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Entry struct { // real model after validation
|
Entry struct { // real model after validation
|
||||||
Alias T.Alias
|
Alias T.Alias
|
||||||
Scheme T.Scheme
|
Scheme T.Scheme
|
||||||
Host T.Host
|
Host T.Host
|
||||||
Port T.Port
|
Port T.Port
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
NoTLSVerify bool
|
NoTLSVerify bool
|
||||||
Path T.Path
|
PathPatterns T.PathPatterns
|
||||||
SetHeaders http.Header
|
SetHeaders http.Header
|
||||||
HideHeaders []string
|
HideHeaders []string
|
||||||
}
|
}
|
||||||
StreamEntry struct {
|
StreamEntry struct {
|
||||||
Alias T.Alias `json:"alias"`
|
Alias T.Alias `json:"alias"`
|
||||||
|
@ -51,7 +51,11 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||||
if err.IsNotNil() {
|
if err.IsNotNil() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
path, err := T.NewPath(m.Path)
|
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||||
|
if err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||||
if err.IsNotNil() {
|
if err.IsNotNil() {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -60,15 +64,15 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Entry{
|
return &Entry{
|
||||||
Alias: T.NewAlias(m.Alias),
|
Alias: T.NewAlias(m.Alias),
|
||||||
Scheme: s,
|
Scheme: s,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
URL: url,
|
URL: url,
|
||||||
NoTLSVerify: m.NoTLSVerify,
|
NoTLSVerify: m.NoTLSVerify,
|
||||||
Path: path,
|
PathPatterns: pathPatterns,
|
||||||
SetHeaders: m.SetHeaders,
|
SetHeaders: setHeaders,
|
||||||
HideHeaders: m.HideHeaders,
|
HideHeaders: m.HideHeaders,
|
||||||
}, E.Nil()
|
}, E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
19
src/proxy/fields/headers.go
Normal file
19
src/proxy/fields/headers.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) {
|
||||||
|
h := make(http.Header)
|
||||||
|
for k, v := range headers {
|
||||||
|
vSplit := strings.Split(v, ",")
|
||||||
|
for _, header := range vSplit {
|
||||||
|
h.Add(k, strings.TrimSpace(header))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h, E.Nil()
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
package fields
|
|
||||||
|
|
||||||
import (
|
|
||||||
E "github.com/yusing/go-proxy/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Path string
|
|
||||||
|
|
||||||
func NewPath(s string) (Path, E.NestedError) {
|
|
||||||
if s == "" || s[0] == '/' {
|
|
||||||
return Path(s), E.Nil()
|
|
||||||
}
|
|
||||||
return "", E.Invalid("path", s).With("must be empty or start with '/'")
|
|
||||||
}
|
|
37
src/proxy/fields/path_pattern.go
Normal file
37
src/proxy/fields/path_pattern.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package fields
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PathPattern string
|
||||||
|
type PathPatterns = []PathPattern
|
||||||
|
|
||||||
|
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return "", E.Invalid("path", "must not be empty")
|
||||||
|
}
|
||||||
|
if !pathPattern.MatchString(string(s)) {
|
||||||
|
return "", E.Invalid("path pattern", s)
|
||||||
|
}
|
||||||
|
return PathPattern(s), E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return []PathPattern{"/"}, E.Nil()
|
||||||
|
}
|
||||||
|
pp := make(PathPatterns, len(s))
|
||||||
|
for i, v := range s {
|
||||||
|
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
pp[i] = pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pp, E.Nil()
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
|
@ -107,6 +107,7 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||||
func (p *Provider) StopAllRoutes() E.NestedError {
|
func (p *Provider) StopAllRoutes() E.NestedError {
|
||||||
if p.watcherCancel != nil {
|
if p.watcherCancel != nil {
|
||||||
p.watcherCancel()
|
p.watcherCancel()
|
||||||
|
p.watcherCancel = nil
|
||||||
}
|
}
|
||||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||||
nStopped := 0
|
nStopped := 0
|
||||||
|
@ -126,17 +127,9 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
||||||
func (p *Provider) ReloadRoutes() {
|
func (p *Provider) ReloadRoutes() {
|
||||||
defer p.l.Info("routes reloaded")
|
defer p.l.Info("routes reloaded")
|
||||||
|
|
||||||
select {
|
p.StopAllRoutes()
|
||||||
case p.reloadReqCh <- struct{}{}:
|
p.loadRoutes()
|
||||||
defer func() {
|
p.StartAllRoutes()
|
||||||
<-p.reloadReqCh
|
|
||||||
}()
|
|
||||||
p.StopAllRoutes()
|
|
||||||
p.loadRoutes()
|
|
||||||
p.StartAllRoutes()
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||||
|
@ -149,13 +142,14 @@ func (p *Provider) watchEvents() {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.reloadReqCh:
|
case <-p.reloadReqCh: // block until last reload is done
|
||||||
p.ReloadRoutes()
|
p.ReloadRoutes()
|
||||||
|
continue // ignore events once after reload
|
||||||
case event, ok := <-events:
|
case event, ok := <-events:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Infof("watcher event: %s", event)
|
l.Info(event)
|
||||||
p.reloadReqCh <- struct{}{}
|
p.reloadReqCh <- struct{}{}
|
||||||
case err, ok := <-errs:
|
case err, ok := <-errs:
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -19,23 +19,18 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
HTTPRoute struct {
|
HTTPRoute struct {
|
||||||
Alias PT.Alias `json:"alias"`
|
Alias PT.Alias `json:"alias"`
|
||||||
Subroutes HTTPSubroutes `json:"subroutes"`
|
|
||||||
|
|
||||||
mux *http.ServeMux
|
TargetURL URL
|
||||||
|
PathPatterns PT.PathPatterns
|
||||||
|
|
||||||
|
mux *http.ServeMux
|
||||||
|
handler *P.ReverseProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPSubroute struct {
|
URL url.URL
|
||||||
TargetURL *URL `json:"targetURL"`
|
PathKey = PT.PathPattern
|
||||||
Path PathKey `json:"path"`
|
SubdomainKey = PT.Alias
|
||||||
|
|
||||||
proxy *P.ReverseProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
URL url.URL
|
|
||||||
PathKey = PT.Path
|
|
||||||
SubdomainKey = PT.Alias
|
|
||||||
HTTPSubroutes = map[PathKey]HTTPSubroute
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
||||||
|
@ -57,49 +52,28 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||||
r, ok := httpRoutes.UnsafeGet(entry.Alias)
|
r, ok := httpRoutes.UnsafeGet(entry.Alias)
|
||||||
if !ok {
|
if !ok {
|
||||||
r = &HTTPRoute{
|
r = &HTTPRoute{
|
||||||
Alias: entry.Alias,
|
Alias: entry.Alias,
|
||||||
Subroutes: make(HTTPSubroutes),
|
TargetURL: URL(*entry.URL),
|
||||||
mux: http.NewServeMux(),
|
PathPatterns: entry.PathPatterns,
|
||||||
|
handler: rp,
|
||||||
}
|
}
|
||||||
httpRoutes.UnsafeSet(entry.Alias, r)
|
httpRoutes.UnsafeSet(entry.Alias, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
path := entry.Path
|
|
||||||
if _, exists := r.Subroutes[path]; exists {
|
|
||||||
return nil, E.Duplicated("path", path)
|
|
||||||
}
|
|
||||||
r.mux.HandleFunc(string(path), rp.ServeHTTP)
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
switch t := err.(type) {
|
|
||||||
case error:
|
|
||||||
// NOTE: likely path pattern error
|
|
||||||
return nil, E.From(t)
|
|
||||||
default:
|
|
||||||
return nil, E.From(fmt.Errorf("%v", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sr := HTTPSubroute{
|
|
||||||
TargetURL: (*URL)(entry.URL),
|
|
||||||
proxy: rp,
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite := rp.Rewrite
|
rewrite := rp.Rewrite
|
||||||
|
|
||||||
if logrus.GetLevel() == logrus.DebugLevel {
|
if logrus.GetLevel() == logrus.DebugLevel {
|
||||||
l := logrus.WithField("alias", entry.Alias)
|
l := logrus.WithField("alias", entry.Alias)
|
||||||
|
|
||||||
sr.proxy.Rewrite = func(pr *P.ProxyRequest) {
|
rp.Rewrite = func(pr *P.ProxyRequest) {
|
||||||
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
||||||
l.Debug("request headers: ", pr.In.Header)
|
l.Debug("request headers: ", pr.In.Header)
|
||||||
rewrite(pr)
|
rewrite(pr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sr.proxy.Rewrite = rewrite
|
rp.Rewrite = rewrite
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Subroutes[path] = sr
|
|
||||||
return r, E.Nil()
|
return r, E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,20 +82,20 @@ func (r *HTTPRoute) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTTPRoute) Start() E.NestedError {
|
func (r *HTTPRoute) Start() E.NestedError {
|
||||||
|
r.mux = http.NewServeMux()
|
||||||
|
for _, p := range r.PathPatterns {
|
||||||
|
r.mux.HandleFunc(string(p), r.handler.ServeHTTP)
|
||||||
|
}
|
||||||
httpRoutes.Set(r.Alias, r)
|
httpRoutes.Set(r.Alias, r)
|
||||||
return E.Nil()
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTTPRoute) Stop() E.NestedError {
|
func (r *HTTPRoute) Stop() E.NestedError {
|
||||||
|
r.mux = nil
|
||||||
httpRoutes.Delete(r.Alias)
|
httpRoutes.Delete(r.Alias)
|
||||||
return E.Nil()
|
return E.Nil()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
|
|
||||||
sr, ok := r.Subroutes[path]
|
|
||||||
return sr, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URL) String() string {
|
func (u *URL) String() string {
|
||||||
return (*url.URL)(u).String()
|
return (*url.URL)(u).String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package watcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
|
@ -47,14 +48,28 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
errCh <- E.From(<-cErrCh)
|
if err := <-cErrCh; err != nil {
|
||||||
|
errCh <- E.From(err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case msg := <-cEventCh:
|
case msg := <-cEventCh:
|
||||||
|
var Action Action
|
||||||
|
switch msg.Action {
|
||||||
|
case events.ActionStart:
|
||||||
|
Action = ActionCreated
|
||||||
|
case events.ActionDie:
|
||||||
|
Action = ActionDeleted
|
||||||
|
default: // NOTE: should not happen
|
||||||
|
Action = ActionModified
|
||||||
|
}
|
||||||
eventCh <- Event{
|
eventCh <- Event{
|
||||||
ActorName: msg.Actor.Attributes["name"],
|
ActorName: fmt.Sprintf("container %q", msg.Actor.Attributes["name"]),
|
||||||
Action: ActionModified,
|
Action: Action,
|
||||||
}
|
}
|
||||||
case err := <-cErrCh:
|
case err := <-cErrCh:
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
errCh <- E.From(err)
|
errCh <- E.From(err)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -74,7 +89,7 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||||
}
|
}
|
||||||
|
|
||||||
var dwOptions = events.ListOptions{Filters: filters.NewArgs(
|
var dwOptions = events.ListOptions{Filters: filters.NewArgs(
|
||||||
filters.Arg("type", "container"),
|
filters.Arg("type", string(events.ContainerEventType)),
|
||||||
filters.Arg("event", "start"),
|
filters.Arg("event", string(events.ActionStart)),
|
||||||
filters.Arg("event", "die"), // 'stop' already triggering 'die'
|
filters.Arg("event", string(events.ActionDie)), // 'stop' already triggering 'die'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -12,8 +12,9 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionModified Action = "MODIFIED"
|
ActionModified Action = "MODIFIED"
|
||||||
ActionDeleted Action = "DELETED"
|
|
||||||
ActionCreated Action = "CREATED"
|
ActionCreated Action = "CREATED"
|
||||||
|
ActionStarted Action = "STARTED"
|
||||||
|
ActionDeleted Action = "DELETED"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e Event) String() string {
|
func (e Event) String() string {
|
||||||
|
@ -23,11 +24,3 @@ func (e Event) String() string {
|
||||||
func (a Action) IsDelete() bool {
|
func (a Action) IsDelete() bool {
|
||||||
return a == ActionDeleted
|
return a == ActionDeleted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Action) IsModify() bool {
|
|
||||||
return a == ActionModified
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Action) IsCreate() bool {
|
|
||||||
return a == ActionCreated
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,10 +18,6 @@ func NewFileWatcher(filename string) Watcher {
|
||||||
return &fileWatcher{filename: filename}
|
return &fileWatcher{filename: filename}
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopAllFileWatchers() {
|
|
||||||
fwHelper.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||||
return fwHelper.Add(ctx, f)
|
return fwHelper.Add(ctx, f)
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,13 +80,6 @@ func (h *fileWatcherHelper) Remove(w *fileWatcher) {
|
||||||
delete(h.m, w.filename)
|
delete(h.m, w.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deinit closes the fs watcher
|
|
||||||
// and waits for the start() loop to finish
|
|
||||||
func (h *fileWatcherHelper) close() {
|
|
||||||
_ = h.w.Close()
|
|
||||||
h.wg.Wait() // wait for `start()` loop to finish
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *fileWatcherHelper) start() {
|
func (h *fileWatcherHelper) start() {
|
||||||
defer h.wg.Done()
|
defer h.wg.Done()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue