mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +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": {
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||
"providers.example.yml",
|
||||
"*.providers.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||
"providers.example.yml",
|
||||
"*.providers.yml",
|
||||
"providers.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ FROM alpine:latest
|
|||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY schema/ /app/schema
|
||||
# copy binary
|
||||
COPY --from=builder /src/go-proxy /app/
|
||||
COPY schema/ /app/schema
|
||||
|
||||
RUN chmod +x /app/go-proxy
|
||||
ENV DOCKER_HOST unix:///var/run/docker.sock
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
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)
|
||||
|
||||
|
@ -93,17 +93,16 @@
|
|||
|
||||
### Fields
|
||||
|
||||
| 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` |
|
||||
| `host` | proxy host | `container_name` | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `0 - 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> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path` | proxy path | empty | **(http/s only)** string |
|
||||
| `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>1</sup>](#1-key-value-mapping-example) |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>2</sup>](#2-list-example) |
|
||||
| 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` |
|
||||
| `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 `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> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `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)) |
|
||||
| `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 |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
#### Key-value mapping example
|
||||
|
||||
|
@ -142,6 +141,9 @@ services:
|
|||
nginx:
|
||||
...
|
||||
labels:
|
||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||
- GET /
|
||||
- POST /auth
|
||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
|
@ -152,6 +154,9 @@ File Provider
|
|||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
|
|
|
@ -23,16 +23,19 @@
|
|||
},
|
||||
"cert_path": {
|
||||
"title": "path of cert file to load/store",
|
||||
"description": "default: certs/cert.crt",
|
||||
"default": "certs/cert.crt",
|
||||
"markdownDescription": "default: `certs/cert.crt`",
|
||||
"type": "string"
|
||||
},
|
||||
"key_path": {
|
||||
"title": "path of key file to load/store",
|
||||
"description": "default: certs/priv.key",
|
||||
"default": "certs/priv.key",
|
||||
"markdownDescription": "default: `certs/priv.key`",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"title": "DNS Challenge Provider",
|
||||
"default": "local",
|
||||
"type": "string",
|
||||
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
||||
},
|
||||
|
@ -44,10 +47,11 @@
|
|||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"not": true,
|
||||
"const": "local"
|
||||
"not": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -151,7 +155,7 @@
|
|||
},
|
||||
"docker": {
|
||||
"title": "Docker provider configuration",
|
||||
"description": "docker clients (name: address)",
|
||||
"description": "docker clients (name-address pairs)",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9-_]+$": {
|
||||
|
@ -194,7 +198,7 @@
|
|||
"minimum": 0
|
||||
},
|
||||
"redirect_to_https": {
|
||||
"title": "Redirect to HTTPS",
|
||||
"title": "Redirect to HTTPS on HTTP requests",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -32,12 +32,17 @@
|
|||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "Auto detect base on port number"
|
||||
"description": "Auto detect base on port format"
|
||||
}
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"default": "localhost",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null",
|
||||
"description": "localhost (default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
|
@ -56,59 +61,38 @@
|
|||
],
|
||||
"title": "Proxy host (ipv4 / ipv6 / hostname)"
|
||||
},
|
||||
"port": {
|
||||
"title": "Proxy port"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"port": {},
|
||||
"no_tls_verify": {},
|
||||
"path_patterns": {},
|
||||
"set_headers": {},
|
||||
"hide_headers": {}
|
||||
},
|
||||
"required": ["host"],
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["http", "https"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"not": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{1,5}$",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"patternErrorMessage": "'port' must be a number"
|
||||
"pattern": "^\\d{1,5}$",
|
||||
"patternErrorMessage": "`port` must be a number"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
|
@ -117,11 +101,16 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"path_patterns": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Proxy path"
|
||||
"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*)+/?$",
|
||||
"patternErrorMessage": "invalid path pattern"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
|
@ -133,7 +122,6 @@
|
|||
"type": "object",
|
||||
"description": "Proxy headers to set",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -151,12 +139,15 @@
|
|||
"else": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "`listening port`:`proxy port | service name`",
|
||||
"markdownDescription": "`listening port:proxy port` or `listening port:service name`",
|
||||
"type": "string",
|
||||
"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
|
||||
},
|
||||
"set_headers": {
|
||||
|
@ -171,15 +162,22 @@
|
|||
},
|
||||
{
|
||||
"if": {
|
||||
"not": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"const": "https"
|
||||
}
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"const": "https"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"description": "Disable TLS verification for https proxy",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"not": true
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
U "github.com/yusing/go-proxy/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/config"
|
||||
PT "github.com/yusing/go-proxy/proxy/fields"
|
||||
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)
|
||||
return
|
||||
case *R.HTTPRoute:
|
||||
path, err := PT.NewPath(r.FormValue("path"))
|
||||
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())
|
||||
ok = U.IsSiteHealthy(route.TargetURL.String())
|
||||
case *R.StreamRoute:
|
||||
ok = U.IsStreamHealthy(
|
||||
string(route.Scheme.ProxyScheme),
|
||||
|
|
|
@ -1,32 +1,37 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func yamlParser[T any](value string) (any, E.NestedError) {
|
||||
var data T
|
||||
func yamlListParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return []string{}, E.Nil()
|
||||
}
|
||||
var data []string
|
||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
func setHeadersParser(value string) (any, E.NestedError) {
|
||||
func yamlStringMappingParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
lines := strings.Split(value, "\n")
|
||||
h := make(http.Header)
|
||||
h := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, E.Invalid("set header statement", line)
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
vals := strings.Split(parts[1], ",")
|
||||
for i := range vals {
|
||||
h.Add(key, strings.TrimSpace(vals[i]))
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if existing, ok := h[key]; ok {
|
||||
h[key] = existing + ", " + val
|
||||
} else {
|
||||
h[key] = val
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
|
@ -56,8 +61,9 @@ const NSProxy = "proxy"
|
|||
var _ = func() int {
|
||||
RegisterNamespace(NSProxy, ValueParserMap{
|
||||
"aliases": commaSepParser,
|
||||
"set_headers": setHeadersParser,
|
||||
"hide_headers": yamlParser[[]string],
|
||||
"path_patterns": yamlListParser,
|
||||
"set_headers": yamlStringMappingParser,
|
||||
"hide_headers": yamlListParser,
|
||||
"no_tls_verify": boolParser,
|
||||
})
|
||||
return 0
|
||||
|
|
|
@ -2,7 +2,6 @@ package docker
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -82,26 +81,22 @@ X-Custom-Header1: foo, bar
|
|||
X-Custom-Header1: baz
|
||||
X-Custom-Header2: boo`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
h := make(http.Header, 0)
|
||||
h.Set("X-Custom-Header1", "foo")
|
||||
h.Add("X-Custom-Header1", "bar")
|
||||
h.Add("X-Custom-Header1", "baz")
|
||||
h.Set("X-Custom-Header2", "boo")
|
||||
h := map[string]string{
|
||||
"X-Custom-Header1": "foo, bar, baz",
|
||||
"X-Custom-Header2": "boo",
|
||||
}
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
hGot, ok := pl.Value.(http.Header)
|
||||
hGot, ok := pl.Value.(map[string]string)
|
||||
if !ok {
|
||||
t.Error("value is not http.Header")
|
||||
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||
return
|
||||
}
|
||||
for k, vWant := range h {
|
||||
vGot := hGot[k]
|
||||
if !reflect.DeepEqual(vGot, vWant) {
|
||||
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
|
||||
}
|
||||
if !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("expected %v, got %v", h, hGot)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ import (
|
|||
R "github.com/yusing/go-proxy/route"
|
||||
"github.com/yusing/go-proxy/server"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
W "github.com/yusing/go-proxy/watcher"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -70,7 +69,6 @@ func main() {
|
|||
|
||||
onShutdown.Add(func() {
|
||||
docker.CloseAllClients()
|
||||
W.StopAllFileWatchers()
|
||||
cfg.Dispose()
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
|
@ -9,14 +8,14 @@ import (
|
|||
|
||||
type (
|
||||
ProxyEntry struct {
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
|
||||
Path string `yaml:"path" json:"path"` // http proxy only
|
||||
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) 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(s) proxy only
|
||||
}
|
||||
|
||||
ProxyEntries = *F.Map[string, *ProxyEntry]
|
||||
|
@ -37,8 +36,8 @@ func (e *ProxyEntry) SetDefaults() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if e.Path == "" {
|
||||
e.Path = "/"
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
|
|
|
@ -12,15 +12,15 @@ import (
|
|||
|
||||
type (
|
||||
Entry struct { // real model after validation
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
Path T.Path
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
PathPatterns T.PathPatterns
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
}
|
||||
StreamEntry struct {
|
||||
Alias T.Alias `json:"alias"`
|
||||
|
@ -51,7 +51,11 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
|||
if err.IsNotNil() {
|
||||
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() {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -60,15 +64,15 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
|||
return nil, err
|
||||
}
|
||||
return &Entry{
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
Path: path,
|
||||
SetHeaders: m.SetHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
}, 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 {
|
||||
if p.watcherCancel != nil {
|
||||
p.watcherCancel()
|
||||
p.watcherCancel = nil
|
||||
}
|
||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||
nStopped := 0
|
||||
|
@ -126,17 +127,9 @@ func (p *Provider) StopAllRoutes() E.NestedError {
|
|||
func (p *Provider) ReloadRoutes() {
|
||||
defer p.l.Info("routes reloaded")
|
||||
|
||||
select {
|
||||
case p.reloadReqCh <- struct{}{}:
|
||||
defer func() {
|
||||
<-p.reloadReqCh
|
||||
}()
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
default:
|
||||
return
|
||||
}
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
}
|
||||
|
||||
func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||
|
@ -149,13 +142,14 @@ func (p *Provider) watchEvents() {
|
|||
|
||||
for {
|
||||
select {
|
||||
case <-p.reloadReqCh:
|
||||
case <-p.reloadReqCh: // block until last reload is done
|
||||
p.ReloadRoutes()
|
||||
continue // ignore events once after reload
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
l.Infof("watcher event: %s", event)
|
||||
l.Info(event)
|
||||
p.reloadReqCh <- struct{}{}
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
|
|
|
@ -19,23 +19,18 @@ import (
|
|||
|
||||
type (
|
||||
HTTPRoute struct {
|
||||
Alias PT.Alias `json:"alias"`
|
||||
Subroutes HTTPSubroutes `json:"subroutes"`
|
||||
Alias PT.Alias `json:"alias"`
|
||||
|
||||
mux *http.ServeMux
|
||||
TargetURL URL
|
||||
PathPatterns PT.PathPatterns
|
||||
|
||||
mux *http.ServeMux
|
||||
handler *P.ReverseProxy
|
||||
}
|
||||
|
||||
HTTPSubroute struct {
|
||||
TargetURL *URL `json:"targetURL"`
|
||||
Path PathKey `json:"path"`
|
||||
|
||||
proxy *P.ReverseProxy
|
||||
}
|
||||
|
||||
URL url.URL
|
||||
PathKey = PT.Path
|
||||
SubdomainKey = PT.Alias
|
||||
HTTPSubroutes = map[PathKey]HTTPSubroute
|
||||
URL url.URL
|
||||
PathKey = PT.PathPattern
|
||||
SubdomainKey = PT.Alias
|
||||
)
|
||||
|
||||
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
||||
|
@ -57,49 +52,28 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
|||
r, ok := httpRoutes.UnsafeGet(entry.Alias)
|
||||
if !ok {
|
||||
r = &HTTPRoute{
|
||||
Alias: entry.Alias,
|
||||
Subroutes: make(HTTPSubroutes),
|
||||
mux: http.NewServeMux(),
|
||||
Alias: entry.Alias,
|
||||
TargetURL: URL(*entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
handler: rp,
|
||||
}
|
||||
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
|
||||
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
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 headers: ", pr.In.Header)
|
||||
rewrite(pr)
|
||||
}
|
||||
} else {
|
||||
sr.proxy.Rewrite = rewrite
|
||||
rp.Rewrite = rewrite
|
||||
}
|
||||
|
||||
r.Subroutes[path] = sr
|
||||
return r, E.Nil()
|
||||
}
|
||||
|
||||
|
@ -108,20 +82,20 @@ func (r *HTTPRoute) String() string {
|
|||
}
|
||||
|
||||
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)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Stop() E.NestedError {
|
||||
r.mux = nil
|
||||
httpRoutes.Delete(r.Alias)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
|
||||
sr, ok := r.Subroutes[path]
|
||||
return sr, ok
|
||||
}
|
||||
|
||||
func (u *URL) String() string {
|
||||
return (*url.URL)(u).String()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package watcher
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/events"
|
||||
|
@ -47,14 +48,28 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
|||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errCh <- E.From(<-cErrCh)
|
||||
if err := <-cErrCh; err != nil {
|
||||
errCh <- E.From(err)
|
||||
}
|
||||
return
|
||||
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{
|
||||
ActorName: msg.Actor.Attributes["name"],
|
||||
Action: ActionModified,
|
||||
ActorName: fmt.Sprintf("container %q", msg.Actor.Attributes["name"]),
|
||||
Action: Action,
|
||||
}
|
||||
case err := <-cErrCh:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
select {
|
||||
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(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("event", "start"),
|
||||
filters.Arg("event", "die"), // 'stop' already triggering 'die'
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("event", string(events.ActionStart)),
|
||||
filters.Arg("event", string(events.ActionDie)), // 'stop' already triggering 'die'
|
||||
)}
|
||||
|
|
|
@ -12,8 +12,9 @@ type (
|
|||
|
||||
const (
|
||||
ActionModified Action = "MODIFIED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
ActionCreated Action = "CREATED"
|
||||
ActionStarted Action = "STARTED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
)
|
||||
|
||||
func (e Event) String() string {
|
||||
|
@ -23,11 +24,3 @@ func (e Event) String() string {
|
|||
func (a Action) IsDelete() bool {
|
||||
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}
|
||||
}
|
||||
|
||||
func StopAllFileWatchers() {
|
||||
fwHelper.close()
|
||||
}
|
||||
|
||||
func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return fwHelper.Add(ctx, f)
|
||||
}
|
||||
|
|
|
@ -80,13 +80,6 @@ func (h *fileWatcherHelper) Remove(w *fileWatcher) {
|
|||
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() {
|
||||
defer h.wg.Done()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue