mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
correcting some behaviors for $DOCKER_HOST, now uses container's private IP instead of localhost
This commit is contained in:
parent
d91b66ae87
commit
7184c9cfe9
68 changed files with 925 additions and 570 deletions
136
.golangci.yml
Normal file
136
.golangci.yml
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
run:
|
||||||
|
timeout: 10m
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
- fieldalignment
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 14
|
||||||
|
goconst:
|
||||||
|
min-len: 3
|
||||||
|
min-occurrences: 4
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
funlen:
|
||||||
|
lines: -1
|
||||||
|
statements: 120
|
||||||
|
forbidigo:
|
||||||
|
forbid:
|
||||||
|
- ^print(ln)?$
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- FIXME
|
||||||
|
tagalign:
|
||||||
|
align: false
|
||||||
|
sort: true
|
||||||
|
order:
|
||||||
|
- description
|
||||||
|
- json
|
||||||
|
- toml
|
||||||
|
- yaml
|
||||||
|
- yml
|
||||||
|
- label
|
||||||
|
- label-slice-as-struct
|
||||||
|
- file
|
||||||
|
- kv
|
||||||
|
- export
|
||||||
|
stylecheck:
|
||||||
|
dot-import-whitelist:
|
||||||
|
- github.com/yusing/go-proxy/internal/utils/testing # go tests only
|
||||||
|
- github.com/yusing/go-proxy/internal/api/v1/utils # api only
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: struct-tag
|
||||||
|
- name: blank-imports
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: context-keys-type
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: exported
|
||||||
|
disabled: true
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: var-naming
|
||||||
|
- name: var-declaration
|
||||||
|
- name: package-comments
|
||||||
|
disabled: true
|
||||||
|
- name: range
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: time-naming
|
||||||
|
- name: unexported-return
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: errorf
|
||||||
|
- name: empty-block
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: unused-parameter
|
||||||
|
disabled: true
|
||||||
|
- name: unreachable-code
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
gomoddirectives:
|
||||||
|
replace-allow-list:
|
||||||
|
- github.com/abbot/go-http-auth
|
||||||
|
- github.com/gorilla/mux
|
||||||
|
- github.com/mailgun/minheap
|
||||||
|
- github.com/mailgun/multibuf
|
||||||
|
- github.com/jaguilar/vt100
|
||||||
|
- github.com/cucumber/godog
|
||||||
|
- github.com/http-wasm/http-wasm-host-go
|
||||||
|
testifylint:
|
||||||
|
disable:
|
||||||
|
- suite-dont-use-pkg
|
||||||
|
- require-error
|
||||||
|
- go-require
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1019
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- fmt.Fprintln
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- execinquery # deprecated
|
||||||
|
- gomnd # deprecated
|
||||||
|
- sqlclosecheck # not relevant (SQL)
|
||||||
|
- rowserrcheck # not relevant (SQL)
|
||||||
|
- cyclop # duplicate of gocyclo
|
||||||
|
- depguard # Not relevant
|
||||||
|
- nakedret # Too strict
|
||||||
|
- lll # Not relevant
|
||||||
|
- gocyclo # FIXME must be fixed
|
||||||
|
- gocognit # Too strict
|
||||||
|
- nestif # Too many false-positive.
|
||||||
|
- prealloc # Too many false-positive.
|
||||||
|
- makezero # Not relevant
|
||||||
|
- dupl # Too strict
|
||||||
|
- gosec # Too strict
|
||||||
|
- gochecknoinits
|
||||||
|
- gochecknoglobals
|
||||||
|
- wsl # Too strict
|
||||||
|
- nlreturn # Not relevant
|
||||||
|
- mnd # Too strict
|
||||||
|
- testpackage # Too strict
|
||||||
|
- tparallel # Not relevant
|
||||||
|
- paralleltest # Not relevant
|
||||||
|
- exhaustive # Not relevant
|
||||||
|
- exhaustruct # Not relevant
|
||||||
|
- err113 # Too strict
|
||||||
|
- wrapcheck # Too strict
|
||||||
|
- noctx # Too strict
|
||||||
|
- bodyclose # too many false-positive
|
||||||
|
- forcetypeassert # Too strict
|
||||||
|
- tagliatelle # Too strict
|
||||||
|
- varnamelen # Not relevant
|
||||||
|
- nilnil # Not relevant
|
||||||
|
- ireturn # Not relevant
|
||||||
|
- contextcheck # too many false-positive
|
||||||
|
- containedctx # too many false-positive
|
||||||
|
- maintidx # kind of duplicate of gocyclo
|
||||||
|
- nonamedreturns # Too strict
|
||||||
|
- gosmopolitan # not relevant
|
||||||
|
- exportloopref # Not relevant since go1.22
|
9
.trunk/.gitignore
vendored
Normal file
9
.trunk/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
*out
|
||||||
|
*logs
|
||||||
|
*actions
|
||||||
|
*notifications
|
||||||
|
*tools
|
||||||
|
plugins
|
||||||
|
user_trunk.yaml
|
||||||
|
user.yaml
|
||||||
|
tmp
|
41
.trunk/trunk.yaml
Normal file
41
.trunk/trunk.yaml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
|
||||||
|
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||||
|
version: 0.1
|
||||||
|
cli:
|
||||||
|
version: 1.22.6
|
||||||
|
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||||
|
plugins:
|
||||||
|
sources:
|
||||||
|
- id: trunk
|
||||||
|
ref: v1.6.3
|
||||||
|
uri: https://github.com/trunk-io/plugins
|
||||||
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
|
runtimes:
|
||||||
|
enabled:
|
||||||
|
- node@18.12.1
|
||||||
|
- python@3.10.8
|
||||||
|
- go@1.23.2
|
||||||
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
|
lint:
|
||||||
|
enabled:
|
||||||
|
- hadolint@2.12.0
|
||||||
|
- actionlint@1.7.3
|
||||||
|
- checkov@3.2.257
|
||||||
|
- git-diff-check
|
||||||
|
- gofmt@1.20.4
|
||||||
|
- golangci-lint@1.61.0
|
||||||
|
- markdownlint@0.42.0
|
||||||
|
- osv-scanner@1.9.0
|
||||||
|
- oxipng@9.1.2
|
||||||
|
- prettier@3.3.3
|
||||||
|
- shellcheck@0.10.0
|
||||||
|
- shfmt@3.6.0
|
||||||
|
- trufflehog@3.82.7
|
||||||
|
- yamllint@1.35.1
|
||||||
|
actions:
|
||||||
|
disabled:
|
||||||
|
- trunk-announce
|
||||||
|
- trunk-check-pre-push
|
||||||
|
- trunk-fmt-pre-commit
|
||||||
|
enabled:
|
||||||
|
- trunk-upgrade-available
|
2
go.mod
2
go.mod
|
@ -19,7 +19,7 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.106.0 // indirect
|
github.com/cloudflare/cloudflare-go v0.107.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cloudflare/cloudflare-go v0.106.0 h1:q41gC5Wc1nfi0D1ZhSHokWcd9mGMbqC7RE7qiP+qE00=
|
github.com/cloudflare/cloudflare-go v0.107.0 h1:cMDIw2tzt6TXCJyMFVyP+BPOVkIfMvcKjhMNSNvuEPc=
|
||||||
github.com/cloudflare/cloudflare-go v0.106.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
|
github.com/cloudflare/cloudflare-go v0.107.0/go.mod h1:5cYGzVBqNTLxMYSLdVjuSs5LJL517wJDSvMPWUrzHzc=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
"github.com/yusing/go-proxy/internal/api/v1/errorpage"
|
||||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
|
@ -36,11 +36,11 @@ func NewHandler(cfg *config.Config) http.Handler {
|
||||||
mux.HandleFunc("PUT", "/v1/file/{filename...}", v1.SetFileContent)
|
mux.HandleFunc("PUT", "/v1/file/{filename...}", v1.SetFileContent)
|
||||||
mux.HandleFunc("GET", "/v1/stats", wrap(cfg, v1.Stats))
|
mux.HandleFunc("GET", "/v1/stats", wrap(cfg, v1.Stats))
|
||||||
mux.HandleFunc("GET", "/v1/stats/ws", wrap(cfg, v1.StatsWS))
|
mux.HandleFunc("GET", "/v1/stats/ws", wrap(cfg, v1.StatsWS))
|
||||||
mux.HandleFunc("GET", "/v1/error_page", error_page.GetHandleFunc())
|
mux.HandleFunc("GET", "/v1/error_page", errorpage.GetHandleFunc())
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow only requests to API server with host matching common.APIHTTPAddr
|
// allow only requests to API server with host matching common.APIHTTPAddr.
|
||||||
func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
||||||
if common.IsDebug {
|
if common.IsDebug {
|
||||||
return f
|
return f
|
||||||
|
@ -48,8 +48,7 @@ func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Host != common.APIHTTPAddr {
|
if r.Host != common.APIHTTPAddr {
|
||||||
Logger.Warnf("invalid request to API server with host: %s, expect %s", r.Host, common.APIHTTPAddr)
|
Logger.Warnf("invalid request to API server with host: %s, expect %s", r.Host, common.APIHTTPAddr)
|
||||||
w.WriteHeader(http.StatusForbidden)
|
http.Error(w, "invalid request", http.StatusForbidden)
|
||||||
w.Write([]byte("invalid request"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f(w, r)
|
f(w, r)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
R "github.com/yusing/go-proxy/internal/route"
|
R "github.com/yusing/go-proxy/internal/route"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,7 @@ import (
|
||||||
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
target := r.FormValue("target")
|
target := r.FormValue("target")
|
||||||
if target == "" {
|
if target == "" {
|
||||||
U.HandleErr(w, r, U.ErrMissingKey("target"), http.StatusBadRequest)
|
HandleErr(w, r, ErrMissingKey("target"), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case route == nil:
|
case route == nil:
|
||||||
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
HandleErr(w, r, ErrNotFound("target", target), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
case route.Type() == R.RouteTypeReverseProxy:
|
case route.Type() == R.RouteTypeReverseProxy:
|
||||||
ok = IsSiteHealthy(route.URL().String())
|
ok = IsSiteHealthy(route.URL().String())
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package error_page
|
package errorpage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -7,7 +7,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
api "github.com/yusing/go-proxy/internal/api/v1/utils"
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
@ -17,6 +17,11 @@ import (
|
||||||
|
|
||||||
const errPagesBasePath = common.ErrorPagesBasePath
|
const errPagesBasePath = common.ErrorPagesBasePath
|
||||||
|
|
||||||
|
var (
|
||||||
|
dirWatcher W.Watcher
|
||||||
|
fileContentMap = F.NewMapOf[string, []byte]()
|
||||||
|
)
|
||||||
|
|
||||||
var setup = sync.OnceFunc(func() {
|
var setup = sync.OnceFunc(func() {
|
||||||
dirWatcher = W.NewDirectoryWatcher(context.Background(), errPagesBasePath)
|
dirWatcher = W.NewDirectoryWatcher(context.Background(), errPagesBasePath)
|
||||||
loadContent()
|
loadContent()
|
||||||
|
@ -27,7 +32,7 @@ func GetStaticFile(filename string) ([]byte, bool) {
|
||||||
return fileContentMap.Load(filename)
|
return fileContentMap.Load(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try <statusCode>.html -> 404.html -> not ok
|
// try <statusCode>.html -> 404.html -> not ok.
|
||||||
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
||||||
content, ok = fileContentMap.Load(fmt.Sprintf("%d.html", statusCode))
|
content, ok = fileContentMap.Load(fmt.Sprintf("%d.html", statusCode))
|
||||||
if !ok && statusCode != 404 {
|
if !ok && statusCode != 404 {
|
||||||
|
@ -39,7 +44,7 @@ func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
||||||
func loadContent() {
|
func loadContent() {
|
||||||
files, err := U.ListFiles(errPagesBasePath, 0)
|
files, err := U.ListFiles(errPagesBasePath, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.Logger.Error(err)
|
Logger.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
@ -48,11 +53,11 @@ func loadContent() {
|
||||||
}
|
}
|
||||||
content, err := os.ReadFile(file)
|
content, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.Logger.Errorf("failed to read error page resource %s: %s", file, err)
|
Logger.Errorf("failed to read error page resource %s: %s", file, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
file = path.Base(file)
|
file = path.Base(file)
|
||||||
api.Logger.Infof("error page resource %s loaded", file)
|
Logger.Infof("error page resource %s loaded", file)
|
||||||
fileContentMap.Store(file, content)
|
fileContentMap.Store(file, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,17 +77,14 @@ func watchDir() {
|
||||||
loadContent()
|
loadContent()
|
||||||
case events.ActionFileDeleted:
|
case events.ActionFileDeleted:
|
||||||
fileContentMap.Delete(filename)
|
fileContentMap.Delete(filename)
|
||||||
api.Logger.Infof("error page resource %s deleted", filename)
|
Logger.Infof("error page resource %s deleted", filename)
|
||||||
case events.ActionFileRenamed:
|
case events.ActionFileRenamed:
|
||||||
api.Logger.Infof("error page resource %s deleted", filename)
|
Logger.Infof("error page resource %s deleted", filename)
|
||||||
fileContentMap.Delete(filename)
|
fileContentMap.Delete(filename)
|
||||||
loadContent()
|
loadContent()
|
||||||
}
|
}
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
api.Logger.Errorf("error watching error page directory: %s", err)
|
Logger.Errorf("error watching error page directory: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirWatcher W.Watcher
|
|
||||||
var fileContentMap = F.NewMapOf[string, []byte]()
|
|
|
@ -1,6 +1,10 @@
|
||||||
package error_page
|
package errorpage
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
func GetHandleFunc() http.HandlerFunc {
|
func GetHandleFunc() http.HandlerFunc {
|
||||||
setup()
|
setup()
|
||||||
|
@ -21,5 +25,7 @@ func serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "404 not found", http.StatusNotFound)
|
http.Error(w, "404 not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(content)
|
if _, err := w.Write(content); err != nil {
|
||||||
|
HandleErr(w, r, err)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@ func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(content)
|
U.WriteBody(w, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -47,11 +47,11 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if validateErr != nil {
|
if validateErr != nil {
|
||||||
U.RespondJson(w, validateErr.JSONObject(), http.StatusBadRequest)
|
U.RespondJSON(w, r, validateErr.JSONObject(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0644)
|
err = os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
)
|
||||||
|
|
||||||
func Index(w http.ResponseWriter, r *http.Request) {
|
func Index(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("API ready"))
|
WriteBody(w, []byte("API ready"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
U.HandleErr(w, r, U.RespondJson(w, routes))
|
U.RespondJSON(w, r, routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -67,21 +67,21 @@ func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
for i := range files {
|
for i := range files {
|
||||||
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
|
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
|
||||||
}
|
}
|
||||||
U.HandleErr(w, r, U.RespondJson(w, files))
|
U.RespondJSON(w, r, files)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listMiddlewareTrace(w http.ResponseWriter, r *http.Request) {
|
func listMiddlewareTrace(w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.RespondJson(w, middleware.GetAllTrace()))
|
U.RespondJSON(w, r, middleware.GetAllTrace())
|
||||||
}
|
}
|
||||||
|
|
||||||
func listMiddlewares(w http.ResponseWriter, r *http.Request) {
|
func listMiddlewares(w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.RespondJson(w, middleware.All()))
|
U.RespondJSON(w, r, middleware.All())
|
||||||
}
|
}
|
||||||
|
|
||||||
func listMatchDomains(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func listMatchDomains(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.RespondJson(w, cfg.Value().MatchDomains))
|
U.RespondJSON(w, r, cfg.Value().MatchDomains)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listHomepageConfig(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func listHomepageConfig(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.RespondJson(w, cfg.HomepageConfig()))
|
U.RespondJSON(w, r, cfg.HomepageConfig())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
if err := cfg.Reload(); err != nil {
|
if err := cfg.Reload(); err != nil {
|
||||||
U.RespondJson(w, err.JSONObject(), http.StatusInternalServerError)
|
U.RespondJSON(w, r, err.JSONObject(), http.StatusInternalServerError)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,17 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
"github.com/yusing/go-proxy/internal/server"
|
"github.com/yusing/go-proxy/internal/server"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
|
||||||
"github.com/coder/websocket/wsjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
U.HandleErr(w, r, U.RespondJson(w, getStats(cfg)))
|
U.RespondJSON(w, r, getStats(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -42,6 +41,7 @@ func StatsWS(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||||
U.Logger.Errorf("/stats/ws failed to upgrade websocket: %s", err)
|
U.Logger.Errorf("/stats/ws failed to upgrade websocket: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
/* trunk-ignore(golangci-lint/errcheck) */
|
||||||
defer conn.CloseNow()
|
defer conn.CloseNow()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
|
@ -8,20 +8,22 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var HTTPClient = &http.Client{
|
var (
|
||||||
Timeout: common.ConnectionTimeout,
|
HTTPClient = &http.Client{
|
||||||
Transport: &http.Transport{
|
Timeout: common.ConnectionTimeout,
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Transport: &http.Transport{
|
||||||
DisableKeepAlives: true,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
ForceAttemptHTTP2: true,
|
DisableKeepAlives: true,
|
||||||
DialContext: (&net.Dialer{
|
ForceAttemptHTTP2: true,
|
||||||
Timeout: common.DialTimeout,
|
DialContext: (&net.Dialer{
|
||||||
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
|
Timeout: common.DialTimeout,
|
||||||
}).DialContext,
|
KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
}).DialContext,
|
||||||
},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var Get = HTTPClient.Get
|
Get = HTTPClient.Get
|
||||||
var Post = HTTPClient.Post
|
Post = HTTPClient.Post
|
||||||
var Head = HTTPClient.Head
|
Head = HTTPClient.Head
|
||||||
|
)
|
||||||
|
|
|
@ -5,16 +5,26 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RespondJson(w http.ResponseWriter, data any, code ...int) error {
|
func WriteBody(w http.ResponseWriter, body []byte) {
|
||||||
|
if _, err := w.Write(body); err != nil {
|
||||||
|
HandleErr(w, nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int) bool {
|
||||||
if len(code) > 0 {
|
if len(code) > 0 {
|
||||||
w.WriteHeader(code[0])
|
w.WriteHeader(code[0])
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
j, err := json.MarshalIndent(data, "", " ")
|
j, err := json.MarshalIndent(data, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
HandleErr(w, r, err)
|
||||||
} else {
|
return false
|
||||||
w.Write(j)
|
|
||||||
}
|
}
|
||||||
return nil
|
_, err = w.Write(j)
|
||||||
|
if err != nil {
|
||||||
|
HandleErr(w, r, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ package v1
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte(pkg.GetVersion()))
|
WriteBody(w, []byte(pkg.GetVersion()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,22 +16,23 @@ import (
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/types"
|
"github.com/yusing/go-proxy/internal/types"
|
||||||
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type (
|
||||||
cfg *Config
|
Provider struct {
|
||||||
user *User
|
cfg *Config
|
||||||
legoCfg *lego.Config
|
user *User
|
||||||
client *lego.Client
|
legoCfg *lego.Config
|
||||||
|
client *lego.Client
|
||||||
|
|
||||||
tlsCert *tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
certExpiries CertExpiries
|
certExpiries CertExpiries
|
||||||
}
|
}
|
||||||
|
ProviderGenerator func(types.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
||||||
|
|
||||||
type ProviderGenerator func(types.AutocertProviderOpt) (challenge.Provider, E.NestedError)
|
CertExpiries map[string]time.Time
|
||||||
type CertExpiries map[string]time.Time
|
)
|
||||||
|
|
||||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if p.tlsCert == nil {
|
if p.tlsCert == nil {
|
||||||
|
@ -192,8 +193,8 @@ func (p *Provider) registerACME() E.NestedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
func (p *Provider) saveCert(cert *certificate.Resource) E.NestedError {
|
||||||
//* This should have been done in setup
|
/* This should have been done in setup
|
||||||
//* but double check is always a good choice
|
but double check is always a good choice.*/
|
||||||
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
|
|
@ -36,14 +36,12 @@ const (
|
||||||
ErrorPagesBasePath = "error_pages"
|
ErrorPagesBasePath = "error_pages"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var RequiredDirectories = []string{
|
||||||
RequiredDirectories = []string{
|
ConfigBasePath,
|
||||||
ConfigBasePath,
|
SchemaBasePath,
|
||||||
SchemaBasePath,
|
ErrorPagesBasePath,
|
||||||
ErrorPagesBasePath,
|
MiddlewareComposeBasePath,
|
||||||
MiddlewareComposeBasePath,
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/autocert"
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
|
||||||
PR "github.com/yusing/go-proxy/internal/proxy/provider"
|
PR "github.com/yusing/go-proxy/internal/proxy/provider"
|
||||||
R "github.com/yusing/go-proxy/internal/route"
|
R "github.com/yusing/go-proxy/internal/route"
|
||||||
"github.com/yusing/go-proxy/internal/types"
|
"github.com/yusing/go-proxy/internal/types"
|
||||||
|
|
|
@ -21,29 +21,21 @@ type Client struct {
|
||||||
l logrus.FieldLogger
|
l logrus.FieldLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseDockerHostname(host string) (string, E.NestedError) {
|
var (
|
||||||
switch host {
|
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
|
||||||
case common.DockerHostFromEnv, "":
|
clientMapMu sync.Mutex
|
||||||
return "localhost", nil
|
|
||||||
}
|
|
||||||
url, err := E.Check(client.ParseHostURL(host))
|
|
||||||
if err != nil {
|
|
||||||
return "", E.Invalid("host", host).With(err)
|
|
||||||
}
|
|
||||||
return url.Hostname(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) DaemonHostname() string {
|
clientOptEnvHost = []client.Opt{
|
||||||
// DaemonHost should always return a valid host
|
client.WithHostFromEnv(),
|
||||||
hostname, _ := ParseDockerHostname(c.DaemonHost())
|
client.WithAPIVersionNegotiation(),
|
||||||
return hostname
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
func (c Client) Connected() bool {
|
func (c Client) Connected() bool {
|
||||||
return c.Client != nil
|
return c.Client != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the client is still referenced, this is no-op
|
// if the client is still referenced, this is no-op.
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
if c.refCount.Add(-1) > 0 {
|
if c.refCount.Add(-1) > 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -86,6 +78,8 @@ func ConnectClient(host string) (Client, E.NestedError) {
|
||||||
var opt []client.Opt
|
var opt []client.Opt
|
||||||
|
|
||||||
switch host {
|
switch host {
|
||||||
|
case "":
|
||||||
|
return Client{}, E.Invalid("docker host", "empty")
|
||||||
case common.DockerHostFromEnv:
|
case common.DockerHostFromEnv:
|
||||||
opt = clientOptEnvHost
|
opt = clientOptEnvHost
|
||||||
default:
|
default:
|
||||||
|
@ -139,15 +133,3 @@ func CloseAllClients() {
|
||||||
clientMap.Clear()
|
clientMap.Clear()
|
||||||
logger.Debug("closed all clients")
|
logger.Debug("closed all clients")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
|
|
||||||
clientMapMu sync.Mutex
|
|
||||||
|
|
||||||
clientOptEnvHost = []client.Opt{
|
|
||||||
client.WithHostFromEnv(),
|
|
||||||
client.WithAPIVersionNegotiation(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = logrus.WithField("module", "docker")
|
|
||||||
)
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,78 @@
|
||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Container struct {
|
type (
|
||||||
*types.Container
|
PortMapping = map[string]types.Port
|
||||||
*ProxyProperties
|
Container struct {
|
||||||
}
|
_ U.NoCopy
|
||||||
|
|
||||||
func FromDocker(c *types.Container, dockerHost string) (res Container) {
|
DockerHost string `json:"docker_host" yaml:"-"`
|
||||||
res.Container = c
|
ContainerName string `json:"container_name" yaml:"-"`
|
||||||
isExplicit := c.Labels[LabelAliases] != ""
|
ContainerID string `json:"container_id" yaml:"-"`
|
||||||
res.ProxyProperties = &ProxyProperties{
|
ImageName string `json:"image_name" yaml:"-"`
|
||||||
DockerHost: dockerHost,
|
|
||||||
ContainerName: res.getName(),
|
Labels map[string]string `json:"labels" yaml:"-"`
|
||||||
ContainerID: c.ID,
|
|
||||||
ImageName: res.getImageName(),
|
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
|
||||||
PublicPortMapping: res.getPublicPortMapping(),
|
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
|
||||||
PrivatePortMapping: res.getPrivatePortMapping(),
|
PublicIP string `json:"public_ip" yaml:"-"`
|
||||||
NetworkMode: c.HostConfig.NetworkMode,
|
PrivateIP string `json:"private_ip" yaml:"-"`
|
||||||
Aliases: res.getAliases(),
|
NetworkMode string `json:"network_mode" yaml:"-"`
|
||||||
IsExcluded: U.ParseBool(res.getDeleteLabel(LabelExclude)),
|
|
||||||
IsExplicit: isExplicit,
|
Aliases []string `json:"aliases" yaml:"-"`
|
||||||
IsDatabase: res.isDatabase(),
|
IsExcluded bool `json:"is_excluded" yaml:"-"`
|
||||||
IdleTimeout: res.getDeleteLabel(LabelIdleTimeout),
|
IsExplicit bool `json:"is_explicit" yaml:"-"`
|
||||||
WakeTimeout: res.getDeleteLabel(LabelWakeTimeout),
|
IsDatabase bool `json:"is_database" yaml:"-"`
|
||||||
StopMethod: res.getDeleteLabel(LabelStopMethod),
|
IdleTimeout string `json:"idle_timeout" yaml:"-"`
|
||||||
StopTimeout: res.getDeleteLabel(LabelStopTimeout),
|
WakeTimeout string `json:"wake_timeout" yaml:"-"`
|
||||||
StopSignal: res.getDeleteLabel(LabelStopSignal),
|
StopMethod string `json:"stop_method" yaml:"-"`
|
||||||
Running: c.Status == "running" || c.State == "running",
|
StopTimeout string `json:"stop_timeout" yaml:"-"` // stop_method = "stop" only
|
||||||
|
StopSignal string `json:"stop_signal" yaml:"-"` // stop_method = "stop" | "kill" only
|
||||||
|
Running bool `json:"running" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
||||||
|
isExplicit := c.Labels[LabelAliases] != ""
|
||||||
|
helper := containerHelper{c}
|
||||||
|
res = &Container{
|
||||||
|
DockerHost: dockerHost,
|
||||||
|
ContainerName: helper.getName(),
|
||||||
|
ContainerID: c.ID,
|
||||||
|
ImageName: helper.getImageName(),
|
||||||
|
|
||||||
|
Labels: c.Labels,
|
||||||
|
|
||||||
|
PublicPortMapping: helper.getPublicPortMapping(),
|
||||||
|
PrivatePortMapping: helper.getPrivatePortMapping(),
|
||||||
|
NetworkMode: c.HostConfig.NetworkMode,
|
||||||
|
|
||||||
|
Aliases: helper.getAliases(),
|
||||||
|
IsExcluded: U.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
||||||
|
IsExplicit: isExplicit,
|
||||||
|
IsDatabase: helper.isDatabase(),
|
||||||
|
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
|
||||||
|
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
|
||||||
|
StopMethod: helper.getDeleteLabel(LabelStopMethod),
|
||||||
|
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
|
||||||
|
StopSignal: helper.getDeleteLabel(LabelStopSignal),
|
||||||
|
Running: c.Status == "running" || c.State == "running",
|
||||||
|
}
|
||||||
|
res.setPrivateIP(helper)
|
||||||
|
res.setPublicIP()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromJson(json types.ContainerJSON, dockerHost string) Container {
|
func FromJSON(json types.ContainerJSON, dockerHost string) *Container {
|
||||||
ports := make([]types.Port, 0)
|
ports := make([]types.Port, 0)
|
||||||
for k, bindings := range json.NetworkSettings.Ports {
|
for k, bindings := range json.NetworkSettings.Ports {
|
||||||
for _, v := range bindings {
|
for _, v := range bindings {
|
||||||
|
@ -65,79 +98,32 @@ func FromJson(json types.ContainerJSON, dockerHost string) Container {
|
||||||
return cont
|
return cont
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Container) getDeleteLabel(label string) string {
|
func (c *Container) setPublicIP() {
|
||||||
if l, ok := c.Labels[label]; ok {
|
if c.PublicPortMapping == nil {
|
||||||
delete(c.Labels, label)
|
return
|
||||||
return l
|
|
||||||
}
|
}
|
||||||
return ""
|
if strings.HasPrefix(c.DockerHost, "unix://") {
|
||||||
|
c.PublicIP = "127.0.0.1"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
url, err := url.Parse(c.DockerHost)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("invalid docker host %q: %v\nfalling back to 127.0.0.1", c.DockerHost, err)
|
||||||
|
c.PublicIP = "127.0.0.1"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.PublicIP = url.Hostname()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Container) getAliases() []string {
|
func (c *Container) setPrivateIP(helper containerHelper) {
|
||||||
if l := c.getDeleteLabel(LabelAliases); l != "" {
|
if !strings.HasPrefix(c.DockerHost, "unix://") {
|
||||||
return U.CommaSeperatedList(l)
|
return
|
||||||
} else {
|
}
|
||||||
return []string{c.getName()}
|
if helper.NetworkSettings == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, v := range helper.NetworkSettings.Networks {
|
||||||
|
c.PrivateIP = v.IPAddress
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Container) getName() string {
|
|
||||||
return strings.TrimPrefix(c.Names[0], "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Container) getImageName() string {
|
|
||||||
colonSep := strings.Split(c.Image, ":")
|
|
||||||
slashSep := strings.Split(colonSep[0], "/")
|
|
||||||
return slashSep[len(slashSep)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Container) getPublicPortMapping() PortMapping {
|
|
||||||
res := make(PortMapping)
|
|
||||||
for _, v := range c.Ports {
|
|
||||||
if v.PublicPort == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res[fmt.Sprint(v.PublicPort)] = v
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Container) getPrivatePortMapping() PortMapping {
|
|
||||||
res := make(PortMapping)
|
|
||||||
for _, v := range c.Ports {
|
|
||||||
res[fmt.Sprint(v.PrivatePort)] = v
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
var databaseMPs = map[string]struct{}{
|
|
||||||
"/var/lib/postgresql/data": {},
|
|
||||||
"/var/lib/mysql": {},
|
|
||||||
"/var/lib/mongodb": {},
|
|
||||||
"/var/lib/mariadb": {},
|
|
||||||
"/var/lib/memcached": {},
|
|
||||||
"/var/lib/rabbitmq": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
var databasePrivPorts = map[uint16]struct{}{
|
|
||||||
5432: {}, // postgres
|
|
||||||
3306: {}, // mysql, mariadb
|
|
||||||
6379: {}, // redis
|
|
||||||
11211: {}, // memcached
|
|
||||||
27017: {}, // mongodb
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Container) isDatabase() bool {
|
|
||||||
for _, m := range c.Container.Mounts {
|
|
||||||
if _, ok := databaseMPs[m.Destination]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range c.Ports {
|
|
||||||
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
90
internal/docker/container_helper.go
Normal file
90
internal/docker/container_helper.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerHelper struct {
|
||||||
|
*types.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDeleteLabel gets the value of a label and then deletes it from the container.
|
||||||
|
// If the label does not exist, an empty string is returned.
|
||||||
|
func (c containerHelper) getDeleteLabel(label string) string {
|
||||||
|
if l, ok := c.Labels[label]; ok {
|
||||||
|
delete(c.Labels, label)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) getAliases() []string {
|
||||||
|
if l := c.getDeleteLabel(LabelAliases); l != "" {
|
||||||
|
return U.CommaSeperatedList(l)
|
||||||
|
}
|
||||||
|
return []string{c.getName()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) getName() string {
|
||||||
|
return strings.TrimPrefix(c.Names[0], "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) getImageName() string {
|
||||||
|
colonSep := strings.Split(c.Image, ":")
|
||||||
|
slashSep := strings.Split(colonSep[0], "/")
|
||||||
|
return slashSep[len(slashSep)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) getPublicPortMapping() PortMapping {
|
||||||
|
res := make(PortMapping)
|
||||||
|
for _, v := range c.Ports {
|
||||||
|
if v.PublicPort == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res[U.PortString(v.PublicPort)] = v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
||||||
|
res := make(PortMapping)
|
||||||
|
for _, v := range c.Ports {
|
||||||
|
res[U.PortString(v.PrivatePort)] = v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
var databaseMPs = map[string]struct{}{
|
||||||
|
"/var/lib/postgresql/data": {},
|
||||||
|
"/var/lib/mysql": {},
|
||||||
|
"/var/lib/mongodb": {},
|
||||||
|
"/var/lib/mariadb": {},
|
||||||
|
"/var/lib/memcached": {},
|
||||||
|
"/var/lib/rabbitmq": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
var databasePrivPorts = map[uint16]struct{}{
|
||||||
|
5432: {}, // postgres
|
||||||
|
3306: {}, // mysql, mariadb
|
||||||
|
6379: {}, // redis
|
||||||
|
11211: {}, // memcached
|
||||||
|
27017: {}, // mongodb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c containerHelper) isDatabase() bool {
|
||||||
|
for _, m := range c.Mounts {
|
||||||
|
if _, ok := databaseMPs[m.Destination]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range c.Ports {
|
||||||
|
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -18,9 +18,9 @@ type templateData struct {
|
||||||
var loadingPage []byte
|
var loadingPage []byte
|
||||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||||
|
|
||||||
const headerCheckRedirect = "X-GoProxy-Check-Redirect"
|
const headerCheckRedirect = "X-Goproxy-Check-Redirect"
|
||||||
|
|
||||||
func (w *watcher) makeRespBody(format string, args ...any) []byte {
|
func (w *Watcher) makeRespBody(format string, args ...any) []byte {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
|
||||||
data := new(templateData)
|
data := new(templateData)
|
||||||
|
|
|
@ -11,13 +11,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Waker struct {
|
type Waker struct {
|
||||||
*watcher
|
*Watcher
|
||||||
|
|
||||||
client *http.Client
|
client *http.Client
|
||||||
rp *gphttp.ReverseProxy
|
rp *gphttp.ReverseProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker {
|
func NewWaker(w *Watcher, rp *gphttp.ReverseProxy) *Waker {
|
||||||
orig := rp.ServeHTTP
|
orig := rp.ServeHTTP
|
||||||
// workaround for stopped containers port become zero
|
// workaround for stopped containers port become zero
|
||||||
rp.ServeHTTP = func(rw http.ResponseWriter, r *http.Request) {
|
rp.ServeHTTP = func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -33,7 +33,7 @@ func NewWaker(w *watcher, rp *gphttp.ReverseProxy) *Waker {
|
||||||
orig(rw, r)
|
orig(rw, r)
|
||||||
}
|
}
|
||||||
return &Waker{
|
return &Waker{
|
||||||
watcher: w,
|
Watcher: w,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
Transport: rp.Transport,
|
Transport: rp.Transport,
|
||||||
|
@ -70,7 +70,9 @@ func (w *Waker) wake(next http.HandlerFunc, rw http.ResponseWriter, r *http.Requ
|
||||||
rw.Header().Add("Cache-Control", "no-cache")
|
rw.Header().Add("Cache-Control", "no-cache")
|
||||||
rw.Header().Add("Cache-Control", "no-store")
|
rw.Header().Add("Cache-Control", "no-store")
|
||||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||||
rw.Write(body)
|
if _, err := rw.Write(body); err != nil {
|
||||||
|
w.l.Errorf("error writing http response: %s", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
watcher struct {
|
Watcher struct {
|
||||||
*P.ReverseProxyEntry
|
*P.ReverseProxyEntry
|
||||||
|
|
||||||
client D.Client
|
client D.Client
|
||||||
|
@ -46,17 +46,17 @@ var (
|
||||||
mainLoopCancel context.CancelFunc
|
mainLoopCancel context.CancelFunc
|
||||||
mainLoopWg sync.WaitGroup
|
mainLoopWg sync.WaitGroup
|
||||||
|
|
||||||
watcherMap = F.NewMapOf[string, *watcher]()
|
watcherMap = F.NewMapOf[string, *Watcher]()
|
||||||
watcherMapMu sync.Mutex
|
watcherMapMu sync.Mutex
|
||||||
|
|
||||||
portHistoryMap = F.NewMapOf[PT.Alias, string]()
|
portHistoryMap = F.NewMapOf[PT.Alias, string]()
|
||||||
|
|
||||||
newWatcherCh = make(chan *watcher)
|
newWatcherCh = make(chan *Watcher)
|
||||||
|
|
||||||
logger = logrus.WithField("module", "idle_watcher")
|
logger = logrus.WithField("module", "idle_watcher")
|
||||||
)
|
)
|
||||||
|
|
||||||
func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
func Register(entry *P.ReverseProxyEntry) (*Watcher, E.NestedError) {
|
||||||
failure := E.Failure("idle_watcher register")
|
failure := E.Failure("idle_watcher register")
|
||||||
|
|
||||||
if entry.IdleTimeout == 0 {
|
if entry.IdleTimeout == 0 {
|
||||||
|
@ -83,7 +83,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||||
return nil, failure.With(err)
|
return nil, failure.With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := &watcher{
|
w := &Watcher{
|
||||||
ReverseProxyEntry: entry,
|
ReverseProxyEntry: entry,
|
||||||
client: client,
|
client: client,
|
||||||
refCount: &sync.WaitGroup{},
|
refCount: &sync.WaitGroup{},
|
||||||
|
@ -104,7 +104,7 @@ func Register(entry *P.ReverseProxyEntry) (*watcher, E.NestedError) {
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) Unregister() {
|
func (w *Watcher) Unregister() {
|
||||||
w.refCount.Add(-1)
|
w.refCount.Add(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,29 +138,30 @@ func Stop() {
|
||||||
mainLoopWg.Wait()
|
mainLoopWg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerStop() error {
|
func (w *Watcher) containerStop() error {
|
||||||
return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{
|
return w.client.ContainerStop(w.ctx, w.ContainerID, container.StopOptions{
|
||||||
Signal: string(w.StopSignal),
|
Signal: string(w.StopSignal),
|
||||||
Timeout: &w.StopTimeout})
|
Timeout: &w.StopTimeout,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerPause() error {
|
func (w *Watcher) containerPause() error {
|
||||||
return w.client.ContainerPause(w.ctx, w.ContainerID)
|
return w.client.ContainerPause(w.ctx, w.ContainerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerKill() error {
|
func (w *Watcher) containerKill() error {
|
||||||
return w.client.ContainerKill(w.ctx, w.ContainerID, string(w.StopSignal))
|
return w.client.ContainerKill(w.ctx, w.ContainerID, string(w.StopSignal))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerUnpause() error {
|
func (w *Watcher) containerUnpause() error {
|
||||||
return w.client.ContainerUnpause(w.ctx, w.ContainerID)
|
return w.client.ContainerUnpause(w.ctx, w.ContainerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerStart() error {
|
func (w *Watcher) containerStart() error {
|
||||||
return w.client.ContainerStart(w.ctx, w.ContainerID, container.StartOptions{})
|
return w.client.ContainerStart(w.ctx, w.ContainerID, container.StartOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) containerStatus() (string, E.NestedError) {
|
func (w *Watcher) containerStatus() (string, E.NestedError) {
|
||||||
json, err := w.client.ContainerInspect(w.ctx, w.ContainerID)
|
json, err := w.client.ContainerInspect(w.ctx, w.ContainerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", E.FailWith("inspect container", err)
|
return "", E.FailWith("inspect container", err)
|
||||||
|
@ -168,7 +169,7 @@ func (w *watcher) containerStatus() (string, E.NestedError) {
|
||||||
return json.State.Status, nil
|
return json.State.Status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) wakeIfStopped() E.NestedError {
|
func (w *Watcher) wakeIfStopped() E.NestedError {
|
||||||
if w.ready.Load() || w.ContainerRunning {
|
if w.ready.Load() || w.ContainerRunning {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -191,7 +192,7 @@ func (w *watcher) wakeIfStopped() E.NestedError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) getStopCallback() StopCallback {
|
func (w *Watcher) getStopCallback() StopCallback {
|
||||||
var cb func() error
|
var cb func() error
|
||||||
switch w.StopMethod {
|
switch w.StopMethod {
|
||||||
case PT.StopMethodPause:
|
case PT.StopMethodPause:
|
||||||
|
@ -215,11 +216,11 @@ func (w *watcher) getStopCallback() StopCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) resetIdleTimer() {
|
func (w *Watcher) resetIdleTimer() {
|
||||||
w.ticker.Reset(w.IdleTimeout)
|
w.ticker.Reset(w.IdleTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *watcher) watchUntilCancel() {
|
func (w *Watcher) watchUntilCancel() {
|
||||||
defer close(w.wakeCh)
|
defer close(w.wakeCh)
|
||||||
|
|
||||||
w.ctx, w.cancel = context.WithCancel(mainLoopCtx)
|
w.ctx, w.cancel = context.WithCancel(mainLoopCtx)
|
||||||
|
|
|
@ -7,13 +7,13 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c Client) Inspect(containerID string) (Container, E.NestedError) {
|
func (c Client) Inspect(containerID string) (*Container, E.NestedError) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
json, err := c.ContainerInspect(ctx, containerID)
|
json, err := c.ContainerInspect(ctx, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Container{}, E.From(err)
|
return nil, E.From(err)
|
||||||
}
|
}
|
||||||
return FromJson(json, c.key), nil
|
return FromJSON(json, c.key), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ func ApplyLabel[T any](obj *T, l *Label) E.NestedError {
|
||||||
case *Label:
|
case *Label:
|
||||||
var field reflect.Value
|
var field reflect.Value
|
||||||
objType := reflect.TypeFor[T]()
|
objType := reflect.TypeFor[T]()
|
||||||
for i := 0; i < reflect.TypeFor[T]().NumField(); i++ {
|
for i := range reflect.TypeFor[T]().NumField() {
|
||||||
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
|
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
|
||||||
field = reflect.ValueOf(obj).Elem().Field(i)
|
field = reflect.ValueOf(obj).Elem().Field(i)
|
||||||
break
|
break
|
||||||
|
|
|
@ -8,14 +8,18 @@ import (
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mName = "middleware1"
|
||||||
|
mAttr = "prop1"
|
||||||
|
v = "value1"
|
||||||
|
)
|
||||||
|
|
||||||
func makeLabel(ns, name, attr string) string {
|
func makeLabel(ns, name, attr string) string {
|
||||||
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
|
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNestedLabel(t *testing.T) {
|
func TestNestedLabel(t *testing.T) {
|
||||||
mName := "middleware1"
|
|
||||||
mAttr := "prop1"
|
mAttr := "prop1"
|
||||||
v := "value1"
|
|
||||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||||
ExpectNoError(t, err.Error())
|
ExpectNoError(t, err.Error())
|
||||||
sGot := ExpectType[*Label](t, pl.Value)
|
sGot := ExpectType[*Label](t, pl.Value)
|
||||||
|
@ -28,9 +32,6 @@ func TestApplyNestedLabel(t *testing.T) {
|
||||||
entry := new(struct {
|
entry := new(struct {
|
||||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||||
})
|
})
|
||||||
mName := "middleware1"
|
|
||||||
mAttr := "prop1"
|
|
||||||
v := "value1"
|
|
||||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
pl, err := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||||
ExpectNoError(t, err.Error())
|
ExpectNoError(t, err.Error())
|
||||||
err = ApplyLabel(entry, pl)
|
err = ApplyLabel(entry, pl)
|
||||||
|
@ -42,10 +43,6 @@ func TestApplyNestedLabel(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyNestedLabelExisting(t *testing.T) {
|
func TestApplyNestedLabelExisting(t *testing.T) {
|
||||||
mName := "middleware1"
|
|
||||||
mAttr := "prop1"
|
|
||||||
v := "value1"
|
|
||||||
|
|
||||||
checkAttr := "prop2"
|
checkAttr := "prop2"
|
||||||
checkV := "value2"
|
checkV := "value2"
|
||||||
entry := new(struct {
|
entry := new(struct {
|
||||||
|
@ -71,9 +68,6 @@ func TestApplyNestedLabelExisting(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyNestedLabelNoAttr(t *testing.T) {
|
func TestApplyNestedLabelNoAttr(t *testing.T) {
|
||||||
mName := "middleware1"
|
|
||||||
v := "value1"
|
|
||||||
|
|
||||||
entry := new(struct {
|
entry := new(struct {
|
||||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||||
})
|
})
|
||||||
|
|
5
internal/docker/logger.go
Normal file
5
internal/docker/logger.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
var logger = logrus.WithField("module", "docker")
|
|
@ -1,25 +0,0 @@
|
||||||
package docker
|
|
||||||
|
|
||||||
import "github.com/docker/docker/api/types"
|
|
||||||
|
|
||||||
type PortMapping = map[string]types.Port
|
|
||||||
type ProxyProperties struct {
|
|
||||||
DockerHost string `yaml:"-" json:"docker_host"`
|
|
||||||
ContainerName string `yaml:"-" json:"container_name"`
|
|
||||||
ContainerID string `yaml:"-" json:"container_id"`
|
|
||||||
ImageName string `yaml:"-" json:"image_name"`
|
|
||||||
PublicPortMapping PortMapping `yaml:"-" json:"public_port_mapping"` // non-zero publicPort:types.Port
|
|
||||||
PrivatePortMapping PortMapping `yaml:"-" json:"private_port_mapping"` // privatePort:types.Port
|
|
||||||
NetworkMode string `yaml:"-" json:"network_mode"`
|
|
||||||
|
|
||||||
Aliases []string `yaml:"-" json:"aliases"`
|
|
||||||
IsExcluded bool `yaml:"-" json:"is_excluded"`
|
|
||||||
IsExplicit bool `yaml:"-" json:"is_explicit"`
|
|
||||||
IsDatabase bool `yaml:"-" json:"is_database"`
|
|
||||||
IdleTimeout string `yaml:"-" json:"idle_timeout"`
|
|
||||||
WakeTimeout string `yaml:"-" json:"wake_timeout"`
|
|
||||||
StopMethod string `yaml:"-" json:"stop_method"`
|
|
||||||
StopTimeout string `yaml:"-" json:"stop_timeout"` // stop_method = "stop" only
|
|
||||||
StopSignal string `yaml:"-" json:"stop_signal"` // stop_method = "stop" | "kill" only
|
|
||||||
Running bool `yaml:"-" json:"running"`
|
|
||||||
}
|
|
|
@ -21,7 +21,7 @@ func NewBuilder(format string, args ...any) Builder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// adding nil / nil is no-op,
|
// adding nil / nil is no-op,
|
||||||
// you may safely pass expressions returning error to it
|
// you may safely pass expressions returning error to it.
|
||||||
func (b Builder) Add(err NestedError) Builder {
|
func (b Builder) Add(err NestedError) Builder {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
|
@ -39,6 +39,13 @@ func (b Builder) Addf(format string, args ...any) Builder {
|
||||||
return b.Add(errorf(format, args...))
|
return b.Add(errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b Builder) AddRangeE(errs ...error) Builder {
|
||||||
|
for _, err := range errs {
|
||||||
|
b.AddE(err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// Build builds a NestedError based on the errors collected in the Builder.
|
// Build builds a NestedError based on the errors collected in the Builder.
|
||||||
//
|
//
|
||||||
// If there are no errors in the Builder, it returns a Nil() NestedError.
|
// If there are no errors in the Builder, it returns a Nil() NestedError.
|
||||||
|
@ -56,12 +63,13 @@ func (b Builder) Build() NestedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b Builder) To(ptr *NestedError) {
|
func (b Builder) To(ptr *NestedError) {
|
||||||
if ptr == nil {
|
switch {
|
||||||
|
case ptr == nil:
|
||||||
return
|
return
|
||||||
} else if *ptr == nil {
|
case *ptr == nil:
|
||||||
*ptr = b.Build()
|
*ptr = b.Build()
|
||||||
} else {
|
default:
|
||||||
(*ptr).With(b.Build())
|
(*ptr).extras = append((*ptr).extras, *b.Build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,15 +33,13 @@ func TestBuilderNested(t *testing.T) {
|
||||||
eb.Add(Failure("Action 2").With(Invalid("Inner", "3")))
|
eb.Add(Failure("Action 2").With(Invalid("Inner", "3")))
|
||||||
|
|
||||||
got := eb.Build().String()
|
got := eb.Build().String()
|
||||||
expected1 :=
|
expected1 := (`error occurred:
|
||||||
(`error occurred:
|
|
||||||
- Action 1 failed:
|
- Action 1 failed:
|
||||||
- invalid Inner: 1
|
- invalid Inner: 1
|
||||||
- invalid Inner: 2
|
- invalid Inner: 2
|
||||||
- Action 2 failed:
|
- Action 2 failed:
|
||||||
- invalid Inner: 3`)
|
- invalid Inner: 3`)
|
||||||
expected2 :=
|
expected2 := (`error occurred:
|
||||||
(`error occurred:
|
|
||||||
- Action 1 failed:
|
- Action 1 failed:
|
||||||
- invalid Inner: "1"
|
- invalid Inner: "1"
|
||||||
- invalid Inner: "2"
|
- invalid Inner: "2"
|
||||||
|
|
|
@ -8,16 +8,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
NestedError = *nestedError
|
NestedError = *NestedErrorImpl
|
||||||
nestedError struct {
|
NestedErrorImpl struct {
|
||||||
subject string
|
subject string
|
||||||
err error
|
err error
|
||||||
extras []nestedError
|
extras []NestedErrorImpl
|
||||||
}
|
}
|
||||||
jsonNestedError struct {
|
JSONNestedError struct {
|
||||||
Subject string
|
Subject string `json:"subject"`
|
||||||
Err string
|
Err string `json:"error"`
|
||||||
Extras []jsonNestedError
|
Extras []JSONNestedError `json:"extras,omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,18 +25,18 @@ func From(err error) NestedError {
|
||||||
if IsNil(err) {
|
if IsNil(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &nestedError{err: err}
|
return &NestedErrorImpl{err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromJSON(data []byte) (NestedError, bool) {
|
func FromJSON(data []byte) (NestedError, bool) {
|
||||||
var j jsonNestedError
|
var j JSONNestedError
|
||||||
if err := json.Unmarshal(data, &j); err != nil {
|
if err := json.Unmarshal(data, &j); err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
if j.Err == "" {
|
if j.Err == "" {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
extras := make([]nestedError, len(j.Extras))
|
extras := make([]NestedErrorImpl, len(j.Extras))
|
||||||
for i, e := range j.Extras {
|
for i, e := range j.Extras {
|
||||||
extra, ok := fromJSONObject(e)
|
extra, ok := fromJSONObject(e)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -44,7 +44,7 @@ func FromJSON(data []byte) (NestedError, bool) {
|
||||||
}
|
}
|
||||||
extras[i] = *extra
|
extras[i] = *extra
|
||||||
}
|
}
|
||||||
return &nestedError{
|
return &NestedErrorImpl{
|
||||||
subject: j.Subject,
|
subject: j.Subject,
|
||||||
err: errors.New(j.Err),
|
err: errors.New(j.Err),
|
||||||
extras: extras,
|
extras: extras,
|
||||||
|
@ -58,26 +58,26 @@ func Check[T any](obj T, err error) (T, NestedError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Join(message string, err ...NestedError) NestedError {
|
func Join(message string, err ...NestedError) NestedError {
|
||||||
extras := make([]nestedError, len(err))
|
extras := make([]NestedErrorImpl, len(err))
|
||||||
nErr := 0
|
nErr := 0
|
||||||
for i, e := range err {
|
for i, e := range err {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
extras[i] = *e
|
extras[i] = *e
|
||||||
nErr += 1
|
nErr++
|
||||||
}
|
}
|
||||||
if nErr == 0 {
|
if nErr == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &nestedError{
|
return &NestedErrorImpl{
|
||||||
err: errors.New(message),
|
err: errors.New(message),
|
||||||
extras: extras,
|
extras: extras,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JoinE(message string, err ...error) NestedError {
|
func JoinE(message string, err ...error) NestedError {
|
||||||
b := NewBuilder(message)
|
b := NewBuilder("%s", message)
|
||||||
for _, e := range err {
|
for _, e := range err {
|
||||||
b.AddE(e)
|
b.AddE(e)
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ func (ne NestedError) Extraf(format string, args ...any) NestedError {
|
||||||
return ne.With(errorf(format, args...))
|
return ne.With(errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) Subject(s any) NestedError {
|
func (ne NestedError) Subject(s any, sep ...string) NestedError {
|
||||||
if ne == nil {
|
if ne == nil {
|
||||||
return ne
|
return ne
|
||||||
}
|
}
|
||||||
|
@ -164,11 +164,12 @@ func (ne NestedError) Subject(s any) NestedError {
|
||||||
default:
|
default:
|
||||||
subject = fmt.Sprint(s)
|
subject = fmt.Sprint(s)
|
||||||
}
|
}
|
||||||
if ne.subject == "" {
|
switch {
|
||||||
|
case ne.subject == "":
|
||||||
ne.subject = subject
|
ne.subject = subject
|
||||||
} else if !strings.ContainsRune(subject, ' ') || strings.ContainsRune(ne.subject, '.') {
|
case len(sep) > 0:
|
||||||
ne.subject = fmt.Sprintf("%s.%s", subject, ne.subject)
|
ne.subject = fmt.Sprintf("%s%s%s", subject, sep[0], ne.subject)
|
||||||
} else {
|
default:
|
||||||
ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject)
|
ne.subject = fmt.Sprintf("%s > %s", subject, ne.subject)
|
||||||
}
|
}
|
||||||
return ne
|
return ne
|
||||||
|
@ -178,21 +179,15 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||||
if ne == nil {
|
if ne == nil {
|
||||||
return ne
|
return ne
|
||||||
}
|
}
|
||||||
if strings.Contains(format, "%q") {
|
|
||||||
panic("Subjectf format should not contain %q")
|
|
||||||
}
|
|
||||||
if strings.Contains(format, "%w") {
|
|
||||||
panic("Subjectf format should not contain %w")
|
|
||||||
}
|
|
||||||
return ne.Subject(fmt.Sprintf(format, args...))
|
return ne.Subject(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) JSONObject() jsonNestedError {
|
func (ne NestedError) JSONObject() JSONNestedError {
|
||||||
extras := make([]jsonNestedError, len(ne.extras))
|
extras := make([]JSONNestedError, len(ne.extras))
|
||||||
for i, e := range ne.extras {
|
for i, e := range ne.extras {
|
||||||
extras[i] = e.JSONObject()
|
extras[i] = e.JSONObject()
|
||||||
}
|
}
|
||||||
return jsonNestedError{
|
return JSONNestedError{
|
||||||
Subject: ne.subject,
|
Subject: ne.subject,
|
||||||
Err: ne.err.Error(),
|
Err: ne.err.Error(),
|
||||||
Extras: extras,
|
Extras: extras,
|
||||||
|
@ -200,7 +195,10 @@ func (ne NestedError) JSONObject() jsonNestedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) JSON() []byte {
|
func (ne NestedError) JSON() []byte {
|
||||||
b, _ := json.MarshalIndent(ne.JSONObject(), "", " ")
|
b, err := json.MarshalIndent(ne.JSONObject(), "", " ")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +214,7 @@ func errorf(format string, args ...any) NestedError {
|
||||||
return From(fmt.Errorf(format, args...))
|
return From(fmt.Errorf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromJSONObject(obj jsonNestedError) (NestedError, bool) {
|
func fromJSONObject(obj JSONNestedError) (NestedError, bool) {
|
||||||
data, err := json.Marshal(obj)
|
data, err := json.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
|
@ -240,7 +238,7 @@ func (ne NestedError) appendMsg(msg string) NestedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
func (ne NestedError) writeToSB(sb *strings.Builder, level int, prefix string) {
|
||||||
for i := 0; i < level; i++ {
|
for range level {
|
||||||
sb.WriteString(" ")
|
sb.WriteString(" ")
|
||||||
}
|
}
|
||||||
sb.WriteString(prefix)
|
sb.WriteString(prefix)
|
||||||
|
@ -267,7 +265,7 @@ func (ne NestedError) buildError(level int, prefix string) error {
|
||||||
var res error
|
var res error
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
for i := 0; i < level; i++ {
|
for range level {
|
||||||
sb.WriteString(" ")
|
sb.WriteString(" ")
|
||||||
}
|
}
|
||||||
sb.WriteString(prefix)
|
sb.WriteString(prefix)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package error_test
|
package error
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/error"
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,8 +87,7 @@ func TestErrorNested(t *testing.T) {
|
||||||
With("baz").
|
With("baz").
|
||||||
With(inner).
|
With(inner).
|
||||||
With(inner.With(inner2.With(inner3)))
|
With(inner.With(inner2.With(inner3)))
|
||||||
want :=
|
want := `foo failed:
|
||||||
`foo failed:
|
|
||||||
- bar
|
- bar
|
||||||
- baz
|
- baz
|
||||||
- inner failed:
|
- inner failed:
|
||||||
|
|
|
@ -4,12 +4,11 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,8 +20,10 @@ type GitHubContents struct { //! keep this, may reuse in future
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconsCachePath = "/tmp/icons_cache.json"
|
const (
|
||||||
const updateInterval = 1 * time.Hour
|
iconsCachePath = "/tmp/icons_cache.json"
|
||||||
|
updateInterval = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
func ListAvailableIcons() ([]string, error) {
|
func ListAvailableIcons() ([]string, error) {
|
||||||
owner := "walkxcode"
|
owner := "walkxcode"
|
||||||
|
@ -30,13 +31,14 @@ func ListAvailableIcons() ([]string, error) {
|
||||||
ref := "main"
|
ref := "main"
|
||||||
|
|
||||||
var lastUpdate time.Time
|
var lastUpdate time.Time
|
||||||
var icons = make([]string, 0)
|
|
||||||
|
icons := make([]string, 0)
|
||||||
info, err := os.Stat(iconsCachePath)
|
info, err := os.Stat(iconsCachePath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
lastUpdate = info.ModTime().Local()
|
lastUpdate = info.ModTime().Local()
|
||||||
}
|
}
|
||||||
if time.Since(lastUpdate) < updateInterval {
|
if time.Since(lastUpdate) < updateInterval {
|
||||||
err := utils.LoadJson(iconsCachePath, &icons)
|
err := utils.LoadJSON(iconsCachePath, &icons)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return icons, nil
|
return icons, nil
|
||||||
}
|
}
|
||||||
|
@ -51,7 +53,7 @@ func ListAvailableIcons() ([]string, error) {
|
||||||
icons = append(icons, content.Path)
|
icons = append(icons, content.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = utils.SaveJson(iconsCachePath, &icons, 0o644).Error()
|
err = utils.SaveJSON(iconsCachePath, &icons, 0o644).Error()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("error saving cache", err)
|
log.Print("error saving cache", err)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ func ListAvailableIcons() ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
|
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: stats of each server
|
// TODO: stats of each server.
|
||||||
// TODO: support weighted mode
|
// TODO: support weighted mode.
|
||||||
type (
|
type (
|
||||||
impl interface {
|
impl interface {
|
||||||
ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request)
|
ServeHTTP(srvs servers, rw http.ResponseWriter, r *http.Request)
|
||||||
|
|
|
@ -2,14 +2,14 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
"github.com/yusing/go-proxy/internal/api/v1/errorpage"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,14 +23,15 @@ var CustomErrorPage = &Middleware{
|
||||||
// only handles non-success status code and html/plain content type
|
// only handles non-success status code and html/plain content type
|
||||||
contentType := gphttp.GetContentType(resp.Header)
|
contentType := gphttp.GetContentType(resp.Header)
|
||||||
if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
if !gphttp.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) {
|
||||||
errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode)
|
errorPage, ok := errorpage.GetErrorPageByStatus(resp.StatusCode)
|
||||||
if ok {
|
if ok {
|
||||||
errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode)
|
errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode)
|
||||||
|
/* trunk-ignore(golangci-lint/errcheck) */
|
||||||
io.Copy(io.Discard, resp.Body) // drain the original body
|
io.Copy(io.Discard, resp.Body) // drain the original body
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
resp.Body = io.NopCloser(bytes.NewReader(errorPage))
|
resp.Body = io.NopCloser(bytes.NewReader(errorPage))
|
||||||
resp.ContentLength = int64(len(errorPage))
|
resp.ContentLength = int64(len(errorPage))
|
||||||
resp.Header.Set("Content-Length", fmt.Sprint(len(errorPage)))
|
resp.Header.Set("Content-Length", strconv.Itoa(len(errorPage)))
|
||||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
} else {
|
} else {
|
||||||
errPageLogger.Errorf("unable to load error page for status %d", resp.StatusCode)
|
errPageLogger.Errorf("unable to load error page for status %d", resp.StatusCode)
|
||||||
|
@ -48,25 +49,27 @@ func ServeStaticErrorPageFile(w http.ResponseWriter, r *http.Request) bool {
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
|
if strings.HasPrefix(path, gphttp.StaticFilePathPrefix) {
|
||||||
filename := path[len(gphttp.StaticFilePathPrefix):]
|
filename := path[len(gphttp.StaticFilePathPrefix):]
|
||||||
file, ok := error_page.GetStaticFile(filename)
|
file, ok := errorpage.GetStaticFile(filename)
|
||||||
if !ok {
|
if !ok {
|
||||||
errPageLogger.Errorf("unable to load resource %s", filename)
|
errPageLogger.Errorf("unable to load resource %s", filename)
|
||||||
return false
|
return false
|
||||||
} else {
|
|
||||||
ext := filepath.Ext(filename)
|
|
||||||
switch ext {
|
|
||||||
case ".html":
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
case ".js":
|
|
||||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
|
||||||
case ".css":
|
|
||||||
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
|
||||||
default:
|
|
||||||
errPageLogger.Errorf("unexpected file type %q for %s", ext, filename)
|
|
||||||
}
|
|
||||||
w.Write(file)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
switch ext {
|
||||||
|
case ".html":
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
case ".js":
|
||||||
|
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||||
|
case ".css":
|
||||||
|
w.Header().Set("Content-Type", "text/css; charset=utf-8")
|
||||||
|
default:
|
||||||
|
errPageLogger.Errorf("unexpected file type %q for %s", ext, filename)
|
||||||
|
}
|
||||||
|
if _, err := w.Write(file); err != nil {
|
||||||
|
errPageLogger.WithError(err).Errorf("unable to write resource %s", filename)
|
||||||
|
http.Error(w, "Error page failure", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func BuildMiddlewaresFromYAML(data []byte) (middlewares map[string]*Middleware,
|
||||||
}
|
}
|
||||||
middlewares = make(map[string]*Middleware)
|
middlewares = make(map[string]*Middleware)
|
||||||
for name, defs := range rawMap {
|
for name, defs := range rawMap {
|
||||||
chainErr := E.NewBuilder(name)
|
chainErr := E.NewBuilder("%s", name)
|
||||||
chain := make([]*Middleware, 0, len(defs))
|
chain := make([]*Middleware, 0, len(defs))
|
||||||
for i, def := range defs {
|
for i, def := range defs {
|
||||||
if def["use"] == nil || def["use"] == "" {
|
if def["use"] == nil || def["use"] == "" {
|
||||||
|
@ -64,7 +64,7 @@ func BuildMiddlewaresFromYAML(data []byte) (middlewares map[string]*Middleware,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: check conflict or duplicates
|
// TODO: check conflict or duplicates.
|
||||||
func BuildMiddlewareFromChain(name string, chain []*Middleware) *Middleware {
|
func BuildMiddlewareFromChain(name string, chain []*Middleware) *Middleware {
|
||||||
m := &Middleware{name: name, children: chain}
|
m := &Middleware{name: name, children: chain}
|
||||||
|
|
||||||
|
|
|
@ -92,18 +92,18 @@ func userIsAuthenticated(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func exchangeCodeForToken(code string, opts *oAuth2Opts, requestUri string) (string, error) {
|
func exchangeCodeForToken(code string, opts *oAuth2Opts, requestURI string) (string, error) {
|
||||||
// Prepare the request body
|
// Prepare the request body
|
||||||
data := url.Values{
|
data := url.Values{
|
||||||
"client_id": {opts.ClientID},
|
"client_id": {opts.ClientID},
|
||||||
"client_secret": {opts.ClientSecret},
|
"client_secret": {opts.ClientSecret},
|
||||||
"code": {code},
|
"code": {code},
|
||||||
"grant_type": {"authorization_code"},
|
"grant_type": {"authorization_code"},
|
||||||
"redirect_uri": {requestUri},
|
"redirect_uri": {requestURI},
|
||||||
}
|
}
|
||||||
resp, err := http.PostForm(opts.TokenURL, data)
|
resp, err := http.PostForm(opts.TokenURL, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to request token: %v", err)
|
return "", fmt.Errorf("failed to request token: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
@ -114,7 +114,7 @@ func exchangeCodeForToken(code string, opts *oAuth2Opts, requestUri string) (str
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||||
return "", fmt.Errorf("failed to decode token response: %v", err)
|
return "", fmt.Errorf("failed to decode token response: %w", err)
|
||||||
}
|
}
|
||||||
return tokenResp.AccessToken, nil
|
return tokenResp.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ type testArgs struct {
|
||||||
|
|
||||||
func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.NestedError) {
|
func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.NestedError) {
|
||||||
var body io.Reader
|
var body io.Reader
|
||||||
var rr = new(requestRecorder)
|
var rr requestRecorder
|
||||||
var proxyURL *url.URL
|
var proxyURL *url.URL
|
||||||
var requestTarget string
|
var requestTarget string
|
||||||
var err error
|
var err error
|
||||||
|
@ -87,11 +87,14 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N
|
||||||
body = bytes.NewReader(args.body)
|
body = bytes.NewReader(args.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.scheme == "" || args.scheme == "http" {
|
switch args.scheme {
|
||||||
|
case "":
|
||||||
|
fallthrough
|
||||||
|
case "http":
|
||||||
requestTarget = "http://" + testHost
|
requestTarget = "http://" + testHost
|
||||||
} else if args.scheme == "https" {
|
case "https":
|
||||||
requestTarget = "https://" + testHost
|
requestTarget = "https://" + testHost
|
||||||
} else {
|
default:
|
||||||
panic("typo?")
|
panic("typo?")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +114,7 @@ func newMiddlewareTest(middleware *Middleware, args *testArgs) (*TestResult, E.N
|
||||||
} else {
|
} else {
|
||||||
proxyURL, _ = url.Parse("https://" + testHost) // dummy url, no actual effect
|
proxyURL, _ = url.Parse("https://" + testHost) // dummy url, no actual effect
|
||||||
}
|
}
|
||||||
rp := gphttp.NewReverseProxy(types.NewURL(proxyURL), rr)
|
rp := gphttp.NewReverseProxy(types.NewURL(proxyURL), &rr)
|
||||||
mid, setOptErr := middleware.WithOptionsClone(args.middlewareOpt)
|
mid, setOptErr := middleware.WithOptionsClone(args.middlewareOpt)
|
||||||
if setOptErr != nil {
|
if setOptErr != nil {
|
||||||
return nil, setOptErr
|
return nil, setOptErr
|
||||||
|
|
|
@ -24,10 +24,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/http/httpguts"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/net/types"
|
"github.com/yusing/go-proxy/internal/net/types"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"golang.org/x/net/http/httpguts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].
|
// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].
|
||||||
|
@ -222,6 +221,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
transport := p.Transport
|
transport := p.Transport
|
||||||
|
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
|
/* trunk-ignore(golangci-lint/revive) */
|
||||||
if ctx.Done() != nil {
|
if ctx.Done() != nil {
|
||||||
// CloseNotifier predates context.Context, and has been
|
// CloseNotifier predates context.Context, and has been
|
||||||
// entirely superseded by it. If the request contains
|
// entirely superseded by it. If the request contains
|
||||||
|
@ -460,7 +460,7 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||||
|
|
||||||
backConn, ok := res.Body.(io.ReadWriteCloser)
|
backConn, ok := res.Body.(io.ReadWriteCloser)
|
||||||
if !ok {
|
if !ok {
|
||||||
p.errorHandler(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"), true)
|
p.errorHandler(rw, req, errors.New("internal error: 101 switching protocols response with non-writable body"), true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -494,21 +494,24 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R
|
||||||
res.Header = rw.Header()
|
res.Header = rw.Header()
|
||||||
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above
|
||||||
if err := res.Write(brw); err != nil {
|
if err := res.Write(brw); err != nil {
|
||||||
|
/* trunk-ignore(golangci-lint/errorlint) */
|
||||||
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err), true)
|
p.errorHandler(rw, req, fmt.Errorf("response write: %s", err), true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := brw.Flush(); err != nil {
|
if err := brw.Flush(); err != nil {
|
||||||
|
/* trunk-ignore(golangci-lint/errorlint) */
|
||||||
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true)
|
p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bdp := U.NewBidirectionalPipe(req.Context(), conn, backConn)
|
bdp := U.NewBidirectionalPipe(req.Context(), conn, backConn)
|
||||||
|
/* trunk-ignore(golangci-lint/errcheck) */
|
||||||
bdp.Start()
|
bdp.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsPrint(s string) bool {
|
func IsPrint(s string) bool {
|
||||||
for i := 0; i < len(s); i++ {
|
for _, r := range s {
|
||||||
if s[i] < ' ' || s[i] > '~' {
|
if r < ' ' || r > '~' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Host string
|
type (
|
||||||
type Subdomain = Alias
|
Host string
|
||||||
|
Subdomain = Alias
|
||||||
|
)
|
||||||
|
|
||||||
func ValidateHost[String ~string](s String) (Host, E.NestedError) {
|
func ValidateHost[String ~string](s String) (Host, E.NestedError) {
|
||||||
return Host(s), nil
|
return Host(s), nil
|
||||||
|
|
|
@ -6,8 +6,12 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PathPattern string
|
type (
|
||||||
type PathPatterns = []PathPattern
|
PathPattern string
|
||||||
|
PathPatterns = []PathPattern
|
||||||
|
)
|
||||||
|
|
||||||
|
var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)
|
||||||
|
|
||||||
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
|
@ -25,13 +29,11 @@ func ValidatePathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||||
}
|
}
|
||||||
pp := make(PathPatterns, len(s))
|
pp := make(PathPatterns, len(s))
|
||||||
for i, v := range s {
|
for i, v := range s {
|
||||||
if pattern, err := NewPathPattern(v); err.HasError() {
|
pattern, err := NewPathPattern(v)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
|
||||||
pp[i] = pattern
|
|
||||||
}
|
}
|
||||||
|
pp[i] = pattern
|
||||||
}
|
}
|
||||||
return pp, nil
|
return pp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathPattern = regexp.MustCompile(`^(/[-\w./]*({\$\})?|((GET|POST|DELETE|PUT|HEAD|OPTION) /[-\w./]*({\$\})?))$`)
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
|
||||||
R "github.com/yusing/go-proxy/internal/route"
|
R "github.com/yusing/go-proxy/internal/route"
|
||||||
"github.com/yusing/go-proxy/internal/types"
|
"github.com/yusing/go-proxy/internal/types"
|
||||||
W "github.com/yusing/go-proxy/internal/watcher"
|
W "github.com/yusing/go-proxy/internal/watcher"
|
||||||
|
@ -17,23 +17,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type DockerProvider struct {
|
type DockerProvider struct {
|
||||||
name, dockerHost, hostname string
|
name, dockerHost string
|
||||||
ExplicitOnly bool
|
ExplicitOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var AliasRefRegex = regexp.MustCompile(`#\d+`)
|
var (
|
||||||
var AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||||
|
AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||||
|
)
|
||||||
|
|
||||||
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, E.NestedError) {
|
||||||
hostname, err := D.ParseDockerHostname(dockerHost)
|
if dockerHost == common.DockerHostFromEnv {
|
||||||
if err.HasError() {
|
dockerHost = common.GetEnv("DOCKER_HOST", client.DefaultDockerHost)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return &DockerProvider{name, dockerHost, hostname, explicitOnly}, nil
|
return &DockerProvider{name, dockerHost, explicitOnly}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) String() string {
|
func (p *DockerProvider) String() string {
|
||||||
return fmt.Sprintf("docker: %s", p.name)
|
return "docker: " + p.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) NewWatcher() W.Watcher {
|
func (p *DockerProvider) NewWatcher() W.Watcher {
|
||||||
|
@ -49,7 +50,7 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||||
return routes, E.FailWith("connect to docker", err)
|
return routes, E.FailWith("connect to docker", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errors := E.NewBuilder("errors when parse docker labels")
|
errors := E.NewBuilder("errors in docker labels")
|
||||||
|
|
||||||
for _, c := range info.Containers {
|
for _, c := range info.Containers {
|
||||||
container := D.FromDocker(&c, p.dockerHost)
|
container := D.FromDocker(&c, p.dockerHost)
|
||||||
|
@ -80,7 +81,7 @@ func (p *DockerProvider) LoadRoutesImpl() (routes R.Routes, err E.NestedError) {
|
||||||
return routes, errors.Build()
|
return routes, errors.Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) shouldIgnore(container D.Container) bool {
|
func (p *DockerProvider) shouldIgnore(container *D.Container) bool {
|
||||||
return container.IsExcluded ||
|
return container.IsExcluded ||
|
||||||
!container.IsExplicit && p.ExplicitOnly ||
|
!container.IsExplicit && p.ExplicitOnly ||
|
||||||
!container.IsExplicit && container.IsDatabase ||
|
!container.IsExplicit && container.IsDatabase ||
|
||||||
|
@ -172,8 +173,8 @@ func (p *DockerProvider) OnEvent(event W.Event, routes R.Routes) (res EventResul
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a list of proxy entries for a container.
|
// Returns a list of proxy entries for a container.
|
||||||
// Always non-nil
|
// Always non-nil.
|
||||||
func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entries types.RawEntries, _ E.NestedError) {
|
func (p *DockerProvider) entriesFromContainerLabels(container *D.Container) (entries types.RawEntries, _ E.NestedError) {
|
||||||
entries = types.NewProxyEntries()
|
entries = types.NewProxyEntries()
|
||||||
|
|
||||||
if p.shouldIgnore(container) {
|
if p.shouldIgnore(container) {
|
||||||
|
@ -183,9 +184,8 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entr
|
||||||
// init entries map for all aliases
|
// init entries map for all aliases
|
||||||
for _, a := range container.Aliases {
|
for _, a := range container.Aliases {
|
||||||
entries.Store(a, &types.RawEntry{
|
entries.Store(a, &types.RawEntry{
|
||||||
Alias: a,
|
Alias: a,
|
||||||
Host: p.hostname,
|
Container: container,
|
||||||
ProxyProperties: container.ProxyProperties,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,11 +202,11 @@ func (p *DockerProvider) entriesFromContainerLabels(container D.Container) (entr
|
||||||
return entries, errors.Build().Subject(container.ContainerName)
|
return entries, errors.Build().Subject(container.ContainerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DockerProvider) applyLabel(container D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
|
func (p *DockerProvider) applyLabel(container *D.Container, entries types.RawEntries, key, val string) (res E.NestedError) {
|
||||||
b := E.NewBuilder("errors in label %s", key)
|
b := E.NewBuilder("errors in label %s", key)
|
||||||
defer b.To(&res)
|
defer b.To(&res)
|
||||||
|
|
||||||
refErr := E.NewBuilder("errors parsing alias references")
|
refErr := E.NewBuilder("errors in alias references")
|
||||||
replaceIndexRef := func(ref string) string {
|
replaceIndexRef := func(ref string) string {
|
||||||
index, err := strconv.Atoi(ref[1:])
|
index, err := strconv.Atoi(ref[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -231,7 +231,7 @@ func (p *DockerProvider) applyLabel(container D.Container, entries types.RawEntr
|
||||||
// apply label for all aliases
|
// apply label for all aliases
|
||||||
entries.RangeAll(func(a string, e *types.RawEntry) {
|
entries.RangeAll(func(a string, e *types.RawEntry) {
|
||||||
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
if err = D.ApplyLabel(e, lbl); err.HasError() {
|
||||||
b.Add(err.Subjectf("alias %s", lbl.Target))
|
b.Add(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -250,7 +250,7 @@ func (p *DockerProvider) applyLabel(container D.Container, entries types.RawEntr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = D.ApplyLabel(config, lbl); err.HasError() {
|
if err = D.ApplyLabel(config, lbl); err.HasError() {
|
||||||
b.Add(err.Subjectf("alias %s", lbl.Target))
|
b.Add(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
P "github.com/yusing/go-proxy/internal/proxy"
|
P "github.com/yusing/go-proxy/internal/proxy"
|
||||||
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
T "github.com/yusing/go-proxy/internal/proxy/fields"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,7 +59,8 @@ func TestApplyLabelFieldValidity(t *testing.T) {
|
||||||
},
|
},
|
||||||
Ports: []types.Port{
|
Ports: []types.Port{
|
||||||
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
|
{Type: "tcp", PrivatePort: 4567, PublicPort: 8888},
|
||||||
}}, ""))
|
},
|
||||||
|
}, ""))
|
||||||
ExpectNoError(t, err.Error())
|
ExpectNoError(t, err.Error())
|
||||||
|
|
||||||
a, ok := entries.Load("a")
|
a, ok := entries.Load("a")
|
||||||
|
@ -116,8 +116,8 @@ func TestApplyLabel(t *testing.T) {
|
||||||
Ports: []types.Port{
|
Ports: []types.Port{
|
||||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 1111},
|
{Type: "tcp", PrivatePort: 3333, PublicPort: 1111},
|
||||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 1234},
|
{Type: "tcp", PrivatePort: 4444, PublicPort: 1234},
|
||||||
}}, "",
|
},
|
||||||
))
|
}, ""))
|
||||||
a, ok := entries.Load("a")
|
a, ok := entries.Load("a")
|
||||||
ExpectTrue(t, ok)
|
ExpectTrue(t, ok)
|
||||||
b, ok := entries.Load("b")
|
b, ok := entries.Load("b")
|
||||||
|
@ -152,7 +152,8 @@ func TestApplyLabelWithRef(t *testing.T) {
|
||||||
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
|
{Type: "tcp", PrivatePort: 3333, PublicPort: 9999},
|
||||||
{Type: "tcp", PrivatePort: 4444, PublicPort: 5555},
|
{Type: "tcp", PrivatePort: 4444, PublicPort: 5555},
|
||||||
{Type: "tcp", PrivatePort: 1111, PublicPort: 2222},
|
{Type: "tcp", PrivatePort: 1111, PublicPort: 2222},
|
||||||
}}, ""))
|
},
|
||||||
|
}, ""))
|
||||||
a, ok := entries.Load("a")
|
a, ok := entries.Load("a")
|
||||||
ExpectTrue(t, ok)
|
ExpectTrue(t, ok)
|
||||||
b, ok := entries.Load("b")
|
b, ok := entries.Load("b")
|
||||||
|
@ -171,13 +172,14 @@ func TestApplyLabelWithRef(t *testing.T) {
|
||||||
|
|
||||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||||
var p DockerProvider
|
var p DockerProvider
|
||||||
var c = D.FromDocker(&types.Container{
|
c := D.FromDocker(&types.Container{
|
||||||
Names: dummyNames,
|
Names: dummyNames,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
D.LabelAliases: "a,b",
|
D.LabelAliases: "a,b",
|
||||||
"proxy.#1.host": "localhost",
|
"proxy.#1.host": "localhost",
|
||||||
"proxy.#4.scheme": "https",
|
"proxy.#4.scheme": "https",
|
||||||
}}, "")
|
},
|
||||||
|
}, "")
|
||||||
_, err := p.entriesFromContainerLabels(c)
|
_, err := p.entriesFromContainerLabels(c)
|
||||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||||
|
@ -187,14 +189,15 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
D.LabelAliases: "a,b",
|
D.LabelAliases: "a,b",
|
||||||
"proxy.#0.host": "localhost",
|
"proxy.#0.host": "localhost",
|
||||||
}}, ""))
|
},
|
||||||
|
}, ""))
|
||||||
ExpectError(t, E.ErrOutOfRange, err.Error())
|
ExpectError(t, E.ErrOutOfRange, err.Error())
|
||||||
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
ExpectTrue(t, strings.Contains(err.String(), "index out of range"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStreamDefaultValues(t *testing.T) {
|
func TestStreamDefaultValues(t *testing.T) {
|
||||||
var p DockerProvider
|
var p DockerProvider
|
||||||
var c = D.FromDocker(&types.Container{
|
c := D.FromDocker(&types.Container{
|
||||||
Names: dummyNames,
|
Names: dummyNames,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
D.LabelAliases: "a",
|
D.LabelAliases: "a",
|
||||||
|
@ -202,7 +205,8 @@ func TestStreamDefaultValues(t *testing.T) {
|
||||||
},
|
},
|
||||||
Ports: []types.Port{
|
Ports: []types.Port{
|
||||||
{Type: "udp", PrivatePort: 1234, PublicPort: 5678},
|
{Type: "udp", PrivatePort: 1234, PublicPort: 5678},
|
||||||
}}, "",
|
},
|
||||||
|
}, "",
|
||||||
)
|
)
|
||||||
entries, err := p.entriesFromContainerLabels(c)
|
entries, err := p.entriesFromContainerLabels(c)
|
||||||
ExpectNoError(t, err.Error())
|
ExpectNoError(t, err.Error())
|
||||||
|
@ -228,7 +232,8 @@ func TestExplicitExclude(t *testing.T) {
|
||||||
D.LabelAliases: "a",
|
D.LabelAliases: "a",
|
||||||
D.LabelExclude: "true",
|
D.LabelExclude: "true",
|
||||||
"proxy.a.no_tls_verify": "true",
|
"proxy.a.no_tls_verify": "true",
|
||||||
}}, ""))
|
},
|
||||||
|
}, ""))
|
||||||
ExpectNoError(t, err.Error())
|
ExpectNoError(t, err.Error())
|
||||||
|
|
||||||
_, ok := entries.Load("a")
|
_, ok := entries.Load("a")
|
||||||
|
|
|
@ -99,31 +99,21 @@ func (p *Provider) GetType() ProviderType {
|
||||||
return p.t
|
return p.t
|
||||||
}
|
}
|
||||||
|
|
||||||
// to work with json marshaller
|
// to work with json marshaller.
|
||||||
func (p *Provider) MarshalText() ([]byte, error) {
|
func (p *Provider) MarshalText() ([]byte, error) {
|
||||||
return []byte(p.String()), nil
|
return []byte(p.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
func (p *Provider) StartAllRoutes() (res E.NestedError) {
|
||||||
errors := E.NewBuilder("errors in routes")
|
errors := E.NewBuilder("errors starting routes")
|
||||||
defer errors.To(&res)
|
defer errors.To(&res)
|
||||||
|
|
||||||
// start watcher no matter load success or not
|
// start watcher no matter load success or not
|
||||||
go p.watchEvents()
|
go p.watchEvents()
|
||||||
|
|
||||||
nStarted := 0
|
|
||||||
nFailed := 0
|
|
||||||
|
|
||||||
p.routes.RangeAllParallel(func(alias string, r R.Route) {
|
p.routes.RangeAllParallel(func(alias string, r R.Route) {
|
||||||
if err := r.Start(); err.HasError() {
|
errors.Add(r.Start().Subject(r))
|
||||||
errors.Add(err.Subject(r))
|
|
||||||
nFailed++
|
|
||||||
} else {
|
|
||||||
nStarted++
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
p.l.Debugf("%d routes started, %d failed", nStarted, nFailed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,20 +123,12 @@ func (p *Provider) StopAllRoutes() (res E.NestedError) {
|
||||||
p.watcherCancel = nil
|
p.watcherCancel = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
errors := E.NewBuilder("errors stopping routes")
|
||||||
defer errors.To(&res)
|
defer errors.To(&res)
|
||||||
|
|
||||||
nStopped := 0
|
|
||||||
nFailed := 0
|
|
||||||
p.routes.RangeAllParallel(func(alias string, r R.Route) {
|
p.routes.RangeAllParallel(func(alias string, r R.Route) {
|
||||||
if err := r.Stop(); err.HasError() {
|
errors.Add(r.Stop().Subject(r))
|
||||||
errors.Add(err.Subject(r))
|
|
||||||
nFailed++
|
|
||||||
} else {
|
|
||||||
nStopped++
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
p.l.Debugf("%d routes stopped, %d failed", nStopped, nFailed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +147,9 @@ func (p *Provider) LoadRoutes() E.NestedError {
|
||||||
p.l.Infof("loaded %d routes", p.routes.Size())
|
p.l.Infof("loaded %d routes", p.routes.Size())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return E.FailWith("loading routes", err)
|
return E.FailWith("loading routes", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const udpBufferSize = 8192
|
const (
|
||||||
const streamStopListenTimeout = 1 * time.Second
|
udpBufferSize = 8192
|
||||||
|
streamStopListenTimeout = 1 * time.Second
|
||||||
|
)
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
package route
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/error_page"
|
"github.com/yusing/go-proxy/internal/api/v1/errorpage"
|
||||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
. "github.com/yusing/go-proxy/internal/net/http"
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
P "github.com/yusing/go-proxy/internal/proxy"
|
P "github.com/yusing/go-proxy/internal/proxy"
|
||||||
|
@ -26,13 +26,13 @@ type (
|
||||||
|
|
||||||
server *loadbalancer.Server
|
server *loadbalancer.Server
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
rp *ReverseProxy
|
rp *gphttp.ReverseProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
SubdomainKey = PT.Alias
|
SubdomainKey = PT.Alias
|
||||||
|
|
||||||
ReverseProxyHandler struct {
|
ReverseProxyHandler struct {
|
||||||
*ReverseProxy
|
*gphttp.ReverseProxy
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ var (
|
||||||
|
|
||||||
httpRoutes = F.NewMapOf[string, *HTTPRoute]()
|
httpRoutes = F.NewMapOf[string, *HTTPRoute]()
|
||||||
httpRoutesMu sync.Mutex
|
httpRoutesMu sync.Mutex
|
||||||
globalMux = http.NewServeMux() // TODO: support regex subdomain matching
|
// globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rp ReverseProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (rp ReverseProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -60,12 +60,12 @@ func NewHTTPRoute(entry *P.ReverseProxyEntry) (*HTTPRoute, E.NestedError) {
|
||||||
var trans *http.Transport
|
var trans *http.Transport
|
||||||
|
|
||||||
if entry.NoTLSVerify {
|
if entry.NoTLSVerify {
|
||||||
trans = DefaultTransportNoTLS.Clone()
|
trans = gphttp.DefaultTransportNoTLS.Clone()
|
||||||
} else {
|
} else {
|
||||||
trans = DefaultTransport.Clone()
|
trans = gphttp.DefaultTransport.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
rp := NewReverseProxy(entry.URL, trans)
|
rp := gphttp.NewReverseProxy(entry.URL, trans)
|
||||||
|
|
||||||
if len(entry.Middlewares) > 0 {
|
if len(entry.Middlewares) > 0 {
|
||||||
err := middleware.PatchReverseProxy(string(entry.Alias), rp, entry.Middlewares)
|
err := middleware.PatchReverseProxy(string(entry.Alias), rp, entry.Middlewares)
|
||||||
|
@ -122,7 +122,7 @@ func (r *HTTPRoute) Start() E.NestedError {
|
||||||
}
|
}
|
||||||
|
|
||||||
var lb *loadbalancer.LoadBalancer
|
var lb *loadbalancer.LoadBalancer
|
||||||
linked, ok := httpRoutes.Load(string(r.LoadBalance.Link))
|
linked, ok := httpRoutes.Load(r.LoadBalance.Link)
|
||||||
if ok {
|
if ok {
|
||||||
lb = linked.LoadBalancer
|
lb = linked.LoadBalancer
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,7 +132,7 @@ func (r *HTTPRoute) Start() E.NestedError {
|
||||||
LoadBalancer: lb,
|
LoadBalancer: lb,
|
||||||
handler: lb,
|
handler: lb,
|
||||||
}
|
}
|
||||||
httpRoutes.Store(string(r.LoadBalance.Link), linked)
|
httpRoutes.Store(r.LoadBalance.Link, linked)
|
||||||
}
|
}
|
||||||
r.server = loadbalancer.NewServer(string(r.Alias), r.rp.TargetURL, r.LoadBalance.Weight, r.handler)
|
r.server = loadbalancer.NewServer(string(r.Alias), r.rp.TargetURL, r.LoadBalance.Weight, r.handler)
|
||||||
lb.AddServer(r.server)
|
lb.AddServer(r.server)
|
||||||
|
@ -152,12 +152,12 @@ func (r *HTTPRoute) Stop() (_ E.NestedError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.server != nil {
|
if r.server != nil {
|
||||||
linked, ok := httpRoutes.Load(string(r.LoadBalance.Link))
|
linked, ok := httpRoutes.Load(r.LoadBalance.Link)
|
||||||
if ok {
|
if ok {
|
||||||
linked.LoadBalancer.RemoveServer(r.server)
|
linked.LoadBalancer.RemoveServer(r.server)
|
||||||
}
|
}
|
||||||
if linked.LoadBalancer.IsEmpty() {
|
if linked.LoadBalancer.IsEmpty() {
|
||||||
httpRoutes.Delete(string(r.LoadBalance.Link))
|
httpRoutes.Delete(r.LoadBalance.Link)
|
||||||
}
|
}
|
||||||
r.server = nil
|
r.server = nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -180,11 +180,13 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
logrus.Error(E.Failure("request").
|
logrus.Error(E.Failure("request").
|
||||||
Subjectf("%s %s", r.Method, r.URL.String()).
|
Subjectf("%s %s", r.Method, r.URL.String()).
|
||||||
With(err))
|
With(err))
|
||||||
errorPage, ok := error_page.GetErrorPageByStatus(http.StatusNotFound)
|
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
||||||
if ok {
|
if ok {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write(errorPage)
|
if _, err := w.Write(errorPage); err != nil {
|
||||||
|
logrus.Errorf("failed to respond error page to %s: %s", r.RemoteAddr, err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
@ -198,7 +200,7 @@ func findMuxAnyDomain(host string) (http.Handler, error) {
|
||||||
hostSplit := strings.Split(host, ".")
|
hostSplit := strings.Split(host, ".")
|
||||||
n := len(hostSplit)
|
n := len(hostSplit)
|
||||||
if n <= 2 {
|
if n <= 2 {
|
||||||
return nil, fmt.Errorf("missing subdomain in url")
|
return nil, errors.New("missing subdomain in url")
|
||||||
}
|
}
|
||||||
sd := strings.Join(hostSplit[:n-2], ".")
|
sd := strings.Join(hostSplit[:n-2], ".")
|
||||||
if r, ok := httpRoutes.Load(sd); ok {
|
if r, ok := httpRoutes.Load(sd); ok {
|
||||||
|
|
|
@ -30,7 +30,7 @@ type StreamRoute struct {
|
||||||
type StreamImpl interface {
|
type StreamImpl interface {
|
||||||
Setup() error
|
Setup() error
|
||||||
Accept() (any, error)
|
Accept() (any, error)
|
||||||
Handle(any) error
|
Handle(conn any) error
|
||||||
CloseListeners()
|
CloseListeners()
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,6 @@ func (route *UDPRoute) Accept() (any, error) {
|
||||||
|
|
||||||
buffer := make([]byte, udpBufferSize)
|
buffer := make([]byte, udpBufferSize)
|
||||||
nRead, srcAddr, err := in.ReadFromUDP(buffer)
|
nRead, srcAddr, err := in.ReadFromUDP(buffer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
var proxyServer, apiServer *server
|
var proxyServer, apiServer *Server
|
||||||
|
|
||||||
func InitProxyServer(opt Options) *server {
|
func InitProxyServer(opt Options) *Server {
|
||||||
if proxyServer == nil {
|
if proxyServer == nil {
|
||||||
proxyServer = NewServer(opt)
|
proxyServer = NewServer(opt)
|
||||||
}
|
}
|
||||||
return proxyServer
|
return proxyServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitAPIServer(opt Options) *server {
|
func InitAPIServer(opt Options) *Server {
|
||||||
if apiServer == nil {
|
if apiServer == nil {
|
||||||
apiServer = NewServer(opt)
|
apiServer = NewServer(opt)
|
||||||
}
|
}
|
||||||
return apiServer
|
return apiServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetProxyServer() *server {
|
func GetProxyServer() *Server {
|
||||||
return proxyServer
|
return proxyServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAPIServer() *server {
|
func GetAPIServer() *Server {
|
||||||
return apiServer
|
return apiServer
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -11,7 +12,7 @@ import (
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
type server struct {
|
type Server struct {
|
||||||
Name string
|
Name string
|
||||||
CertProvider *autocert.Provider
|
CertProvider *autocert.Provider
|
||||||
http *http.Server
|
http *http.Server
|
||||||
|
@ -38,7 +39,7 @@ func (l LogrusWrapper) Write(b []byte) (int, error) {
|
||||||
return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
|
return l.Logger.WriterLevel(logrus.ErrorLevel).Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(opt Options) (s *server) {
|
func NewServer(opt Options) (s *Server) {
|
||||||
var httpSer, httpsSer *http.Server
|
var httpSer, httpsSer *http.Server
|
||||||
var httpHandler http.Handler
|
var httpHandler http.Handler
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ func NewServer(opt Options) (s *server) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &server{
|
return &Server{
|
||||||
Name: opt.Name,
|
Name: opt.Name,
|
||||||
CertProvider: opt.CertProvider,
|
CertProvider: opt.CertProvider,
|
||||||
http: httpSer,
|
http: httpSer,
|
||||||
|
@ -88,8 +89,8 @@ func NewServer(opt Options) (s *server) {
|
||||||
//
|
//
|
||||||
// If both are not set, this does nothing.
|
// If both are not set, this does nothing.
|
||||||
//
|
//
|
||||||
// Start() is non-blocking
|
// Start() is non-blocking.
|
||||||
func (s *server) Start() {
|
func (s *Server) Start() {
|
||||||
if s.http == nil && s.https == nil {
|
if s.http == nil && s.https == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -112,7 +113,7 @@ func (s *server) Start() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) Stop() {
|
func (s *Server) Stop() {
|
||||||
if s.http == nil && s.https == nil {
|
if s.http == nil && s.https == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -133,13 +134,13 @@ func (s *server) Stop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) Uptime() time.Duration {
|
func (s *Server) Uptime() time.Duration {
|
||||||
return time.Since(s.startTime)
|
return time.Since(s.startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) handleErr(scheme string, err error) {
|
func (s *Server) handleErr(scheme string, err error) {
|
||||||
switch err {
|
switch {
|
||||||
case nil, http.ErrServerClosed:
|
case err == nil, errors.Is(err, http.ErrServerClosed):
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
logrus.Fatalf("failed to start %s %s server: %s", scheme, s.Name, err)
|
logrus.Fatalf("failed to start %s %s server: %s", scheme, s.Name, err)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,16 +8,18 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var branch = GetEnv("GOPROXY_BRANCH", "v0.5")
|
var (
|
||||||
var baseUrl = fmt.Sprintf("https://github.com/yusing/go-proxy/raw/%s", branch)
|
branch = common.GetEnv("GOPROXY_BRANCH", "v0.6")
|
||||||
var requiredConfigs = []Config{
|
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
|
||||||
{ConfigBasePath, true, false, ""},
|
requiredConfigs = []Config{
|
||||||
{ComposeFileName, false, true, ComposeExampleFileName},
|
{common.ConfigBasePath, true, false, ""},
|
||||||
{path.Join(ConfigBasePath, ConfigFileName), false, true, ConfigExampleFileName},
|
{common.ComposeFileName, false, true, common.ComposeExampleFileName},
|
||||||
}
|
{path.Join(common.ConfigBasePath, common.ConfigFileName), false, true, common.ConfigExampleFileName},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Pathname string
|
Pathname string
|
||||||
|
@ -31,7 +32,9 @@ func Setup() {
|
||||||
log.Println("setting up go-proxy")
|
log.Println("setting up go-proxy")
|
||||||
log.Println("branch:", branch)
|
log.Println("branch:", branch)
|
||||||
|
|
||||||
os.Chdir("/setup")
|
if err := os.Chdir("/setup"); err != nil {
|
||||||
|
log.Fatalf("failed: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, config := range requiredConfigs {
|
for _, config := range requiredConfigs {
|
||||||
config.setup()
|
config.setup()
|
||||||
|
@ -83,6 +86,7 @@ func touch(pathname string) {
|
||||||
log.Fatalf("failed: %s\n", err)
|
log.Fatalf("failed: %s\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch(remoteFilename string, outFileName string) {
|
func fetch(remoteFilename string, outFileName string) {
|
||||||
if hasFileOrDir(outFileName) {
|
if hasFileOrDir(outFileName) {
|
||||||
if remoteFilename == outFileName {
|
if remoteFilename == outFileName {
|
||||||
|
@ -94,7 +98,7 @@ func fetch(remoteFilename string, outFileName string) {
|
||||||
}
|
}
|
||||||
log.Printf("downloading %q\n", remoteFilename)
|
log.Printf("downloading %q\n", remoteFilename)
|
||||||
|
|
||||||
url, err := url.JoinPath(baseUrl, remoteFilename)
|
url, err := url.JoinPath(baseURL, remoteFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("unexpected error: %s\n", err)
|
log.Fatalf("unexpected error: %s\n", err)
|
||||||
}
|
}
|
||||||
|
@ -104,17 +108,19 @@ func fetch(remoteFilename string, outFileName string) {
|
||||||
log.Fatalf("http request failed: %s\n", err)
|
log.Fatalf("http request failed: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
log.Fatalf("error reading response body: %s\n", err)
|
log.Fatalf("error reading response body: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(outFileName, body, 0o644)
|
err = os.WriteFile(outFileName, body, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
log.Fatalf("failed to write to file: %s\n", err)
|
log.Fatalf("failed to write to file: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("downloaded to %q\n", outFileName)
|
log.Printf("downloaded to %q\n", outFileName)
|
||||||
|
|
||||||
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ package types
|
||||||
|
|
||||||
type (
|
type (
|
||||||
AutoCertConfig struct {
|
AutoCertConfig struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email,omitempty" yaml:"email"`
|
||||||
Domains []string `yaml:",flow" json:"domains"`
|
Domains []string `json:"domains,omitempty" yaml:",flow"`
|
||||||
CertPath string `yaml:"cert_path" json:"cert_path"`
|
CertPath string `json:"cert_path,omitempty" yaml:"cert_path"`
|
||||||
KeyPath string `yaml:"key_path" json:"key_path"`
|
KeyPath string `json:"key_path,omitempty" yaml:"key_path"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider,omitempty" yaml:"provider"`
|
||||||
Options AutocertProviderOpt `yaml:",flow" json:"options"`
|
Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"`
|
||||||
}
|
}
|
||||||
AutocertProviderOpt map[string]any
|
AutocertProviderOpt map[string]any
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Providers ProxyProviders `yaml:",flow" json:"providers"`
|
Providers ProxyProviders `json:"providers" yaml:",flow"`
|
||||||
AutoCert AutoCertConfig `yaml:",flow" json:"autocert"`
|
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
||||||
ExplicitOnly bool `yaml:"explicit_only" json:"explicit_only"`
|
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
|
||||||
MatchDomains []string `yaml:"match_domains" json:"match_domains"`
|
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
||||||
TimeoutShutdown int `yaml:"timeout_shutdown" json:"timeout_shutdown"`
|
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
||||||
RedirectToHTTPS bool `yaml:"redirect_to_https" json:"redirect_to_https"`
|
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
type ProxyProviders struct {
|
type ProxyProviders struct {
|
||||||
Files []string `yaml:"include" json:"include"` // docker, file
|
Files []string `json:"include" yaml:"include"` // docker, file
|
||||||
Docker map[string]string `yaml:"docker" json:"docker"`
|
Docker map[string]string `json:"docker" yaml:"docker"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,35 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
D "github.com/yusing/go-proxy/internal/docker"
|
D "github.com/yusing/go-proxy/internal/docker"
|
||||||
H "github.com/yusing/go-proxy/internal/homepage"
|
H "github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
RawEntry struct {
|
RawEntry struct {
|
||||||
|
_ U.NoCopy
|
||||||
|
|
||||||
// raw entry object before validation
|
// raw entry object before validation
|
||||||
// loaded from docker labels or yaml file
|
// loaded from docker labels or yaml file
|
||||||
Alias string `yaml:"-" json:"-"`
|
Alias string `json:"-" yaml:"-"`
|
||||||
Scheme string `yaml:"scheme" json:"scheme"`
|
Scheme string `json:"scheme" yaml:"scheme"`
|
||||||
Host string `yaml:"host" json:"host"`
|
Host string `json:"host" yaml:"host"`
|
||||||
Port string `yaml:"port" json:"port"`
|
Port string `json:"port" yaml:"port"`
|
||||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify,omitempty"` // https proxy only
|
NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
|
||||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns,omitempty"` // http(s) proxy only
|
PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
|
||||||
LoadBalance loadbalancer.Config `yaml:"load_balance" json:"load_balance"`
|
LoadBalance loadbalancer.Config `json:"load_balance" yaml:"load_balance"`
|
||||||
Middlewares D.NestedLabelMap `yaml:"middlewares" json:"middlewares,omitempty"`
|
Middlewares D.NestedLabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
||||||
Homepage *H.HomePageItem `yaml:"homepage" json:"homepage,omitempty"`
|
Homepage *H.HomePageItem `json:"homepage,omitempty" yaml:"homepage"`
|
||||||
|
|
||||||
/* Docker only */
|
/* Docker only */
|
||||||
*D.ProxyProperties `yaml:"-" json:"proxy_properties"`
|
*D.Container `json:"container" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
RawEntries = F.Map[string, *RawEntry]
|
RawEntries = F.Map[string, *RawEntry]
|
||||||
|
@ -36,21 +38,32 @@ type (
|
||||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||||
|
|
||||||
func (e *RawEntry) FillMissingFields() {
|
func (e *RawEntry) FillMissingFields() {
|
||||||
isDocker := e.ProxyProperties != nil
|
isDocker := e.Container != nil
|
||||||
if !isDocker {
|
if !isDocker {
|
||||||
e.ProxyProperties = &D.ProxyProperties{}
|
e.Container = &D.Container{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Host == "" {
|
||||||
|
switch {
|
||||||
|
case e.PrivateIP != "":
|
||||||
|
e.Host = e.PrivateIP
|
||||||
|
case e.PublicIP != "":
|
||||||
|
e.Host = e.PublicIP
|
||||||
|
default:
|
||||||
|
e.Host = "localhost"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lp, pp, extra := e.splitPorts()
|
lp, pp, extra := e.splitPorts()
|
||||||
|
|
||||||
if port, ok := ServiceNamePortMapTCP[e.ImageName]; ok {
|
if port, ok := common.ServiceNamePortMapTCP[e.ImageName]; ok {
|
||||||
if pp == "" {
|
if pp == "" {
|
||||||
pp = strconv.Itoa(port)
|
pp = strconv.Itoa(port)
|
||||||
}
|
}
|
||||||
if e.Scheme == "" {
|
if e.Scheme == "" {
|
||||||
e.Scheme = "tcp"
|
e.Scheme = "tcp"
|
||||||
}
|
}
|
||||||
} else if port, ok := ImageNamePortMap[e.ImageName]; ok {
|
} else if port, ok := common.ImageNamePortMap[e.ImageName]; ok {
|
||||||
if pp == "" {
|
if pp == "" {
|
||||||
pp = strconv.Itoa(port)
|
pp = strconv.Itoa(port)
|
||||||
}
|
}
|
||||||
|
@ -61,58 +74,68 @@ func (e *RawEntry) FillMissingFields() {
|
||||||
pp = "443"
|
pp = "443"
|
||||||
} else if pp == "" {
|
} else if pp == "" {
|
||||||
if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok {
|
if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok {
|
||||||
pp = fmt.Sprint(p.PrivatePort)
|
pp = U.PortString(p.PrivatePort)
|
||||||
} else if !isDocker {
|
} else if !isDocker {
|
||||||
pp = "80"
|
pp = "80"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace private port with public port (if any)
|
// replace private port with public port if using public IP.
|
||||||
if isDocker && e.NetworkMode != "host" {
|
if e.Host == e.PublicIP {
|
||||||
if p, ok := e.PrivatePortMapping[pp]; ok {
|
if p, ok := e.PrivatePortMapping[pp]; ok {
|
||||||
pp = fmt.Sprint(p.PublicPort)
|
pp = U.PortString(p.PublicPort)
|
||||||
}
|
}
|
||||||
if _, ok := e.PublicPortMapping[pp]; !ok { // port is not exposed, but specified
|
if _, ok := e.PublicPortMapping[pp]; !ok { // port is not exposed, but specified
|
||||||
// try to fallback to first public port
|
// try to fallback to first public port
|
||||||
if p, ok := F.FirstValueOf(e.PublicPortMapping); ok {
|
if p, ok := F.FirstValueOf(e.PublicPortMapping); ok {
|
||||||
pp = fmt.Sprint(p.PublicPort)
|
pp = U.PortString(p.PublicPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// replace public port with private port if using private IP.
|
||||||
|
if e.Host == e.PrivateIP {
|
||||||
|
if p, ok := e.PublicPortMapping[pp]; ok {
|
||||||
|
pp = U.PortString(p.PrivatePort)
|
||||||
|
}
|
||||||
|
if _, ok := e.PrivatePortMapping[pp]; !ok { // port is not exposed, but specified
|
||||||
|
// try to fallback to first private port
|
||||||
|
if p, ok := F.FirstValueOf(e.PrivatePortMapping); ok {
|
||||||
|
pp = U.PortString(p.PrivatePort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Scheme == "" && isDocker {
|
if e.Scheme == "" && isDocker {
|
||||||
if p, ok := e.PublicPortMapping[pp]; ok && p.Type == "udp" {
|
switch {
|
||||||
|
case e.Host == e.PublicIP && e.PublicPortMapping[pp].Type == "udp":
|
||||||
|
e.Scheme = "udp"
|
||||||
|
case e.Host == e.PrivateIP && e.PrivatePortMapping[pp].Type == "udp":
|
||||||
e.Scheme = "udp"
|
e.Scheme = "udp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Scheme == "" {
|
if e.Scheme == "" {
|
||||||
if lp != "" {
|
switch {
|
||||||
|
case lp != "":
|
||||||
e.Scheme = "tcp"
|
e.Scheme = "tcp"
|
||||||
} else if strings.HasSuffix(pp, "443") {
|
case strings.HasSuffix(pp, "443"):
|
||||||
e.Scheme = "https"
|
e.Scheme = "https"
|
||||||
} else if _, ok := WellKnownHTTPPorts[pp]; ok {
|
default: // assume its http
|
||||||
e.Scheme = "http"
|
|
||||||
} else {
|
|
||||||
// assume its http
|
|
||||||
e.Scheme = "http"
|
e.Scheme = "http"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Host == "" {
|
|
||||||
e.Host = "localhost"
|
|
||||||
}
|
|
||||||
if e.IdleTimeout == "" {
|
if e.IdleTimeout == "" {
|
||||||
e.IdleTimeout = IdleTimeoutDefault
|
e.IdleTimeout = common.IdleTimeoutDefault
|
||||||
}
|
}
|
||||||
if e.WakeTimeout == "" {
|
if e.WakeTimeout == "" {
|
||||||
e.WakeTimeout = WakeTimeoutDefault
|
e.WakeTimeout = common.WakeTimeoutDefault
|
||||||
}
|
}
|
||||||
if e.StopTimeout == "" {
|
if e.StopTimeout == "" {
|
||||||
e.StopTimeout = StopTimeoutDefault
|
e.StopTimeout = common.StopTimeoutDefault
|
||||||
}
|
}
|
||||||
if e.StopMethod == "" {
|
if e.StopMethod == "" {
|
||||||
e.StopMethod = StopMethodDefault
|
e.StopMethod = common.StopMethodDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Port = joinPorts(lp, pp, extra)
|
e.Port = joinPorts(lp, pp, extra)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Recursively lists all files in a directory until `maxDepth` is reached
|
// Recursively lists all files in a directory until `maxDepth` is reached
|
||||||
// Returns a slice of file paths relative to `dir`
|
// Returns a slice of file paths relative to `dir`.
|
||||||
func ListFiles(dir string, maxDepth int) ([]string, error) {
|
func ListFiles(dir string, maxDepth int) ([]string, error) {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,9 +4,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/puzpuzpuz/xsync/v3"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Map[KT comparable, VT any] struct {
|
type Map[KT comparable, VT any] struct {
|
||||||
|
@ -25,6 +24,17 @@ func NewMapFrom[KT comparable, VT any](m map[KT]VT) (res Map[KT, VT]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapFind iterates over the map and returns the first value
|
||||||
|
// that satisfies the given criteria. The iteration is stopped
|
||||||
|
// once a value is found. If no value satisfies the criteria,
|
||||||
|
// the function returns the zero value of CT.
|
||||||
|
//
|
||||||
|
// The criteria function takes a value of type VT and returns a
|
||||||
|
// value of type CT and a boolean indicating whether the value
|
||||||
|
// satisfies the criteria. The boolean value is used to determine
|
||||||
|
// whether the iteration should be stopped.
|
||||||
|
//
|
||||||
|
// The function is safe for concurrent use.
|
||||||
func MapFind[KT comparable, VT, CT any](m Map[KT, VT], criteria func(VT) (CT, bool)) (_ CT) {
|
func MapFind[KT comparable, VT, CT any](m Map[KT, VT], criteria func(VT) (CT, bool)) (_ CT) {
|
||||||
result := make(chan CT, 1)
|
result := make(chan CT, 1)
|
||||||
|
|
||||||
|
@ -49,13 +59,15 @@ func MapFind[KT comparable, VT, CT any](m Map[KT, VT], criteria func(VT) (CT, bo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeFrom add contents from another `Map`, ignore duplicated keys
|
// MergeFrom merges the contents of another Map into this one, ignoring duplicated keys.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - other: `Map` of values to add from
|
|
||||||
//
|
//
|
||||||
// Return:
|
// other: Map of values to add from
|
||||||
// - Map: a `Map` of duplicated keys-value pairs
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// Map of duplicated keys-value pairs
|
||||||
func (m Map[KT, VT]) MergeFrom(other Map[KT, VT]) Map[KT, VT] {
|
func (m Map[KT, VT]) MergeFrom(other Map[KT, VT]) Map[KT, VT] {
|
||||||
dups := NewMapOf[KT, VT]()
|
dups := NewMapOf[KT, VT]()
|
||||||
|
|
||||||
|
@ -70,6 +82,15 @@ func (m Map[KT, VT]) MergeFrom(other Map[KT, VT]) Map[KT, VT] {
|
||||||
return dups
|
return dups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RangeAll calls the given function for each key-value pair in the map.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// do: function to call for each key-value pair
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// nothing
|
||||||
func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
|
func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
|
||||||
m.Range(func(k KT, v VT) bool {
|
m.Range(func(k KT, v VT) bool {
|
||||||
do(k, v)
|
do(k, v)
|
||||||
|
@ -77,6 +98,16 @@ func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RangeAllParallel calls the given function for each key-value pair in the map,
|
||||||
|
// in parallel. The map is not safe for modification from within the function.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// do: function to call for each key-value pair
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// nothing
|
||||||
func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
|
func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(m.Size())
|
wg.Add(m.Size())
|
||||||
|
@ -91,6 +122,15 @@ func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveAll removes all key-value pairs from the map where the value matches the given criteria.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// criteria: function to determine whether a value should be removed
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// nothing
|
||||||
func (m Map[KT, VT]) RemoveAll(criteria func(VT) bool) {
|
func (m Map[KT, VT]) RemoveAll(criteria func(VT) bool) {
|
||||||
m.Range(func(k KT, v VT) bool {
|
m.Range(func(k KT, v VT) bool {
|
||||||
if criteria(v) {
|
if criteria(v) {
|
||||||
|
@ -105,6 +145,17 @@ func (m Map[KT, VT]) Has(k KT) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalFromYAML unmarshals a yaml byte slice into the map.
|
||||||
|
//
|
||||||
|
// It overwrites all existing key-value pairs in the map.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
//
|
||||||
|
// data: yaml byte slice to unmarshal
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
//
|
||||||
|
// error: if the unmarshaling fails
|
||||||
func (m Map[KT, VT]) UnmarshalFromYAML(data []byte) E.NestedError {
|
func (m Map[KT, VT]) UnmarshalFromYAML(data []byte) E.NestedError {
|
||||||
if m.Size() != 0 {
|
if m.Size() != 0 {
|
||||||
return E.FailedWhy("unmarshal from yaml", "map is not empty")
|
return E.FailedWhy("unmarshal from yaml", "map is not empty")
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: move to "utils/io"
|
// TODO: move to "utils/io".
|
||||||
type (
|
type (
|
||||||
FileReader struct {
|
FileReader struct {
|
||||||
Path string
|
Path string
|
||||||
|
@ -108,7 +108,7 @@ func Copy2(ctx context.Context, dst io.Writer, src io.Reader) error {
|
||||||
return Copy(&ContextWriter{ctx: ctx, Writer: dst}, &ContextReader{ctx: ctx, Reader: src})
|
return Copy(&ContextWriter{ctx: ctx, Writer: dst}, &ContextReader{ctx: ctx, Reader: src})
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadJson[T any](path string, pointer *T) E.NestedError {
|
func LoadJSON[T any](path string, pointer *T) E.NestedError {
|
||||||
data, err := E.Check(os.ReadFile(path))
|
data, err := E.Check(os.ReadFile(path))
|
||||||
if err.HasError() {
|
if err.HasError() {
|
||||||
return err
|
return err
|
||||||
|
@ -116,7 +116,7 @@ func LoadJson[T any](path string, pointer *T) E.NestedError {
|
||||||
return E.From(json.Unmarshal(data, pointer))
|
return E.From(json.Unmarshal(data, pointer))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveJson[T any](path string, pointer *T, perm os.FileMode) E.NestedError {
|
func SaveJSON[T any](path string, pointer *T, perm os.FileMode) E.NestedError {
|
||||||
data, err := E.Check(json.Marshal(pointer))
|
data, err := E.Check(json.Marshal(pointer))
|
||||||
if err.HasError() {
|
if err.HasError() {
|
||||||
return err
|
return err
|
||||||
|
|
8
internal/utils/nocopy.go
Normal file
8
internal/utils/nocopy.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// empty struct that implements Locker interface
|
||||||
|
// for hinting that no copy should be performed.
|
||||||
|
type NoCopy struct{}
|
||||||
|
|
||||||
|
func (*NoCopy) Lock() {}
|
||||||
|
func (*NoCopy) Unlock() {}
|
|
@ -3,6 +3,7 @@ package utils
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -14,10 +15,12 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SerializedObject = map[string]any
|
type (
|
||||||
type Converter interface {
|
SerializedObject = map[string]any
|
||||||
ConvertFrom(value any) (any, E.NestedError)
|
Converter interface {
|
||||||
}
|
ConvertFrom(value any) (any, E.NestedError)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
|
func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
|
||||||
var i any
|
var i any
|
||||||
|
@ -37,11 +40,16 @@ func ValidateYaml(schema *jsonschema.Schema, data []byte) E.NestedError {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
errors := E.NewBuilder("yaml validation error")
|
var valErr *jsonschema.ValidationError
|
||||||
for _, e := range err.(*jsonschema.ValidationError).Causes {
|
if !errors.As(err, &valErr) {
|
||||||
errors.AddE(e)
|
return E.UnexpectedError(err)
|
||||||
}
|
}
|
||||||
return errors.Build()
|
|
||||||
|
b := E.NewBuilder("yaml validation error")
|
||||||
|
for _, e := range valErr.Causes {
|
||||||
|
b.AddE(e)
|
||||||
|
}
|
||||||
|
return b.Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialize converts the given data into a map[string]any representation.
|
// Serialize converts the given data into a map[string]any representation.
|
||||||
|
@ -80,7 +88,7 @@ func Serialize(data any) (SerializedObject, E.NestedError) {
|
||||||
result[key.String()] = value.MapIndex(key).Interface()
|
result[key.String()] = value.MapIndex(key).Interface()
|
||||||
}
|
}
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
for i := 0; i < value.NumField(); i++ {
|
for i := range value.NumField() {
|
||||||
field := value.Type().Field(i)
|
field := value.Type().Field(i)
|
||||||
if !field.IsExported() {
|
if !field.IsExported() {
|
||||||
continue
|
continue
|
||||||
|
@ -91,9 +99,10 @@ func Serialize(data any) (SerializedObject, E.NestedError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the json tag is not empty, use it as the key
|
// If the json tag is not empty, use it as the key
|
||||||
if jsonTag != "" {
|
switch {
|
||||||
|
case jsonTag != "":
|
||||||
result[jsonTag] = value.Field(i).Interface()
|
result[jsonTag] = value.Field(i).Interface()
|
||||||
} else if field.Anonymous {
|
case field.Anonymous:
|
||||||
// If the field is an embedded struct, add its fields to the result
|
// If the field is an embedded struct, add its fields to the result
|
||||||
fieldMap, err := Serialize(value.Field(i).Interface())
|
fieldMap, err := Serialize(value.Field(i).Interface())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -102,7 +111,7 @@ func Serialize(data any) (SerializedObject, E.NestedError) {
|
||||||
for k, v := range fieldMap {
|
for k, v := range fieldMap {
|
||||||
result[k] = v
|
result[k] = v
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
result[field.Name] = value.Field(i).Interface()
|
result[field.Name] = value.Field(i).Interface()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +156,8 @@ func Deserialize(src SerializedObject, dst any) E.NestedError {
|
||||||
|
|
||||||
// TODO: use E.Builder to collect errors from all fields
|
// TODO: use E.Builder to collect errors from all fields
|
||||||
|
|
||||||
if dstV.Kind() == reflect.Struct {
|
switch dstV.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
mapping := make(map[string]reflect.Value)
|
mapping := make(map[string]reflect.Value)
|
||||||
for _, field := range reflect.VisibleFields(dstT) {
|
for _, field := range reflect.VisibleFields(dstT) {
|
||||||
mapping[ToLowerNoSnake(field.Name)] = dstV.FieldByName(field.Name)
|
mapping[ToLowerNoSnake(field.Name)] = dstV.FieldByName(field.Name)
|
||||||
|
@ -162,7 +172,7 @@ func Deserialize(src SerializedObject, dst any) E.NestedError {
|
||||||
return E.Unexpected("field", k).Subjectf("%T", dst)
|
return E.Unexpected("field", k).Subjectf("%T", dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if dstV.Kind() == reflect.Map && dstT.Key().Kind() == reflect.String {
|
case reflect.Map:
|
||||||
if dstV.IsNil() {
|
if dstV.IsNil() {
|
||||||
dstV.Set(reflect.MakeMap(dstT))
|
dstV.Set(reflect.MakeMap(dstT))
|
||||||
}
|
}
|
||||||
|
@ -174,8 +184,7 @@ func Deserialize(src SerializedObject, dst any) E.NestedError {
|
||||||
}
|
}
|
||||||
dstV.SetMapIndex(reflect.ValueOf(ToLowerNoSnake(k)), tmp)
|
dstV.SetMapIndex(reflect.ValueOf(ToLowerNoSnake(k)), tmp)
|
||||||
}
|
}
|
||||||
return nil
|
default:
|
||||||
} else {
|
|
||||||
return E.Unsupported("target type", fmt.Sprintf("%T", dst))
|
return E.Unsupported("target type", fmt.Sprintf("%T", dst))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,7 +371,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.N
|
||||||
return true, Convert(reflect.ValueOf(tmp), dst)
|
return true, Convert(reflect.ValueOf(tmp), dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeserializeJson(j map[string]string, target any) E.NestedError {
|
func DeserializeJSON(j map[string]string, target any) E.NestedError {
|
||||||
data, err := E.Check(json.Marshal(j))
|
data, err := E.Check(json.Marshal(j))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: support other languages
|
// TODO: support other languages.
|
||||||
var titleCaser = cases.Title(language.AmericanEnglish)
|
var titleCaser = cases.Title(language.AmericanEnglish)
|
||||||
|
|
||||||
func CommaSeperatedList(s string) []string {
|
func CommaSeperatedList(s string) []string {
|
||||||
|
@ -31,3 +31,7 @@ func ExtractPort(fullURL string) (int, error) {
|
||||||
}
|
}
|
||||||
return strconv.Atoi(url.Port())
|
return strconv.Atoi(url.Port())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PortString(port uint16) string {
|
||||||
|
return strconv.FormatUint(uint64(port), 10)
|
||||||
|
}
|
||||||
|
|
|
@ -92,7 +92,6 @@ func ExpectType[T any](t *testing.T, got any) (_ T) {
|
||||||
_, ok := got.(T)
|
_, ok := got.(T)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem())
|
t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem())
|
||||||
t.FailNow()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return got.(T)
|
return got.(T)
|
||||||
|
|
|
@ -7,10 +7,12 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configDirWatcher *dirWatcher
|
var (
|
||||||
var configDirWatcherMu sync.Mutex
|
configDirWatcher *DirWatcher
|
||||||
|
configDirWatcherMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
// create a new file watcher for file under ConfigBasePath
|
// create a new file watcher for file under ConfigBasePath.
|
||||||
func NewConfigFileWatcher(filename string) Watcher {
|
func NewConfigFileWatcher(filename string) Watcher {
|
||||||
configDirWatcherMu.Lock()
|
configDirWatcherMu.Lock()
|
||||||
defer configDirWatcherMu.Unlock()
|
defer configDirWatcherMu.Unlock()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dirWatcher struct {
|
type DirWatcher struct {
|
||||||
dir string
|
dir string
|
||||||
w *fsnotify.Watcher
|
w *fsnotify.Watcher
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ type dirWatcher struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher {
|
func NewDirectoryWatcher(ctx context.Context, dirPath string) *DirWatcher {
|
||||||
//! subdirectories are not watched
|
//! subdirectories are not watched
|
||||||
w, err := fsnotify.NewWatcher()
|
w, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -35,7 +35,7 @@ func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher {
|
||||||
if err = w.Add(dirPath); err != nil {
|
if err = w.Add(dirPath); err != nil {
|
||||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||||
}
|
}
|
||||||
helper := &dirWatcher{
|
helper := &DirWatcher{
|
||||||
dir: dirPath,
|
dir: dirPath,
|
||||||
w: w,
|
w: w,
|
||||||
fwMap: F.NewMapOf[string, *fileWatcher](),
|
fwMap: F.NewMapOf[string, *fileWatcher](),
|
||||||
|
@ -47,11 +47,11 @@ func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher {
|
||||||
return helper
|
return helper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *dirWatcher) Events(_ context.Context) (<-chan Event, <-chan E.NestedError) {
|
func (h *DirWatcher) Events(_ context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||||
return h.eventCh, h.errCh
|
return h.eventCh, h.errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *dirWatcher) Add(relPath string) *fileWatcher {
|
func (h *DirWatcher) Add(relPath string) Watcher {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ func (h *dirWatcher) Add(relPath string) *fileWatcher {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *dirWatcher) start() {
|
func (h *DirWatcher) start() {
|
||||||
defer close(h.eventCh)
|
defer close(h.eventCh)
|
||||||
defer h.w.Close()
|
defer h.w.Close()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue