mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25a2de2a90 | ||
|
|
67b2286df0 | ||
|
|
64728d10ad | ||
|
|
ae69019265 | ||
|
|
c07f2ed722 | ||
|
|
2951304647 | ||
|
|
d936e24692 | ||
|
|
ba26e6a5d6 | ||
|
|
6194bac4c4 | ||
|
|
a1d1325ad6 | ||
|
|
cceebff93a | ||
|
|
f97e3f65fe | ||
|
|
5214ae1760 | ||
|
|
6be3aef367 | ||
|
|
6712e9b109 | ||
|
|
50a0686648 | ||
|
|
d47afa3081 | ||
|
|
1ddfe2fb92 | ||
|
|
3ae3d18566 | ||
|
|
5fdb171d65 | ||
|
|
99e43fe340 | ||
|
|
cf1ecbc826 | ||
|
|
5ff27b9e3d | ||
|
|
291304af75 | ||
|
|
b63ebfcb3b | ||
|
|
c6a9a816f6 | ||
|
|
f5cf716a91 | ||
|
|
6dbee61742 | ||
|
|
d89d97b61f | ||
|
|
01b7ec2a99 | ||
|
|
ddbee9ec19 | ||
|
|
0bbadc6d6d | ||
|
|
64584c73b2 | ||
|
|
8df28628ec | ||
|
|
3bf520541b | ||
|
|
a531896bd6 | ||
|
|
e005b42d18 | ||
|
|
1f6573b6da | ||
|
|
73af381c4c | ||
|
|
625bf4dfdc | ||
|
|
46b4090629 | ||
|
|
91e012987e | ||
|
|
a86d316d07 | ||
|
|
76454df5e6 | ||
|
|
67b6e40f85 | ||
|
|
9889b5a8d3 |
25
.env.example
Normal file
25
.env.example
Normal file
@@ -0,0 +1,25 @@
|
||||
# set timezone to get correct log timestamp
|
||||
TZ=ETC/UTC
|
||||
|
||||
# generate secret with `openssl rand -base64 32`
|
||||
GODOXY_API_JWT_SECRET=
|
||||
|
||||
# the JWT token time-to-live
|
||||
GODOXY_API_JWT_TOKEN_TTL=1h
|
||||
|
||||
# API/WebUI login credentials
|
||||
GODOXY_API_USER=admin
|
||||
GODOXY_API_PASSWORD=password
|
||||
|
||||
# Proxy listening address
|
||||
GODOXY_HTTP_ADDR=:80
|
||||
GODOXY_HTTPS_ADDR=:443
|
||||
|
||||
# API listening address
|
||||
GODOXY_API_ADDR=127.0.0.1:8888
|
||||
|
||||
# Prometheus Metrics listening address (uncomment to enable)
|
||||
#GODOXY_PROMETHEUS_ADDR=:8889
|
||||
|
||||
# Debug mode
|
||||
GODOXY_DEBUG=false
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ config*/
|
||||
certs*/
|
||||
bin/
|
||||
error_pages/
|
||||
!examples/error_pages/
|
||||
|
||||
logs/
|
||||
log/
|
||||
|
||||
@@ -108,6 +108,7 @@ linters:
|
||||
- prealloc # Too many false-positive.
|
||||
- makezero # Not relevant
|
||||
- dupl # Too strict
|
||||
- gci # I don't care
|
||||
- gosec # Too strict
|
||||
- gochecknoinits
|
||||
- gochecknoglobals
|
||||
|
||||
@@ -17,6 +17,9 @@ runtimes:
|
||||
- go@1.23.2
|
||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||
lint:
|
||||
disabled:
|
||||
- markdownlint
|
||||
- yamllint
|
||||
enabled:
|
||||
- hadolint@2.12.0
|
||||
- actionlint@1.7.3
|
||||
@@ -24,14 +27,12 @@ lint:
|
||||
- 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
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Builder
|
||||
FROM golang:1.23.2-alpine AS builder
|
||||
FROM golang:1.23.3-alpine AS builder
|
||||
RUN apk add --no-cache tzdata make
|
||||
|
||||
WORKDIR /src
|
||||
@@ -26,7 +26,7 @@ RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||
--mount=type=bind,src=pkg,dst=/src/pkg \
|
||||
make build && \
|
||||
mkdir -p /app/error_pages /app/certs && \
|
||||
mv bin/go-proxy /app/go-proxy
|
||||
mv bin/godoxy /app/godoxy
|
||||
|
||||
# Stage 2: Final image
|
||||
FROM scratch
|
||||
@@ -43,11 +43,14 @@ COPY --from=builder /app /app
|
||||
# copy schema directory
|
||||
COPY schema/ /app/schema/
|
||||
|
||||
# copy example config
|
||||
COPY config.example.yml /app/config/config.yml
|
||||
|
||||
# copy certs
|
||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
ENV GODOXY_DEBUG=0
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8888
|
||||
@@ -55,4 +58,4 @@ EXPOSE 443
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/app/go-proxy"]
|
||||
CMD ["/app/godoxy"]
|
||||
|
||||
20
Makefile
20
Makefile
@@ -13,7 +13,7 @@ build:
|
||||
scripts/build.sh
|
||||
|
||||
test:
|
||||
GOPROXY_TEST=1 go test ./internal/...
|
||||
GODOXY_TEST=1 go test ./internal/...
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
@@ -28,22 +28,20 @@ get:
|
||||
go get -u ./cmd && go mod tidy
|
||||
|
||||
debug:
|
||||
make build
|
||||
sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
GODOXY_DEBUG=1 make run
|
||||
|
||||
debug-trace:
|
||||
make build
|
||||
sudo GOPROXY_DEBUG=1 GOPROXY_TRACE=1 bin/go-proxy
|
||||
GODOXY_DEBUG=1 GODOXY_TRACE=1 run
|
||||
|
||||
profile:
|
||||
GODEBUG=gctrace=1 make build
|
||||
sudo GOPROXY_DEBUG=1 bin/go-proxy
|
||||
GODEBUG=gctrace=1 make debug
|
||||
|
||||
run: build
|
||||
sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy
|
||||
bin/godoxy
|
||||
|
||||
mtrace:
|
||||
bin/go-proxy debug-ls-mtrace > mtrace.json
|
||||
|
||||
run:
|
||||
make build && sudo bin/go-proxy
|
||||
bin/godoxy debug-ls-mtrace > mtrace.json
|
||||
|
||||
archive:
|
||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
||||
|
||||
71
README.md
71
README.md
@@ -1,4 +1,4 @@
|
||||
# go-proxy
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
@@ -19,11 +19,14 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [go-proxy](#go-proxy)
|
||||
- [GoDoxy](#godoxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Features](#key-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
@@ -51,30 +54,43 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### Setup
|
||||
|
||||
1. Pull docker image
|
||||
1. Pull the latest docker images
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
```
|
||||
|
||||
2. Create new directory, `cd` into it, then run setup
|
||||
2. Create new directory, `cd` into it, then run setup, or [set up manually](#manual-setup)
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/go-proxy setup
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. Setup DNS Records point to machine which runs `go-proxy`, e.g.
|
||||
3. _(Optional)_ setup WebUI login
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
- set random JWT secret
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
4. Setup `docker-socket-proxy` other docker nodes _(if any)_ (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) and then them inside `config.yml`
|
||||
- change username and password for WebUI authentication
|
||||
```shell
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=admin|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=some-strong-password|g" .env
|
||||
```
|
||||
|
||||
5. Run go-proxy `docker compose up -d`
|
||||
then list all routes to see if further configurations are needed:
|
||||
`docker exec go-proxy /app/go-proxy ls-routes`
|
||||
4. _(Optional)_ setup `docker-socket-proxy` other docker nodes (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) then add them inside `config.yml`
|
||||
|
||||
5. Start the container `docker compose up -d`
|
||||
|
||||
6. You may now do some extra configuration
|
||||
- With text editor (e.g. Visual Studio Code)
|
||||
@@ -83,6 +99,37 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/config.example.yml -O config/config.yml`
|
||||
|
||||
2. Grab `.env.example` into `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/.env.example -O .env`
|
||||
|
||||
3. Grab `compose.example.yml` into `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.7/compose.example.yml -O compose.yml`
|
||||
|
||||
### Folder structrue
|
||||
|
||||
```shell
|
||||
├── certs
|
||||
│ ├── cert.crt
|
||||
│ └── priv.key
|
||||
├── compose.yml
|
||||
├── config
|
||||
│ ├── config.yml
|
||||
│ ├── middlewares
|
||||
│ │ ├── middleware1.yml
|
||||
│ │ ├── middleware2.yml
|
||||
│ ├── provider1.yml
|
||||
│ └── provider2.yml
|
||||
└── .env
|
||||
```
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
||||
|
||||
89
cmd/main.go
89
cmd/main.go
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/server"
|
||||
@@ -26,21 +27,43 @@ import (
|
||||
func main() {
|
||||
args := common.GetArgs()
|
||||
|
||||
if args.Command == common.CommandSetup {
|
||||
switch args.Command {
|
||||
case common.CommandSetup:
|
||||
internal.Setup()
|
||||
return
|
||||
}
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
case common.CommandReload:
|
||||
if err := query.ReloadServer(); err != nil {
|
||||
E.LogFatal("server reload error", err)
|
||||
}
|
||||
logging.Info().Msg("ok")
|
||||
return
|
||||
case common.CommandListIcons:
|
||||
icons, err := internal.ListAvailableIcons()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(icons)
|
||||
return
|
||||
case common.CommandListRoutes:
|
||||
routes, err := query.ListRoutes()
|
||||
if err != nil {
|
||||
log.Printf("failed to connect to api server: %s", err)
|
||||
log.Printf("falling back to config file")
|
||||
} else {
|
||||
printJSON(routes)
|
||||
return
|
||||
}
|
||||
case common.CommandDebugListMTrace:
|
||||
trace, err := query.ListMiddlewareTraces()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(trace)
|
||||
return
|
||||
}
|
||||
|
||||
if args.Command == common.CommandStart {
|
||||
logging.Info().Msgf("go-proxy version %s", pkg.GetVersion())
|
||||
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||
logging.Trace().Msg("trace enabled")
|
||||
// logging.AddHook(notif.GetDispatcher())
|
||||
} else {
|
||||
@@ -72,46 +95,23 @@ func main() {
|
||||
}
|
||||
|
||||
switch args.Command {
|
||||
case common.CommandListRoutes:
|
||||
cfg.StartProxyProviders()
|
||||
printJSON(config.RoutesByAlias())
|
||||
return
|
||||
case common.CommandListConfigs:
|
||||
printJSON(config.Value())
|
||||
return
|
||||
case common.CommandListRoutes:
|
||||
routes, err := query.ListRoutes()
|
||||
if err != nil {
|
||||
log.Printf("failed to connect to api server: %s", err)
|
||||
log.Printf("falling back to config file")
|
||||
printJSON(config.RoutesByAlias())
|
||||
} else {
|
||||
printJSON(routes)
|
||||
}
|
||||
return
|
||||
case common.CommandListIcons:
|
||||
icons, err := internal.ListAvailableIcons()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(icons)
|
||||
return
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(config.DumpEntries())
|
||||
return
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(config.DumpProviders())
|
||||
return
|
||||
case common.CommandDebugListMTrace:
|
||||
trace, err := query.ListMiddlewareTraces()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(trace)
|
||||
return
|
||||
case common.CommandDebugListTasks:
|
||||
tasks, err := query.DebugListTasks()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
printJSON(tasks)
|
||||
return
|
||||
}
|
||||
|
||||
if common.APIJWTSecret == nil {
|
||||
logging.Warn().Msg("API JWT secret is empty, authentication is disabled")
|
||||
}
|
||||
|
||||
cfg.StartProxyProviders()
|
||||
@@ -131,7 +131,7 @@ func main() {
|
||||
logging.Info().Msg("autocert not configured")
|
||||
}
|
||||
|
||||
proxyServer := server.InitProxyServer(server.Options{
|
||||
server.StartServer(server.Options{
|
||||
Name: "proxy",
|
||||
CertProvider: autocert,
|
||||
HTTPAddr: common.ProxyHTTPAddr,
|
||||
@@ -139,7 +139,7 @@ func main() {
|
||||
Handler: http.HandlerFunc(R.ProxyHandler),
|
||||
RedirectToHTTPS: config.Value().RedirectToHTTPS,
|
||||
})
|
||||
apiServer := server.InitAPIServer(server.Options{
|
||||
server.StartServer(server.Options{
|
||||
Name: "api",
|
||||
CertProvider: autocert,
|
||||
HTTPAddr: common.APIHTTPAddr,
|
||||
@@ -147,8 +147,15 @@ func main() {
|
||||
RedirectToHTTPS: config.Value().RedirectToHTTPS,
|
||||
})
|
||||
|
||||
proxyServer.Start()
|
||||
apiServer.Start()
|
||||
if common.PrometheusEnabled {
|
||||
server.StartServer(server.Options{
|
||||
Name: "metrics",
|
||||
CertProvider: autocert,
|
||||
HTTPAddr: common.MetricsHTTPAddr,
|
||||
Handler: metrics.NewHandler(),
|
||||
RedirectToHTTPS: config.Value().RedirectToHTTPS,
|
||||
})
|
||||
}
|
||||
|
||||
// wait for signal
|
||||
<-sig
|
||||
@@ -173,5 +180,5 @@ func printJSON(obj any) {
|
||||
logging.Fatal().Err(err).Send()
|
||||
}
|
||||
rawLogger := log.New(os.Stdout, "", 0)
|
||||
rawLogger.Printf("%s", j) // raw output for convenience using "jq"
|
||||
rawLogger.Print(string(j)) // raw output for convenience using "jq"
|
||||
}
|
||||
|
||||
@@ -1,45 +1,42 @@
|
||||
---
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
container_name: go-proxy-frontend
|
||||
container_name: godoxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
env_file: .env
|
||||
depends_on:
|
||||
- app
|
||||
# if you also want to proxy the WebUI and access it via gp.y.z
|
||||
# labels:
|
||||
# - proxy.aliases=gp
|
||||
# - proxy.gp.port=3000
|
||||
|
||||
# Make sure the value is same as `GOPROXY_API_ADDR` below (if you have changed it)
|
||||
#
|
||||
# environment:
|
||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
||||
# modify below to fit your needs
|
||||
labels:
|
||||
proxy.aliases: gp
|
||||
proxy.#1.port: 3000
|
||||
proxy.#1.middlewares.cidr_whitelist.status_code: 403
|
||||
proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
|
||||
proxy.#1.middlewares.cidr_whitelist.allow: |
|
||||
- 127.0.0.1
|
||||
- 10.0.0.0/8
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
app:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
container_name: go-proxy
|
||||
container_name: godoxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
environment:
|
||||
# (Optional) change this to your timezone to get correct log timestamp
|
||||
TZ: ETC/UTC
|
||||
|
||||
# Change these if you need
|
||||
#
|
||||
# GOPROXY_HTTP_ADDR: :80
|
||||
# GOPROXY_HTTPS_ADDR: :443
|
||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
||||
env_file: .env
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
- ./error_pages:/app/error_pages
|
||||
|
||||
# (Optional) choose one of below to enable https
|
||||
# 1. use existing certificate
|
||||
# if your cert is not named `cert.crt` change `cert_path` in `config/config.yml`
|
||||
# if your cert key is not named `priv.key` change `key_path` in `config/config.yml`
|
||||
|
||||
# - /path/to/certs:/app/certs
|
||||
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||
|
||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
||||
# 2. use autocert, certs will be stored in ./certs
|
||||
# you can also use a docker volume to store it
|
||||
|
||||
# - ./certs:/app/certs
|
||||
|
||||
@@ -57,13 +57,19 @@ providers:
|
||||
# - my.site
|
||||
# - node1.my.app
|
||||
|
||||
# homepage config
|
||||
#
|
||||
homepage:
|
||||
# use default app categories detected from alias or docker image name
|
||||
use_default_categories: true
|
||||
|
||||
# Below are fixed options (non hot-reloadable)
|
||||
|
||||
# timeout for shutdown (in seconds)
|
||||
#
|
||||
# timeout_shutdown: 5
|
||||
timeout_shutdown: 5
|
||||
|
||||
# global setting redirect http requests to https (if https available, otherwise this will be ignored)
|
||||
# proxy.<alias>.middlewares.redirect_http will override this
|
||||
#
|
||||
# redirect_to_https: false
|
||||
redirect_to_https: false
|
||||
|
||||
288
examples/error_pages/404.css
Normal file
288
examples/error_pages/404.css
Normal file
@@ -0,0 +1,288 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Audiowide&display=swap");
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
background: radial-gradient(circle, #240015 0%, #12000b 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
h2 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: 150px;
|
||||
font-size: 32px;
|
||||
text-transform: uppercase;
|
||||
transform: translate(-50%, -50%);
|
||||
display: block;
|
||||
color: #12000a;
|
||||
font-weight: 300;
|
||||
font-family: Audiowide;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
animation: fadeInText 3s ease-in 3.5s forwards,
|
||||
flicker4 5s linear 7.5s infinite, hueRotate 6s ease-in-out 3s infinite;
|
||||
}
|
||||
|
||||
#svgWrap_1,
|
||||
#svgWrap_2 {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
width: 600px;
|
||||
max-width: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#svgWrap_1,
|
||||
#svgWrap_2,
|
||||
div {
|
||||
animation: hueRotate 6s ease-in-out 3s infinite;
|
||||
}
|
||||
|
||||
#id1_1,
|
||||
#id2_1,
|
||||
#id3_1 {
|
||||
stroke: #ff005d;
|
||||
stroke-width: 3px;
|
||||
fill: transparent;
|
||||
filter: url(#glow);
|
||||
}
|
||||
|
||||
#id1_2,
|
||||
#id2_2,
|
||||
#id3_2 {
|
||||
stroke: #12000a;
|
||||
stroke-width: 3px;
|
||||
fill: transparent;
|
||||
filter: url(#glow);
|
||||
}
|
||||
|
||||
#id3_1 {
|
||||
stroke-dasharray: 940px;
|
||||
stroke-dashoffset: -940px;
|
||||
animation: drawLine3 2.5s ease-in-out 0s forwards,
|
||||
flicker3 4s linear 4s infinite;
|
||||
}
|
||||
|
||||
#id2_1 {
|
||||
stroke-dasharray: 735px;
|
||||
stroke-dashoffset: -735px;
|
||||
animation: drawLine2 2.5s ease-in-out 0.5s forwards,
|
||||
flicker2 4s linear 4.5s infinite;
|
||||
}
|
||||
|
||||
#id1_1 {
|
||||
stroke-dasharray: 940px;
|
||||
stroke-dashoffset: -940px;
|
||||
animation: drawLine1 2.5s ease-in-out 1s forwards,
|
||||
flicker1 4s linear 5s infinite;
|
||||
}
|
||||
|
||||
@keyframes drawLine1 {
|
||||
0% {
|
||||
stroke-dashoffset: -940px;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drawLine2 {
|
||||
0% {
|
||||
stroke-dashoffset: -735px;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drawLine3 {
|
||||
0% {
|
||||
stroke-dashoffset: -940px;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker1 {
|
||||
0% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
1% {
|
||||
stroke: transparent;
|
||||
}
|
||||
3% {
|
||||
stroke: transparent;
|
||||
}
|
||||
4% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
6% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
7% {
|
||||
stroke: transparent;
|
||||
}
|
||||
13% {
|
||||
stroke: transparent;
|
||||
}
|
||||
14% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
100% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker2 {
|
||||
0% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
50% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
51% {
|
||||
stroke: transparent;
|
||||
}
|
||||
61% {
|
||||
stroke: transparent;
|
||||
}
|
||||
62% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
100% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker3 {
|
||||
0% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
1% {
|
||||
stroke: transparent;
|
||||
}
|
||||
10% {
|
||||
stroke: transparent;
|
||||
}
|
||||
11% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
40% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
41% {
|
||||
stroke: transparent;
|
||||
}
|
||||
45% {
|
||||
stroke: transparent;
|
||||
}
|
||||
46% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
100% {
|
||||
stroke: #ff005d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flicker4 {
|
||||
0% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
30% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
31% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
32% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
36% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
37% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
41% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
42% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
85% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
86% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
95% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
96% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
100% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInText {
|
||||
1% {
|
||||
color: #12000a;
|
||||
text-shadow: 0px 0px 4px #12000a;
|
||||
}
|
||||
70% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 14px #ff005d;
|
||||
}
|
||||
100% {
|
||||
color: #ff005d;
|
||||
text-shadow: 0px 0px 4px #ff005d;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hueRotate {
|
||||
0% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
filter: hue-rotate(-120deg);
|
||||
}
|
||||
100% {
|
||||
filter: hue-rotate(0deg);
|
||||
}
|
||||
}
|
||||
51
examples/error_pages/404.html
Normal file
51
examples/error_pages/404.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{{/* Credit: https://codepen.io/code2rithik/pen/XWpVvYL */}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Page Not Found</title>
|
||||
<link rel="stylesheet" href="/$gperrorpage/404.css" type="text/css">
|
||||
<!-- <script src="/$gperrorpage/404.js"> </script> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>0</script>
|
||||
<div></div>
|
||||
<svg id="svgWrap_2" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
|
||||
<g>
|
||||
<path id="id3_2"
|
||||
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
|
||||
<path id="id2_2"
|
||||
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
|
||||
<path id="id1_2"
|
||||
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="svgWrap_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 700 250">
|
||||
<g>
|
||||
<path id="id3_1"
|
||||
d="M195.7 232.67h-37.1V149.7H27.76c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98H158.6V29.62h37.1v203.05z" />
|
||||
<path id="id2_1"
|
||||
d="M470.69 147.71c0 8.31-1.06 16.17-3.19 23.58-2.12 7.41-5.12 14.28-8.99 20.6-3.87 6.33-8.45 11.99-13.74 16.99-5.29 5-11.07 9.28-17.35 12.81a85.146 85.146 0 0 1-20.04 8.14 83.637 83.637 0 0 1-21.67 2.83H319.3c-7.46 0-14.73-.94-21.81-2.83-7.08-1.89-13.76-4.6-20.04-8.14a88.292 88.292 0 0 1-17.35-12.81c-5.29-5-9.84-10.67-13.66-16.99-3.82-6.32-6.8-13.19-8.92-20.6-2.12-7.41-3.19-15.27-3.19-23.58v-33.13c0-12.46 2.34-23.88 7.01-34.27 4.67-10.38 10.92-19.33 18.76-26.83 7.83-7.5 16.87-13.36 27.12-17.56 10.24-4.2 20.93-6.3 32.07-6.3h66.41c7.36 0 14.58.94 21.67 2.83 7.08 1.89 13.76 4.6 20.04 8.14a88.292 88.292 0 0 1 17.35 12.81c5.29 5 9.86 10.67 13.74 16.99 3.87 6.33 6.87 13.19 8.99 20.6 2.13 7.41 3.19 15.27 3.19 23.58v33.14zm-37.1-33.13c0-7.27-1.32-13.88-3.96-19.82-2.64-5.95-6.16-11.04-10.55-15.29-4.39-4.25-9.46-7.5-15.22-9.77-5.76-2.27-11.8-3.35-18.13-3.26h-66.41c-6.14-.09-12.11.97-17.91 3.19-5.81 2.22-10.95 5.43-15.44 9.63-4.48 4.2-8.07 9.3-10.76 15.29-2.69 6-4.04 12.67-4.04 20.04v33.13c0 7.36 1.32 14.02 3.96 19.97 2.64 5.95 6.18 11.02 10.62 15.22 4.44 4.2 9.56 7.43 15.36 9.7 5.8 2.27 11.87 3.35 18.2 3.26h66.41c7.27 0 13.85-1.2 19.75-3.61s10.93-5.73 15.08-9.98 7.36-9.32 9.63-15.22c2.27-5.9 3.4-12.34 3.4-19.33v-33.15zm-16-26.91a17.89 17.89 0 0 1 2.83 6.73c.47 2.41.47 4.77 0 7.08-.47 2.31-1.39 4.48-2.76 6.51-1.37 2.03-3.14 3.75-5.31 5.17l-99.4 66.41c-1.61 1.23-3.26 2.08-4.96 2.55-1.7.47-3.45.71-5.24.71-3.02 0-5.9-.71-8.64-2.12-2.74-1.42-4.96-3.44-6.66-6.09a17.89 17.89 0 0 1-2.83-6.73c-.47-2.41-.5-4.77-.07-7.08.43-2.31 1.3-4.48 2.62-6.51 1.32-2.03 3.07-3.75 5.24-5.17l99.69-66.41a17.89 17.89 0 0 1 6.73-2.83c2.41-.47 4.77-.47 7.08 0 2.31.47 4.48 1.37 6.51 2.69 2.03 1.32 3.75 3.02 5.17 5.09z" />
|
||||
<path id="id1_1"
|
||||
d="M688.33 232.67h-37.1V149.7H520.39c-2.64 0-5.1-.5-7.36-1.49-2.27-.99-4.23-2.31-5.88-3.96-1.65-1.65-2.95-3.61-3.89-5.88s-1.42-4.67-1.42-7.22V29.62h36.82v82.98h112.57V29.62h37.1v203.05z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg>
|
||||
<defs>
|
||||
<filter id="glow">
|
||||
<fegaussianblur class="blur" result="coloredBlur" stddeviation="4"></fegaussianblur>
|
||||
<femerge>
|
||||
<femergenode in="coloredBlur"></femergenode>
|
||||
<femergenode in="SourceGraphic"></femergenode>
|
||||
</femerge>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<h2>Page Not Found</h2>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1002
examples/grafana_template.json
Normal file
1002
examples/grafana_template.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
container_name: microbin
|
||||
cpu_shares: 10
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
env_file: .env
|
||||
image: danielszabo99/microbin:latest
|
||||
ports:
|
||||
- 8080
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/microbin_data
|
||||
# microbin.domain.tld
|
||||
@@ -1,16 +0,0 @@
|
||||
services:
|
||||
main:
|
||||
image: b3log/siyuan:v3.1.0
|
||||
container_name: siyuan
|
||||
command:
|
||||
- --workspace=/siyuan/workspace/
|
||||
- --accessAuthCode=<some password>
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- ./workspace:/siyuan/workspace
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Hong_Kong
|
||||
ports:
|
||||
- 6806
|
||||
# siyuan.domain.tld
|
||||
18
go.mod
18
go.mod
@@ -1,27 +1,31 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.23.2
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/docker/cli v27.3.1+incompatible
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gotify/server/v2 v2.5.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.org/x/time v0.7.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.109.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
@@ -33,17 +37,21 @@ require (
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
@@ -57,8 +65,8 @@ require (
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
33
go.sum
33
go.sum
@@ -2,16 +2,19 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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/cloudflare/cloudflare-go v0.108.0 h1:C4Skfjd8I8X3uEOGmQUT4/iGyZcWdkIU7HwvMoLkEE0=
|
||||
github.com/cloudflare/cloudflare-go v0.108.0/go.mod h1:m492eNahT/9MsN7Ppnoge8AaI7QhVFtEgVm3I9HJFeU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/cloudflare-go v0.109.0 h1:Wjp+RfJD1lidIFUlrTBqUQnCBrUnmVsLxgzWYiURueg=
|
||||
github.com/cloudflare/cloudflare-go v0.109.0/go.mod h1:m492eNahT/9MsN7Ppnoge8AaI7QhVFtEgVm3I9HJFeU=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -28,8 +31,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
|
||||
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
@@ -61,10 +64,14 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
@@ -81,21 +88,29 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@@ -181,8 +196,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
|
||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
@@ -9,7 +8,8 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
)
|
||||
|
||||
type ServeMux struct{ *http.ServeMux }
|
||||
@@ -19,15 +19,13 @@ func NewServeMux() ServeMux {
|
||||
}
|
||||
|
||||
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
|
||||
mux.ServeMux.HandleFunc(fmt.Sprintf("%s %s", method, endpoint), checkHost(handler))
|
||||
mux.ServeMux.HandleFunc(method+" "+endpoint, checkHost(rateLimited(handler)))
|
||||
}
|
||||
|
||||
func NewHandler() http.Handler {
|
||||
mux := NewServeMux()
|
||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||
// mux.HandleFunc("GET", "/v1/checkhealth", v1.CheckHealth)
|
||||
// mux.HandleFunc("HEAD", "/v1/checkhealth", v1.CheckHealth)
|
||||
mux.HandleFunc("POST", "/v1/login", auth.LoginHandler)
|
||||
mux.HandleFunc("GET", "/v1/logout", auth.LogoutHandler)
|
||||
mux.HandleFunc("POST", "/v1/logout", auth.LogoutHandler)
|
||||
@@ -56,12 +54,20 @@ func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
LogDebug(r).Interface("headers", r.Header).Msg("API request")
|
||||
f(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||
func rateLimited(f http.HandlerFunc) http.HandlerFunc {
|
||||
m, err := middleware.RateLimiter.WithOptionsClone(middleware.OptionsRaw{
|
||||
"average": 10,
|
||||
"burst": 10,
|
||||
})
|
||||
if err != nil {
|
||||
logging.Fatal().Err(err).Msg("unable to create API rate limiter")
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
f(cfg, w, r)
|
||||
m.ModifyRequest(f, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ var (
|
||||
ErrInvalidPassword = E.New("invalid password")
|
||||
)
|
||||
|
||||
const tokenExpiration = 24 * time.Hour
|
||||
|
||||
const jwtClaimKeyUsername = "username"
|
||||
|
||||
func validatePassword(cred *Credentials) error {
|
||||
if cred.Username != common.APIUser {
|
||||
return ErrInvalidUsername.Subject(cred.Username)
|
||||
@@ -56,7 +52,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(tokenExpiration)
|
||||
expiresAt := time.Now().Add(common.APIJWTTokenTTL)
|
||||
claim := &Claims{
|
||||
Username: creds.Username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
@@ -94,7 +90,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
if common.IsDebugSkipAuth {
|
||||
if common.IsDebugSkipAuth || common.APIJWTSecret == nil {
|
||||
return next
|
||||
}
|
||||
|
||||
@@ -114,7 +110,7 @@ func checkToken(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
var claims Claims
|
||||
token, err := jwt.ParseWithClaims(tokenCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return common.APIJWTSecret, nil
|
||||
})
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
target := r.FormValue("target")
|
||||
if target == "" {
|
||||
HandleErr(w, r, ErrMissingKey("target"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
result, ok := health.Inspect(target)
|
||||
if !ok {
|
||||
HandleErr(w, r, ErrNotFound("target", target), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json, err := result.MarshalJSON()
|
||||
if err != nil {
|
||||
HandleErr(w, r, err)
|
||||
return
|
||||
}
|
||||
RespondJSON(w, r, json)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func ReloadServer() E.Error {
|
||||
resp, err := U.Post(fmt.Sprintf("%s/v1/reload", common.APIHTTPURL), "", nil)
|
||||
resp, err := U.Post(common.APIHTTPURL+"/v1/reload", "", nil)
|
||||
if err != nil {
|
||||
return E.From(err)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
"github.com/yusing/go-proxy/internal/server"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -19,15 +18,17 @@ func Stats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func StatsWS(w http.ResponseWriter, r *http.Request) {
|
||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||
originPats := make([]string, len(config.Value().MatchDomains)+len(localAddresses))
|
||||
var originPats []string
|
||||
|
||||
if len(originPats) == 0 {
|
||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||
|
||||
if len(config.Value().MatchDomains) == 0 {
|
||||
U.LogWarn(r).Msg("no match domains configured, accepting websocket API request from all origins")
|
||||
originPats = []string{"*"}
|
||||
} else {
|
||||
originPats = make([]string, len(config.Value().MatchDomains))
|
||||
for i, domain := range config.Value().MatchDomains {
|
||||
originPats[i] = "*." + domain
|
||||
originPats[i] = "*" + domain
|
||||
}
|
||||
originPats = append(originPats, localAddresses...)
|
||||
}
|
||||
@@ -59,9 +60,11 @@ func StatsWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func getStats() map[string]any {
|
||||
return map[string]any{
|
||||
"proxies": config.Statistics(),
|
||||
"uptime": strutils.FormatDuration(server.GetProxyServer().Uptime()),
|
||||
"uptime": strutils.FormatDuration(time.Since(startTime)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,4 @@ func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
|
||||
func LogError(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.ErrorLevel) }
|
||||
func LogWarn(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.WarnLevel) }
|
||||
func LogInfo(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.InfoLevel) }
|
||||
func LogDebug(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.DebugLevel) }
|
||||
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
@@ -23,7 +24,7 @@ func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int)
|
||||
|
||||
switch data := data.(type) {
|
||||
case string:
|
||||
j = []byte(`"` + data + `"`)
|
||||
j = []byte(fmt.Sprintf("%q", data))
|
||||
case []byte:
|
||||
j = data
|
||||
default:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
@@ -31,7 +29,3 @@ var providersGenMap = map[string]ProviderGenerator{
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
}
|
||||
|
||||
var (
|
||||
ErrGetCertFailure = errors.New("get certificate failed")
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package autocert
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
@@ -35,6 +36,8 @@ type (
|
||||
CertExpiries map[string]time.Time
|
||||
)
|
||||
|
||||
var ErrGetCertFailure = errors.New("get certificate failed")
|
||||
|
||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if p.tlsCert == nil {
|
||||
return nil, ErrGetCertFailure
|
||||
@@ -248,10 +251,7 @@ func (p *Provider) renewIfNeeded() E.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.ObtainCert(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return p.ObtainCert()
|
||||
}
|
||||
|
||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
||||
|
||||
@@ -21,7 +21,6 @@ const (
|
||||
CommandDebugListEntries = "debug-ls-entries"
|
||||
CommandDebugListProviders = "debug-ls-providers"
|
||||
CommandDebugListMTrace = "debug-ls-mtrace"
|
||||
CommandDebugListTasks = "debug-ls-tasks"
|
||||
)
|
||||
|
||||
var ValidCommands = []string{
|
||||
@@ -35,7 +34,6 @@ var ValidCommands = []string{
|
||||
CommandDebugListEntries,
|
||||
CommandDebugListProviders,
|
||||
CommandDebugListMTrace,
|
||||
CommandDebugListTasks,
|
||||
}
|
||||
|
||||
func GetArgs() Args {
|
||||
|
||||
@@ -13,7 +13,8 @@ const (
|
||||
// file, folder structure
|
||||
|
||||
const (
|
||||
DotEnvPath = ".env"
|
||||
DotEnvPath = ".env"
|
||||
DotEnvExamplePath = ".env.example"
|
||||
|
||||
ConfigBasePath = "config"
|
||||
ConfigFileName = "config.yml"
|
||||
|
||||
@@ -23,6 +23,9 @@ func generateJWTKey(size int) string {
|
||||
}
|
||||
|
||||
func decodeJWTKey(key string) []byte {
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
bytes, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to decode jwt key")
|
||||
|
||||
@@ -6,59 +6,83 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", true)
|
||||
IsTest = GetEnvBool("GOPROXY_TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
||||
IsDebug = GetEnvBool("GOPROXY_DEBUG", IsTest)
|
||||
IsDebugSkipAuth = GetEnvBool("GOPROXY_DEBUG_SKIP_AUTH", false)
|
||||
IsTrace = GetEnvBool("GOPROXY_TRACE", false) && IsDebug
|
||||
prefixes = []string{"GODOXY_", "GOPROXY_", ""}
|
||||
|
||||
NoSchemaValidation = GetEnvBool("NO_SCHEMA_VALIDATION", true)
|
||||
IsTest = GetEnvBool("TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
||||
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||
IsDebugSkipAuth = GetEnvBool("DEBUG_SKIP_AUTH", false)
|
||||
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||
IsProduction = !IsTest && !IsDebug
|
||||
|
||||
ProxyHTTPAddr,
|
||||
ProxyHTTPHost,
|
||||
ProxyHTTPPort,
|
||||
ProxyHTTPURL = GetAddrEnv("GOPROXY_HTTP_ADDR", ":80", "http")
|
||||
ProxyHTTPURL = GetAddrEnv("HTTP_ADDR", ":80", "http")
|
||||
|
||||
ProxyHTTPSAddr,
|
||||
ProxyHTTPSHost,
|
||||
ProxyHTTPSPort,
|
||||
ProxyHTTPSURL = GetAddrEnv("GOPROXY_HTTPS_ADDR", ":443", "https")
|
||||
ProxyHTTPSURL = GetAddrEnv("HTTPS_ADDR", ":443", "https")
|
||||
|
||||
APIHTTPAddr,
|
||||
APIHTTPHost,
|
||||
APIHTTPPort,
|
||||
APIHTTPURL = GetAddrEnv("GOPROXY_API_ADDR", "127.0.0.1:8888", "http")
|
||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||
|
||||
APIJWTSecret = decodeJWTKey(GetEnv("GOPROXY_API_JWT_SECRET", generateJWTKey(32)))
|
||||
APIUser = GetEnv("GOPROXY_API_USER", "admin")
|
||||
APIPasswordHash = HashPassword(GetEnv("GOPROXY_API_PASSWORD", "password"))
|
||||
MetricsHTTPAddr,
|
||||
MetricsHTTPHost,
|
||||
MetricsHTTPPort,
|
||||
MetricsHTTPURL = GetAddrEnv("PROMETHEUS_ADDR", "", "http")
|
||||
PrometheusEnabled = MetricsHTTPURL != ""
|
||||
|
||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
||||
APIUser = GetEnvString("API_USER", "admin")
|
||||
APIPasswordHash = HashPassword(GetEnvString("API_PASSWORD", "password"))
|
||||
)
|
||||
|
||||
func GetEnvBool(key string, defaultValue bool) bool {
|
||||
value, ok := os.LookupEnv(key)
|
||||
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
||||
var value string
|
||||
var ok bool
|
||||
for _, prefix := range prefixes {
|
||||
value, ok = os.LookupEnv(prefix + key)
|
||||
if ok && value != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok || value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("env %s: invalid boolean value: %s", key, value)
|
||||
parsed, err := parser(value)
|
||||
if err == nil {
|
||||
return parsed
|
||||
}
|
||||
return b
|
||||
log.Fatal().Err(err).Msgf("env %s: invalid %T value: %s", key, parsed, value)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func GetEnv(key, defaultValue string) string {
|
||||
value, ok := os.LookupEnv(key)
|
||||
if !ok || value == "" {
|
||||
value = defaultValue
|
||||
}
|
||||
return value
|
||||
func GetEnvString(key string, defaultValue string) string {
|
||||
return GetEnv(key, defaultValue, func(s string) (string, error) {
|
||||
return s, nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetEnvBool(key string, defaultValue bool) bool {
|
||||
return GetEnv(key, defaultValue, strconv.ParseBool)
|
||||
}
|
||||
|
||||
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
|
||||
addr = GetEnv(key, defaultValue)
|
||||
addr = GetEnvString(key, defaultValue)
|
||||
if addr == "" {
|
||||
return
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
|
||||
@@ -69,3 +93,7 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL str
|
||||
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
||||
return
|
||||
}
|
||||
|
||||
func GetDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
||||
return GetEnv(key, defaultValue, time.ParseDuration)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -177,6 +178,11 @@ func (cfg *Config) load() E.Error {
|
||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||
|
||||
cfg.value = model
|
||||
for i, domain := range model.MatchDomains {
|
||||
if !strings.HasPrefix(domain, ".") {
|
||||
model.MatchDomains[i] = "." + domain
|
||||
}
|
||||
}
|
||||
route.SetFindMuxDomains(model.MatchDomains)
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -69,17 +68,32 @@ func HomepageConfig() homepage.Config {
|
||||
)
|
||||
}
|
||||
|
||||
if entry.IsDocker(r) {
|
||||
if instance.value.Homepage.UseDefaultCategories {
|
||||
if en.Container != nil && item.Category == "" {
|
||||
if category, ok := homepage.PredefinedCategories[en.Container.ImageName]; ok {
|
||||
item.Category = category
|
||||
}
|
||||
}
|
||||
|
||||
if item.Category == "" {
|
||||
if category, ok := homepage.PredefinedCategories[strings.ToLower(alias)]; ok {
|
||||
item.Category = category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsDocker(r):
|
||||
if item.Category == "" {
|
||||
item.Category = "Docker"
|
||||
}
|
||||
item.SourceType = string(proxy.ProviderTypeDocker)
|
||||
} else if entry.UseLoadBalance(r) {
|
||||
case entry.UseLoadBalance(r):
|
||||
if item.Category == "" {
|
||||
item.Category = "Load-balanced"
|
||||
}
|
||||
item.SourceType = "loadbalancer"
|
||||
} else {
|
||||
default:
|
||||
if item.Category == "" {
|
||||
item.Category = "Others"
|
||||
}
|
||||
@@ -88,7 +102,7 @@ func HomepageConfig() homepage.Config {
|
||||
|
||||
if item.URL == "" {
|
||||
if len(domains) > 0 {
|
||||
item.URL = fmt.Sprintf("%s://%s.%s:%s", proto, strings.ToLower(alias), domains[0], port)
|
||||
item.URL = fmt.Sprintf("%s://%s%s:%s", proto, strings.ToLower(alias), domains[0], port)
|
||||
}
|
||||
}
|
||||
item.AltURL = r.TargetURL().String()
|
||||
@@ -124,13 +138,12 @@ func Statistics() map[string]any {
|
||||
providerStats := make(map[string]proxy.ProviderStats)
|
||||
|
||||
instance.providers.RangeAll(func(name string, p *proxy.Provider) {
|
||||
providerStats[name] = p.Statistics()
|
||||
})
|
||||
stats := p.Statistics()
|
||||
providerStats[name] = stats
|
||||
|
||||
for _, stats := range providerStats {
|
||||
nTotalRPs += stats.NumRPs
|
||||
nTotalStreams += stats.NumStreams
|
||||
}
|
||||
})
|
||||
|
||||
return map[string]any{
|
||||
"num_total_streams": nTotalStreams,
|
||||
@@ -138,14 +151,3 @@ func Statistics() map[string]any {
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func FindRoute(alias string) *route.Route {
|
||||
return F.MapFind(instance.providers,
|
||||
func(p *proxy.Provider) (*route.Route, bool) {
|
||||
if route, ok := p.GetRoute(alias); ok {
|
||||
return route, true
|
||||
}
|
||||
return nil, false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ type (
|
||||
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
||||
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
|
||||
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
||||
Homepage HomepageConfig `json:"homepage" yaml:"homepage"`
|
||||
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
||||
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
|
||||
}
|
||||
@@ -18,8 +19,10 @@ type (
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Providers: Providers{},
|
||||
TimeoutShutdown: 3,
|
||||
Homepage: HomepageConfig{
|
||||
UseDefaultCategories: true,
|
||||
},
|
||||
RedirectToHTTPS: false,
|
||||
}
|
||||
}
|
||||
|
||||
5
internal/config/types/homepage_config.go
Normal file
5
internal/config/types/homepage_config.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package types
|
||||
|
||||
type HomepageConfig struct {
|
||||
UseDefaultCategories bool `json:"use_default_categories" yaml:"use_default_categories"`
|
||||
}
|
||||
@@ -52,13 +52,10 @@ func (c *SharedClient) Connected() bool {
|
||||
}
|
||||
|
||||
// if the client is still referenced, this is no-op.
|
||||
func (c *SharedClient) Close() error {
|
||||
if !c.Connected() {
|
||||
return nil
|
||||
func (c *SharedClient) Close() {
|
||||
if c.Connected() {
|
||||
c.refCount.Sub()
|
||||
}
|
||||
|
||||
c.refCount.Sub()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectClient creates a new Docker client connection to the specified host.
|
||||
@@ -115,7 +112,6 @@ func ConnectClient(host string) (Client, error) {
|
||||
}
|
||||
|
||||
client, err := client.NewClientWithOpts(opt...)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type (
|
||||
ContainerID string `json:"container_id" yaml:"-"`
|
||||
ImageName string `json:"image_name" yaml:"-"`
|
||||
|
||||
Labels map[string]string `json:"labels" yaml:"-"`
|
||||
Labels map[string]string `json:"-" yaml:"-"`
|
||||
|
||||
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
|
||||
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
|
||||
|
||||
@@ -3,7 +3,6 @@ package idlewatcher
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -21,7 +20,7 @@ var loadingPage []byte
|
||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||
|
||||
func (w *Watcher) makeLoadingPageBody() []byte {
|
||||
msg := fmt.Sprintf("%s is starting...", w.ContainerName)
|
||||
msg := w.ContainerName + " is starting..."
|
||||
|
||||
data := new(templateData)
|
||||
data.CheckRedirectHeader = common.HeaderCheckRedirect
|
||||
|
||||
@@ -11,4 +11,5 @@ type Waker interface {
|
||||
health.HealthMonitor
|
||||
http.Handler
|
||||
net.Stream
|
||||
Wake() error
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
. "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
@@ -20,6 +23,7 @@ type waker struct {
|
||||
rp *gphttp.ReverseProxy
|
||||
stream net.Stream
|
||||
hc health.HealthChecker
|
||||
metric *metrics.Gauge
|
||||
|
||||
ready atomic.Bool
|
||||
}
|
||||
@@ -45,17 +49,25 @@ func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReversePr
|
||||
return nil, E.Errorf("register watcher: %w", err)
|
||||
}
|
||||
|
||||
if rp != nil {
|
||||
waker.hc = health.NewHTTPHealthChecker(entry.TargetURL(), hcCfg, rp.Transport)
|
||||
} else if stream != nil {
|
||||
switch {
|
||||
case rp != nil:
|
||||
waker.hc = health.NewHTTPHealthChecker(entry.TargetURL(), hcCfg)
|
||||
case stream != nil:
|
||||
waker.hc = health.NewRawHealthChecker(entry.TargetURL(), hcCfg)
|
||||
} else {
|
||||
default:
|
||||
panic("both nil")
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
m := metrics.GetServiceMetrics()
|
||||
fqn := providerSubTask.Parent().Name() + "/" + entry.TargetName()
|
||||
waker.metric = m.HealthStatus.With(metrics.HealthMetricLabels(fqn))
|
||||
waker.metric.Set(float64(watcher.Status()))
|
||||
}
|
||||
return watcher, nil
|
||||
}
|
||||
|
||||
// lifetime should follow route provider
|
||||
// lifetime should follow route provider.
|
||||
func NewHTTPWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy) (Waker, E.Error) {
|
||||
return newWaker(providerSubTask, entry, rp, nil)
|
||||
}
|
||||
@@ -67,8 +79,11 @@ func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Str
|
||||
// Start implements health.HealthMonitor.
|
||||
func (w *Watcher) Start(routeSubTask task.Task) E.Error {
|
||||
routeSubTask.Finish("ignored")
|
||||
w.task.OnCancel("stop route", func() {
|
||||
w.task.OnCancel("stop route and cleanup", func() {
|
||||
routeSubTask.Parent().Finish(w.task.FinishCause())
|
||||
if w.metric != nil {
|
||||
prometheus.Unregister(w.metric)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -95,8 +110,16 @@ func (w *Watcher) Uptime() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Status implements health.HealthMonitor.
|
||||
func (w *Watcher) Status() health.Status {
|
||||
status := w.getStatusUpdateReady()
|
||||
if w.metric != nil {
|
||||
w.metric.Set(float64(status))
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// Status implements health.HealthMonitor.
|
||||
func (w *Watcher) getStatusUpdateReady() health.Status {
|
||||
if !w.ContainerRunning {
|
||||
return health.StatusNapping
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
shouldNext := w.wakeFromHTTP(rw, r)
|
||||
if !shouldNext {
|
||||
@@ -81,7 +81,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
||||
w.WakeTrace().Msg("signal received")
|
||||
err := w.wakeIfStopped()
|
||||
if err != nil {
|
||||
w.WakeError(err).Send()
|
||||
w.WakeError(err)
|
||||
http.Error(rw, "Error waking container", http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (w *Watcher) Accept() (conn types.StreamConn, err error) {
|
||||
return
|
||||
}
|
||||
if wakeErr := w.wakeFromStream(); wakeErr != nil {
|
||||
w.WakeError(wakeErr).Msg("error waking from stream")
|
||||
w.WakeError(wakeErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (w *Watcher) wakeFromStream() error {
|
||||
wakeErr := w.wakeIfStopped()
|
||||
if wakeErr != nil {
|
||||
wakeErr = fmt.Errorf("%s failed: %w", w.String(), wakeErr)
|
||||
w.WakeError(wakeErr).Msg("wake failed")
|
||||
w.WakeError(wakeErr)
|
||||
return wakeErr
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
@@ -99,6 +98,10 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *Watcher) Wake() error {
|
||||
return w.wakeIfStopped()
|
||||
}
|
||||
|
||||
// WakeDebug logs a debug message related to waking the container.
|
||||
func (w *Watcher) WakeDebug() *zerolog.Event {
|
||||
return w.Debug().Str("action", "wake")
|
||||
@@ -108,8 +111,8 @@ func (w *Watcher) WakeTrace() *zerolog.Event {
|
||||
return w.Trace().Str("action", "wake")
|
||||
}
|
||||
|
||||
func (w *Watcher) WakeError(err error) *zerolog.Event {
|
||||
return w.Err(err).Str("action", "wake")
|
||||
func (w *Watcher) WakeError(err error) {
|
||||
w.Err(err).Str("action", "wake").Msg("error")
|
||||
}
|
||||
|
||||
func (w *Watcher) LogReason(action, reason string) {
|
||||
@@ -204,17 +207,17 @@ func (w *Watcher) resetIdleTimer() {
|
||||
|
||||
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask task.Task, eventCh <-chan events.Event, errCh <-chan E.Error) {
|
||||
eventTask = w.task.Subtask("docker event watcher")
|
||||
eventCh, errCh = dockerWatcher.EventsWithOptions(eventTask.Context(), W.DockerListOptions{
|
||||
Filters: W.NewDockerFilter(
|
||||
W.DockerFilterContainer,
|
||||
W.DockerFilterContainerNameID(w.ContainerID),
|
||||
W.DockerFilterStart,
|
||||
W.DockerFilterStop,
|
||||
W.DockerFilterDie,
|
||||
W.DockerFilterKill,
|
||||
W.DockerFilterDestroy,
|
||||
W.DockerFilterPause,
|
||||
W.DockerFilterUnpause,
|
||||
eventCh, errCh = dockerWatcher.EventsWithOptions(eventTask.Context(), watcher.DockerListOptions{
|
||||
Filters: watcher.NewDockerFilter(
|
||||
watcher.DockerFilterContainer,
|
||||
watcher.DockerFilterContainerNameID(w.ContainerID),
|
||||
watcher.DockerFilterStart,
|
||||
watcher.DockerFilterStop,
|
||||
watcher.DockerFilterDie,
|
||||
watcher.DockerFilterKill,
|
||||
watcher.DockerFilterDestroy,
|
||||
watcher.DockerFilterPause,
|
||||
watcher.DockerFilterUnpause,
|
||||
),
|
||||
})
|
||||
return
|
||||
@@ -230,9 +233,9 @@ func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask tas
|
||||
// stop method.
|
||||
//
|
||||
// it exits only if the context is canceled, the container is destroyed,
|
||||
// errors occured on docker client, or route provider died (mainly caused by config reload).
|
||||
// errors occurred on docker client, or route provider died (mainly caused by config reload).
|
||||
func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
||||
dockerWatcher := watcher.NewDockerWatcherWithClient(w.client)
|
||||
eventTask, dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
|
||||
defer eventTask.Finish("stopped")
|
||||
|
||||
@@ -279,9 +282,13 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
case <-w.ticker.C:
|
||||
w.ticker.Stop()
|
||||
if w.ContainerRunning {
|
||||
if err := w.stopByMethod(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
err := w.stopByMethod()
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
continue
|
||||
case err != nil:
|
||||
w.Err(err).Msgf("container stop with method %q failed", w.StopMethod)
|
||||
} else {
|
||||
default:
|
||||
w.LogReason("container stopped", "idle timeout")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,125 +1,51 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Formats:
|
||||
- namespace.attribute
|
||||
- namespace.target.attribute
|
||||
- namespace.target.attribute.namespace2.attribute
|
||||
*/
|
||||
type (
|
||||
Label struct {
|
||||
Namespace string
|
||||
Target string
|
||||
Attribute string
|
||||
Value any
|
||||
}
|
||||
NestedLabelMap map[string]U.SerializedObject
|
||||
)
|
||||
type LabelMap = map[string]any
|
||||
|
||||
var (
|
||||
ErrApplyToNil = E.New("label value is nil")
|
||||
ErrFieldNotExist = E.New("field does not exist")
|
||||
)
|
||||
func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
|
||||
nestedMap := make(LabelMap)
|
||||
errs := E.NewBuilder("labels error")
|
||||
|
||||
func (l *Label) String() string {
|
||||
if l.Attribute == "" {
|
||||
return l.Namespace + "." + l.Target
|
||||
}
|
||||
return l.Namespace + "." + l.Target + "." + l.Attribute
|
||||
}
|
||||
|
||||
// Apply applies the value of a Label to the corresponding field in the given object.
|
||||
//
|
||||
// Parameters:
|
||||
// - obj: a pointer to the object to which the Label value will be applied.
|
||||
// - l: a pointer to the Label containing the attribute and value to be applied.
|
||||
//
|
||||
// Returns:
|
||||
// - error: an error if the field does not exist.
|
||||
func ApplyLabel[T any](obj *T, l *Label) E.Error {
|
||||
if obj == nil {
|
||||
return ErrApplyToNil.Subject(l.String())
|
||||
}
|
||||
switch nestedLabel := l.Value.(type) {
|
||||
case *Label:
|
||||
var field reflect.Value
|
||||
objType := reflect.TypeFor[T]()
|
||||
for i := range reflect.TypeFor[T]().NumField() {
|
||||
if objType.Field(i).Tag.Get("yaml") == l.Attribute {
|
||||
field = reflect.ValueOf(obj).Elem().Field(i)
|
||||
break
|
||||
}
|
||||
for lbl, value := range labels {
|
||||
parts := strings.Split(lbl, ".")
|
||||
if parts[0] != NSProxy {
|
||||
continue
|
||||
}
|
||||
if !field.IsValid() {
|
||||
return ErrFieldNotExist.Subject(l.Attribute).Subject(l.String())
|
||||
if len(parts) == 1 {
|
||||
errs.Add(E.Errorf("invalid label %s", lbl).Subject(lbl))
|
||||
continue
|
||||
}
|
||||
dst, ok := field.Interface().(NestedLabelMap)
|
||||
if !ok {
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
parts = parts[1:]
|
||||
currentMap := nestedMap
|
||||
|
||||
for i, k := range parts {
|
||||
if i == len(parts)-1 {
|
||||
// Last element, set the value
|
||||
currentMap[k] = value
|
||||
} else {
|
||||
field = field.Addr()
|
||||
// If the key doesn't exist, create a new map
|
||||
if _, exists := currentMap[k]; !exists {
|
||||
currentMap[k] = make(LabelMap)
|
||||
}
|
||||
// Move deeper into the nested map
|
||||
m, ok := currentMap[k].(LabelMap)
|
||||
if !ok && currentMap[k] != "" {
|
||||
errs.Add(E.Errorf("expect mapping, got %T", currentMap[k]).Subject(lbl))
|
||||
continue
|
||||
} else if !ok {
|
||||
m = make(LabelMap)
|
||||
currentMap[k] = m
|
||||
}
|
||||
currentMap = m
|
||||
}
|
||||
err := U.Deserialize(U.SerializedObject{nestedLabel.Namespace: nestedLabel.Value}, field.Interface())
|
||||
if err != nil {
|
||||
return err.Subject(l.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if dst == nil {
|
||||
field.Set(reflect.MakeMap(reflect.TypeFor[NestedLabelMap]()))
|
||||
dst = field.Interface().(NestedLabelMap)
|
||||
}
|
||||
if dst[nestedLabel.Namespace] == nil {
|
||||
dst[nestedLabel.Namespace] = make(U.SerializedObject)
|
||||
}
|
||||
dst[nestedLabel.Namespace][nestedLabel.Attribute] = nestedLabel.Value
|
||||
return nil
|
||||
default:
|
||||
err := U.Deserialize(U.SerializedObject{l.Attribute: l.Value}, obj)
|
||||
if err != nil {
|
||||
return err.Subject(l.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParseLabel(label string, value string) *Label {
|
||||
parts := strings.Split(label, ".")
|
||||
|
||||
if len(parts) < 2 {
|
||||
return &Label{
|
||||
Namespace: label,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Namespace: parts[0],
|
||||
Target: parts[1],
|
||||
Value: value,
|
||||
}
|
||||
|
||||
switch len(parts) {
|
||||
case 2:
|
||||
l.Attribute = l.Target
|
||||
case 3:
|
||||
l.Attribute = parts[2]
|
||||
default:
|
||||
l.Attribute = parts[2]
|
||||
nestedLabel := ParseLabel(strings.Join(parts[3:], "."), value)
|
||||
l.Value = nestedLabel
|
||||
}
|
||||
|
||||
return l
|
||||
|
||||
return nestedMap, errs.Error()
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
const (
|
||||
mName = "middleware1"
|
||||
mAttr = "prop1"
|
||||
v = "value1"
|
||||
)
|
||||
|
||||
func makeLabel(ns, name, attr string) string {
|
||||
return fmt.Sprintf("%s.%s.%s", ns, name, attr)
|
||||
}
|
||||
|
||||
func TestNestedLabel(t *testing.T) {
|
||||
mAttr := "prop1"
|
||||
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
sGot := ExpectType[*Label](t, lbl.Value)
|
||||
ExpectFalse(t, sGot == nil)
|
||||
ExpectEqual(t, sGot.Namespace, mName)
|
||||
ExpectEqual(t, sGot.Attribute, mAttr)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabel(t *testing.T) {
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
err := ApplyLabel(entry, lbl)
|
||||
ExpectNoError(t, err)
|
||||
middleware1, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
got := ExpectType[string](t, middleware1[mAttr])
|
||||
ExpectEqual(t, got, v)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabelExisting(t *testing.T) {
|
||||
checkAttr := "prop2"
|
||||
checkV := "value2"
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
entry.Middlewares = make(NestedLabelMap)
|
||||
entry.Middlewares[mName] = make(U.SerializedObject)
|
||||
entry.Middlewares[mName][checkAttr] = checkV
|
||||
|
||||
lbl := ParseLabel(makeLabel(NSProxy, "foo", makeLabel("middlewares", mName, mAttr)), v)
|
||||
err := ApplyLabel(entry, lbl)
|
||||
ExpectNoError(t, err)
|
||||
middleware1, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
got := ExpectType[string](t, middleware1[mAttr])
|
||||
ExpectEqual(t, got, v)
|
||||
|
||||
// check if prop2 is affected
|
||||
ExpectFalse(t, middleware1[checkAttr] == nil)
|
||||
got = ExpectType[string](t, middleware1[checkAttr])
|
||||
ExpectEqual(t, got, checkV)
|
||||
}
|
||||
|
||||
func TestApplyNestedLabelNoAttr(t *testing.T) {
|
||||
entry := new(struct {
|
||||
Middlewares NestedLabelMap `yaml:"middlewares"`
|
||||
})
|
||||
entry.Middlewares = make(NestedLabelMap)
|
||||
entry.Middlewares[mName] = make(U.SerializedObject)
|
||||
|
||||
lbl := ParseLabel(makeLabel(NSProxy, "foo", fmt.Sprintf("%s.%s", "middlewares", mName)), v)
|
||||
err := ApplyLabel(entry, lbl)
|
||||
ExpectNoError(t, err)
|
||||
_, ok := entry.Middlewares[mName]
|
||||
ExpectTrue(t, ok)
|
||||
}
|
||||
@@ -3,8 +3,7 @@ package docker
|
||||
const (
|
||||
WildcardAlias = "*"
|
||||
|
||||
NSProxy = "proxy"
|
||||
NSHomePage = "homepage"
|
||||
NSProxy = "proxy"
|
||||
|
||||
LabelAliases = NSProxy + ".aliases"
|
||||
LabelExclude = NSProxy + ".exclude"
|
||||
|
||||
@@ -27,26 +27,31 @@ func (b *Builder) HasError() bool {
|
||||
return len(b.errs) > 0
|
||||
}
|
||||
|
||||
func (b *Builder) Error() Error {
|
||||
func (b *Builder) error() Error {
|
||||
if !b.HasError() {
|
||||
return nil
|
||||
}
|
||||
if len(b.errs) == 1 {
|
||||
return From(b.errs[0])
|
||||
}
|
||||
return &nestedError{Err: New(b.about), Extras: b.errs}
|
||||
}
|
||||
|
||||
func (b *Builder) Error() Error {
|
||||
if len(b.errs) == 1 {
|
||||
return From(b.errs[0])
|
||||
}
|
||||
return b.error()
|
||||
}
|
||||
|
||||
func (b *Builder) String() string {
|
||||
if !b.HasError() {
|
||||
err := b.error()
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return (&nestedError{Err: New(b.about), Extras: b.errs}).Error()
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// Add adds an error to the Builder.
|
||||
//
|
||||
// adding nil is no-op,
|
||||
// adding nil is no-op.
|
||||
func (b *Builder) Add(err error) *Builder {
|
||||
if err == nil {
|
||||
return b
|
||||
@@ -90,6 +95,21 @@ func (b *Builder) Addf(format string, args ...any) *Builder {
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
|
||||
if other == nil || !other.HasError() {
|
||||
return b
|
||||
}
|
||||
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
if flatten {
|
||||
b.errs = append(b.errs, other.errs...)
|
||||
} else {
|
||||
b.errs = append(b.errs, other.error())
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) AddRange(errs ...error) *Builder {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
@@ -22,7 +22,7 @@ type Error interface {
|
||||
Subjectf(format string, args ...any) Error
|
||||
}
|
||||
|
||||
// this makes JSON marshalling work,
|
||||
// this makes JSON marshaling work,
|
||||
// as the builtin one doesn't.
|
||||
type errStr string
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ErrInvalidErrorJson = errors.New("invalid error json")
|
||||
|
||||
func newError(message string) error {
|
||||
return errStr(message)
|
||||
}
|
||||
|
||||
64
internal/homepage/categories.go
Normal file
64
internal/homepage/categories.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package homepage
|
||||
|
||||
// PredefinedCategories by alias or docker image name
|
||||
var PredefinedCategories = map[string]string{
|
||||
"sonarr": "Torrenting",
|
||||
"radarr": "Torrenting",
|
||||
"bazarr": "Torrenting",
|
||||
"lidarr": "Torrenting",
|
||||
"readarr": "Torrenting",
|
||||
"prowlarr": "Torrenting",
|
||||
"watcharr": "Torrenting",
|
||||
"qbittorrent": "Torrenting",
|
||||
"qbit": "Torrenting",
|
||||
"qbt": "Torrenting",
|
||||
"transmission": "Torrenting",
|
||||
|
||||
"jellyfin": "Media",
|
||||
"jellyseerr": "Media",
|
||||
"emby": "Media",
|
||||
"plex": "Media",
|
||||
"navidrome": "Media",
|
||||
"immich": "Media",
|
||||
"tautulli": "Media",
|
||||
"nextcloud": "Media",
|
||||
"invidious": "Media",
|
||||
|
||||
"uptime": "Monitoring",
|
||||
"uptime-kuma": "Monitoring",
|
||||
"prometheus": "Monitoring",
|
||||
"grafana": "Monitoring",
|
||||
"netdata": "Monitoring",
|
||||
"changedetection.io": "Monitoring",
|
||||
"changedetection": "Monitoring",
|
||||
"influxdb": "Monitoring",
|
||||
"influx": "Monitoring",
|
||||
"dozzle": "Monitoring",
|
||||
|
||||
"adguardhome": "Networking",
|
||||
"adgh": "Networking",
|
||||
"adg": "Networking",
|
||||
"pihole": "Networking",
|
||||
"flaresolverr": "Networking",
|
||||
|
||||
"homebridge": "Home Automation",
|
||||
"home-assistant": "Home Automation",
|
||||
|
||||
"dockge": "Container Management",
|
||||
"portainer-ce": "Container Management",
|
||||
"portainer-be": "Container Management",
|
||||
|
||||
"rss": "RSS",
|
||||
"rsshub": "RSS",
|
||||
"rss-bridge": "RSS",
|
||||
"miniflux": "RSS",
|
||||
"freshrss": "RSS",
|
||||
|
||||
"paperless": "Documents",
|
||||
"paperless-ngx": "Documents",
|
||||
"s-pdf": "Documents",
|
||||
|
||||
"minio": "Storage",
|
||||
"filebrowser": "Storage",
|
||||
"rclone": "Storage",
|
||||
}
|
||||
@@ -15,13 +15,14 @@ func init() {
|
||||
var level zerolog.Level
|
||||
var exclude []string
|
||||
|
||||
if common.IsTrace {
|
||||
switch {
|
||||
case common.IsTrace:
|
||||
timeFmt = "04:05"
|
||||
level = zerolog.TraceLevel
|
||||
} else if common.IsDebug {
|
||||
case common.IsDebug:
|
||||
timeFmt = "01-02 15:04"
|
||||
level = zerolog.DebugLevel
|
||||
} else {
|
||||
default:
|
||||
timeFmt = "01-02 15:04"
|
||||
level = zerolog.InfoLevel
|
||||
exclude = []string{"module"}
|
||||
@@ -50,7 +51,7 @@ func init() {
|
||||
).Level(level).With().Timestamp().Logger()
|
||||
}
|
||||
|
||||
func DiscardLogger() { logger = zerolog.Nop() }
|
||||
func DiscardLogger() { zerolog.SetGlobalLevel(zerolog.Disabled) }
|
||||
|
||||
func AddHook(h zerolog.Hook) { logger = logger.Hook(h) }
|
||||
|
||||
|
||||
13
internal/metrics/http_handler.go
Normal file
13
internal/metrics/http_handler.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func NewHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
return mux
|
||||
}
|
||||
36
internal/metrics/labels.go
Normal file
36
internal/metrics/labels.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
type (
|
||||
HTTPRouteMetricLabels struct {
|
||||
Service, Method, Host, Visitor, Path string
|
||||
}
|
||||
StreamRouteMetricLabels struct {
|
||||
Service, Visitor string
|
||||
}
|
||||
HealthMetricLabels string
|
||||
)
|
||||
|
||||
func (lbl *HTTPRouteMetricLabels) toPromLabels() prometheus.Labels {
|
||||
return prometheus.Labels{
|
||||
"service": lbl.Service,
|
||||
"method": lbl.Method,
|
||||
"host": lbl.Host,
|
||||
"visitor": lbl.Visitor,
|
||||
"path": lbl.Path,
|
||||
}
|
||||
}
|
||||
|
||||
func (lbl *StreamRouteMetricLabels) toPromLabels() prometheus.Labels {
|
||||
return prometheus.Labels{
|
||||
"service": lbl.Service,
|
||||
"visitor": lbl.Visitor,
|
||||
}
|
||||
}
|
||||
|
||||
func (lbl HealthMetricLabels) toPromLabels() prometheus.Labels {
|
||||
return prometheus.Labels{
|
||||
"service": string(lbl),
|
||||
}
|
||||
}
|
||||
73
internal/metrics/metric.go
Normal file
73
internal/metrics/metric.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
type (
|
||||
Counter struct {
|
||||
mv *prometheus.CounterVec
|
||||
collector prometheus.Counter
|
||||
}
|
||||
Gauge struct {
|
||||
mv *prometheus.GaugeVec
|
||||
collector prometheus.Gauge
|
||||
}
|
||||
Labels interface {
|
||||
toPromLabels() prometheus.Labels
|
||||
}
|
||||
)
|
||||
|
||||
func NewCounter(opts prometheus.CounterOpts, labels ...string) *Counter {
|
||||
m := &Counter{
|
||||
mv: prometheus.NewCounterVec(opts, labels),
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
m.collector = m.mv.WithLabelValues()
|
||||
m.collector.Add(0)
|
||||
}
|
||||
prometheus.MustRegister(m)
|
||||
return m
|
||||
}
|
||||
|
||||
func NewGauge(opts prometheus.GaugeOpts, labels ...string) *Gauge {
|
||||
m := &Gauge{
|
||||
mv: prometheus.NewGaugeVec(opts, labels),
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
m.collector = m.mv.WithLabelValues()
|
||||
m.collector.Set(0)
|
||||
}
|
||||
prometheus.MustRegister(m)
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *Counter) Collect(ch chan<- prometheus.Metric) {
|
||||
c.mv.Collect(ch)
|
||||
}
|
||||
|
||||
func (c *Counter) Describe(ch chan<- *prometheus.Desc) {
|
||||
c.mv.Describe(ch)
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.collector.Inc()
|
||||
}
|
||||
|
||||
func (c *Counter) With(l Labels) *Counter {
|
||||
return &Counter{mv: c.mv, collector: c.mv.With(l.toPromLabels())}
|
||||
}
|
||||
|
||||
func (g *Gauge) Collect(ch chan<- prometheus.Metric) {
|
||||
g.mv.Collect(ch)
|
||||
}
|
||||
|
||||
func (g *Gauge) Describe(ch chan<- *prometheus.Desc) {
|
||||
g.mv.Describe(ch)
|
||||
}
|
||||
|
||||
func (g *Gauge) Set(v float64) {
|
||||
g.collector.Set(v)
|
||||
}
|
||||
|
||||
func (g *Gauge) With(l Labels) *Gauge {
|
||||
return &Gauge{mv: g.mv, collector: g.mv.With(l.toPromLabels())}
|
||||
}
|
||||
105
internal/metrics/metrics.go
Normal file
105
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
type (
|
||||
RouteMetrics struct {
|
||||
HTTPReqTotal,
|
||||
HTTP2xx3xx,
|
||||
HTTP4xx,
|
||||
HTTP5xx *Counter
|
||||
HTTPReqElapsed *Gauge
|
||||
}
|
||||
|
||||
ServiceMetrics struct {
|
||||
HealthStatus *Gauge
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
rm RouteMetrics
|
||||
sm ServiceMetrics
|
||||
)
|
||||
|
||||
const (
|
||||
routerNamespace = "router"
|
||||
routerHTTPSubsystem = "http"
|
||||
|
||||
serviceNamespace = "service"
|
||||
)
|
||||
|
||||
func GetRouteMetrics() *RouteMetrics {
|
||||
return &rm
|
||||
}
|
||||
|
||||
func GetServiceMetrics() *ServiceMetrics {
|
||||
return &sm
|
||||
}
|
||||
|
||||
func (rm *RouteMetrics) UnregisterService(service string) {
|
||||
lbls := &HTTPRouteMetricLabels{Service: service}
|
||||
prometheus.Unregister(rm.HTTP2xx3xx.With(lbls))
|
||||
prometheus.Unregister(rm.HTTP4xx.With(lbls))
|
||||
prometheus.Unregister(rm.HTTP5xx.With(lbls))
|
||||
prometheus.Unregister(rm.HTTPReqElapsed.With(lbls))
|
||||
}
|
||||
|
||||
func init() {
|
||||
if !common.PrometheusEnabled {
|
||||
return
|
||||
}
|
||||
initRouteMetrics()
|
||||
initServiceMetrics()
|
||||
}
|
||||
|
||||
func initRouteMetrics() {
|
||||
lbls := []string{"service", "method", "host", "visitor", "path"}
|
||||
partitionsHelp := ", partitioned by " + strings.Join(lbls, ", ")
|
||||
rm = RouteMetrics{
|
||||
HTTPReqTotal: NewCounter(prometheus.CounterOpts{
|
||||
Namespace: routerNamespace,
|
||||
Subsystem: routerHTTPSubsystem,
|
||||
Name: "req_total",
|
||||
Help: "How many requests processed" + partitionsHelp,
|
||||
}),
|
||||
HTTP2xx3xx: NewCounter(prometheus.CounterOpts{
|
||||
Namespace: routerNamespace,
|
||||
Subsystem: routerHTTPSubsystem,
|
||||
Name: "req_ok_count",
|
||||
Help: "How many 2xx-3xx requests processed" + partitionsHelp,
|
||||
}, lbls...),
|
||||
HTTP4xx: NewCounter(prometheus.CounterOpts{
|
||||
Namespace: routerNamespace,
|
||||
Subsystem: routerHTTPSubsystem,
|
||||
Name: "req_4xx_count",
|
||||
Help: "How many 4xx requests processed" + partitionsHelp,
|
||||
}, lbls...),
|
||||
HTTP5xx: NewCounter(prometheus.CounterOpts{
|
||||
Namespace: routerNamespace,
|
||||
Subsystem: routerHTTPSubsystem,
|
||||
Name: "req_5xx_count",
|
||||
Help: "How many 5xx requests processed" + partitionsHelp,
|
||||
}, lbls...),
|
||||
HTTPReqElapsed: NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: routerNamespace,
|
||||
Subsystem: routerHTTPSubsystem,
|
||||
Name: "req_elapsed_ms",
|
||||
Help: "How long it took to process the request and respond a status code" + partitionsHelp,
|
||||
}, lbls...),
|
||||
}
|
||||
}
|
||||
|
||||
func initServiceMetrics() {
|
||||
sm = ServiceMetrics{
|
||||
HealthStatus: NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: serviceNamespace,
|
||||
Name: "health_status",
|
||||
Help: "The health status of the router by service",
|
||||
}, "service"),
|
||||
}
|
||||
}
|
||||
26
internal/metrics/router_metrics.go
Normal file
26
internal/metrics/router_metrics.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
func InitRouterMetrics(getRPsCount func() int, getStreamsCount func() int) {
|
||||
if !common.PrometheusEnabled {
|
||||
return
|
||||
}
|
||||
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: "entrypoint",
|
||||
Name: "num_reverse_proxies",
|
||||
Help: "The number of reverse proxies",
|
||||
}, func() float64 {
|
||||
return float64(getRPsCount())
|
||||
}))
|
||||
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Namespace: "entrypoint",
|
||||
Name: "num_streams",
|
||||
Help: "The number of streams",
|
||||
}, func() float64 {
|
||||
return float64(getStreamsCount())
|
||||
}))
|
||||
}
|
||||
@@ -16,13 +16,12 @@ var (
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: defaultDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
DefaultTransportNoTLS = func() *http.Transport {
|
||||
var clone = DefaultTransport.Clone()
|
||||
clone := DefaultTransport.Clone()
|
||||
clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return clone
|
||||
}()
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ContentType string
|
||||
type AcceptContentType []ContentType
|
||||
type (
|
||||
ContentType string
|
||||
AcceptContentType []ContentType
|
||||
)
|
||||
|
||||
func GetContentType(h http.Header) ContentType {
|
||||
ct := h.Get("Content-Type")
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
type DummyResponseWriter struct{}
|
||||
|
||||
func (w DummyResponseWriter) Header() http.Header {
|
||||
return make(http.Header)
|
||||
}
|
||||
|
||||
func (w DummyResponseWriter) Write([]byte) (_ int, _ error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (w DummyResponseWriter) WriteHeader(int) {}
|
||||
@@ -1,7 +1,6 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
@@ -55,7 +53,6 @@ func New(cfg *Config) *LoadBalancer {
|
||||
Logger: logger.With().Str("name", cfg.Link).Logger(),
|
||||
Config: new(Config),
|
||||
pool: newPool(),
|
||||
task: task.DummyTask(),
|
||||
}
|
||||
lb.UpdateConfigIfNeeded(cfg)
|
||||
return lb
|
||||
@@ -226,18 +223,15 @@ func (lb *LoadBalancer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Header.Get(common.HeaderCheckRedirect) != "" {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
||||
defer cancel()
|
||||
// send dummy request to wake all servers
|
||||
var dummyRW gphttp.DummyResponseWriter
|
||||
// wake all servers
|
||||
for _, srv := range srvs {
|
||||
// wake only if server implements Waker
|
||||
_, ok := srv.handler.(idlewatcher.Waker)
|
||||
if !ok {
|
||||
continue
|
||||
waker, ok := srv.handler.(idlewatcher.Waker)
|
||||
if ok {
|
||||
if err := waker.Wake(); err != nil {
|
||||
lb.Err(err).Msgf("failed to wake server %s", srv.Name)
|
||||
}
|
||||
}
|
||||
wakeReq := r.Clone(ctx)
|
||||
srv.ServeHTTP(dummyRW, wakeReq)
|
||||
}
|
||||
}
|
||||
lb.impl.ServeHTTP(srvs, rw, r)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type Mode string
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
)
|
||||
|
||||
func (mode *Mode) ValidateUpdate() bool {
|
||||
switch U.ToLowerNoSnake(string(*mode)) {
|
||||
switch strutils.ToLowerNoSnake(string(*mode)) {
|
||||
case "":
|
||||
return true
|
||||
case string(RoundRobin):
|
||||
|
||||
@@ -10,30 +10,25 @@ import (
|
||||
)
|
||||
|
||||
type cidrWhitelist struct {
|
||||
*cidrWhitelistOpts
|
||||
m *Middleware
|
||||
cidrWhitelistOpts
|
||||
m *Middleware
|
||||
cachedAddr F.Map[string, bool] // cache for trusted IPs
|
||||
}
|
||||
|
||||
type cidrWhitelistOpts struct {
|
||||
Allow []*types.CIDR `json:"allow"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
|
||||
cachedAddr F.Map[string, bool] // cache for trusted IPs
|
||||
}
|
||||
|
||||
var CIDRWhiteList = &cidrWhitelist{
|
||||
m: &Middleware{withOptions: NewCIDRWhitelist},
|
||||
}
|
||||
|
||||
var cidrWhitelistDefaults = func() *cidrWhitelistOpts {
|
||||
return &cidrWhitelistOpts{
|
||||
var (
|
||||
CIDRWhiteList = &Middleware{withOptions: NewCIDRWhitelist}
|
||||
cidrWhitelistDefaults = cidrWhitelistOpts{
|
||||
Allow: []*types.CIDR{},
|
||||
StatusCode: http.StatusForbidden,
|
||||
Message: "IP not allowed",
|
||||
cachedAddr: F.NewMapOf[string, bool](),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func NewCIDRWhitelist(opts OptionsRaw) (*Middleware, E.Error) {
|
||||
wl := new(cidrWhitelist)
|
||||
@@ -41,8 +36,9 @@ func NewCIDRWhitelist(opts OptionsRaw) (*Middleware, E.Error) {
|
||||
impl: wl,
|
||||
before: wl.checkIP,
|
||||
}
|
||||
wl.cidrWhitelistOpts = cidrWhitelistDefaults()
|
||||
err := Deserialize(opts, wl.cidrWhitelistOpts)
|
||||
wl.cidrWhitelistOpts = cidrWhitelistDefaults
|
||||
wl.cachedAddr = F.NewMapOf[string, bool]()
|
||||
err := Deserialize(opts, &wl.cidrWhitelistOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ func TestCIDRWhitelist(t *testing.T) {
|
||||
for range 10 {
|
||||
result, err := newMiddlewareTest(deny, nil)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, result.ResponseStatus, cidrWhitelistDefaults().StatusCode)
|
||||
ExpectEqual(t, string(result.Data), cidrWhitelistDefaults().Message)
|
||||
ExpectEqual(t, result.ResponseStatus, cidrWhitelistDefaults.StatusCode)
|
||||
ExpectEqual(t, string(result.Data), cidrWhitelistDefaults.Message)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -29,9 +29,7 @@ var (
|
||||
cfCIDRsLogger = logger.With().Str("name", "CloudflareRealIP").Logger()
|
||||
)
|
||||
|
||||
var CloudflareRealIP = &realIP{
|
||||
m: &Middleware{withOptions: NewCloudflareRealIP},
|
||||
}
|
||||
var CloudflareRealIP = &Middleware{withOptions: NewCloudflareRealIP}
|
||||
|
||||
func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.Error) {
|
||||
cri := new(realIP)
|
||||
@@ -46,7 +44,7 @@ func NewCloudflareRealIP(_ OptionsRaw) (*Middleware, E.Error) {
|
||||
next(w, r)
|
||||
},
|
||||
}
|
||||
cri.realIPOpts = &realIPOpts{
|
||||
cri.realIPOpts = realIPOpts{
|
||||
Header: "CF-Connecting-IP",
|
||||
Recursive: true,
|
||||
}
|
||||
@@ -109,9 +107,9 @@ func fetchUpdateCFIPRange(endpoint string, cfCIDRs []*types.CIDR) error {
|
||||
_, cidr, err := net.ParseCIDR(line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare responeded an invalid CIDR: %s", line)
|
||||
} else {
|
||||
cfCIDRs = append(cfCIDRs, (*types.CIDR)(cidr))
|
||||
}
|
||||
|
||||
cfCIDRs = append(cfCIDRs, (*types.CIDR)(cidr))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -19,24 +19,33 @@ import (
|
||||
const errPagesBasePath = common.ErrorPagesBasePath
|
||||
|
||||
var (
|
||||
setupMu sync.Mutex
|
||||
dirWatcher W.Watcher
|
||||
fileContentMap = F.NewMapOf[string, []byte]()
|
||||
)
|
||||
|
||||
var setup = sync.OnceFunc(func() {
|
||||
func setup() {
|
||||
setupMu.Lock()
|
||||
defer setupMu.Unlock()
|
||||
|
||||
if dirWatcher != nil {
|
||||
return
|
||||
}
|
||||
|
||||
task := task.GlobalTask("error page")
|
||||
dirWatcher = W.NewDirectoryWatcher(task.Subtask("dir watcher"), errPagesBasePath)
|
||||
loadContent()
|
||||
go watchDir(task)
|
||||
})
|
||||
}
|
||||
|
||||
func GetStaticFile(filename string) ([]byte, bool) {
|
||||
setup()
|
||||
return fileContentMap.Load(filename)
|
||||
}
|
||||
|
||||
// try <statusCode>.html -> 404.html -> not ok.
|
||||
func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
||||
content, ok = fileContentMap.Load(fmt.Sprintf("%d.html", statusCode))
|
||||
content, ok = GetStaticFile(fmt.Sprintf("%d.html", statusCode))
|
||||
if !ok && statusCode != 404 {
|
||||
return fileContentMap.Load("404.html")
|
||||
}
|
||||
|
||||
5
internal/net/http/middleware/errors.go
Normal file
5
internal/net/http/middleware/errors.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package middleware
|
||||
|
||||
import E "github.com/yusing/go-proxy/internal/error"
|
||||
|
||||
var ErrZeroValue = E.New("cannot be zero")
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
type (
|
||||
forwardAuth struct {
|
||||
*forwardAuthOpts
|
||||
forwardAuthOpts
|
||||
m *Middleware
|
||||
client http.Client
|
||||
}
|
||||
@@ -33,14 +33,11 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var ForwardAuth = &forwardAuth{
|
||||
m: &Middleware{withOptions: NewForwardAuthfunc},
|
||||
}
|
||||
var ForwardAuth = &Middleware{withOptions: NewForwardAuthfunc}
|
||||
|
||||
func NewForwardAuthfunc(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
fa := new(forwardAuth)
|
||||
fa.forwardAuthOpts = new(forwardAuthOpts)
|
||||
if err := Deserialize(optsRaw, fa.forwardAuthOpts); err != nil {
|
||||
if err := Deserialize(optsRaw, &fa.forwardAuthOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := url.Parse(fa.Address); err != nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -28,7 +27,6 @@ type (
|
||||
CloneWithOptFunc func(opts OptionsRaw) (*Middleware, E.Error)
|
||||
|
||||
OptionsRaw = map[string]any
|
||||
Options any
|
||||
|
||||
Middleware struct {
|
||||
_ U.NoCopy
|
||||
@@ -158,8 +156,8 @@ func patchReverseProxy(rpName string, rp *ReverseProxy, middlewares []*Middlewar
|
||||
mid := BuildMiddlewareFromChain(rpName, middlewares)
|
||||
|
||||
if mid.before != nil {
|
||||
ori := rp.ServeHTTP
|
||||
rp.ServeHTTP = func(w http.ResponseWriter, r *http.Request) {
|
||||
ori := rp.HandlerFunc
|
||||
rp.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
mid.before(ori, w, r)
|
||||
}
|
||||
}
|
||||
@@ -168,7 +166,10 @@ func patchReverseProxy(rpName string, rp *ReverseProxy, middlewares []*Middlewar
|
||||
if rp.ModifyResponse != nil {
|
||||
ori := rp.ModifyResponse
|
||||
rp.ModifyResponse = func(res *http.Response) error {
|
||||
return errors.Join(mid.modifyResponse(res), ori(res))
|
||||
if err := mid.modifyResponse(res); err != nil {
|
||||
return err
|
||||
}
|
||||
return ori(res)
|
||||
}
|
||||
} else {
|
||||
rp.ModifyResponse = mid.modifyResponse
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -21,7 +20,7 @@ var (
|
||||
)
|
||||
|
||||
func Get(name string) (*Middleware, Error) {
|
||||
middleware, ok := allMiddlewares[U.ToLowerNoSnake(name)]
|
||||
middleware, ok := allMiddlewares[strutils.ToLowerNoSnake(name)]
|
||||
if !ok {
|
||||
return nil, ErrUnknownMiddleware.
|
||||
Subject(name).
|
||||
@@ -34,22 +33,25 @@ func All() map[string]*Middleware {
|
||||
return allMiddlewares
|
||||
}
|
||||
|
||||
// initialize middleware names and label parsers
|
||||
// initialize middleware names and label parsers.
|
||||
func init() {
|
||||
// snakes and cases will be stripped on `Get`
|
||||
// so keys are lowercase without snake.
|
||||
allMiddlewares = map[string]*Middleware{
|
||||
"setxforwarded": SetXForwarded,
|
||||
"hidexforwarded": HideXForwarded,
|
||||
"redirecthttp": RedirectHTTP,
|
||||
"modifyresponse": ModifyResponse.m,
|
||||
"modifyrequest": ModifyRequest.m,
|
||||
"modifyresponse": ModifyResponse,
|
||||
"modifyrequest": ModifyRequest,
|
||||
"errorpage": CustomErrorPage,
|
||||
"customerrorpage": CustomErrorPage,
|
||||
"realip": RealIP.m,
|
||||
"cloudflarerealip": CloudflareRealIP.m,
|
||||
"cidrwhitelist": CIDRWhiteList.m,
|
||||
"realip": RealIP,
|
||||
"cloudflarerealip": CloudflareRealIP,
|
||||
"cidrwhitelist": CIDRWhiteList,
|
||||
"ratelimit": RateLimiter,
|
||||
|
||||
// !experimental
|
||||
"forwardauth": ForwardAuth.m,
|
||||
"forwardauth": ForwardAuth,
|
||||
// "oauth2": OAuth2.m,
|
||||
}
|
||||
names := make(map[*Middleware][]string)
|
||||
@@ -67,7 +69,7 @@ func init() {
|
||||
|
||||
func LoadComposeFiles() {
|
||||
errs := E.NewBuilder("middleware compile errors")
|
||||
middlewareDefs, err := U.ListFiles(common.MiddlewareComposeBasePath, 0)
|
||||
middlewareDefs, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("failed to list middleware definitions")
|
||||
return
|
||||
@@ -82,7 +84,7 @@ func LoadComposeFiles() {
|
||||
errs.Add(ErrDuplicatedMiddleware.Subject(name))
|
||||
continue
|
||||
}
|
||||
allMiddlewares[U.ToLowerNoSnake(name)] = m
|
||||
allMiddlewares[strutils.ToLowerNoSnake(name)] = m
|
||||
logger.Info().
|
||||
Str("name", name).
|
||||
Str("src", path.Base(defFile)).
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type (
|
||||
modifyRequest struct {
|
||||
*modifyRequestOpts
|
||||
modifyRequestOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
@@ -18,24 +20,19 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var ModifyRequest = &modifyRequest{
|
||||
m: &Middleware{withOptions: NewModifyRequest},
|
||||
}
|
||||
var ModifyRequest = &Middleware{withOptions: NewModifyRequest}
|
||||
|
||||
func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
mr := new(modifyRequest)
|
||||
var mrFunc RewriteFunc
|
||||
mrFunc := mr.modifyRequest
|
||||
if common.IsDebug {
|
||||
mrFunc = mr.modifyRequestWithTrace
|
||||
} else {
|
||||
mrFunc = mr.modifyRequest
|
||||
}
|
||||
mr.m = &Middleware{
|
||||
impl: mr,
|
||||
before: Rewrite(mrFunc),
|
||||
}
|
||||
mr.modifyRequestOpts = new(modifyRequestOpts)
|
||||
err := Deserialize(optsRaw, mr.modifyRequestOpts)
|
||||
err := Deserialize(optsRaw, &mr.modifyRequestOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,6 +41,9 @@ func NewModifyRequest(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
|
||||
func (mr *modifyRequest) modifyRequest(req *Request) {
|
||||
for k, v := range mr.SetHeaders {
|
||||
if http.CanonicalHeaderKey(k) == "Host" {
|
||||
req.Host = v
|
||||
}
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range mr.AddHeaders {
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
|
||||
func TestSetModifyRequest(t *testing.T) {
|
||||
opts := OptionsRaw{
|
||||
"set_headers": map[string]string{"User-Agent": "go-proxy/v0.5.0"},
|
||||
"set_headers": map[string]string{
|
||||
"User-Agent": "go-proxy/v0.5.0",
|
||||
"Host": "test.example.com",
|
||||
},
|
||||
"add_headers": map[string]string{"Accept-Encoding": "test-value"},
|
||||
"hide_headers": []string{"Accept"},
|
||||
}
|
||||
|
||||
t.Run("set_options", func(t *testing.T) {
|
||||
mr, err := ModifyRequest.m.WithOptionsClone(opts)
|
||||
mr, err := ModifyRequest.WithOptionsClone(opts)
|
||||
ExpectNoError(t, err)
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyRequest).SetHeaders, opts["set_headers"].(map[string]string))
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyRequest).AddHeaders, opts["add_headers"].(map[string]string))
|
||||
@@ -23,11 +26,12 @@ func TestSetModifyRequest(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("request_headers", func(t *testing.T) {
|
||||
result, err := newMiddlewareTest(ModifyRequest.m, &testArgs{
|
||||
result, err := newMiddlewareTest(ModifyRequest, &testArgs{
|
||||
middlewareOpt: opts,
|
||||
})
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, result.RequestHeaders.Get("User-Agent"), "go-proxy/v0.5.0")
|
||||
ExpectEqual(t, result.RequestHeaders.Get("Host"), "test.example.com")
|
||||
ExpectTrue(t, slices.Contains(result.RequestHeaders.Values("Accept-Encoding"), "test-value"))
|
||||
ExpectEqual(t, result.RequestHeaders.Get("Accept"), "")
|
||||
})
|
||||
|
||||
@@ -9,16 +9,14 @@ import (
|
||||
|
||||
type (
|
||||
modifyResponse struct {
|
||||
*modifyResponseOpts
|
||||
modifyResponseOpts
|
||||
m *Middleware
|
||||
}
|
||||
// order: set_headers -> add_headers -> hide_headers
|
||||
modifyResponseOpts = modifyRequestOpts
|
||||
)
|
||||
|
||||
var ModifyResponse = &modifyResponse{
|
||||
m: &Middleware{withOptions: NewModifyResponse},
|
||||
}
|
||||
var ModifyResponse = &Middleware{withOptions: NewModifyResponse}
|
||||
|
||||
func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
mr := new(modifyResponse)
|
||||
@@ -28,8 +26,7 @@ func NewModifyResponse(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
} else {
|
||||
mr.m.modifyResponse = mr.modifyResponse
|
||||
}
|
||||
mr.modifyResponseOpts = new(modifyResponseOpts)
|
||||
err := Deserialize(optsRaw, mr.modifyResponseOpts)
|
||||
err := Deserialize(optsRaw, &mr.modifyResponseOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestSetModifyResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("set_options", func(t *testing.T) {
|
||||
mr, err := ModifyResponse.m.WithOptionsClone(opts)
|
||||
mr, err := ModifyResponse.WithOptionsClone(opts)
|
||||
ExpectNoError(t, err)
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyResponse).SetHeaders, opts["set_headers"].(map[string]string))
|
||||
ExpectDeepEqual(t, mr.impl.(*modifyResponse).AddHeaders, opts["add_headers"].(map[string]string))
|
||||
@@ -23,7 +23,7 @@ func TestSetModifyResponse(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("request_headers", func(t *testing.T) {
|
||||
result, err := newMiddlewareTest(ModifyResponse.m, &testArgs{
|
||||
result, err := newMiddlewareTest(ModifyResponse, &testArgs{
|
||||
middlewareOpt: opts,
|
||||
})
|
||||
ExpectNoError(t, err)
|
||||
|
||||
87
internal/net/http/middleware/rate_limit.go
Normal file
87
internal/net/http/middleware/rate_limit.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type (
|
||||
requestMap = map[string]*rate.Limiter
|
||||
rateLimiter struct {
|
||||
requestMap requestMap
|
||||
newLimiter func() *rate.Limiter
|
||||
m *Middleware
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
rateLimiterOpts struct {
|
||||
Average int `json:"average"`
|
||||
Burst int `json:"burst"`
|
||||
Period time.Duration `json:"period"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
RateLimiter = &Middleware{withOptions: NewRateLimiter}
|
||||
rateLimiterOptsDefault = rateLimiterOpts{
|
||||
Period: time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func NewRateLimiter(optsRaw OptionsRaw) (*Middleware, E.Error) {
|
||||
rl := new(rateLimiter)
|
||||
opts := rateLimiterOptsDefault
|
||||
err := Deserialize(optsRaw, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case opts.Average == 0:
|
||||
return nil, ErrZeroValue.Subject("average")
|
||||
case opts.Burst == 0:
|
||||
return nil, ErrZeroValue.Subject("burst")
|
||||
case opts.Period == 0:
|
||||
return nil, ErrZeroValue.Subject("period")
|
||||
}
|
||||
rl.requestMap = make(requestMap, 0)
|
||||
rl.newLimiter = func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Limit(opts.Average)*rate.Every(opts.Period), opts.Burst)
|
||||
}
|
||||
rl.m = &Middleware{
|
||||
impl: rl,
|
||||
before: rl.limit,
|
||||
}
|
||||
return rl.m, nil
|
||||
}
|
||||
|
||||
func (rl *rateLimiter) limit(next http.HandlerFunc, w ResponseWriter, r *Request) {
|
||||
rl.mu.Lock()
|
||||
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
rl.m.Debug().Msgf("unable to parse remote address %s", r.RemoteAddr)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
limiter, ok := rl.requestMap[host]
|
||||
if !ok {
|
||||
limiter = rl.newLimiter()
|
||||
rl.requestMap[host] = limiter
|
||||
}
|
||||
|
||||
rl.mu.Unlock()
|
||||
|
||||
if limiter.Allow() {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
|
||||
}
|
||||
27
internal/net/http/middleware/rate_limit_test.go
Normal file
27
internal/net/http/middleware/rate_limit_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
opts := OptionsRaw{
|
||||
"average": "10",
|
||||
"burst": "10",
|
||||
"period": "1s",
|
||||
}
|
||||
|
||||
rl, err := NewRateLimiter(opts)
|
||||
ExpectNoError(t, err)
|
||||
for range 10 {
|
||||
result, err := newMiddlewareTest(rl, nil)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusOK)
|
||||
}
|
||||
result, err := newMiddlewareTest(rl, nil)
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusTooManyRequests)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package middleware
|
||||
|
||||
type (
|
||||
rateLimiter struct {
|
||||
*rateLimiterOpts
|
||||
m *Middleware
|
||||
}
|
||||
|
||||
rateLimiterOpts struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
)
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// https://nginx.org/en/docs/http/ngx_http_realip_module.html
|
||||
|
||||
type realIP struct {
|
||||
*realIPOpts
|
||||
realIPOpts
|
||||
m *Middleware
|
||||
}
|
||||
|
||||
@@ -30,16 +30,13 @@ type realIPOpts struct {
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
var RealIP = &realIP{
|
||||
m: &Middleware{withOptions: NewRealIP},
|
||||
}
|
||||
|
||||
var realIPOptsDefault = func() *realIPOpts {
|
||||
return &realIPOpts{
|
||||
var (
|
||||
RealIP = &Middleware{withOptions: NewRealIP}
|
||||
realIPOptsDefault = realIPOpts{
|
||||
Header: "X-Real-IP",
|
||||
From: []*types.CIDR{},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
func NewRealIP(opts OptionsRaw) (*Middleware, E.Error) {
|
||||
riWithOpts := new(realIP)
|
||||
@@ -47,11 +44,14 @@ func NewRealIP(opts OptionsRaw) (*Middleware, E.Error) {
|
||||
impl: riWithOpts,
|
||||
before: Rewrite(riWithOpts.setRealIP),
|
||||
}
|
||||
riWithOpts.realIPOpts = realIPOptsDefault()
|
||||
err := Deserialize(opts, riWithOpts.realIPOpts)
|
||||
riWithOpts.realIPOpts = realIPOptsDefault
|
||||
err := Deserialize(opts, &riWithOpts.realIPOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(riWithOpts.From) == 0 {
|
||||
return nil, E.New("no allowed CIDRs").Subject("from")
|
||||
}
|
||||
return riWithOpts.m, nil
|
||||
}
|
||||
|
||||
@@ -70,9 +70,10 @@ func (ri *realIP) setRealIP(req *Request) {
|
||||
if err != nil {
|
||||
clientIPStr = req.RemoteAddr
|
||||
}
|
||||
clientIP := net.ParseIP(clientIPStr)
|
||||
|
||||
var isTrusted = false
|
||||
clientIP := net.ParseIP(clientIPStr)
|
||||
isTrusted := false
|
||||
|
||||
for _, CIDR := range ri.From {
|
||||
if CIDR.Contains(clientIP) {
|
||||
isTrusted = true
|
||||
|
||||
@@ -22,8 +22,10 @@ type Trace struct {
|
||||
|
||||
type Traces []*Trace
|
||||
|
||||
var traces = Traces{}
|
||||
var tracesMu sync.Mutex
|
||||
var (
|
||||
traces = make(Traces, 0)
|
||||
tracesMu sync.Mutex
|
||||
)
|
||||
|
||||
const MaxTraceNum = 100
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,10 +16,7 @@ const (
|
||||
|
||||
var SetXForwarded = &Middleware{
|
||||
before: Rewrite(func(req *Request) {
|
||||
req.Header.Del("Forwarded")
|
||||
req.Header.Del(xForwardedFor)
|
||||
req.Header.Del(xForwardedHost)
|
||||
req.Header.Del(xForwardedProto)
|
||||
delXForwarded(req)
|
||||
clientIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err == nil {
|
||||
req.Header.Set(xForwardedFor, clientIP)
|
||||
@@ -35,10 +33,18 @@ var SetXForwarded = &Middleware{
|
||||
}
|
||||
|
||||
var HideXForwarded = &Middleware{
|
||||
before: Rewrite(func(req *Request) {
|
||||
req.Header.Del("Forwarded")
|
||||
req.Header.Del(xForwardedFor)
|
||||
req.Header.Del(xForwardedHost)
|
||||
req.Header.Del(xForwardedProto)
|
||||
}),
|
||||
before: Rewrite(delXForwarded),
|
||||
}
|
||||
|
||||
func delXForwarded(req *Request) {
|
||||
req.Header.Del("Forwarded")
|
||||
toRemove := make([]string, 0)
|
||||
for k := range req.Header {
|
||||
if strings.HasPrefix(k, "X-Forwarded-") {
|
||||
toRemove = append(toRemove, k)
|
||||
}
|
||||
}
|
||||
for _, k := range toRemove {
|
||||
req.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,20 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ModifyResponseFunc func(*http.Response) error
|
||||
type ModifyResponseWriter struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
type (
|
||||
ModifyResponseFunc func(*http.Response) error
|
||||
ModifyResponseWriter struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
headerSent bool
|
||||
code int
|
||||
headerSent bool
|
||||
code int
|
||||
|
||||
modifier ModifyResponseFunc
|
||||
modified bool
|
||||
modifierErr error
|
||||
}
|
||||
modifier ModifyResponseFunc
|
||||
modified bool
|
||||
modifierErr error
|
||||
}
|
||||
)
|
||||
|
||||
func NewModifyResponseWriter(w http.ResponseWriter, r *http.Request, f ModifyResponseFunc) *ModifyResponseWriter {
|
||||
return &ModifyResponseWriter{
|
||||
|
||||
@@ -22,8 +22,11 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
@@ -86,12 +89,43 @@ type ReverseProxy struct {
|
||||
// implementation is used.
|
||||
ModifyResponse func(*http.Response) error
|
||||
|
||||
ServeHTTP http.HandlerFunc
|
||||
HandlerFunc http.HandlerFunc
|
||||
|
||||
TargetName string
|
||||
TargetURL types.URL
|
||||
}
|
||||
|
||||
type httpMetricLogger struct {
|
||||
http.ResponseWriter
|
||||
timestamp time.Time
|
||||
labels *metrics.HTTPRouteMetricLabels
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (l *httpMetricLogger) WriteHeader(status int) {
|
||||
l.ResponseWriter.WriteHeader(status)
|
||||
duration := time.Since(l.timestamp)
|
||||
go func() {
|
||||
m := metrics.GetRouteMetrics()
|
||||
m.HTTPReqTotal.Inc()
|
||||
m.HTTPReqElapsed.With(l.labels).Set(float64(duration.Milliseconds()))
|
||||
|
||||
// ignore 1xx
|
||||
switch {
|
||||
case status >= 500:
|
||||
m.HTTP5xx.With(l.labels).Inc()
|
||||
case status >= 400:
|
||||
m.HTTP4xx.With(l.labels).Inc()
|
||||
case status >= 200:
|
||||
m.HTTP2xx3xx.With(l.labels).Inc()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (l *httpMetricLogger) Unwrap() http.ResponseWriter {
|
||||
return l.ResponseWriter
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
@@ -157,10 +191,14 @@ func NewReverseProxy(name string, target types.URL, transport http.RoundTripper)
|
||||
TargetName: name,
|
||||
TargetURL: target,
|
||||
}
|
||||
rp.ServeHTTP = rp.serveHTTP
|
||||
rp.HandlerFunc = rp.handler
|
||||
return rp
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) UnregisterMetrics() {
|
||||
metrics.GetRouteMetrics().UnregisterService(p.TargetName)
|
||||
}
|
||||
|
||||
func rewriteRequestURL(req *http.Request, target *url.URL) {
|
||||
targetQuery := target.RawQuery
|
||||
req.URL.Scheme = target.Scheme
|
||||
@@ -225,9 +263,41 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if _, ok := rw.(DummyResponseWriter); ok {
|
||||
return
|
||||
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
p.HandlerFunc(rw, req)
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
if common.PrometheusEnabled {
|
||||
t := time.Now()
|
||||
var visitor string
|
||||
if realIPs := req.Header.Values("X-Real-IP"); len(realIPs) > 0 {
|
||||
visitor = realIPs[len(realIPs)-1]
|
||||
}
|
||||
if visitor == "" {
|
||||
if fwdIPs := req.Header.Values("X-Forwarded-For"); len(fwdIPs) > 0 {
|
||||
visitor = fwdIPs[len(fwdIPs)-1]
|
||||
}
|
||||
}
|
||||
if visitor == "" {
|
||||
var err error
|
||||
visitor, _, err = net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
visitor = req.RemoteAddr
|
||||
}
|
||||
}
|
||||
lbls := &metrics.HTTPRouteMetricLabels{
|
||||
Service: p.TargetName,
|
||||
Method: req.Method,
|
||||
Host: req.Host,
|
||||
Visitor: visitor,
|
||||
Path: req.URL.Path,
|
||||
}
|
||||
rw = &httpMetricLogger{
|
||||
ResponseWriter: rw,
|
||||
timestamp: t,
|
||||
labels: lbls,
|
||||
}
|
||||
}
|
||||
|
||||
transport := p.Transport
|
||||
@@ -367,7 +437,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
Proto: outreq.Proto,
|
||||
ProtoMajor: outreq.ProtoMajor,
|
||||
ProtoMinor: outreq.ProtoMinor,
|
||||
Header: make(http.Header),
|
||||
Header: http.Header{},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("Origin server is not reachable."))),
|
||||
Request: outreq,
|
||||
TLS: outreq.TLS,
|
||||
@@ -404,7 +474,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
|
||||
err = U.Copy2(req.Context(), rw, res.Body)
|
||||
_, err = io.Copy(rw, res.Body)
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
p.errorHandler(rw, req, err, true)
|
||||
|
||||
@@ -53,15 +53,15 @@ func RegisterProvider(configSubTask task.Task, cfg ProviderConfig) (Provider, er
|
||||
Subject(name).
|
||||
Withf(strutils.DoYouMean(utils.NearestField(name, Providers)))
|
||||
}
|
||||
if provider, err := createFunc(cfg); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
provider, err := createFunc(cfg)
|
||||
if err == nil {
|
||||
dispatcher.providers.Add(provider)
|
||||
configSubTask.OnCancel("remove provider", func() {
|
||||
dispatcher.providers.Remove(provider)
|
||||
})
|
||||
return provider, nil
|
||||
}
|
||||
return provider, err
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) start() {
|
||||
|
||||
@@ -19,8 +19,6 @@ type Entry interface {
|
||||
}
|
||||
|
||||
func ValidateEntry(m *RawEntry) (Entry, E.Error) {
|
||||
m.FillMissingFields()
|
||||
|
||||
scheme, err := T.NewScheme(m.Scheme)
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
@@ -36,6 +34,9 @@ func ValidateEntry(m *RawEntry) (Entry, E.Error) {
|
||||
if errs.HasError() {
|
||||
return nil, errs.Error()
|
||||
}
|
||||
if !UseHealthCheck(entry) && (UseLoadBalance(entry) || UseIdleWatcher(entry)) {
|
||||
return nil, E.New("healthCheck.disable cannot be true when loadbalancer or idlewatcher is enabled")
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,19 +22,21 @@ type (
|
||||
|
||||
// raw entry object before validation
|
||||
// loaded from docker labels or yaml file
|
||||
Alias string `json:"-" yaml:"-"`
|
||||
Scheme string `json:"scheme,omitempty" yaml:"scheme"`
|
||||
Host string `json:"host,omitempty" yaml:"host"`
|
||||
Port string `json:"port,omitempty" yaml:"port"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
||||
Middlewares docker.NestedLabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
||||
Alias string `json:"-" yaml:"-"`
|
||||
Scheme string `json:"scheme,omitempty" yaml:"scheme"`
|
||||
Host string `json:"host,omitempty" yaml:"host"`
|
||||
Port string `json:"port,omitempty" yaml:"port"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty" yaml:"load_balance"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty" yaml:"middlewares"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty" yaml:"homepage"`
|
||||
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty" yaml:"-"`
|
||||
|
||||
finalized bool
|
||||
}
|
||||
|
||||
RawEntries = F.Map[string, *RawEntry]
|
||||
@@ -42,7 +44,11 @@ type (
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) FillMissingFields() {
|
||||
func (e *RawEntry) Finalize() {
|
||||
if e.finalized {
|
||||
return
|
||||
}
|
||||
|
||||
isDocker := e.Container != nil
|
||||
cont := e.Container
|
||||
if !isDocker {
|
||||
@@ -124,14 +130,7 @@ func (e *RawEntry) FillMissingFields() {
|
||||
}
|
||||
|
||||
if e.HealthCheck == nil {
|
||||
e.HealthCheck = new(health.HealthCheckConfig)
|
||||
}
|
||||
|
||||
if e.HealthCheck.Interval == 0 {
|
||||
e.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if e.HealthCheck.Timeout == 0 {
|
||||
e.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
e.HealthCheck = health.DefaultHealthCheckConfig()
|
||||
}
|
||||
|
||||
if e.HealthCheck.Disable {
|
||||
@@ -159,6 +158,8 @@ func (e *RawEntry) FillMissingFields() {
|
||||
e.Port = "0"
|
||||
}
|
||||
}
|
||||
|
||||
e.finalized = true
|
||||
}
|
||||
|
||||
func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
|
||||
@@ -168,9 +169,9 @@ func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
|
||||
} else {
|
||||
lp = portSplit[0]
|
||||
pp = portSplit[1]
|
||||
}
|
||||
if len(portSplit) > 2 {
|
||||
extra = strings.Join(portSplit[2:], ":")
|
||||
if len(portSplit) > 2 {
|
||||
extra = strings.Join(portSplit[2:], ":")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ import (
|
||||
type ReverseProxyEntry struct { // real model after validation
|
||||
Raw *RawEntry `json:"raw"`
|
||||
|
||||
Alias fields.Alias `json:"alias"`
|
||||
Scheme fields.Scheme `json:"scheme"`
|
||||
URL net.URL `json:"url"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||
PathPatterns fields.PathPatterns `json:"path_patterns,omitempty"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty"`
|
||||
Middlewares docker.NestedLabelMap `json:"middlewares,omitempty"`
|
||||
Alias fields.Alias `json:"alias"`
|
||||
Scheme fields.Scheme `json:"scheme"`
|
||||
URL net.URL `json:"url"`
|
||||
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
|
||||
PathPatterns fields.PathPatterns `json:"path_patterns,omitempty"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalancer.Config `json:"load_balance,omitempty"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||
|
||||
/* Docker only */
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
@@ -68,7 +68,7 @@ func validateRPEntry(m *RawEntry, s fields.Scheme, errs *E.Builder) *ReverseProx
|
||||
port := E.Collect(errs, fields.ValidatePort, m.Port)
|
||||
pathPats := E.Collect(errs, fields.ValidatePathPatterns, m.PathPatterns)
|
||||
url := E.Collect(errs, url.Parse, fmt.Sprintf("%s://%s:%d", s, host, port))
|
||||
iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, m.Container)
|
||||
iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
|
||||
|
||||
if errs.HasError() {
|
||||
return nil
|
||||
|
||||
@@ -60,8 +60,8 @@ func validateStreamEntry(m *RawEntry, errs *E.Builder) *StreamEntry {
|
||||
host := E.Collect(errs, fields.ValidateHost, m.Host)
|
||||
port := E.Collect(errs, fields.ValidateStreamPort, m.Port)
|
||||
scheme := E.Collect(errs, fields.ValidateStreamScheme, m.Scheme)
|
||||
url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ListeningScheme, host, port.ListeningPort))
|
||||
idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, m.Container)
|
||||
url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ListeningScheme, host, port.ProxyPort))
|
||||
idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
|
||||
|
||||
if errs.HasError() {
|
||||
return nil
|
||||
|
||||
@@ -32,7 +32,7 @@ func ValidatePathPattern(s string) (PathPattern, error) {
|
||||
|
||||
func ValidatePathPatterns(s []string) (PathPatterns, E.Error) {
|
||||
if len(s) == 0 {
|
||||
return []PathPattern{"/"}, nil
|
||||
return nil, nil
|
||||
}
|
||||
errs := E.NewBuilder("invalid path patterns")
|
||||
pp := make(PathPatterns, len(s))
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
@@ -38,24 +38,15 @@ type (
|
||||
}
|
||||
|
||||
SubdomainKey = PT.Alias
|
||||
|
||||
ReverseProxyHandler struct {
|
||||
*gphttp.ReverseProxy
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
findMuxFunc = findMuxAnyDomain
|
||||
|
||||
httpRoutes = F.NewMapOf[string, *HTTPRoute]()
|
||||
httpRoutesMu sync.Mutex
|
||||
httpRoutes = F.NewMapOf[string, *HTTPRoute]()
|
||||
// globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
|
||||
)
|
||||
|
||||
func (rp ReverseProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rp.ReverseProxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func GetReverseProxies() F.Map[string, *HTTPRoute] {
|
||||
return httpRoutes
|
||||
}
|
||||
@@ -70,17 +61,17 @@ func SetFindMuxDomains(domains []string) {
|
||||
|
||||
func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) {
|
||||
var trans *http.Transport
|
||||
|
||||
if entry.NoTLSVerify {
|
||||
trans = gphttp.DefaultTransportNoTLS.Clone()
|
||||
trans = gphttp.DefaultTransportNoTLS
|
||||
} else {
|
||||
trans = gphttp.DefaultTransport.Clone()
|
||||
trans = gphttp.DefaultTransport
|
||||
}
|
||||
|
||||
rp := gphttp.NewReverseProxy(string(entry.Alias), entry.URL, trans)
|
||||
service := string(entry.Alias)
|
||||
rp := gphttp.NewReverseProxy(service, entry.URL, trans)
|
||||
|
||||
if len(entry.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(string(entry.Alias), rp, entry.Middlewares)
|
||||
err := middleware.PatchReverseProxy(service, rp, entry.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,7 +80,6 @@ func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) {
|
||||
r := &HTTPRoute{
|
||||
ReverseProxyEntry: entry,
|
||||
rp: rp,
|
||||
task: task.DummyTask(),
|
||||
l: logger.With().
|
||||
Str("type", string(entry.Scheme)).
|
||||
Str("name", string(entry.Alias)).
|
||||
@@ -109,16 +99,7 @@ func (r *HTTPRoute) Start(providerSubtask task.Task) E.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
httpRoutesMu.Lock()
|
||||
defer httpRoutesMu.Unlock()
|
||||
|
||||
if !entry.UseHealthCheck(r) && (entry.UseLoadBalance(r) || entry.UseIdleWatcher(r)) {
|
||||
r.l.Error().Msg("healthCheck.disabled cannot be false when loadbalancer or idlewatcher is enabled")
|
||||
if r.HealthCheck == nil {
|
||||
r.HealthCheck = new(health.HealthCheckConfig)
|
||||
}
|
||||
r.HealthCheck.Disable = false
|
||||
}
|
||||
r.task = providerSubtask
|
||||
|
||||
switch {
|
||||
case entry.UseIdleWatcher(r):
|
||||
@@ -130,18 +111,19 @@ func (r *HTTPRoute) Start(providerSubtask task.Task) E.Error {
|
||||
r.handler = waker
|
||||
r.HealthMon = waker
|
||||
case entry.UseHealthCheck(r):
|
||||
r.HealthMon = health.NewHTTPHealthMonitor(r.TargetURL(), r.HealthCheck, r.rp.Transport)
|
||||
r.HealthMon = health.NewHTTPHealthMonitor(r.rp.TargetURL, r.HealthCheck)
|
||||
}
|
||||
r.task = providerSubtask
|
||||
|
||||
if r.handler == nil {
|
||||
switch {
|
||||
case len(r.PathPatterns) == 0:
|
||||
r.handler = r.rp
|
||||
case len(r.PathPatterns) == 1 && r.PathPatterns[0] == "/":
|
||||
r.handler = ReverseProxyHandler{r.rp}
|
||||
r.handler = r.rp
|
||||
default:
|
||||
mux := http.NewServeMux()
|
||||
for _, p := range r.PathPatterns {
|
||||
mux.HandleFunc(string(p), r.rp.ServeHTTP)
|
||||
mux.HandleFunc(string(p), r.rp.HandlerFunc)
|
||||
}
|
||||
r.handler = mux
|
||||
}
|
||||
@@ -164,6 +146,9 @@ func (r *HTTPRoute) Start(providerSubtask task.Task) E.Error {
|
||||
})
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
r.task.OnFinished("unreg metrics", r.rp.UnregisterMetrics)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -211,40 +196,51 @@ func (r *HTTPRoute) addToLoadBalancer() {
|
||||
|
||||
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
mux, err := findMuxFunc(r.Host)
|
||||
if err == nil {
|
||||
mux.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Why use StatusNotFound instead of StatusBadRequest or StatusBadGateway?
|
||||
// On nginx, when route for domain does not exist, it returns StatusBadGateway.
|
||||
// Then scraper / scanners will know the subdomain is invalid.
|
||||
// With StatusNotFound, they won't know whether it's the path, or the subdomain that is invalid.
|
||||
if err != nil {
|
||||
if !middleware.ServeStaticErrorPageFile(w, r) {
|
||||
logger.Err(err).Str("method", r.Method).Str("url", r.URL.String()).Msg("request")
|
||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
||||
if ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if _, err := w.Write(errorPage); err != nil {
|
||||
logger.Err(err).Msg("failed to write error page")
|
||||
}
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
if !middleware.ServeStaticErrorPageFile(w, r) {
|
||||
logger.Err(err).Str("method", r.Method).Str("url", r.URL.String()).Msg("request")
|
||||
errorPage, ok := errorpage.GetErrorPageByStatus(http.StatusNotFound)
|
||||
if ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if _, err := w.Write(errorPage); err != nil {
|
||||
logger.Err(err).Msg("failed to write error page")
|
||||
}
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func findMuxAnyDomain(host string) (http.Handler, error) {
|
||||
hostSplit := strings.Split(host, ".")
|
||||
n := len(hostSplit)
|
||||
if n <= 2 {
|
||||
switch {
|
||||
case n == 3:
|
||||
host = hostSplit[0]
|
||||
case n > 3:
|
||||
var builder strings.Builder
|
||||
builder.Grow(2*n - 3)
|
||||
builder.WriteString(hostSplit[0])
|
||||
for _, part := range hostSplit[:n-2] {
|
||||
builder.WriteRune('.')
|
||||
builder.WriteString(part)
|
||||
}
|
||||
host = builder.String()
|
||||
default:
|
||||
return nil, errors.New("missing subdomain in url")
|
||||
}
|
||||
sd := strings.Join(hostSplit[:n-2], ".")
|
||||
if r, ok := httpRoutes.Load(sd); ok {
|
||||
if r, ok := httpRoutes.Load(host); ok {
|
||||
return r.handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", sd)
|
||||
return nil, fmt.Errorf("no such route: %s", host)
|
||||
}
|
||||
|
||||
func findMuxByDomains(domains []string) func(host string) (http.Handler, error) {
|
||||
@@ -252,20 +248,18 @@ func findMuxByDomains(domains []string) func(host string) (http.Handler, error)
|
||||
var subdomain string
|
||||
|
||||
for _, domain := range domains {
|
||||
if !strings.HasPrefix(domain, ".") {
|
||||
domain = "." + domain
|
||||
}
|
||||
subdomain = strings.TrimSuffix(host, domain)
|
||||
if len(subdomain) < len(host) {
|
||||
if strings.HasSuffix(host, domain) {
|
||||
subdomain = strings.TrimSuffix(host, domain)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(subdomain) == len(host) { // not matched
|
||||
return nil, fmt.Errorf("%s does not match any base domain", host)
|
||||
|
||||
if subdomain != "" { // matched
|
||||
if r, ok := httpRoutes.Load(subdomain); ok {
|
||||
return r.handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", subdomain)
|
||||
}
|
||||
if r, ok := httpRoutes.Load(subdomain); ok {
|
||||
return r.handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no such route: %s", subdomain)
|
||||
return nil, fmt.Errorf("%s does not match any base domain", host)
|
||||
}
|
||||
}
|
||||
|
||||
7
internal/route/metrics.go
Normal file
7
internal/route/metrics.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package route
|
||||
|
||||
import "github.com/yusing/go-proxy/internal/metrics"
|
||||
|
||||
func init() {
|
||||
metrics.InitRouterMetrics(httpRoutes.Size, streamRoutes.Size)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
)
|
||||
@@ -22,16 +22,16 @@ type DockerProvider struct {
|
||||
l zerolog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
AliasRefRegex = regexp.MustCompile(`#\d+`)
|
||||
AliasRefRegexOld = regexp.MustCompile(`\$\d+`)
|
||||
|
||||
ErrAliasRefIndexOutOfRange = E.New("index out of range")
|
||||
const (
|
||||
aliasRefPrefix = '#'
|
||||
aliasRefPrefixAlt = '$'
|
||||
)
|
||||
|
||||
var ErrAliasRefIndexOutOfRange = E.New("index out of range")
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string, explicitOnly bool) (ProviderImpl, error) {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnv("DOCKER_HOST", client.DefaultDockerHost)
|
||||
dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
return &DockerProvider{
|
||||
name,
|
||||
@@ -114,65 +114,70 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
|
||||
}
|
||||
|
||||
errs := E.NewBuilder("label errors")
|
||||
for key, val := range container.Labels {
|
||||
errs.Add(p.applyLabel(container, entries, key, val))
|
||||
}
|
||||
|
||||
// remove all entries that failed to fill in missing fields
|
||||
entries.RangeAll(func(_ string, re *entry.RawEntry) {
|
||||
re.FillMissingFields()
|
||||
})
|
||||
m, err := docker.ParseLabels(container.Labels)
|
||||
errs.Add(err)
|
||||
|
||||
var wildcardProps docker.LabelMap
|
||||
|
||||
for alias, entryMapAny := range m {
|
||||
if len(alias) == 0 {
|
||||
errs.Add(E.New("empty alias"))
|
||||
continue
|
||||
}
|
||||
|
||||
var ok bool
|
||||
entryMap, ok := entryMapAny.(docker.LabelMap)
|
||||
if !ok {
|
||||
errs.Add(E.Errorf("expect mapping, got %T", entryMap).Subject(alias))
|
||||
continue
|
||||
}
|
||||
|
||||
if alias == docker.WildcardAlias {
|
||||
wildcardProps = entryMap
|
||||
continue
|
||||
}
|
||||
|
||||
// check if it is an alias reference
|
||||
switch alias[0] {
|
||||
case aliasRefPrefix, aliasRefPrefixAlt:
|
||||
index, err := strutils.Atoi(alias[1:])
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
break
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
errs.Add(ErrAliasRefIndexOutOfRange.Subject(strconv.Itoa(index)))
|
||||
break
|
||||
}
|
||||
alias = container.Aliases[index-1]
|
||||
}
|
||||
|
||||
// init entry if not exist
|
||||
var en *entry.RawEntry
|
||||
if en, ok = entries.Load(alias); !ok {
|
||||
en = &entry.RawEntry{
|
||||
Alias: alias,
|
||||
Container: container,
|
||||
}
|
||||
entries.Store(alias, en)
|
||||
}
|
||||
|
||||
// deserialize map into entry object
|
||||
err := U.Deserialize(entryMap, en)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(alias))
|
||||
} else {
|
||||
entries.Store(alias, en)
|
||||
}
|
||||
}
|
||||
if wildcardProps != nil {
|
||||
entries.RangeAll(func(alias string, re *entry.RawEntry) {
|
||||
if err := U.Deserialize(wildcardProps, re); err != nil {
|
||||
errs.Add(err.Subject(alias))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return entries, errs.Error()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) applyLabel(container *docker.Container, entries entry.RawEntries, key, val string) E.Error {
|
||||
lbl := docker.ParseLabel(key, val)
|
||||
if lbl.Namespace != docker.NSProxy {
|
||||
return nil
|
||||
}
|
||||
if lbl.Target == docker.WildcardAlias {
|
||||
// apply label for all aliases
|
||||
labelErrs := entries.CollectErrors(func(a string, e *entry.RawEntry) error {
|
||||
return docker.ApplyLabel(e, lbl)
|
||||
})
|
||||
if err := E.Join(labelErrs...); err != nil {
|
||||
return err.Subject(lbl.Target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
refErrs := E.NewBuilder("alias ref errors")
|
||||
replaceIndexRef := func(ref string) string {
|
||||
index, err := strutils.Atoi(ref[1:])
|
||||
if err != nil {
|
||||
refErrs.Add(err)
|
||||
return ref
|
||||
}
|
||||
if index < 1 || index > len(container.Aliases) {
|
||||
refErrs.Add(ErrAliasRefIndexOutOfRange.Subject(strconv.Itoa(index)))
|
||||
return ref
|
||||
}
|
||||
return container.Aliases[index-1]
|
||||
}
|
||||
|
||||
lbl.Target = AliasRefRegex.ReplaceAllStringFunc(lbl.Target, replaceIndexRef)
|
||||
lbl.Target = AliasRefRegexOld.ReplaceAllStringFunc(lbl.Target, func(ref string) string {
|
||||
p.l.Warn().Msgf("%q should now be %q, old syntax will be removed in a future version", lbl, strings.ReplaceAll(lbl.String(), "$", "#"))
|
||||
return replaceIndexRef(ref)
|
||||
})
|
||||
if refErrs.HasError() {
|
||||
return refErrs.Error().Subject(lbl.String())
|
||||
}
|
||||
|
||||
en, ok := entries.Load(lbl.Target)
|
||||
if !ok {
|
||||
en = &entry.RawEntry{
|
||||
Alias: lbl.Target,
|
||||
Container: container,
|
||||
}
|
||||
entries.Store(lbl.Target, en)
|
||||
}
|
||||
|
||||
return docker.ApplyLabel(en, lbl)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestApplyLabel(t *testing.T) {
|
||||
"POST /upload/{$}",
|
||||
"GET /static",
|
||||
}
|
||||
middlewaresExpect := D.NestedLabelMap{
|
||||
middlewaresExpect := map[string]map[string]any{
|
||||
"middleware1": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
@@ -55,7 +55,6 @@ func TestApplyLabel(t *testing.T) {
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.middleware1.prop1": "value1",
|
||||
"proxy.a.middlewares.middleware1.prop2": "value2",
|
||||
@@ -215,6 +214,21 @@ func TestDynamicAliases(t *testing.T) {
|
||||
ExpectEqual(t, raw.Port, "5678")
|
||||
}
|
||||
|
||||
func TestDisableHealthCheck(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
"proxy.a.healthcheck.disable": "true",
|
||||
"proxy.a.port": "1234",
|
||||
},
|
||||
}, client.DefaultDockerHost)
|
||||
raw, ok := E.Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, raw.HealthCheck, nil)
|
||||
}
|
||||
|
||||
func TestPublicIPLocalhost(t *testing.T) {
|
||||
c := D.FromDocker(&types.Container{Names: dummyNames, State: "running"}, client.DefaultDockerHost)
|
||||
raw, ok := E.Must(p.entriesFromContainerLabels(c)).Load("a")
|
||||
|
||||
@@ -18,9 +18,9 @@ type EventHandler struct {
|
||||
updated *E.Builder
|
||||
}
|
||||
|
||||
func (provider *Provider) newEventHandler() *EventHandler {
|
||||
func (p *Provider) newEventHandler() *EventHandler {
|
||||
return &EventHandler{
|
||||
provider: provider,
|
||||
provider: p,
|
||||
errs: E.NewBuilder("event errors"),
|
||||
added: E.NewBuilder("added"),
|
||||
removed: E.NewBuilder("removed"),
|
||||
@@ -60,11 +60,12 @@ func (handler *EventHandler) Handle(parent task.Task, events []watcher.Event) {
|
||||
|
||||
oldRoutes.RangeAll(func(k string, oldr *route.Route) {
|
||||
newr, ok := newRoutes.Load(k)
|
||||
if !ok {
|
||||
switch {
|
||||
case !ok:
|
||||
handler.Remove(oldr)
|
||||
} else if handler.matchAny(events, newr) {
|
||||
case handler.matchAny(events, newr):
|
||||
handler.Update(parent, oldr, newr)
|
||||
} else if entry.ShouldNotServe(newr) {
|
||||
case entry.ShouldNotServe(newr):
|
||||
handler.Remove(oldr)
|
||||
}
|
||||
})
|
||||
@@ -122,11 +123,11 @@ func (handler *EventHandler) Update(parent task.Task, oldRoute *route.Route, new
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Log() {
|
||||
results := E.NewBuilder("event occured")
|
||||
results.Add(handler.added.Error())
|
||||
results.Add(handler.removed.Error())
|
||||
results.Add(handler.updated.Error())
|
||||
results.Add(handler.errs.Error())
|
||||
results := E.NewBuilder("event occurred")
|
||||
results.AddFrom(handler.added, false)
|
||||
results.AddFrom(handler.removed, false)
|
||||
results.AddFrom(handler.updated, false)
|
||||
results.AddFrom(handler.errs, false)
|
||||
if result := results.String(); result != "" {
|
||||
handler.provider.Logger().Info().Msg(result)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (p *FileProvider) loadRoutesImpl() (R.Routes, E.Error) {
|
||||
}
|
||||
|
||||
if err := Validate(data); err != nil {
|
||||
E.LogWarn(p.fileName+": validation failure", err)
|
||||
E.LogWarn("validation failure", err.Subject(p.fileName))
|
||||
}
|
||||
|
||||
return R.FromEntries(entries)
|
||||
|
||||
@@ -45,9 +45,7 @@ const (
|
||||
providerEventFlushInterval = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyProviderName = errors.New("empty provider name")
|
||||
)
|
||||
var ErrEmptyProviderName = errors.New("empty provider name")
|
||||
|
||||
func newProvider(name string, t ProviderType) *Provider {
|
||||
return &Provider{
|
||||
@@ -109,12 +107,11 @@ func (p *Provider) startRoute(parent task.Task, r *R.Route) E.Error {
|
||||
p.routes.Delete(r.Entry.Alias)
|
||||
subtask.Finish(err) // just to ensure
|
||||
return err.Subject(r.Entry.Alias)
|
||||
} else {
|
||||
p.routes.Store(r.Entry.Alias, r)
|
||||
subtask.OnFinished("del from provider", func() {
|
||||
p.routes.Delete(r.Entry.Alias)
|
||||
})
|
||||
}
|
||||
p.routes.Store(r.Entry.Alias, r)
|
||||
subtask.OnFinished("del from provider", func() {
|
||||
p.routes.Delete(r.Entry.Alias)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func (rt *Route) Container() *docker.Container {
|
||||
}
|
||||
|
||||
func NewRoute(raw *entry.RawEntry) (*Route, E.Error) {
|
||||
raw.Finalize()
|
||||
en, err := entry.ValidateEntry(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -80,11 +81,12 @@ func FromEntries(entries entry.RawEntries) (Routes, E.Error) {
|
||||
entries.RangeAllParallel(func(alias string, en *entry.RawEntry) {
|
||||
en.Alias = alias
|
||||
r, err := NewRoute(en)
|
||||
if err != nil {
|
||||
switch {
|
||||
case err != nil:
|
||||
b.Add(err.Subject(alias))
|
||||
} else if entry.ShouldNotServe(r) {
|
||||
case entry.ShouldNotServe(r):
|
||||
return
|
||||
} else {
|
||||
default:
|
||||
routes.Store(alias, r)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/docker/idlewatcher"
|
||||
@@ -19,7 +18,7 @@ import (
|
||||
type StreamRoute struct {
|
||||
*entry.StreamEntry
|
||||
|
||||
stream net.Stream `json:"-"`
|
||||
stream net.Stream
|
||||
|
||||
HealthMon health.HealthMonitor `json:"health"`
|
||||
|
||||
@@ -28,10 +27,7 @@ type StreamRoute struct {
|
||||
l zerolog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
streamRoutes = F.NewMapOf[string, *StreamRoute]()
|
||||
streamRoutesMu sync.Mutex
|
||||
)
|
||||
var streamRoutes = F.NewMapOf[string, *StreamRoute]()
|
||||
|
||||
func GetStreamProxies() F.Map[string, *StreamRoute] {
|
||||
return streamRoutes
|
||||
@@ -44,7 +40,6 @@ func NewStreamRoute(entry *entry.StreamEntry) (impl, E.Error) {
|
||||
}
|
||||
return &StreamRoute{
|
||||
StreamEntry: entry,
|
||||
task: task.DummyTask(),
|
||||
l: logger.With().
|
||||
Str("type", string(entry.Scheme.ListeningScheme)).
|
||||
Str("name", entry.TargetName()).
|
||||
@@ -63,14 +58,6 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
streamRoutesMu.Lock()
|
||||
defer streamRoutesMu.Unlock()
|
||||
|
||||
if r.HealthCheck.Disable && (entry.UseLoadBalance(r) || entry.UseIdleWatcher(r)) {
|
||||
r.l.Error().Msg("healthCheck.disabled cannot be false when loadbalancer or idlewatcher is enabled")
|
||||
r.HealthCheck.Disable = false
|
||||
}
|
||||
|
||||
r.task = providerSubtask
|
||||
r.stream = NewStream(r)
|
||||
|
||||
@@ -112,6 +99,7 @@ func (r *StreamRoute) Start(providerSubtask task.Task) E.Error {
|
||||
}
|
||||
|
||||
go r.acceptConnections()
|
||||
|
||||
streamRoutes.Store(string(r.Alias), r)
|
||||
r.task.OnFinished("remove from route table", func() {
|
||||
streamRoutes.Delete(string(r.Alias))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package server
|
||||
|
||||
var proxyServer, apiServer *Server
|
||||
|
||||
func InitProxyServer(opt Options) *Server {
|
||||
if proxyServer == nil {
|
||||
proxyServer = NewServer(opt)
|
||||
}
|
||||
return proxyServer
|
||||
}
|
||||
|
||||
func InitAPIServer(opt Options) *Server {
|
||||
if apiServer == nil {
|
||||
apiServer = NewServer(opt)
|
||||
}
|
||||
return apiServer
|
||||
}
|
||||
|
||||
func GetProxyServer() *Server {
|
||||
return proxyServer
|
||||
}
|
||||
|
||||
func GetAPIServer() *Server {
|
||||
return apiServer
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -39,6 +38,12 @@ type Options struct {
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
func StartServer(opt Options) (s *Server) {
|
||||
s = NewServer(opt)
|
||||
s.Start()
|
||||
return s
|
||||
}
|
||||
|
||||
func NewServer(opt Options) (s *Server) {
|
||||
var httpSer, httpsSer *http.Server
|
||||
var httpHandler http.Handler
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
branch = common.GetEnv("GOPROXY_BRANCH", "v0.7")
|
||||
branch = common.GetEnvString("BRANCH", "v0.7")
|
||||
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
|
||||
requiredConfigs = []Config{
|
||||
{common.ConfigBasePath, true, false, ""},
|
||||
{common.DotEnvPath, false, true, common.DotEnvExamplePath},
|
||||
{common.ComposeFileName, false, true, common.ComposeExampleFileName},
|
||||
{path.Join(common.ConfigBasePath, common.ConfigFileName), false, true, common.ConfigExampleFileName},
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func Setup() {
|
||||
config.setup()
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
log.Println("setup finished")
|
||||
}
|
||||
|
||||
func (c *Config) setup() {
|
||||
@@ -96,7 +97,7 @@ func fetch(remoteFilename string, outFileName string) {
|
||||
log.Printf("%q already exists, downloading to %q\n", outFileName, remoteFilename)
|
||||
outFileName = remoteFilename
|
||||
}
|
||||
log.Printf("downloading %q\n", remoteFilename)
|
||||
log.Printf("downloading %q to %q\n", remoteFilename, outFileName)
|
||||
|
||||
url, err := url.JoinPath(baseURL, remoteFilename)
|
||||
if err != nil {
|
||||
@@ -120,7 +121,7 @@ func fetch(remoteFilename string, outFileName string) {
|
||||
log.Fatalf("failed to write to file: %s\n", err)
|
||||
}
|
||||
|
||||
log.Printf("downloaded to %q\n", outFileName)
|
||||
log.Print("done")
|
||||
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package task
|
||||
|
||||
import "context"
|
||||
|
||||
type dummyTask struct{}
|
||||
|
||||
func DummyTask() (_ Task) {
|
||||
return
|
||||
}
|
||||
|
||||
// Context implements Task.
|
||||
func (d dummyTask) Context() context.Context {
|
||||
panic("call of dummyTask.Context")
|
||||
}
|
||||
|
||||
// Finish implements Task.
|
||||
func (d dummyTask) Finish() {}
|
||||
|
||||
// Name implements Task.
|
||||
func (d dummyTask) Name() string {
|
||||
return "Dummy Task"
|
||||
}
|
||||
|
||||
// OnComplete implements Task.
|
||||
func (d dummyTask) OnComplete(about string, fn func()) {
|
||||
panic("call of dummyTask.OnComplete")
|
||||
}
|
||||
|
||||
// Parent implements Task.
|
||||
func (d dummyTask) Parent() Task {
|
||||
panic("call of dummyTask.Parent")
|
||||
}
|
||||
|
||||
// Subtask implements Task.
|
||||
func (d dummyTask) Subtask(usageFmt string, args ...any) Task {
|
||||
panic("call of dummyTask.Subtask")
|
||||
}
|
||||
|
||||
// Wait implements Task.
|
||||
func (d dummyTask) Wait() {}
|
||||
|
||||
// WaitSubTasks implements Task.
|
||||
func (d dummyTask) WaitSubTasks() {}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
@@ -28,8 +27,6 @@ func createGlobalTask() (t *task) {
|
||||
type (
|
||||
// Task controls objects' lifetime.
|
||||
//
|
||||
// Task must be initialized, use DummyTask if the task is not yet started.
|
||||
//
|
||||
// Objects that uses a task should implement the TaskStarter and the TaskFinisher interface.
|
||||
//
|
||||
// When passing a Task object to another function,
|
||||
@@ -167,8 +164,8 @@ func GlobalContextWait(timeout time.Duration) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *task) trace() *zerolog.Event {
|
||||
return logger.Trace().Str("name", t.name)
|
||||
func (t *task) trace(msg string) {
|
||||
logger.Trace().Str("name", t.name).Msg(msg)
|
||||
}
|
||||
|
||||
func (t *task) Name() string {
|
||||
@@ -244,7 +241,7 @@ func (t *task) OnCancel(about string, fn func()) {
|
||||
<-t.ctx.Done()
|
||||
fn()
|
||||
onCompTask.Finish("done")
|
||||
t.trace().Msg("onCancel done: " + about)
|
||||
t.trace("onCancel done: " + about)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -284,10 +281,10 @@ func (t *task) newSubTask(ctx context.Context, cancel context.CancelCauseFunc, n
|
||||
parent.subTasksWg.Add(1)
|
||||
parent.subtasks.Add(subtask)
|
||||
if common.IsTrace {
|
||||
subtask.trace().Msg("started")
|
||||
subtask.trace("started")
|
||||
go func() {
|
||||
subtask.Wait()
|
||||
subtask.trace().Msg("finished: " + subtask.FinishCause().Error())
|
||||
subtask.trace("finished: " + subtask.FinishCause().Error())
|
||||
}()
|
||||
}
|
||||
go func() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user