mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-18 06:29:42 +02:00
Compare commits
303 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc3b8575e | ||
|
|
136a2ec89f | ||
|
|
021c68f2a7 | ||
|
|
989a09274f | ||
|
|
39c5886d7a | ||
|
|
1a5f3735cf | ||
|
|
4d47eb0e91 | ||
|
|
af7c59b5c2 | ||
|
|
693bf68864 | ||
|
|
c9ddf3d165 | ||
|
|
1549b56866 | ||
|
|
2cd1f22e68 | ||
|
|
688f38943d | ||
|
|
043bbd7a11 | ||
|
|
f997423fd7 | ||
|
|
1871ef3d38 | ||
|
|
7c56c88dd4 | ||
|
|
4d7422dd90 | ||
|
|
eccabc0588 | ||
|
|
0c7b188587 | ||
|
|
4c97b79adf | ||
|
|
8ae9573b07 | ||
|
|
43fce6e739 | ||
|
|
78900772bb | ||
|
|
c16a0444ca | ||
|
|
0d518166ee | ||
|
|
6ae391a3c9 | ||
|
|
357897a0cd | ||
|
|
10a0a8fe09 | ||
|
|
98443be80c | ||
|
|
bf7f6e99c5 | ||
|
|
b6e468e54e | ||
|
|
dfc634a362 | ||
|
|
d9b6b82f07 | ||
|
|
4ad6257dab | ||
|
|
e3e3f1dfdc | ||
|
|
60f83bb7bf | ||
|
|
bbc10cb105 | ||
|
|
83ea19dd92 | ||
|
|
7ec42dce4d | ||
|
|
a9da7ce6fc | ||
|
|
1586610a44 | ||
|
|
254224c0e8 | ||
|
|
9b66772a12 | ||
|
|
322878b0b7 | ||
|
|
9e181d25ce | ||
|
|
4c311fd78e | ||
|
|
9936b3af5b | ||
|
|
648fd23a57 | ||
|
|
7dd00d2424 | ||
|
|
9e83fe7329 | ||
|
|
166c9c75e9 | ||
|
|
b9882f8985 | ||
|
|
37a166731d | ||
|
|
66db583432 | ||
|
|
f7eb80a6ea | ||
|
|
79f40f3d22 | ||
|
|
ed3b26653c | ||
|
|
2bb13129de | ||
|
|
fc29e8f9fa | ||
|
|
495c2c7390 | ||
|
|
b984386bab | ||
|
|
3781bb93e1 | ||
|
|
3a4dc3f876 | ||
|
|
2c43f1412e | ||
|
|
5d3a93f103 | ||
|
|
5faba1b5a9 | ||
|
|
4e7bd3579b | ||
|
|
49da8a31d2 | ||
|
|
dd2b8f600d | ||
|
|
8b1a3a31ff | ||
|
|
d429374924 | ||
|
|
dd0bbdc7b4 | ||
|
|
64e85c3076 | ||
|
|
68771ce399 | ||
|
|
bcc7faa8e5 | ||
|
|
fb0dc7dea0 | ||
|
|
0fad7b3411 | ||
|
|
1adba05065 | ||
|
|
fe7740f1b0 | ||
|
|
b253dce7e1 | ||
|
|
589b3a7a13 | ||
|
|
26d259b952 | ||
|
|
04e118c081 | ||
|
|
2af2346e35 | ||
|
|
7cd44b5ad3 | ||
|
|
81d96394b9 | ||
|
|
76fe5345d8 | ||
|
|
ef277ef57f | ||
|
|
9a12dab600 | ||
|
|
51f6391ded | ||
|
|
e10e6cfe4d | ||
|
|
d887a37f60 | ||
|
|
1abd1e257f | ||
|
|
137b0820b0 | ||
|
|
3f85d7f813 | ||
|
|
6b6dae129f | ||
|
|
2c3672a7ea | ||
|
|
645a58464c | ||
|
|
fcbb51dce7 | ||
|
|
c7c6a097f0 | ||
|
|
0ce7f29976 | ||
|
|
f2df756c17 | ||
|
|
28b5d44e11 | ||
|
|
e7bb6bc798 | ||
|
|
c572382f6a | ||
|
|
e28c4a1b4d | ||
|
|
f5708fd539 | ||
|
|
5769abb626 | ||
|
|
4ebe0abba0 | ||
|
|
8109c9ac4f | ||
|
|
2ce1ceb460 | ||
|
|
9d701ad671 | ||
|
|
4aee44fe11 | ||
|
|
adb41a80c5 | ||
|
|
642e6ebdc8 | ||
|
|
74828943a6 | ||
|
|
f906e04581 | ||
|
|
b3c47e759f | ||
|
|
8bbb5d2e09 | ||
|
|
7fe03be73f | ||
|
|
abb0124011 | ||
|
|
a98b2bb71a | ||
|
|
bc1702e6cf | ||
|
|
577a5366e8 | ||
|
|
7fedd5729e | ||
|
|
35c0463829 | ||
|
|
1b40f81fcc | ||
|
|
afefd925ea | ||
|
|
0850562bf9 | ||
|
|
bc2335a54e | ||
|
|
5a9fc3ad18 | ||
|
|
29f85db022 | ||
|
|
6034908a95 | ||
|
|
ef3dbc217b | ||
|
|
01357617ae | ||
|
|
4775f4ea31 | ||
|
|
ae7b27e1c9 | ||
|
|
70c8c4b4aa | ||
|
|
b7802f4e3e | ||
|
|
6f35a6f5e9 | ||
|
|
5e2ce9e1e6 | ||
|
|
e04080bf1c | ||
|
|
55134c8426 | ||
|
|
0e886f5ddf | ||
|
|
1e97d1230a | ||
|
|
d44ce0ee6f | ||
|
|
c30d3f585f | ||
|
|
112859caa5 | ||
|
|
6b669fc540 | ||
|
|
cb9b7d55fd | ||
|
|
c506db1ef4 | ||
|
|
65afc73f25 | ||
|
|
7e60d1803c | ||
|
|
3ecc0f95bf | ||
|
|
c1db404c0d | ||
|
|
b38bff41d8 | ||
|
|
6e30d39b78 | ||
|
|
753e193d62 | ||
|
|
4819972399 | ||
|
|
ba8705fb84 | ||
|
|
9f71fc2dd5 | ||
|
|
a587ada170 | ||
|
|
320e29ba84 | ||
|
|
cd74b76483 | ||
|
|
2fe0b888bd | ||
|
|
af14966b09 | ||
|
|
5fa0d47c0d | ||
|
|
659ad29875 | ||
|
|
a0a81240ce | ||
|
|
89f08f0da7 | ||
|
|
85c1a48d3a | ||
|
|
846c1a104e | ||
|
|
4dda54c9e6 | ||
|
|
1ab34ed46f | ||
|
|
e7aaa95ec5 | ||
|
|
1042d12df6 | ||
|
|
751594860a | ||
|
|
84675b5c0f | ||
|
|
e7be27413c | ||
|
|
654194b274 | ||
|
|
36069cbe6d | ||
|
|
34d5edd6b9 | ||
|
|
57a7c04a4c | ||
|
|
87279688e6 | ||
|
|
783b352e3b | ||
|
|
f683ab64ab | ||
|
|
942651dc16 | ||
|
|
2e86f8e6d8 | ||
|
|
c66694aa32 | ||
|
|
f2a9ddd1a6 | ||
|
|
6aefe4d5d9 | ||
|
|
00f60a6e78 | ||
|
|
34858a1ba0 | ||
|
|
4ae3d5344c | ||
|
|
276684f076 | ||
|
|
2baeb6a572 | ||
|
|
adb067a57f | ||
|
|
0995c8b839 | ||
|
|
0aa00ab226 | ||
|
|
c5d96f96e1 | ||
|
|
4d94d12e9c | ||
|
|
d82594bf09 | ||
|
|
2f275ca81e | ||
|
|
59f4eaf3ea | ||
|
|
8a9cb2527e | ||
|
|
e53d6d216d | ||
|
|
ec78a92234 | ||
|
|
f948d05b90 | ||
|
|
48430fd9c3 | ||
|
|
843d7b2231 | ||
|
|
51b8806184 | ||
|
|
68b2d79700 | ||
|
|
17e8532e6f | ||
|
|
be81415a75 | ||
|
|
b6c806a789 | ||
|
|
32871a8a3c | ||
|
|
c6630a9f20 | ||
|
|
2cbee10527 | ||
|
|
aff8a3b401 | ||
|
|
a9f6c4eb20 | ||
|
|
28d4373f67 | ||
|
|
452bb0b0d7 | ||
|
|
eabdd3de00 | ||
|
|
fcfb7a0105 | ||
|
|
5d5c623f09 | ||
|
|
cebc0c5405 | ||
|
|
52d5e2f36d | ||
|
|
ef1863f810 | ||
|
|
cd749ac6a4 | ||
|
|
3f9d73d784 | ||
|
|
58cfba7695 | ||
|
|
d1cb7a5ce4 | ||
|
|
863bb3f474 | ||
|
|
a4f44348ef | ||
|
|
51f9afb471 | ||
|
|
f8bdc7044c | ||
|
|
796a4a693a | ||
|
|
1c1ba1b55e | ||
|
|
3af3a88f66 | ||
|
|
25eeabb9f9 | ||
|
|
fb9de4c4ad | ||
|
|
497879fb4b | ||
|
|
6e9b5cc113 | ||
|
|
edc1ad952d | ||
|
|
4188bbc5bd | ||
|
|
10591452e4 | ||
|
|
c269bd04d3 | ||
|
|
acdb324f7d | ||
|
|
d3842ec3c3 | ||
|
|
e1cac9f92f | ||
|
|
4533cc592f | ||
|
|
23614fe0d0 | ||
|
|
d723403b6b | ||
|
|
f3b21e6bd9 | ||
|
|
6a2638c70c | ||
|
|
b162dcbfbe | ||
|
|
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 |
49
.env.example
Normal file
49
.env.example
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# set timezone to get correct log timestamp
|
||||||
|
TZ=ETC/UTC
|
||||||
|
|
||||||
|
# API/WebUI user password login credentials (optional)
|
||||||
|
# These fields are not required for OIDC authentication
|
||||||
|
GODOXY_API_USER=admin
|
||||||
|
GODOXY_API_PASSWORD=password
|
||||||
|
# generate secret with `openssl rand -base64 32`
|
||||||
|
GODOXY_API_JWT_SECRET=
|
||||||
|
# the JWT token time-to-live
|
||||||
|
GODOXY_API_JWT_TOKEN_TTL=1h
|
||||||
|
|
||||||
|
# OIDC Configuration (optional)
|
||||||
|
# Uncomment and configure these values to enable OIDC authentication.
|
||||||
|
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
|
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||||
|
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
||||||
|
# GODOXY_OIDC_REDIRECT_URL=https://your-domain/api/auth/callback
|
||||||
|
# Comma-separated list of scopes
|
||||||
|
# GODOXY_OIDC_SCOPES=openid, profile, email
|
||||||
|
#
|
||||||
|
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||||
|
# These two fields act as a logical AND operator. For example, given the following membership:
|
||||||
|
# user1, group1
|
||||||
|
# user2, group1
|
||||||
|
# user3, group2
|
||||||
|
# user1, group2
|
||||||
|
# You can allow access to user3 AND all users of group1 by providing:
|
||||||
|
# # GODOXY_OIDC_ALLOWED_USERS=user3
|
||||||
|
# # GODOXY_OIDC_ALLOWED_GROUPS=group1
|
||||||
|
#
|
||||||
|
# Comma-separated list of allowed users.
|
||||||
|
# GODOXY_OIDC_ALLOWED_USERS=user1,user2
|
||||||
|
# Optional: Comma-separated list of allowed groups.
|
||||||
|
# GODOXY_OIDC_ALLOWED_GROUPS=group1,group2
|
||||||
|
|
||||||
|
# Proxy listening address
|
||||||
|
GODOXY_HTTP_ADDR=:80
|
||||||
|
GODOXY_HTTPS_ADDR=:443
|
||||||
|
|
||||||
|
# API listening address
|
||||||
|
GODOXY_API_ADDR=127.0.0.1:8888
|
||||||
|
|
||||||
|
# Prometheus Metrics
|
||||||
|
GODOXY_PROMETHEUS_ENABLED=true
|
||||||
|
|
||||||
|
# Debug mode
|
||||||
|
GODOXY_DEBUG=false
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: yusing # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: yusingwysq # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,9 +4,11 @@ compose.yml
|
|||||||
config
|
config
|
||||||
certs
|
certs
|
||||||
config*/
|
config*/
|
||||||
|
!schemas/**
|
||||||
certs*/
|
certs*/
|
||||||
bin/
|
bin/
|
||||||
error_pages/
|
error_pages/
|
||||||
|
!examples/error_pages/
|
||||||
|
|
||||||
logs/
|
logs/
|
||||||
log/
|
log/
|
||||||
@@ -25,3 +27,6 @@ todo.md
|
|||||||
mtrace.json
|
mtrace.json
|
||||||
.env
|
.env
|
||||||
test.Dockerfile
|
test.Dockerfile
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
@@ -9,9 +9,6 @@ linters-settings:
|
|||||||
- fieldalignment
|
- fieldalignment
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 14
|
min-complexity: 14
|
||||||
goconst:
|
|
||||||
min-len: 3
|
|
||||||
min-occurrences: 4
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
funlen:
|
funlen:
|
||||||
@@ -102,12 +99,14 @@ linters:
|
|||||||
- depguard # Not relevant
|
- depguard # Not relevant
|
||||||
- nakedret # Too strict
|
- nakedret # Too strict
|
||||||
- lll # Not relevant
|
- lll # Not relevant
|
||||||
- gocyclo # FIXME must be fixed
|
- gocyclo # must be fixed
|
||||||
- gocognit # Too strict
|
- gocognit # Too strict
|
||||||
- nestif # Too many false-positive.
|
- nestif # Too many false-positive.
|
||||||
- prealloc # Too many false-positive.
|
- prealloc # Too many false-positive.
|
||||||
- makezero # Not relevant
|
- makezero # Not relevant
|
||||||
- dupl # Too strict
|
- dupl # Too strict
|
||||||
|
- gci # I don't care
|
||||||
|
- goconst # Too annoying
|
||||||
- gosec # Too strict
|
- gosec # Too strict
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
|
|||||||
@@ -2,36 +2,37 @@
|
|||||||
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||||
version: 0.1
|
version: 0.1
|
||||||
cli:
|
cli:
|
||||||
version: 1.22.6
|
version: 1.22.9
|
||||||
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.6.3
|
ref: v1.6.7
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- node@18.12.1
|
- node@18.20.5
|
||||||
- python@3.10.8
|
- python@3.10.8
|
||||||
- go@1.23.2
|
- go@1.23.2
|
||||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
lint:
|
lint:
|
||||||
|
disabled:
|
||||||
|
- markdownlint
|
||||||
|
- yamllint
|
||||||
enabled:
|
enabled:
|
||||||
- hadolint@2.12.0
|
- hadolint@2.12.1-beta
|
||||||
- actionlint@1.7.3
|
- actionlint@1.7.7
|
||||||
- checkov@3.2.257
|
- checkov@3.2.360
|
||||||
- git-diff-check
|
- git-diff-check
|
||||||
- gofmt@1.20.4
|
- gofmt@1.20.4
|
||||||
- golangci-lint@1.61.0
|
- golangci-lint@1.63.4
|
||||||
- markdownlint@0.42.0
|
- osv-scanner@1.9.2
|
||||||
- osv-scanner@1.9.0
|
- oxipng@9.1.3
|
||||||
- oxipng@9.1.2
|
- prettier@3.4.2
|
||||||
- prettier@3.3.3
|
|
||||||
- shellcheck@0.10.0
|
- shellcheck@0.10.0
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
- trufflehog@3.82.7
|
- trufflehog@3.88.4
|
||||||
- yamllint@1.35.1
|
|
||||||
actions:
|
actions:
|
||||||
disabled:
|
disabled:
|
||||||
- trunk-announce
|
- trunk-announce
|
||||||
|
|||||||
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/config.schema.json": [
|
||||||
"config.example.yml",
|
"config.example.yml",
|
||||||
"config.yml"
|
"config.yml"
|
||||||
],
|
],
|
||||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
"https://github.com/yusing/go-proxy/raw/v0.9/schemas/routes.schema.json": [
|
||||||
"providers.example.yml"
|
"providers.example.yml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
30
Dockerfile
30
Dockerfile
@@ -1,6 +1,10 @@
|
|||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
FROM golang:1.23.2-alpine AS builder
|
FROM golang:1.23.5-alpine AS builder
|
||||||
RUN apk add --no-cache tzdata make
|
HEALTHCHECK NONE
|
||||||
|
|
||||||
|
# package version does not matter
|
||||||
|
# trunk-ignore(hadolint/DL3018)
|
||||||
|
RUN apk add --no-cache tzdata make libcap-setcap
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -13,20 +17,22 @@ RUN --mount=type=cache,target="/go/pkg/mod" \
|
|||||||
|
|
||||||
ENV GOCACHE=/root/.cache/go-build
|
ENV GOCACHE=/root/.cache/go-build
|
||||||
|
|
||||||
|
COPY Makefile /src/
|
||||||
|
COPY cmd /src/cmd
|
||||||
|
COPY internal /src/internal
|
||||||
|
COPY pkg /src/pkg
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ENV VERSION=${VERSION}
|
ENV VERSION=${VERSION}
|
||||||
|
|
||||||
COPY scripts /src/scripts
|
ARG BUILD_FLAGS
|
||||||
COPY Makefile /src/
|
ENV BUILD_FLAGS=${BUILD_FLAGS}
|
||||||
|
|
||||||
RUN --mount=type=cache,target="/go/pkg/mod" \
|
RUN --mount=type=cache,target="/go/pkg/mod" \
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
--mount=type=cache,target="/root/.cache/go-build" \
|
||||||
--mount=type=bind,src=cmd,dst=/src/cmd \
|
|
||||||
--mount=type=bind,src=internal,dst=/src/internal \
|
|
||||||
--mount=type=bind,src=pkg,dst=/src/pkg \
|
|
||||||
make build && \
|
make build && \
|
||||||
mkdir -p /app/error_pages /app/certs && \
|
mkdir -p /app/error_pages /app/certs && \
|
||||||
mv bin/go-proxy /app/go-proxy
|
mv bin/godoxy /app/godoxy
|
||||||
|
|
||||||
# Stage 2: Final image
|
# Stage 2: Final image
|
||||||
FROM scratch
|
FROM scratch
|
||||||
@@ -40,14 +46,14 @@ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
|||||||
# copy binary
|
# copy binary
|
||||||
COPY --from=builder /app /app
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
# copy schema directory
|
# copy example config
|
||||||
COPY schema/ /app/schema/
|
COPY config.example.yml /app/config/config.yml
|
||||||
|
|
||||||
# copy certs
|
# copy certs
|
||||||
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
|
||||||
|
|
||||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
ENV GOPROXY_DEBUG=0
|
ENV GODOXY_DEBUG=0
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
@@ -55,4 +61,4 @@ EXPOSE 443
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
CMD ["/app/go-proxy"]
|
CMD ["/app/godoxy"]
|
||||||
146
Makefile
146
Makefile
@@ -1,63 +1,66 @@
|
|||||||
VERSION ?= $(shell git describe --tags --abbrev=0)
|
export VERSION ?= $(shell git describe --tags --abbrev=0)
|
||||||
BUILD_FLAGS ?= -s -w -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
|
||||||
export VERSION
|
|
||||||
export BUILD_FLAGS
|
|
||||||
export CGO_ENABLED = 0
|
|
||||||
export GOOS = linux
|
export GOOS = linux
|
||||||
|
|
||||||
.PHONY: all setup build test up restart logs get debug run archive repush rapid-crash debug-list-containers
|
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||||
|
|
||||||
all: debug
|
ifeq ($(trace), 1)
|
||||||
|
debug = 1
|
||||||
|
GODOXY_TRACE ?= 1
|
||||||
|
endif
|
||||||
|
|
||||||
build:
|
ifeq ($(debug), 1)
|
||||||
scripts/build.sh
|
CGO_ENABLED = 0
|
||||||
|
GODOXY_DEBUG = 1
|
||||||
|
BUILD_FLAGS = -tags production
|
||||||
|
else ifeq ($(pprof), 1)
|
||||||
|
CGO_ENABLED = 1
|
||||||
|
GODEBUG = gctrace=1 inittrace=1 schedtrace=3000
|
||||||
|
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/
|
||||||
|
BUILD_FLAGS = -race -gcflags=all='-N -l' -tags pprof
|
||||||
|
DOCKER_TAG = pprof
|
||||||
|
VERSION += -pprof
|
||||||
|
else
|
||||||
|
CGO_ENABLED = 0
|
||||||
|
LDFLAGS += -s -w
|
||||||
|
BUILD_FLAGS = -pgo=auto -tags production
|
||||||
|
DOCKER_TAG = latest
|
||||||
|
endif
|
||||||
|
|
||||||
|
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
|
||||||
|
|
||||||
|
export CGO_ENABLED
|
||||||
|
export GODOXY_DEBUG
|
||||||
|
export GODOXY_TRACE
|
||||||
|
export GODEBUG
|
||||||
|
export GORACE
|
||||||
|
export BUILD_FLAGS
|
||||||
|
export DOCKER_TAG
|
||||||
|
|
||||||
test:
|
test:
|
||||||
GOPROXY_TEST=1 go test ./internal/...
|
GODOXY_TEST=1 go test ./internal/...
|
||||||
|
|
||||||
up:
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
restart:
|
|
||||||
docker compose restart -t 0
|
|
||||||
|
|
||||||
logs:
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
get:
|
get:
|
||||||
go get -u ./cmd && go mod tidy
|
go get -u ./cmd && go mod tidy
|
||||||
|
|
||||||
debug:
|
build:
|
||||||
make build
|
mkdir -p bin
|
||||||
sudo GOPROXY_DEBUG=1 bin/go-proxy
|
go build ${BUILD_FLAGS} -o bin/godoxy ./cmd
|
||||||
|
if [ $(shell id -u) -eq 0 ]; \
|
||||||
debug-trace:
|
then setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
|
||||||
make build
|
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
|
||||||
sudo GOPROXY_DEBUG=1 GOPROXY_TRACE=1 bin/go-proxy
|
fi
|
||||||
|
|
||||||
profile:
|
|
||||||
GODEBUG=gctrace=1 make build
|
|
||||||
sudo GOPROXY_DEBUG=1 bin/go-proxy
|
|
||||||
|
|
||||||
mtrace:
|
|
||||||
bin/go-proxy debug-ls-mtrace > mtrace.json
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
make build && sudo bin/go-proxy
|
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
|
||||||
|
|
||||||
archive:
|
mtrace:
|
||||||
git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip
|
bin/godoxy debug-ls-mtrace > mtrace.json
|
||||||
|
|
||||||
repush:
|
|
||||||
git reset --soft HEAD^
|
|
||||||
git add -A
|
|
||||||
git commit -m "repush"
|
|
||||||
git push gitlab dev --force
|
|
||||||
|
|
||||||
rapid-crash:
|
rapid-crash:
|
||||||
sudo docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
docker run --restart=always --name test_crash -p 80 debian:bookworm-slim /bin/cat &&\
|
||||||
sleep 3 &&\
|
sleep 3 &&\
|
||||||
sudo docker rm -f test_crash
|
docker rm -f test_crash
|
||||||
|
|
||||||
debug-list-containers:
|
debug-list-containers:
|
||||||
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
bash -c 'echo -e "GET /containers/json HTTP/1.0\r\n" | sudo netcat -U /var/run/docker.sock | tail -n +9 | jq'
|
||||||
@@ -67,4 +70,57 @@ ci-test:
|
|||||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||||
|
|
||||||
cloc:
|
cloc:
|
||||||
cloc --not-match-f '_test.go$$' cmd internal pkg
|
cloc --not-match-f '_test.go$$' cmd internal pkg
|
||||||
|
|
||||||
|
push-docker-io:
|
||||||
|
BUILDER=build docker buildx build \
|
||||||
|
--platform linux/arm64,linux/amd64 \
|
||||||
|
-f Dockerfile \
|
||||||
|
-t docker.io/yusing/godoxy-nightly:${DOCKER_TAG} \
|
||||||
|
-t docker.io/yusing/godoxy-nightly:${VERSION}-${BUILD_DATE} \
|
||||||
|
--build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" \
|
||||||
|
--build-arg BUILD_FLAGS="${BUILD_FLAGS}" \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
docker build -t godoxy-nightly \
|
||||||
|
--build-arg VERSION="${VERSION}-nightly-${BUILD_DATE}" \
|
||||||
|
--build-arg BUILD_FLAGS="${BUILD_FLAGS}" \
|
||||||
|
.
|
||||||
|
|
||||||
|
# To generate schema
|
||||||
|
# comment out this part from typescript-json-schema.js#L884
|
||||||
|
#
|
||||||
|
# if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
|
||||||
|
# throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
|
||||||
|
# }
|
||||||
|
|
||||||
|
gen-schema-single:
|
||||||
|
bun --bun run typescript-json-schema --noExtraProps --required --skipLibCheck --tsNodeRegister=true -o schemas/${OUT} schemas/${IN} ${CLASS}
|
||||||
|
# minify
|
||||||
|
python3 -c "import json; f=open('schemas/${OUT}', 'r'); j=json.load(f); f.close(); f=open('schemas/${OUT}', 'w'); json.dump(j, f, separators=(',', ':'));"
|
||||||
|
|
||||||
|
gen-schema:
|
||||||
|
bun --bun tsc
|
||||||
|
make IN=config/config.ts \
|
||||||
|
CLASS=Config \
|
||||||
|
OUT=config.schema.json \
|
||||||
|
gen-schema-single
|
||||||
|
make IN=providers/routes.ts \
|
||||||
|
CLASS=Routes \
|
||||||
|
OUT=routes.schema.json \
|
||||||
|
gen-schema-single
|
||||||
|
make IN=middlewares/middleware_compose.ts \
|
||||||
|
CLASS=MiddlewareCompose \
|
||||||
|
OUT=middleware_compose.schema.json \
|
||||||
|
gen-schema-single
|
||||||
|
make IN=docker.ts \
|
||||||
|
CLASS=DockerRoutes \
|
||||||
|
OUT=docker_routes.schema.json \
|
||||||
|
gen-schema-single
|
||||||
|
|
||||||
|
update-schema-generator:
|
||||||
|
pnpm up -g typescript-json-schema
|
||||||
|
|
||||||
|
push-github:
|
||||||
|
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
131
README.md
131
README.md
@@ -1,93 +1,121 @@
|
|||||||
# go-proxy
|
<div align="center">
|
||||||
|
|
||||||
|
# GoDoxy
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://discord.gg/umReR62nRd)
|
||||||
|
|
||||||
|
A lightweight, simple, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||||
|
|
||||||
|
For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||||
|
|
||||||
|
**EN** | <a href="README_CHT.md">中文</a>
|
||||||
|
|
||||||
|
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||||
[](https://discord.gg/umReR62nRd)
|
|
||||||
|
|
||||||
[繁體中文文檔請看此](README_CHT.md)
|
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||||
|
|
||||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
</div>
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
|
||||||
|
|
||||||
## Table of content
|
## Table of content
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [go-proxy](#go-proxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [Table of content](#table-of-content)
|
- [Table of content](#table-of-content)
|
||||||
- [Key Features](#key-features)
|
- [Key Features](#key-features)
|
||||||
- [Getting Started](#getting-started)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
- [Manual Setup](#manual-setup)
|
||||||
|
- [Folder structrue](#folder-structrue)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [idlesleeper](#idlesleeper)
|
||||||
- [Build it yourself](#build-it-yourself)
|
- [Build it yourself](#build-it-yourself)
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- Easy to use
|
- Easy to use
|
||||||
- Effortless configuration
|
- Effortless configuration
|
||||||
- Simple multi-node setup
|
- Simple multi-node setup
|
||||||
- Error messages is clear and detailed, easy troubleshooting
|
- Error messages is clear and detailed, easy troubleshooting
|
||||||
- Auto SSL cert management (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
- Auto SSL cert management (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||||
- Auto configuration for docker containers
|
- Auto configuration for docker containers
|
||||||
- Auto hot-reload on container state / config file changes
|
- Auto hot-reload on container state / config file changes
|
||||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||||
- HTTP(s) reserve proxy
|
- HTTP(s) reserve proxy
|
||||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
- OpenID Connect support
|
||||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||||
- TCP and UDP port forwarding
|
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||||
- **Web UI with App dashboard**
|
- TCP and UDP port forwarding
|
||||||
- Supports linux/amd64, linux/arm64
|
- **Web UI with App dashboard and config editor**
|
||||||
- Written in **[Go](https://go.dev)**
|
- Supports linux/amd64, linux/arm64
|
||||||
|
- Written in **[Go](https://go.dev)**
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## Getting Started
|
## Prerequisites
|
||||||
|
|
||||||
### Setup
|
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 the latest docker images
|
||||||
|
|
||||||
1. Pull docker image
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull ghcr.io/yusing/go-proxy:latest
|
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
|
```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 `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`
|
||||||
|
|
||||||
- A Record: `*.y.z` -> `10.0.10.1`
|
4. Start the container `docker compose up -d`
|
||||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
|
||||||
|
|
||||||
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`
|
5. You may now do some extra configuration on WebUI `https://godoxy.domain.com`
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
6. You may now do some extra configuration
|
|
||||||
- With text editor (e.g. Visual Studio Code)
|
|
||||||
- With Web UI via `http://localhost:3000` or `https://gp.y.z`
|
|
||||||
- For more info, [See Wiki]([wiki](https://github.com/yusing/go-proxy/wiki))
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
### Use JSON Schema in VSCode
|
### Manual Setup
|
||||||
|
|
||||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||||
|
|
||||||
|
2. Grab `.env.example` into `.env`
|
||||||
|
|
||||||
|
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||||
|
|
||||||
|
3. Grab `compose.example.yml` into `compose.yml`
|
||||||
|
|
||||||
|
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/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
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -95,7 +123,6 @@ Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscod
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
[🔼Back to top](#table-of-content)
|
[🔼Back to top](#table-of-content)
|
||||||
|
|
||||||
## Build it yourself
|
## Build it yourself
|
||||||
|
|||||||
178
README_CHT.md
178
README_CHT.md
@@ -1,130 +1,140 @@
|
|||||||
# go-proxy
|
<div align="center">
|
||||||
|
|
||||||
|
# GoDoxy
|
||||||
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
|

|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
[](https://discord.gg/umReR62nRd)
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
|
||||||
[](https://discord.gg/umReR62nRd)
|
|
||||||
|
|
||||||
一個輕量化、易用且[高效]([docs/benchmark_result.md](https://github.com/yusing/go-proxy/wiki/Benchmarks)))的反向代理和端口轉發工具
|
輕量、易用、 [高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
||||||
|
|
||||||
|
完整文檔請查閱 **[Wiki](https://github.com/yusing/go-proxy/wiki)**(暫未有中文翻譯)
|
||||||
|
|
||||||
|
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||||
|
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||||
|
|
||||||
|
<a href="README.md">EN</a> | **中文**
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## 目錄
|
## 目錄
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [go-proxy](#go-proxy)
|
- [GoDoxy](#godoxy)
|
||||||
- [目錄](#目錄)
|
- [目錄](#目錄)
|
||||||
- [重點](#重點)
|
- [主要特點](#主要特點)
|
||||||
- [入門指南](#入門指南)
|
- [前置需求](#前置需求)
|
||||||
- [安裝](#安裝)
|
- [安裝](#安裝)
|
||||||
- [命令行參數](#命令行參數)
|
- [手動安裝](#手動安裝)
|
||||||
- [環境變量](#環境變量)
|
- [資料夾結構](#資料夾結構)
|
||||||
- [VSCode 中使用 JSON Schema](#vscode-中使用-json-schema)
|
- [截圖](#截圖)
|
||||||
- [展示](#展示)
|
- [閒置休眠](#閒置休眠)
|
||||||
- [idlesleeper](#idlesleeper)
|
- [自行編譯](#自行編譯)
|
||||||
- [源碼編譯](#源碼編譯)
|
|
||||||
|
|
||||||
## 重點
|
## 主要特點
|
||||||
|
|
||||||
- 易用
|
- 容易使用
|
||||||
- 不需花費太多時間就能輕鬆配置
|
- 輕鬆配置
|
||||||
- 支持多個docker節點
|
- 簡單的多節點設置
|
||||||
- 除錯簡單
|
- 錯誤訊息清晰詳細,易於排除故障
|
||||||
- 自動配置 SSL 證書(參見[可用的 DNS 供應商](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
- 自動 SSL 憑證管理(參見 [支援的 DNS-01 驗證提供商](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||||
- 透過 Docker 容器自動配置
|
- 自動配置 Docker 容器
|
||||||
- 容器狀態變更時自動熱重載
|
- 容器狀態/配置文件變更時自動熱重載
|
||||||
- **idlesleeper** 容器閒置時自動暫停/停止,入站時自動喚醒 (可選, 參見 [展示](#idlesleeper))
|
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||||
- HTTP(s) 反向代理
|
- HTTP(s) 反向代理
|
||||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
- OpenID Connect 支持
|
||||||
- [自訂 error pages](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
- [HTTP 中介軟體支援](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||||
- TCP/UDP 端口轉發
|
- [自訂錯誤頁面支援](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||||
- Web 面板 (內置App dashboard)
|
- TCP 和 UDP 埠轉發
|
||||||
- 支持 linux/amd64、linux/arm64 平台
|
- **網頁介面,具有應用儀表板和配置編輯器**
|
||||||
- 使用 **[Go](https://go.dev)** 編寫
|
- 支援 linux/amd64、linux/arm64
|
||||||
|
- 使用 **[Go](https://go.dev)** 編寫
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
## 入門指南
|
## 前置需求
|
||||||
|
|
||||||
### 安裝
|
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||||
|
|
||||||
1. 抓取Docker鏡像
|
- A 記錄:`*.y.z` -> `10.0.10.1`
|
||||||
|
- AAAA 記錄:`*.y.z` -> `::ffff:a00:a01`
|
||||||
|
|
||||||
|
## 安裝
|
||||||
|
|
||||||
|
1. 拉取最新的 Docker 映像
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull ghcr.io/yusing/go-proxy:latest
|
docker pull ghcr.io/yusing/go-proxy:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 建立新的目錄,並切換到該目錄,並執行
|
2. 建立新目錄,`cd` 進入後運行安裝,或[手動安裝](#手動安裝)
|
||||||
|
|
||||||
```shell
|
```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. 設置 DNS 記錄,例如:
|
3. _(可選)_ 設置其他 Docker 節點的 `docker-socket-proxy`(參見 [多 Docker 節點設置](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)),然後在 `config.yml` 中添加它們
|
||||||
|
|
||||||
- A 記錄: `*.y.z` -> `10.0.10.1`
|
4. 啟動容器 `docker compose up -d`
|
||||||
- AAAA 記錄: `*.y.z` -> `::ffff:a00:a01`
|
|
||||||
|
|
||||||
4. 配置 `docker-socket-proxy` 其他 Docker 節點(如有) (參見 [範例](docs/docker_socket_proxy.md)) 然後加到 `config.yml` 中
|
5. 大功告成!可前往WebUI `https://gp.domain.com` 進行額外的配置
|
||||||
|
|
||||||
5. 大功告成,你可以做一些額外的配置
|
[🔼回到頂部](#目錄)
|
||||||
- 使用文本編輯器 (推薦 Visual Studio Code [參見 VSCode 使用 schema](#vscode-中使用-json-schema))
|
|
||||||
- 或通過 `http://localhost:3000` 使用網頁配置編輯器
|
|
||||||
- 詳情請參閱 [docker.md](docs/docker.md)
|
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
### 手動安裝
|
||||||
|
|
||||||
### 命令行參數
|
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||||
|
|
||||||
| 參數 | 描述 | 示例 |
|
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/config.example.yml -O config/config.yml`
|
||||||
| ------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
||||||
| 空 | 啟動代理服務器 | |
|
|
||||||
| `validate` | 驗證配置並退出 | |
|
|
||||||
| `reload` | 強制刷新配置 | |
|
|
||||||
| `ls-config` | 列出配置並退出 | `go-proxy ls-config \| jq` |
|
|
||||||
| `ls-route` | 列出路由並退出 | `go-proxy ls-route \| jq` |
|
|
||||||
| `go-proxy ls-route \| jq` |
|
|
||||||
| `ls-icons` | 列出 [dashboard-icons](https://github.com/walkxcode/dashboard-icons/tree/main) 並退出 | `go-proxy ls-icons \| grep adguard` |
|
|
||||||
| `debug-ls-mtrace` | 列出middleware追蹤 **(僅限於 debug 模式)** | `go-proxy debug-ls-mtrace \| jq` |
|
|
||||||
|
|
||||||
**使用 `docker exec go-proxy /app/go-proxy <參數>` 運行**
|
2. 將 `.env.example` 下載到 `.env`
|
||||||
|
|
||||||
### 環境變量
|
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/.env.example -O .env`
|
||||||
|
|
||||||
| 環境變量 | 描述 | 默認 | 格式 |
|
3. 將 `compose.example.yml` 下載到 `compose.yml`
|
||||||
| ------------------------------ | ---------------- | ---------------- | ------------- |
|
|
||||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | 禁用 schema 驗證 | `false` | boolean |
|
|
||||||
| `GOPROXY_DEBUG` | 啟用調試輸出 | `false` | boolean |
|
|
||||||
| `GOPROXY_HTTP_ADDR` | http 收聽地址 | `:80` | `[host]:port` |
|
|
||||||
| `GOPROXY_HTTPS_ADDR` | https 收聽地址 | `:443` | `[host]:port` |
|
|
||||||
| `GOPROXY_API_ADDR` | api 收聽地址 | `127.0.0.1:8888` | `[host]:port` |
|
|
||||||
|
|
||||||
### VSCode 中使用 JSON Schema
|
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.9/compose.example.yml -O compose.yml`
|
||||||
|
|
||||||
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需求修改
|
### 資料夾結構
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
```shell
|
||||||
|
├── certs
|
||||||
|
│ ├── cert.crt
|
||||||
|
│ └── priv.key
|
||||||
|
├── compose.yml
|
||||||
|
├── config
|
||||||
|
│ ├── config.yml
|
||||||
|
│ ├── middlewares
|
||||||
|
│ │ ├── middleware1.yml
|
||||||
|
│ │ ├── middleware2.yml
|
||||||
|
│ ├── provider1.yml
|
||||||
|
│ └── provider2.yml
|
||||||
|
└── .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 截圖
|
||||||
|
|
||||||
## 展示
|
### 閒置休眠
|
||||||
|
|
||||||
### idlesleeper
|

|
||||||
|
|
||||||

|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
## 自行編譯
|
||||||
|
|
||||||
## 源碼編譯
|
1. 克隆儲存庫 `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||||
|
|
||||||
1. 獲取源碼 `git clone https://github.com/yusing/go-proxy --depth=1`
|
2. 如果尚未安裝,請安裝/升級 [go (>=1.22)](https://go.dev/doc/install) 和 `make`
|
||||||
|
|
||||||
2. 安裝/升級 [go 版本 (>=1.22)](https://go.dev/doc/install) 和 `make`(如果尚未安裝)
|
3. 如果之前編譯過(go < 1.22),請使用 `go clean -cache` 清除快取
|
||||||
|
|
||||||
3. 如果之前編譯過(go 版本 < 1.22),請使用 `go clean -cache` 清除緩存
|
4. 使用 `make get` 獲取依賴
|
||||||
|
|
||||||
4. 使用 `make get` 獲取依賴項
|
5. 使用 `make build` 編譯二進制檔案
|
||||||
|
|
||||||
5. 使用 `make build` 編譯
|
[🔼回到頂部](#目錄)
|
||||||
|
|
||||||
[🔼 返回頂部](#目錄)
|
|
||||||
|
|||||||
120
bun.lock
Normal file
120
bun.lock
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "godoxy-types",
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-json-schema": "^0.65.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||||
|
|
||||||
|
"@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="],
|
||||||
|
|
||||||
|
"@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="],
|
||||||
|
|
||||||
|
"@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="],
|
||||||
|
|
||||||
|
"@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="],
|
||||||
|
|
||||||
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@18.19.74", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
|
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
|
||||||
|
|
||||||
|
"diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
|
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"path-equal": ["path-equal@1.2.5", "", {}, "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g=="],
|
||||||
|
|
||||||
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||||
|
|
||||||
|
"typescript-json-schema": ["typescript-json-schema@0.65.1", "", { "dependencies": { "@types/json-schema": "^7.0.9", "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
|
|
||||||
|
"v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
|
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||||
|
|
||||||
|
"typescript-json-schema/typescript": ["typescript@5.5.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
148
cmd/main.go
148
cmd/main.go
@@ -2,45 +2,82 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/yusing/go-proxy/internal"
|
"github.com/yusing/go-proxy/internal"
|
||||||
"github.com/yusing/go-proxy/internal/api"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
|
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||||
|
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
"github.com/yusing/go-proxy/internal/api/v1/query"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
"github.com/yusing/go-proxy/internal/config"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
R "github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||||
"github.com/yusing/go-proxy/internal/server"
|
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/pkg"
|
"github.com/yusing/go-proxy/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var rawLogger = log.New(os.Stdout, "", 0)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var out io.Writer = os.Stderr
|
||||||
|
if common.EnableLogStreaming {
|
||||||
|
out = zerolog.MultiLevelWriter(out, v1.GetMemLogger())
|
||||||
|
}
|
||||||
|
logging.InitLogger(out)
|
||||||
|
// logging.AddHook(v1.GetMemLogger())
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
initProfiling()
|
||||||
args := common.GetArgs()
|
args := common.GetArgs()
|
||||||
|
|
||||||
if args.Command == common.CommandSetup {
|
switch args.Command {
|
||||||
|
case common.CommandSetup:
|
||||||
internal.Setup()
|
internal.Setup()
|
||||||
return
|
return
|
||||||
}
|
case common.CommandReload:
|
||||||
|
|
||||||
if args.Command == common.CommandReload {
|
|
||||||
if err := query.ReloadServer(); err != nil {
|
if err := query.ReloadServer(); err != nil {
|
||||||
E.LogFatal("server reload error", err)
|
E.LogFatal("server reload error", err)
|
||||||
}
|
}
|
||||||
logging.Info().Msg("ok")
|
rawLogger.Println("ok")
|
||||||
|
return
|
||||||
|
case common.CommandListIcons:
|
||||||
|
icons, err := internal.ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
rawLogger.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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.Command == common.CommandStart {
|
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.Trace().Msg("trace enabled")
|
||||||
// logging.AddHook(notif.GetDispatcher())
|
// logging.AddHook(notif.GetDispatcher())
|
||||||
} else {
|
} else {
|
||||||
@@ -72,49 +109,36 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch args.Command {
|
switch args.Command {
|
||||||
case common.CommandListConfigs:
|
|
||||||
printJSON(config.Value())
|
|
||||||
return
|
|
||||||
case common.CommandListRoutes:
|
case common.CommandListRoutes:
|
||||||
routes, err := query.ListRoutes()
|
cfg.StartProxyProviders()
|
||||||
if err != nil {
|
printJSON(routequery.RoutesByAlias())
|
||||||
log.Printf("failed to connect to api server: %s", err)
|
|
||||||
log.Printf("falling back to config file")
|
|
||||||
printJSON(config.RoutesByAlias())
|
|
||||||
} else {
|
|
||||||
printJSON(routes)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
case common.CommandListIcons:
|
case common.CommandListConfigs:
|
||||||
icons, err := internal.ListAvailableIcons()
|
printJSON(cfg.Value())
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
printJSON(icons)
|
|
||||||
return
|
return
|
||||||
case common.CommandDebugListEntries:
|
case common.CommandDebugListEntries:
|
||||||
printJSON(config.DumpEntries())
|
printJSON(cfg.DumpRoutes())
|
||||||
return
|
return
|
||||||
case common.CommandDebugListProviders:
|
case common.CommandDebugListProviders:
|
||||||
printJSON(config.DumpProviders())
|
printJSON(cfg.DumpRouteProviders())
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.StartProxyProviders()
|
go internal.InitIconListCache()
|
||||||
|
go homepage.InitOverridesConfig()
|
||||||
|
go favicon.InitIconCache()
|
||||||
|
|
||||||
|
cfg.Start(&config.StartServersOptions{
|
||||||
|
Proxy: true,
|
||||||
|
})
|
||||||
|
if err := auth.Initialize(); err != nil {
|
||||||
|
logging.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||||
|
}
|
||||||
|
// API Handler needs to start after auth is initialized.
|
||||||
|
cfg.StartServers(&config.StartServersOptions{
|
||||||
|
API: true,
|
||||||
|
})
|
||||||
|
|
||||||
config.WatchChanges()
|
config.WatchChanges()
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
@@ -122,41 +146,12 @@ func main() {
|
|||||||
signal.Notify(sig, syscall.SIGTERM)
|
signal.Notify(sig, syscall.SIGTERM)
|
||||||
signal.Notify(sig, syscall.SIGHUP)
|
signal.Notify(sig, syscall.SIGHUP)
|
||||||
|
|
||||||
autocert := config.GetAutoCertProvider()
|
|
||||||
if autocert != nil {
|
|
||||||
if err := autocert.Setup(); err != nil {
|
|
||||||
E.LogFatal("autocert setup error", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logging.Info().Msg("autocert not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyServer := server.InitProxyServer(server.Options{
|
|
||||||
Name: "proxy",
|
|
||||||
CertProvider: autocert,
|
|
||||||
HTTPAddr: common.ProxyHTTPAddr,
|
|
||||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
|
||||||
Handler: http.HandlerFunc(R.ProxyHandler),
|
|
||||||
RedirectToHTTPS: config.Value().RedirectToHTTPS,
|
|
||||||
})
|
|
||||||
apiServer := server.InitAPIServer(server.Options{
|
|
||||||
Name: "api",
|
|
||||||
CertProvider: autocert,
|
|
||||||
HTTPAddr: common.APIHTTPAddr,
|
|
||||||
Handler: api.NewHandler(),
|
|
||||||
RedirectToHTTPS: config.Value().RedirectToHTTPS,
|
|
||||||
})
|
|
||||||
|
|
||||||
proxyServer.Start()
|
|
||||||
apiServer.Start()
|
|
||||||
|
|
||||||
// wait for signal
|
// wait for signal
|
||||||
<-sig
|
<-sig
|
||||||
|
|
||||||
// grafully shutdown
|
// gracefully shutdown
|
||||||
logging.Info().Msg("shutting down")
|
logging.Info().Msg("shutting down")
|
||||||
task.CancelGlobalContext()
|
_ = task.GracefulShutdown(time.Second * time.Duration(cfg.Value().TimeoutShutdown))
|
||||||
task.GlobalContextWait(time.Second * time.Duration(config.Value().TimeoutShutdown))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDirectory(dir string) {
|
func prepareDirectory(dir string) {
|
||||||
@@ -172,6 +167,5 @@ func printJSON(obj any) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Fatal().Err(err).Send()
|
logging.Fatal().Err(err).Send()
|
||||||
}
|
}
|
||||||
rawLogger := log.New(os.Stdout, "", 0)
|
rawLogger.Print(string(j)) // raw output for convenience using "jq"
|
||||||
rawLogger.Printf("%s", j) // raw output for convenience using "jq"
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
cmd/main_production.go
Normal file
7
cmd/main_production.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build production
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func initProfiling() {
|
||||||
|
// no profiling in production
|
||||||
|
}
|
||||||
20
cmd/main_prof.go
Normal file
20
cmd/main_prof.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build pprof
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initProfiling() {
|
||||||
|
runtime.GOMAXPROCS(2)
|
||||||
|
debug.SetMemoryLimit(100 * 1024 * 1024)
|
||||||
|
debug.SetMaxStack(15 * 1024 * 1024)
|
||||||
|
go func() {
|
||||||
|
log.Println(http.ListenAndServe(":7777", nil))
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -1,45 +1,41 @@
|
|||||||
|
---
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||||
container_name: go-proxy-frontend
|
container_name: godoxy-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
depends_on:
|
env_file: .env
|
||||||
- app
|
depends_on:
|
||||||
# if you also want to proxy the WebUI and access it via gp.y.z
|
- app
|
||||||
# labels:
|
# modify below to fit your needs
|
||||||
# - proxy.aliases=gp
|
labels:
|
||||||
# - proxy.gp.port=3000
|
proxy.aliases: godoxy
|
||||||
|
proxy.godoxy.port: 3000
|
||||||
|
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||||
|
# status: 403
|
||||||
|
# message: IP not allowed
|
||||||
|
# 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: godoxy
|
||||||
|
restart: always
|
||||||
|
network_mode: host
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./config:/app/config
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./error_pages:/app/error_pages
|
||||||
|
|
||||||
# Make sure the value is same as `GOPROXY_API_ADDR` below (if you have changed it)
|
# To use autocert, certs will be stored in "./certs".
|
||||||
#
|
# You can also use a docker volume to store it
|
||||||
# environment:
|
- ./certs:/app/certs
|
||||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
|
||||||
app:
|
|
||||||
image: ghcr.io/yusing/go-proxy:latest
|
|
||||||
container_name: go-proxy
|
|
||||||
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
|
# remove "./certs:/app/certs" and uncomment below to use existing certificate
|
||||||
#
|
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||||
# GOPROXY_HTTP_ADDR: :80
|
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||||
# GOPROXY_HTTPS_ADDR: :443
|
|
||||||
# GOPROXY_API_ADDR: 127.0.0.1:8888
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./config:/app/config
|
|
||||||
|
|
||||||
# (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
|
|
||||||
|
|
||||||
# 2. use autocert, certs will be stored in ./certs (or other path you specify)
|
|
||||||
|
|
||||||
# - ./certs:/app/certs
|
|
||||||
|
|||||||
@@ -1,24 +1,42 @@
|
|||||||
# Autocert (choose one below and uncomment to enable)
|
# Autocert (choose one below and uncomment to enable)
|
||||||
#
|
#
|
||||||
# 1. use existing cert
|
# 1. use existing cert
|
||||||
#
|
|
||||||
# autocert:
|
# autocert:
|
||||||
# provider: local
|
# provider: local
|
||||||
#
|
|
||||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
|
||||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
|
||||||
#
|
|
||||||
# 2. cloudflare
|
# 2. cloudflare
|
||||||
#
|
|
||||||
# autocert:
|
# autocert:
|
||||||
# provider: cloudflare
|
# provider: cloudflare
|
||||||
# email: abc@gmail.com # ACME Email
|
# email: abc@gmail.com # ACME Email
|
||||||
# domains: # a list of domains for cert registration
|
# domains: # a list of domains for cert registration
|
||||||
# - "*.y.z" # remember to use double quotes to surround wildcard domain
|
# - "*.domain.com"
|
||||||
|
# - "domain.com"
|
||||||
# options:
|
# options:
|
||||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||||
#
|
|
||||||
# 3. other providers, check docs/dns_providers.md for more
|
# 3. other providers, see https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||||
|
|
||||||
|
entrypoint:
|
||||||
|
# Below define an example of middleware config
|
||||||
|
# 1. block non local IP connections
|
||||||
|
# 2. redirect HTTP to HTTPS
|
||||||
|
#
|
||||||
|
# middlewares:
|
||||||
|
# - use: CIDRWhitelist
|
||||||
|
# allow:
|
||||||
|
# - "127.0.0.1"
|
||||||
|
# - "10.0.0.0/8"
|
||||||
|
# - "172.16.0.0/12"
|
||||||
|
# - "192.168.0.0/16"
|
||||||
|
# status: 403
|
||||||
|
# message: "Forbidden"
|
||||||
|
# - use: RedirectHTTP
|
||||||
|
|
||||||
|
# below enables access log
|
||||||
|
access_log:
|
||||||
|
format: combined
|
||||||
|
path: /app/logs/entrypoint.log
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
# include files are standalone yaml files under `config/` directory
|
# include files are standalone yaml files under `config/` directory
|
||||||
@@ -30,6 +48,7 @@ providers:
|
|||||||
docker:
|
docker:
|
||||||
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
|
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
|
||||||
local: $DOCKER_HOST
|
local: $DOCKER_HOST
|
||||||
|
|
||||||
# explicit only mode
|
# explicit only mode
|
||||||
# only containers with explicit aliases will be proxied
|
# only containers with explicit aliases will be proxied
|
||||||
# add "!" after provider name to enable explicit only mode
|
# add "!" after provider name to enable explicit only mode
|
||||||
@@ -41,29 +60,32 @@ providers:
|
|||||||
#
|
#
|
||||||
# remote-1: tcp://10.0.2.1:2375
|
# remote-1: tcp://10.0.2.1:2375
|
||||||
# remote-2: ssh://root:1234@10.0.2.2
|
# remote-2: ssh://root:1234@10.0.2.2
|
||||||
# if match_domains not defined
|
|
||||||
# any host = alias+[any domain] will match
|
# notification providers (notify when service health changes)
|
||||||
# i.e. https://app1.y.z will match alias app1 for any domain y.z
|
#
|
||||||
# but https://app1.node1.y.z will only match alias "app.node1"
|
# notification:
|
||||||
#
|
# - name: gotify
|
||||||
# if match_domains defined
|
# provider: gotify
|
||||||
# only host = alias+[one of match_domains] will match
|
# url: https://gotify.domain.tld
|
||||||
# i.e. match_domains = [node1.my.app, my.site]
|
# token: abcd
|
||||||
# https://app1.my.app, https://app1.my.net, etc. will not match even if app1 exists
|
# - name: discord
|
||||||
# only https://*.node1.my.app and https://*.my.site will match
|
# provider: webhook
|
||||||
#
|
# url: https://discord.com/api/webhooks/...
|
||||||
|
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||||
|
|
||||||
|
# Check https://github.com/yusing/go-proxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||||
|
# for explaination of `match_domains`
|
||||||
#
|
#
|
||||||
# match_domains:
|
# match_domains:
|
||||||
# - my.site
|
# - my.site
|
||||||
# - node1.my.app
|
# - 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)
|
# Below are fixed options (non hot-reloadable)
|
||||||
|
|
||||||
# timeout for shutdown (in seconds)
|
# 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
|
|
||||||
|
|||||||
27
examples/docker-compose/n8n.yml
Normal file
27
examples/docker-compose/n8n.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n
|
||||||
|
container_name: n8n
|
||||||
|
restart: always
|
||||||
|
expose:
|
||||||
|
- 5678
|
||||||
|
labels:
|
||||||
|
proxy.n8n.middlewares.request.set_headers: |
|
||||||
|
SSLRedirect: true
|
||||||
|
STSSeconds: 315360000
|
||||||
|
browserXSSFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
SSLHost: ${DOMAIN_NAME}
|
||||||
|
STSIncludeSubdomains: true
|
||||||
|
STSPreload: true
|
||||||
|
environment:
|
||||||
|
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
|
||||||
|
- N8N_PORT=5678
|
||||||
|
- N8N_PROTOCOL=https
|
||||||
|
- NODE_ENV=production
|
||||||
|
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
|
||||||
|
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
|
||||||
|
volumes:
|
||||||
|
- ./data:/home/node/.n8n
|
||||||
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
|
|
||||||
72
go.mod
72
go.mod
@@ -1,64 +1,82 @@
|
|||||||
module github.com/yusing/go-proxy
|
module github.com/yusing/go-proxy
|
||||||
|
|
||||||
go 1.23.2
|
go 1.23.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.1
|
||||||
github.com/coder/websocket v1.8.12
|
github.com/coder/websocket v1.8.12
|
||||||
github.com/docker/cli v27.3.1+incompatible
|
github.com/coreos/go-oidc/v3 v3.12.0
|
||||||
github.com/docker/docker v27.3.1+incompatible
|
github.com/docker/cli v27.5.1+incompatible
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/docker/docker v27.5.1+incompatible
|
||||||
github.com/go-acme/lego/v4 v4.19.2
|
github.com/fsnotify/fsnotify v1.8.0
|
||||||
|
github.com/go-acme/lego/v4 v4.21.0
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0
|
||||||
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/gotify/server/v2 v2.5.0
|
github.com/gotify/server/v2 v2.6.1
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
github.com/lithammer/fuzzysearch v1.1.8
|
||||||
|
github.com/prometheus/client_golang v1.20.5
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
github.com/vincent-petithory/dataurl v1.0.0
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/text v0.19.0
|
golang.org/x/net v0.34.0
|
||||||
|
golang.org/x/oauth2 v0.25.0
|
||||||
|
golang.org/x/text v0.21.0
|
||||||
|
golang.org/x/time v0.9.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cloudflare/cloudflare-go v0.108.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.3 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // 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/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.63 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.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/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/oauth2 v0.23.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/tools v0.29.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
google.golang.org/protobuf v1.36.4 // indirect
|
||||||
golang.org/x/time v0.7.0 // indirect
|
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gotest.tools/v3 v3.5.1 // indirect
|
gotest.tools/v3 v3.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
214
go.sum
214
go.sum
@@ -2,36 +2,47 @@ 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/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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cloudflare/cloudflare-go v0.108.0 h1:C4Skfjd8I8X3uEOGmQUT4/iGyZcWdkIU7HwvMoLkEE0=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cloudflare/cloudflare-go v0.108.0/go.mod h1:m492eNahT/9MsN7Ppnoge8AaI7QhVFtEgVm3I9HJFeU=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
|
||||||
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
||||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
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/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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
|
||||||
|
github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
@@ -39,8 +50,18 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
@@ -53,136 +74,207 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
|
|||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gotify/server/v2 v2.5.0 h1:tJd+a5bb17X52f0EV2KxqLuyjQFKmVK1+t/iNUkP16Y=
|
github.com/gotify/server/v2 v2.6.1 h1:Kf7v5fzBxzELzZa/jonWfwJMkqYqh1LBzBpCmt5QIAI=
|
||||||
github.com/gotify/server/v2 v2.5.0/go.mod h1:DKPMQI/FZ69iKbZvrOL6VWwRaoB9O+HDvJWVd/kiGbc=
|
github.com/gotify/server/v2 v2.6.1/go.mod h1:Dk8HLyTVDqmXM8YEg6tjROBen6mxyHZFRggJFHTwZLc=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||||
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
|
||||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
|
||||||
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
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/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 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
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 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
|
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.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
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/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
|
||||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||||
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
||||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
|
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||||
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
|
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.36.4/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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -1,67 +1,70 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||||
. "github.com/yusing/go-proxy/internal/api/v1/utils"
|
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeMux struct{ *http.ServeMux }
|
type ServeMux struct{ *http.ServeMux }
|
||||||
|
|
||||||
func NewServeMux() ServeMux {
|
func (mux ServeMux) HandleFunc(methods, endpoint string, handler http.HandlerFunc) {
|
||||||
return ServeMux{http.NewServeMux()}
|
for _, m := range strutils.CommaSeperatedList(methods) {
|
||||||
|
mux.ServeMux.HandleFunc(m+" "+endpoint, handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) {
|
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
mux.ServeMux.HandleFunc(fmt.Sprintf("%s %s", method, endpoint), checkHost(handler))
|
mux := ServeMux{http.NewServeMux()}
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler() http.Handler {
|
|
||||||
mux := NewServeMux()
|
|
||||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||||
// mux.HandleFunc("GET", "/v1/checkhealth", v1.CheckHealth)
|
mux.HandleFunc("POST", "/v1/reload", useCfg(cfg, v1.Reload))
|
||||||
// mux.HandleFunc("HEAD", "/v1/checkhealth", v1.CheckHealth)
|
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||||
mux.HandleFunc("POST", "/v1/login", auth.LoginHandler)
|
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||||
mux.HandleFunc("GET", "/v1/logout", auth.LogoutHandler)
|
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(useCfg(cfg, v1.List)))
|
||||||
mux.HandleFunc("POST", "/v1/logout", auth.LogoutHandler)
|
mux.HandleFunc("GET", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.GetFileContent))
|
||||||
mux.HandleFunc("POST", "/v1/reload", v1.Reload)
|
mux.HandleFunc("POST,PUT", "/v1/file/{type}/{filename}", auth.RequireAuth(v1.SetFileContent))
|
||||||
mux.HandleFunc("GET", "/v1/list", auth.RequireAuth(v1.List))
|
mux.HandleFunc("POST", "/v1/file/validate/{type}", auth.RequireAuth(v1.ValidateFile))
|
||||||
mux.HandleFunc("GET", "/v1/list/{what}", auth.RequireAuth(v1.List))
|
mux.HandleFunc("GET", "/v1/stats", useCfg(cfg, v1.Stats))
|
||||||
mux.HandleFunc("GET", "/v1/list/{what}/{which}", auth.RequireAuth(v1.List))
|
mux.HandleFunc("GET", "/v1/stats/ws", useCfg(cfg, v1.StatsWS))
|
||||||
mux.HandleFunc("GET", "/v1/file", auth.RequireAuth(v1.GetFileContent))
|
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
||||||
mux.HandleFunc("GET", "/v1/file/{filename...}", auth.RequireAuth(v1.GetFileContent))
|
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
|
||||||
mux.HandleFunc("POST", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
|
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||||
mux.HandleFunc("PUT", "/v1/file/{filename...}", auth.RequireAuth(v1.SetFileContent))
|
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||||
mux.HandleFunc("GET", "/v1/stats", v1.Stats)
|
|
||||||
mux.HandleFunc("GET", "/v1/stats/ws", v1.StatsWS)
|
if common.PrometheusEnabled {
|
||||||
|
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||||
|
logging.Info().Msg("prometheus metrics enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultAuth := auth.GetDefaultAuth()
|
||||||
|
if defaultAuth != nil {
|
||||||
|
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||||
|
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
|
||||||
|
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutCallbackHandler)
|
||||||
|
} else {
|
||||||
|
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
}
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow only requests to API server with localhost.
|
func useCfg(cfg config.ConfigInstance, handler func(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||||
func checkHost(f http.HandlerFunc) http.HandlerFunc {
|
|
||||||
if common.IsDebug {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
handler(cfg, w, r)
|
||||||
if host != "127.0.0.1" && host != "localhost" && host != "[::1]" {
|
|
||||||
LogWarn(r).Msgf("blocked API request from %s", host)
|
|
||||||
http.Error(w, "forbidden", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
f(cfg, w, r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +1,54 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
var defaultAuth Provider
|
||||||
Credentials struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
Claims struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
// Initialize sets up authentication providers.
|
||||||
ErrInvalidUsername = E.New("invalid username")
|
func Initialize() error {
|
||||||
ErrInvalidPassword = E.New("invalid password")
|
if !IsEnabled() {
|
||||||
)
|
logging.Warn().Msg("authentication is disabled, please set API_JWT_SECRET or OIDC_* to enable authentication")
|
||||||
|
return nil
|
||||||
const tokenExpiration = 24 * time.Hour
|
|
||||||
|
|
||||||
const jwtClaimKeyUsername = "username"
|
|
||||||
|
|
||||||
func validatePassword(cred *Credentials) error {
|
|
||||||
if cred.Username != common.APIUser {
|
|
||||||
return ErrInvalidUsername.Subject(cred.Username)
|
|
||||||
}
|
}
|
||||||
if !bytes.Equal(common.HashPassword(cred.Password), common.APIPasswordHash) {
|
|
||||||
return ErrInvalidPassword.Subject(cred.Password)
|
var err error
|
||||||
|
// Initialize OIDC if configured.
|
||||||
|
if common.OIDCIssuerURL != "" {
|
||||||
|
defaultAuth, err = NewOIDCProviderFromEnv()
|
||||||
|
} else {
|
||||||
|
defaultAuth, err = NewUserPassAuthFromEnv()
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
func GetDefaultAuth() Provider {
|
||||||
var creds Credentials
|
return defaultAuth
|
||||||
err := json.NewDecoder(r.Body).Decode(&creds)
|
|
||||||
if err != nil {
|
|
||||||
U.HandleErr(w, r, err, http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := validatePassword(&creds); err != nil {
|
|
||||||
U.HandleErr(w, r, err, http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expiresAt := time.Now().Add(tokenExpiration)
|
|
||||||
claim := &Claims{
|
|
||||||
Username: creds.Username,
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claim)
|
|
||||||
tokenStr, err := token.SignedString(common.APIJWTSecret)
|
|
||||||
if err != nil {
|
|
||||||
U.HandleErr(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "token",
|
|
||||||
Value: tokenStr,
|
|
||||||
Expires: expiresAt,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteStrictMode,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
func IsEnabled() bool {
|
||||||
http.SetCookie(w, &http.Cookie{
|
return common.APIJWTSecret != nil || IsOIDCEnabled()
|
||||||
Name: "token",
|
}
|
||||||
Value: "",
|
|
||||||
Expires: time.Unix(0, 0),
|
func IsOIDCEnabled() bool {
|
||||||
HttpOnly: true,
|
return common.OIDCIssuerURL != ""
|
||||||
SameSite: http.SameSiteStrictMode,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
w.Header().Set("location", "/login")
|
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
if common.IsDebugSkipAuth {
|
if IsEnabled() {
|
||||||
return next
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
if err := defaultAuth.CheckToken(r); err != nil {
|
||||||
|
U.RespondError(w, err, http.StatusUnauthorized)
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
} else {
|
||||||
if checkToken(w, r) {
|
next(w, r)
|
||||||
next(w, r)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return next
|
||||||
|
|
||||||
func checkToken(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|
||||||
tokenCookie, err := r.Cookie("token")
|
|
||||||
if err != nil {
|
|
||||||
U.HandleErr(w, r, E.PrependSubject("token", err), http.StatusUnauthorized)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
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 common.APIJWTSecret, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
break
|
|
||||||
case !token.Valid:
|
|
||||||
err = E.New("invalid token")
|
|
||||||
case claims.Username != common.APIUser:
|
|
||||||
err = E.New("username mismatch").Subject(claims.Username)
|
|
||||||
case claims.ExpiresAt.Before(time.Now()):
|
|
||||||
err = E.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
U.HandleErr(w, r, err, http.StatusForbidden)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|||||||
274
internal/api/v1/auth/oidc.go
Normal file
274
internal/api/v1/auth/oidc.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
CE "github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OIDCProvider struct {
|
||||||
|
oauthConfig *oauth2.Config
|
||||||
|
oidcProvider *oidc.Provider
|
||||||
|
oidcVerifier *oidc.IDTokenVerifier
|
||||||
|
oidcLogoutURL *url.URL
|
||||||
|
allowedUsers []string
|
||||||
|
allowedGroups []string
|
||||||
|
isMiddleware bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const CookieOauthState = "godoxy_oidc_state"
|
||||||
|
|
||||||
|
const (
|
||||||
|
OIDCMiddlewareCallbackPath = "/auth/callback"
|
||||||
|
OIDCLogoutPath = "/auth/logout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL, logoutURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||||
|
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||||
|
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var logout *url.URL
|
||||||
|
var err error
|
||||||
|
if logoutURL != "" {
|
||||||
|
logout, err = url.Parse(logoutURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse logout URL: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OIDCProvider{
|
||||||
|
oauthConfig: &oauth2.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
RedirectURL: redirectURL,
|
||||||
|
Endpoint: provider.Endpoint(),
|
||||||
|
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
||||||
|
},
|
||||||
|
oidcProvider: provider,
|
||||||
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: clientID,
|
||||||
|
}),
|
||||||
|
oidcLogoutURL: logout,
|
||||||
|
allowedUsers: allowedUsers,
|
||||||
|
allowedGroups: allowedGroups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||||
|
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||||
|
return NewOIDCProvider(
|
||||||
|
common.OIDCIssuerURL,
|
||||||
|
common.OIDCClientID,
|
||||||
|
common.OIDCClientSecret,
|
||||||
|
common.OIDCRedirectURL,
|
||||||
|
common.OIDCLogoutURL,
|
||||||
|
common.OIDCAllowedUsers,
|
||||||
|
common.OIDCAllowedGroups,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) TokenCookieName() string {
|
||||||
|
return "godoxy_oidc_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetIsMiddleware(enabled bool) {
|
||||||
|
auth.isMiddleware = enabled
|
||||||
|
auth.oauthConfig.RedirectURL = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||||
|
auth.allowedUsers = users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||||
|
auth.allowedGroups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||||
|
token, err := r.Cookie(auth.TokenCookieName())
|
||||||
|
if err != nil {
|
||||||
|
return ErrMissingToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for Expiry, Audience == ClientID, Issuer, etc.
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(r.Context(), token.Value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to verify ID token: %w: %w", ErrInvalidToken, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(idToken.Audience) == 0 {
|
||||||
|
return ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Username string `json:"preferred_username"`
|
||||||
|
Groups []string `json:"groups"`
|
||||||
|
}
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse claims: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logical AND between allowed users and groups.
|
||||||
|
allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
|
||||||
|
allowedGroup := len(CE.Intersect(claims.Groups, auth.allowedGroups)) > 0
|
||||||
|
if !allowedUser && !allowedGroup {
|
||||||
|
return ErrUserNotAllowed.Subject(claims.Username)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateState generates a random string for OIDC state.
|
||||||
|
const oidcStateLength = 32
|
||||||
|
|
||||||
|
func generateState() (string, error) {
|
||||||
|
b := make([]byte, oidcStateLength)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedirectOIDC initiates the OIDC login flow.
|
||||||
|
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state, err := generateState()
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: CookieOauthState,
|
||||||
|
Value: state,
|
||||||
|
MaxAge: 300,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Secure: true,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
|
||||||
|
redirURL := auth.oauthConfig.AuthCodeURL(state)
|
||||||
|
if auth.isMiddleware {
|
||||||
|
u, err := r.URL.Parse(redirURL)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("redirect_uri", "https://"+r.Host+OIDCMiddlewareCallbackPath+q.Get("redirect_uri"))
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
redirURL = u.String()
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) exchange(r *http.Request) (*oauth2.Token, error) {
|
||||||
|
if auth.isMiddleware {
|
||||||
|
cfg := *auth.oauthConfig
|
||||||
|
cfg.RedirectURL = "https://" + r.Host + OIDCMiddlewareCallbackPath
|
||||||
|
return cfg.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||||
|
}
|
||||||
|
return auth.oauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIDCCallbackHandler handles the OIDC callback.
|
||||||
|
func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// For testing purposes, skip provider verification
|
||||||
|
if common.IsTest {
|
||||||
|
auth.handleTestCallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
if query.Get("state") != state.Value {
|
||||||
|
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Token, err := auth.exchange(r)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, fmt.Errorf("failed to exchange token: %w", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
U.HandleErr(w, r, E.New("missing id_token"), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, fmt.Errorf("failed to verify ID token: %w", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTokenCookie(w, r, auth.TokenCookieName(), rawIDToken, time.Until(idToken.Expiry))
|
||||||
|
|
||||||
|
// Redirect to home page
|
||||||
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if auth.oidcLogoutURL == nil {
|
||||||
|
DefaultLogoutCallbackHandler(auth, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := r.Cookie(auth.TokenCookieName())
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, E.New("missing token cookie"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||||
|
|
||||||
|
logoutURL := *auth.oidcLogoutURL
|
||||||
|
logoutURL.Query().Add("id_token_hint", token.Value)
|
||||||
|
|
||||||
|
http.Redirect(w, r, logoutURL.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTestCallback handles OIDC callback in test environment.
|
||||||
|
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state, err := r.Cookie(CookieOauthState)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("state") != state.Value {
|
||||||
|
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test JWT token
|
||||||
|
setTokenCookie(w, r, auth.TokenCookieName(), "test", time.Hour)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
454
internal/api/v1/auth/oidc_test.go
Normal file
454
internal/api/v1/auth/oidc_test.go
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupMockOIDC configures mock OIDC provider for testing.
|
||||||
|
func setupMockOIDC(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
provider := (&oidc.ProviderConfig{}).NewProvider(context.TODO())
|
||||||
|
defaultAuth = &OIDCProvider{
|
||||||
|
oauthConfig: &oauth2.Config{
|
||||||
|
ClientID: "test-client",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
RedirectURL: "http://localhost/callback",
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: "http://mock-provider/auth",
|
||||||
|
TokenURL: "http://mock-provider/token",
|
||||||
|
},
|
||||||
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
|
},
|
||||||
|
oidcProvider: provider,
|
||||||
|
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||||
|
ClientID: "test-client",
|
||||||
|
}),
|
||||||
|
allowedUsers: []string{"test-user"},
|
||||||
|
allowedGroups: []string{"test-group1", "test-group2"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoveryDocument returns a mock OIDC discovery document.
|
||||||
|
func discoveryDocument(t *testing.T, server *httptest.Server) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
discovery := map[string]any{
|
||||||
|
"issuer": server.URL,
|
||||||
|
"authorization_endpoint": server.URL + "/auth",
|
||||||
|
"token_endpoint": server.URL + "/token",
|
||||||
|
}
|
||||||
|
|
||||||
|
return discovery
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyID = "test-key-id"
|
||||||
|
clientID = "test-client-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
ts *httptest.Server
|
||||||
|
key *rsa.PrivateKey
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *provider) SignClaims(t *testing.T, claims jwt.Claims) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
token.Header["kid"] = keyID
|
||||||
|
signed, err := token.SignedString(j.key)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
return signed
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupProvider(t *testing.T) *provider {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Generate an RSA key pair for the test.
|
||||||
|
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
|
// Build the matching public JWK that will be served by the endpoint.
|
||||||
|
jwk := buildRSAJWK(t, &privKey.PublicKey, keyID)
|
||||||
|
|
||||||
|
// Start a test server that serves the JWKS endpoint.
|
||||||
|
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/.well-known/jwks.json":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"keys": []any{jwk},
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(ts.Close)
|
||||||
|
|
||||||
|
// Create a test OIDCProvider.
|
||||||
|
providerCtx := oidc.ClientContext(context.Background(), ts.Client())
|
||||||
|
keySet := oidc.NewRemoteKeySet(providerCtx, ts.URL+"/.well-known/jwks.json")
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
ts: ts,
|
||||||
|
key: privKey,
|
||||||
|
verifier: oidc.NewVerifier(ts.URL, keySet, &oidc.Config{
|
||||||
|
ClientID: clientID, // matches audience in the token
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRSAJWK is a helper to construct a minimal JWK for the JWKS endpoint.
|
||||||
|
func buildRSAJWK(t *testing.T, pub *rsa.PublicKey, kid string) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
nBytes := pub.N.Bytes()
|
||||||
|
eBytes := []byte{0x01, 0x00, 0x01} // Usually 65537
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"kty": "RSA",
|
||||||
|
"alg": "RS256",
|
||||||
|
"use": "sig",
|
||||||
|
"kid": kid,
|
||||||
|
"n": base64.RawURLEncoding.EncodeToString(nBytes),
|
||||||
|
"e": base64.RawURLEncoding.EncodeToString(eBytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanup() {
|
||||||
|
defaultAuth = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOIDCLoginHandler(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
common.APIJWTSecret = []byte("test-secret")
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
setupMockOIDC(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wantStatus int
|
||||||
|
wantRedirect bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success - Redirects to provider",
|
||||||
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
|
wantRedirect: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/auth/redirect", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
defaultAuth.RedirectLoginPage(w, req)
|
||||||
|
|
||||||
|
if got := w.Code; got != tt.wantStatus {
|
||||||
|
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantRedirect {
|
||||||
|
if loc := w.Header().Get("Location"); loc == "" {
|
||||||
|
t.Error("OIDCLoginHandler() missing redirect location")
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := w.Header().Get("Set-Cookie")
|
||||||
|
if cookie == "" {
|
||||||
|
t.Error("OIDCLoginHandler() missing state cookie")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOIDCCallbackHandler(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
common.APIJWTSecret = []byte("test-secret")
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
state string
|
||||||
|
code string
|
||||||
|
setupMocks bool
|
||||||
|
wantStatus int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success - Valid callback",
|
||||||
|
state: "valid-state",
|
||||||
|
code: "valid-code",
|
||||||
|
setupMocks: true,
|
||||||
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failure - Missing state",
|
||||||
|
code: "valid-code",
|
||||||
|
setupMocks: true,
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.setupMocks {
|
||||||
|
setupMockOIDC(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/auth/callback?code="+tt.code+"&state="+tt.state, nil)
|
||||||
|
if tt.state != "" {
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: CookieOauthState,
|
||||||
|
Value: tt.state,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
defaultAuth.LoginCallbackHandler(w, req)
|
||||||
|
|
||||||
|
if got := w.Code; got != tt.wantStatus {
|
||||||
|
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.wantStatus == http.StatusTemporaryRedirect {
|
||||||
|
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||||
|
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
||||||
|
ExpectTrue(t, setCookie.Value != "")
|
||||||
|
ExpectEqual(t, setCookie.Path, "/")
|
||||||
|
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
||||||
|
ExpectEqual(t, setCookie.HttpOnly, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitOIDC(t *testing.T) {
|
||||||
|
setupMockOIDC(t)
|
||||||
|
// Create a test server that serves the discovery document
|
||||||
|
var server *httptest.Server
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
ExpectNoError(t, json.NewEncoder(w).Encode(discoveryDocument(t, server)))
|
||||||
|
})
|
||||||
|
server = httptest.NewServer(mux)
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
issuerURL string
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
redirectURL string
|
||||||
|
logoutURL string
|
||||||
|
allowedUsers []string
|
||||||
|
allowedGroups []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Fail - Empty configuration",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Valid configuration with users",
|
||||||
|
issuerURL: server.URL,
|
||||||
|
clientID: "client_id",
|
||||||
|
clientSecret: "client_secret",
|
||||||
|
redirectURL: "https://example.com/callback",
|
||||||
|
allowedUsers: []string{"user1", "user2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Valid configuration with groups",
|
||||||
|
issuerURL: server.URL,
|
||||||
|
clientID: "client_id",
|
||||||
|
clientSecret: "client_secret",
|
||||||
|
redirectURL: "https://example.com/callback",
|
||||||
|
allowedGroups: []string{"group1", "group2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Valid configuration with users, groups and logout URL",
|
||||||
|
issuerURL: server.URL,
|
||||||
|
clientID: "client_id",
|
||||||
|
clientSecret: "client_secret",
|
||||||
|
redirectURL: "https://example.com/callback",
|
||||||
|
logoutURL: "https://example.com/logout",
|
||||||
|
allowedUsers: []string{"user1", "user2"},
|
||||||
|
allowedGroups: []string{"group1", "group2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fail - No allowed users or allowed groups",
|
||||||
|
issuerURL: "https://example.com",
|
||||||
|
clientID: "client_id",
|
||||||
|
clientSecret: "client_secret",
|
||||||
|
redirectURL: "https://example.com/callback",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.redirectURL, tt.logoutURL, tt.allowedUsers, tt.allowedGroups)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckToken(t *testing.T) {
|
||||||
|
provider := setupProvider(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
allowedUsers []string
|
||||||
|
allowedGroups []string
|
||||||
|
claims jwt.Claims
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success - Valid token with allowed user",
|
||||||
|
allowedUsers: []string{"user1"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Valid token with allowed group",
|
||||||
|
allowedGroups: []string{"group1"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Server omits groups, but user is allowed",
|
||||||
|
allowedUsers: []string{"user1"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Server omits preferred_username, but group is allowed",
|
||||||
|
allowedGroups: []string{"group1"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success - Valid token with allowed user and group",
|
||||||
|
allowedUsers: []string{"user1"},
|
||||||
|
allowedGroups: []string{"group1"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error - User not allowed",
|
||||||
|
allowedUsers: []string{"user2", "user3"},
|
||||||
|
allowedGroups: []string{"group2", "group3"},
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
wantErr: ErrUserNotAllowed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error - Server returns incorrect issuer",
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": "https://example.com",
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error - Server returns incorrect audience",
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": "some-other-audience",
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error - Server returns expired token",
|
||||||
|
claims: jwt.MapClaims{
|
||||||
|
"iss": provider.ts.URL,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": time.Now().Add(-time.Hour).Unix(),
|
||||||
|
"preferred_username": "user1",
|
||||||
|
"groups": []string{"group1"},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// Create the Auth Provider.
|
||||||
|
auth := &OIDCProvider{
|
||||||
|
oidcVerifier: provider.verifier,
|
||||||
|
allowedUsers: tc.allowedUsers,
|
||||||
|
allowedGroups: tc.allowedGroups,
|
||||||
|
}
|
||||||
|
// Sign the claims to create a token.
|
||||||
|
signedToken := provider.SignClaims(t, tc.claims)
|
||||||
|
// Craft a test HTTP request that includes the token as a cookie.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: auth.TokenCookieName(),
|
||||||
|
Value: signedToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call CheckToken and verify the result.
|
||||||
|
err := auth.CheckToken(req)
|
||||||
|
if tc.wantErr == nil {
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
} else {
|
||||||
|
ExpectError(t, tc.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
internal/api/v1/auth/provider.go
Normal file
13
internal/api/v1/auth/provider.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
TokenCookieName() string
|
||||||
|
CheckToken(r *http.Request) error
|
||||||
|
RedirectLoginPage(w http.ResponseWriter, r *http.Request)
|
||||||
|
LoginCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
LogoutCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||||
|
}
|
||||||
143
internal/api/v1/auth/userpass.go
Normal file
143
internal/api/v1/auth/userpass.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidUsername = E.New("invalid username")
|
||||||
|
ErrInvalidPassword = E.New("invalid password")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UserPassAuth struct {
|
||||||
|
username string
|
||||||
|
pwdHash []byte
|
||||||
|
secret []byte
|
||||||
|
tokenTTL time.Duration
|
||||||
|
}
|
||||||
|
UserPassClaims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &UserPassAuth{
|
||||||
|
username: username,
|
||||||
|
pwdHash: hash,
|
||||||
|
secret: secret,
|
||||||
|
tokenTTL: tokenTTL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserPassAuthFromEnv() (*UserPassAuth, error) {
|
||||||
|
return NewUserPassAuth(
|
||||||
|
common.APIUser,
|
||||||
|
common.APIPassword,
|
||||||
|
common.APIJWTSecret,
|
||||||
|
common.APIJWTTokenTTL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) TokenCookieName() string {
|
||||||
|
return "godoxy_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) NewToken() (token string, err error) {
|
||||||
|
claim := &UserPassClaims{
|
||||||
|
Username: auth.username,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(auth.tokenTTL)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tok := jwt.NewWithClaims(jwt.SigningMethodHS512, claim)
|
||||||
|
token, err = tok.SignedString(auth.secret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||||
|
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
||||||
|
if err != nil {
|
||||||
|
return ErrMissingToken
|
||||||
|
}
|
||||||
|
var claims UserPassClaims
|
||||||
|
token, err := jwt.ParseWithClaims(jwtCookie.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 auth.secret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !token.Valid:
|
||||||
|
return ErrInvalidToken
|
||||||
|
case claims.Username != auth.username:
|
||||||
|
return ErrUserNotAllowed.Subject(claims.Username)
|
||||||
|
case claims.ExpiresAt.Before(time.Now()):
|
||||||
|
return E.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var creds struct {
|
||||||
|
User string `json:"username"`
|
||||||
|
Pass string `json:"password"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&creds)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := auth.validatePassword(creds.User, creds.Pass); err != nil {
|
||||||
|
U.LogError(r).Err(err).Msg("auth: invalid credentials")
|
||||||
|
U.RespondError(w, E.New("invalid credentials"), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := auth.NewToken()
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
DefaultLogoutCallbackHandler(auth, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
||||||
|
if user != auth.username {
|
||||||
|
return ErrInvalidUsername.Subject(user)
|
||||||
|
}
|
||||||
|
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
|
||||||
|
return ErrInvalidPassword.With(err).Subject(pass)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
115
internal/api/v1/auth/userpass_test.go
Normal file
115
internal/api/v1/auth/userpass_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMockUserPassAuth() *UserPassAuth {
|
||||||
|
return &UserPassAuth{
|
||||||
|
username: "username",
|
||||||
|
pwdHash: Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)),
|
||||||
|
secret: []byte("abcdefghijklmnopqrstuvwxyz"),
|
||||||
|
tokenTTL: time.Hour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserPassValidateCredentials(t *testing.T) {
|
||||||
|
auth := newMockUserPassAuth()
|
||||||
|
err := auth.validatePassword("username", "password")
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
err = auth.validatePassword("username", "wrong-password")
|
||||||
|
ExpectError(t, ErrInvalidPassword, err)
|
||||||
|
err = auth.validatePassword("wrong-username", "password")
|
||||||
|
ExpectError(t, ErrInvalidUsername, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserPassCheckToken(t *testing.T) {
|
||||||
|
auth := newMockUserPassAuth()
|
||||||
|
token, err := auth.NewToken()
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
tests := []struct {
|
||||||
|
token string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
token: token,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "invalid-token",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
req := &http.Request{Header: http.Header{}}
|
||||||
|
if tt.token != "" {
|
||||||
|
req.Header.Set("Cookie", auth.TokenCookieName()+"="+tt.token)
|
||||||
|
}
|
||||||
|
err = auth.CheckToken(req)
|
||||||
|
if tt.wantErr {
|
||||||
|
ExpectTrue(t, err != nil)
|
||||||
|
} else {
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserPassLoginCallbackHandler(t *testing.T) {
|
||||||
|
type cred struct {
|
||||||
|
User string `json:"username"`
|
||||||
|
Pass string `json:"password"`
|
||||||
|
}
|
||||||
|
auth := newMockUserPassAuth()
|
||||||
|
tests := []struct {
|
||||||
|
creds cred
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
creds: cred{
|
||||||
|
User: "username",
|
||||||
|
Pass: "password",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
creds: cred{
|
||||||
|
User: "username",
|
||||||
|
Pass: "wrong-password",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := &http.Request{
|
||||||
|
Host: "app.example.com",
|
||||||
|
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
||||||
|
}
|
||||||
|
auth.LoginCallbackHandler(w, req)
|
||||||
|
if tt.wantErr {
|
||||||
|
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||||
|
} else {
|
||||||
|
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||||
|
ExpectTrue(t, setCookie.Name == auth.TokenCookieName())
|
||||||
|
ExpectTrue(t, setCookie.Value != "")
|
||||||
|
ExpectEqual(t, setCookie.Domain, "example.com")
|
||||||
|
ExpectEqual(t, setCookie.Path, "/")
|
||||||
|
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
||||||
|
ExpectEqual(t, setCookie.HttpOnly, true)
|
||||||
|
ExpectEqual(t, w.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
internal/api/v1/auth/utils.go
Normal file
69
internal/api/v1/auth/utils.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissingToken = E.New("missing token")
|
||||||
|
ErrInvalidToken = E.New("invalid token")
|
||||||
|
ErrUserNotAllowed = E.New("user not allowed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// cookieFQDN returns the fully qualified domain name of the request host
|
||||||
|
// with subdomain stripped.
|
||||||
|
//
|
||||||
|
// If the request host does not have a subdomain,
|
||||||
|
// an empty string is returned
|
||||||
|
//
|
||||||
|
// "abc.example.com" -> "example.com"
|
||||||
|
// "example.com" -> ""
|
||||||
|
func cookieFQDN(r *http.Request) string {
|
||||||
|
host, _, err := net.SplitHostPort(r.Host)
|
||||||
|
if err != nil {
|
||||||
|
host = r.Host
|
||||||
|
}
|
||||||
|
parts := strutils.SplitRune(host, '.')
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parts[0] = ""
|
||||||
|
return strutils.JoinRune(parts, '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string, ttl time.Duration) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
MaxAge: int(ttl.Seconds()),
|
||||||
|
Domain: cookieFQDN(r),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
Domain: cookieFQDN(r),
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultLogoutCallbackHandler clears the token cookie and redirects to the login page..
|
||||||
|
func DefaultLogoutCallbackHandler(auth Provider, w http.ResponseWriter, r *http.Request) {
|
||||||
|
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||||
|
auth.RedirectLoginPage(w, r)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
136
internal/api/v1/favicon/cache.go
Normal file
136
internal/api/v1/favicon/cache.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package favicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
Icon []byte `json:"icon"`
|
||||||
|
LastAccess time.Time `json:"lastAccess"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache key can be absolute url or route name.
|
||||||
|
var (
|
||||||
|
iconCache = make(map[string]*cacheEntry)
|
||||||
|
iconCacheMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
iconCacheTTL = 24 * time.Hour
|
||||||
|
cleanUpInterval = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitIconCache() {
|
||||||
|
iconCacheMu.Lock()
|
||||||
|
defer iconCacheMu.Unlock()
|
||||||
|
|
||||||
|
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to load icon cache")
|
||||||
|
} else {
|
||||||
|
logging.Info().Msgf("icon cache loaded (%d icons)", len(iconCache))
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
cleanupTicker := time.NewTicker(cleanUpInterval)
|
||||||
|
defer cleanupTicker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-task.RootContextCanceled():
|
||||||
|
return
|
||||||
|
case <-cleanupTicker.C:
|
||||||
|
pruneExpiredIconCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
task.OnProgramExit("save_favicon_cache", func() {
|
||||||
|
iconCacheMu.Lock()
|
||||||
|
defer iconCacheMu.Unlock()
|
||||||
|
|
||||||
|
if len(iconCache) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.SaveJSON(common.IconCachePath, &iconCache, 0o644); err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to save icon cache")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneExpiredIconCache() {
|
||||||
|
iconCacheMu.Lock()
|
||||||
|
defer iconCacheMu.Unlock()
|
||||||
|
|
||||||
|
nPruned := 0
|
||||||
|
for key, icon := range iconCache {
|
||||||
|
if icon.IsExpired() {
|
||||||
|
delete(iconCache, key)
|
||||||
|
nPruned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeKey(r route.HTTPRoute) string {
|
||||||
|
return r.ProviderName() + ":" + r.TargetName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func PruneRouteIconCache(route route.HTTPRoute) {
|
||||||
|
iconCacheMu.Lock()
|
||||||
|
defer iconCacheMu.Unlock()
|
||||||
|
delete(iconCache, routeKey(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadIconCache(key string) *fetchResult {
|
||||||
|
iconCacheMu.RLock()
|
||||||
|
defer iconCacheMu.RUnlock()
|
||||||
|
|
||||||
|
icon, ok := iconCache[key]
|
||||||
|
if ok && icon != nil {
|
||||||
|
logging.Debug().
|
||||||
|
Str("key", key).
|
||||||
|
Msg("icon found in cache")
|
||||||
|
icon.LastAccess = time.Now()
|
||||||
|
return &fetchResult{icon: icon.Icon}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeIconCache(key string, icon []byte) {
|
||||||
|
iconCacheMu.Lock()
|
||||||
|
defer iconCacheMu.Unlock()
|
||||||
|
iconCache[key] = &cacheEntry{Icon: icon, LastAccess: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *cacheEntry) IsExpired() bool {
|
||||||
|
return time.Since(e.LastAccess) > iconCacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
||||||
|
attempt := struct {
|
||||||
|
Icon []byte `json:"icon"`
|
||||||
|
LastAccess time.Time `json:"lastAccess"`
|
||||||
|
}{}
|
||||||
|
err := json.Unmarshal(data, &attempt)
|
||||||
|
if err == nil {
|
||||||
|
e.Icon = attempt.Icon
|
||||||
|
e.LastAccess = attempt.LastAccess
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// fallback to bytes
|
||||||
|
err = json.Unmarshal(data, &e.Icon)
|
||||||
|
if err == nil {
|
||||||
|
e.LastAccess = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
37
internal/api/v1/favicon/content.go
Normal file
37
internal/api/v1/favicon/content.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package favicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type content struct {
|
||||||
|
header http.Header
|
||||||
|
data []byte
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContent() *content {
|
||||||
|
return &content{
|
||||||
|
header: make(http.Header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) Header() http.Header {
|
||||||
|
return c.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) Write(data []byte) (int, error) {
|
||||||
|
c.data = append(c.data, data...)
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) WriteHeader(statusCode int) {
|
||||||
|
c.status = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *content) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
return nil, nil, errors.New("not supported")
|
||||||
|
}
|
||||||
284
internal/api/v1/favicon/favicon.go
Normal file
284
internal/api/v1/favicon/favicon.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package favicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/vincent-petithory/dataurl"
|
||||||
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fetchResult struct {
|
||||||
|
icon []byte
|
||||||
|
contentType string
|
||||||
|
statusCode int
|
||||||
|
errMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *fetchResult) OK() bool {
|
||||||
|
return res.icon != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *fetchResult) ContentType() string {
|
||||||
|
if res.contentType == "" {
|
||||||
|
if bytes.HasPrefix(res.icon, []byte("<svg")) || bytes.HasPrefix(res.icon, []byte("<?xml")) {
|
||||||
|
return "image/svg+xml"
|
||||||
|
} else {
|
||||||
|
return "image/x-icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.contentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFavIcon returns the favicon of the route
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - 200 OK: if icon found
|
||||||
|
// - 400 Bad Request: if alias is empty or route is not HTTPRoute
|
||||||
|
// - 404 Not Found: if route or icon not found
|
||||||
|
// - 500 Internal Server Error: if internal error
|
||||||
|
// - others: depends on route handler response
|
||||||
|
func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
|
url, alias := req.FormValue("url"), req.FormValue("alias")
|
||||||
|
if url == "" && alias == "" {
|
||||||
|
U.RespondError(w, U.ErrMissingKey("url or alias"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if url != "" && alias != "" {
|
||||||
|
U.RespondError(w, U.ErrInvalidKey("url and alias are mutually exclusive"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try with url
|
||||||
|
if url != "" {
|
||||||
|
var iconURL homepage.IconURL
|
||||||
|
if err := iconURL.Parse(url); err != nil {
|
||||||
|
U.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchResult := getFavIconFromURL(&iconURL)
|
||||||
|
if !fetchResult.OK() {
|
||||||
|
http.Error(w, fetchResult.errMsg, fetchResult.statusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", fetchResult.ContentType())
|
||||||
|
U.WriteBody(w, fetchResult.icon)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try with route.Homepage.Icon
|
||||||
|
r, ok := routes.GetHTTPRoute(alias)
|
||||||
|
if !ok {
|
||||||
|
U.RespondError(w, errors.New("no such route"), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var result *fetchResult
|
||||||
|
hp := r.HomepageConfig().GetOverride()
|
||||||
|
if !hp.IsEmpty() && hp.Icon != nil {
|
||||||
|
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||||
|
result = findIcon(r, req, hp.Icon.Value)
|
||||||
|
} else {
|
||||||
|
result = getFavIconFromURL(hp.Icon)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// try extract from "link[rel=icon]"
|
||||||
|
result = findIcon(r, req, "/")
|
||||||
|
}
|
||||||
|
if result.statusCode == 0 {
|
||||||
|
result.statusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
if !result.OK() {
|
||||||
|
http.Error(w, result.errMsg, result.statusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", result.ContentType())
|
||||||
|
U.WriteBody(w, result.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFavIconFromURL(iconURL *homepage.IconURL) *fetchResult {
|
||||||
|
switch iconURL.IconSource {
|
||||||
|
case homepage.IconSourceAbsolute:
|
||||||
|
return fetchIconAbsolute(iconURL.URL())
|
||||||
|
case homepage.IconSourceRelative:
|
||||||
|
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "unexpected relative icon"}
|
||||||
|
case homepage.IconSourceWalkXCode, homepage.IconSourceSelfhSt:
|
||||||
|
return fetchKnownIcon(iconURL)
|
||||||
|
}
|
||||||
|
return &fetchResult{statusCode: http.StatusBadRequest, errMsg: "invalid icon source"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIconAbsolute(url string) *fetchResult {
|
||||||
|
if result := loadIconCache(url); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := U.Get(url)
|
||||||
|
if err != nil || resp.StatusCode != http.StatusOK {
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
logging.Error().Err(err).
|
||||||
|
Str("url", url).
|
||||||
|
Msg("failed to get icon")
|
||||||
|
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
icon, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).
|
||||||
|
Str("url", url).
|
||||||
|
Msg("failed to read icon")
|
||||||
|
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
storeIconCache(url, icon)
|
||||||
|
return &fetchResult{icon: icon}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameSanitizer = strings.NewReplacer(
|
||||||
|
"_", "-",
|
||||||
|
" ", "-",
|
||||||
|
"(", "",
|
||||||
|
")", "",
|
||||||
|
)
|
||||||
|
|
||||||
|
func sanitizeName(name string) string {
|
||||||
|
return strings.ToLower(nameSanitizer.Replace(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchKnownIcon(url *homepage.IconURL) *fetchResult {
|
||||||
|
// if icon isn't in the list, no need to fetch
|
||||||
|
if !url.HasIcon() {
|
||||||
|
logging.Debug().
|
||||||
|
Str("value", url.String()).
|
||||||
|
Str("url", url.URL()).
|
||||||
|
Msg("no such icon")
|
||||||
|
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "no such icon"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchIconAbsolute(url.URL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIcon(filetype, filename string) *fetchResult {
|
||||||
|
result := fetchKnownIcon(homepage.NewSelfhStIconURL(filename, filetype))
|
||||||
|
if result.icon == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return fetchKnownIcon(homepage.NewWalkXCodeIconURL(filename, filetype))
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIcon(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
||||||
|
key := routeKey(r)
|
||||||
|
if result := loadIconCache(key); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fetchIcon("png", sanitizeName(r.TargetName()))
|
||||||
|
cont := r.ContainerInfo()
|
||||||
|
if !result.OK() && cont != nil {
|
||||||
|
result = fetchIcon("png", sanitizeName(cont.ImageName))
|
||||||
|
}
|
||||||
|
if !result.OK() {
|
||||||
|
// fallback to parse html
|
||||||
|
result = findIconSlow(r, req, uri)
|
||||||
|
}
|
||||||
|
if result.OK() {
|
||||||
|
storeIconCache(key, result.icon)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
||||||
|
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
||||||
|
defer cancel()
|
||||||
|
newReq := req.WithContext(ctx)
|
||||||
|
newReq.Header.Set("Accept-Encoding", "identity") // disable compression
|
||||||
|
if !strings.HasPrefix(uri, "/") {
|
||||||
|
uri = "/" + uri
|
||||||
|
}
|
||||||
|
u, err := url.ParseRequestURI(uri)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).
|
||||||
|
Str("route", r.TargetName()).
|
||||||
|
Str("path", uri).
|
||||||
|
Msg("failed to parse uri")
|
||||||
|
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "cannot parse uri"}
|
||||||
|
}
|
||||||
|
newReq.URL.Path = u.Path
|
||||||
|
newReq.URL.RawPath = u.RawPath
|
||||||
|
newReq.URL.RawQuery = u.RawQuery
|
||||||
|
newReq.RequestURI = u.String()
|
||||||
|
|
||||||
|
c := newContent()
|
||||||
|
r.ServeHTTP(c, newReq)
|
||||||
|
if c.status != http.StatusOK {
|
||||||
|
switch c.status {
|
||||||
|
case 0:
|
||||||
|
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "connection error"}
|
||||||
|
default:
|
||||||
|
if loc := c.Header().Get("Location"); loc != "" {
|
||||||
|
loc = path.Clean(loc)
|
||||||
|
if !strings.HasPrefix(loc, "/") {
|
||||||
|
loc = "/" + loc
|
||||||
|
}
|
||||||
|
if loc == newReq.URL.Path {
|
||||||
|
return &fetchResult{statusCode: http.StatusBadGateway, errMsg: "circular redirect"}
|
||||||
|
}
|
||||||
|
return findIconSlow(r, req, loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &fetchResult{statusCode: c.status, errMsg: "upstream error: " + string(c.data)}
|
||||||
|
}
|
||||||
|
// return icon data
|
||||||
|
if !gphttp.GetContentType(c.header).IsHTML() {
|
||||||
|
return &fetchResult{icon: c.data, contentType: c.header.Get("Content-Type")}
|
||||||
|
}
|
||||||
|
// try extract from "link[rel=icon]" from path "/"
|
||||||
|
doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(c.data))
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).
|
||||||
|
Str("route", r.TargetName()).
|
||||||
|
Msg("failed to parse html")
|
||||||
|
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||||
|
}
|
||||||
|
ele := doc.Find("head > link[rel=icon]").First()
|
||||||
|
if ele.Length() == 0 {
|
||||||
|
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon element not found"}
|
||||||
|
}
|
||||||
|
href := ele.AttrOr("href", "")
|
||||||
|
if href == "" {
|
||||||
|
return &fetchResult{statusCode: http.StatusNotFound, errMsg: "icon href not found"}
|
||||||
|
}
|
||||||
|
// https://en.wikipedia.org/wiki/Data_URI_scheme
|
||||||
|
if strings.HasPrefix(href, "data:image/") {
|
||||||
|
dataURI, err := dataurl.DecodeString(href)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).
|
||||||
|
Str("route", r.TargetName()).
|
||||||
|
Msg("failed to decode favicon")
|
||||||
|
return &fetchResult{statusCode: http.StatusInternalServerError, errMsg: "internal error"}
|
||||||
|
}
|
||||||
|
return &fetchResult{icon: dataURI.Data, contentType: dataURI.ContentType()}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
||||||
|
return fetchIconAbsolute(href)
|
||||||
|
default:
|
||||||
|
return findIconSlow(r, req, path.Clean(href))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,17 +9,65 @@ import (
|
|||||||
|
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route/provider"
|
"github.com/yusing/go-proxy/internal/route/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
type FileType string
|
||||||
filename := r.PathValue("filename")
|
|
||||||
if filename == "" {
|
const (
|
||||||
filename = common.ConfigFileName
|
FileTypeConfig FileType = "config"
|
||||||
|
FileTypeProvider FileType = "provider"
|
||||||
|
FileTypeMiddleware FileType = "middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fileType(file string) FileType {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(path.Base(file), "config."):
|
||||||
|
return FileTypeConfig
|
||||||
|
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
|
||||||
|
return FileTypeMiddleware
|
||||||
}
|
}
|
||||||
content, err := os.ReadFile(path.Join(common.ConfigBasePath, filename))
|
return FileTypeProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t FileType) IsValid() bool {
|
||||||
|
switch t {
|
||||||
|
case FileTypeConfig, FileTypeProvider, FileTypeMiddleware:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t FileType) GetPath(filename string) string {
|
||||||
|
if t == FileTypeMiddleware {
|
||||||
|
return path.Join(common.MiddlewareComposeBasePath, filename)
|
||||||
|
}
|
||||||
|
return path.Join(common.ConfigBasePath, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||||
|
fileType = FileType(r.PathValue("type"))
|
||||||
|
if !fileType.IsValid() {
|
||||||
|
err = U.ErrInvalidKey("type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filename = r.PathValue("filename")
|
||||||
|
if filename == "" {
|
||||||
|
err = U.ErrMissingKey("filename")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileType, filename, err := getArgs(r)
|
||||||
|
if err != nil {
|
||||||
|
U.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(fileType.GetPath(filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
@@ -27,10 +75,42 @@ func GetFileContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
U.WriteBody(w, content)
|
U.WriteBody(w, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateFile(fileType FileType, content []byte) error {
|
||||||
|
switch fileType {
|
||||||
|
case FileTypeConfig:
|
||||||
|
return config.Validate(content)
|
||||||
|
case FileTypeMiddleware:
|
||||||
|
errs := E.NewBuilder("middleware errors")
|
||||||
|
middleware.BuildMiddlewaresFromYAML("", content, errs)
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
return provider.Validate(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileType := FileType(r.PathValue("type"))
|
||||||
|
if !fileType.IsValid() {
|
||||||
|
U.RespondError(w, U.ErrInvalidKey("type"), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Body.Close()
|
||||||
|
err = validateFile(fileType, content)
|
||||||
|
if err != nil {
|
||||||
|
U.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
||||||
filename := r.PathValue("filename")
|
fileType, filename, err := getArgs(r)
|
||||||
if filename == "" {
|
if err != nil {
|
||||||
U.HandleErr(w, r, U.ErrMissingKey("filename"), http.StatusBadRequest)
|
U.RespondError(w, err, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := io.ReadAll(r.Body)
|
content, err := io.ReadAll(r.Body)
|
||||||
@@ -39,20 +119,12 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var valErr E.Error
|
if valErr := validateFile(fileType, content); valErr != nil {
|
||||||
if filename == common.ConfigFileName {
|
U.RespondError(w, valErr, http.StatusBadRequest)
|
||||||
valErr = config.Validate(content)
|
|
||||||
} else if !strings.HasPrefix(filename, path.Base(common.MiddlewareComposeBasePath)) {
|
|
||||||
valErr = provider.Validate(content)
|
|
||||||
}
|
|
||||||
// no validation for include files
|
|
||||||
|
|
||||||
if valErr != nil {
|
|
||||||
U.RespondJSON(w, r, valErr, http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path.Join(common.ConfigBasePath, filename), content, 0o644)
|
err = os.WriteFile(fileType.GetPath(filename), content, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
18
internal/api/v1/health.go
Normal file
18
internal/api/v1/health.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HealthWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
|
return wsjson.Write(r.Context(), conn, routequery.HealthMap())
|
||||||
|
})
|
||||||
|
}
|
||||||
90
internal/api/v1/homepage_overrides.go
Normal file
90
internal/api/v1/homepage_overrides.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HomepageOverrideItem = "item"
|
||||||
|
HomepageOverrideItemsBatch = "items_batch"
|
||||||
|
HomepageOverrideCategoryOrder = "category_order"
|
||||||
|
HomepageOverrideItemVisible = "item_visible"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
HomepageOverrideItemParams struct {
|
||||||
|
Which string `json:"which"`
|
||||||
|
Value homepage.ItemConfig `json:"value"`
|
||||||
|
}
|
||||||
|
HomepageOverrideItemsBatchParams struct {
|
||||||
|
Value map[string]*homepage.ItemConfig `json:"value"`
|
||||||
|
}
|
||||||
|
HomepageOverrideCategoryOrderParams struct {
|
||||||
|
Which string `json:"which"`
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
|
HomepageOverrideItemVisibleParams struct {
|
||||||
|
Which []string `json:"which"`
|
||||||
|
Value bool `json:"value"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||||
|
what := r.FormValue("what")
|
||||||
|
if what == "" {
|
||||||
|
http.Error(w, "missing what or which", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
utils.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Body.Close()
|
||||||
|
|
||||||
|
overrides := homepage.GetOverrideConfig()
|
||||||
|
switch what {
|
||||||
|
case HomepageOverrideItem:
|
||||||
|
var params HomepageOverrideItemParams
|
||||||
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
|
utils.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overrides.OverrideItem(params.Which, ¶ms.Value)
|
||||||
|
case HomepageOverrideItemsBatch:
|
||||||
|
var params HomepageOverrideItemsBatchParams
|
||||||
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
|
utils.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overrides.OverrideItems(params.Value)
|
||||||
|
case HomepageOverrideItemVisible: // POST /v1/item_visible [a,b,c], false => hide a, b, c
|
||||||
|
var params HomepageOverrideItemVisibleParams
|
||||||
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
|
utils.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if params.Value {
|
||||||
|
overrides.UnhideItems(params.Which...)
|
||||||
|
} else {
|
||||||
|
overrides.HideItems(params.Which...)
|
||||||
|
}
|
||||||
|
case HomepageOverrideCategoryOrder:
|
||||||
|
var params HomepageOverrideCategoryOrderParams
|
||||||
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
|
utils.RespondError(w, err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overrides.SetCategoryOrder(params.Which, params.Value)
|
||||||
|
default:
|
||||||
|
http.Error(w, "invalid what", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
@@ -2,29 +2,35 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal"
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||||
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ListRoute = "route"
|
ListRoute = "route"
|
||||||
ListRoutes = "routes"
|
ListRoutes = "routes"
|
||||||
ListConfigFiles = "config_files"
|
ListFiles = "files"
|
||||||
ListMiddlewares = "middlewares"
|
ListMiddlewares = "middlewares"
|
||||||
ListMiddlewareTraces = "middleware_trace"
|
ListMiddlewareTraces = "middleware_trace"
|
||||||
ListMatchDomains = "match_domains"
|
ListMatchDomains = "match_domains"
|
||||||
ListHomepageConfig = "homepage_config"
|
ListHomepageConfig = "homepage_config"
|
||||||
ListTasks = "tasks"
|
ListRouteProviders = "route_providers"
|
||||||
|
ListHomepageCategories = "homepage_categories"
|
||||||
|
ListIcons = "icons"
|
||||||
|
ListTasks = "tasks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func List(w http.ResponseWriter, r *http.Request) {
|
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
what := r.PathValue("what")
|
what := r.PathValue("what")
|
||||||
if what == "" {
|
if what == "" {
|
||||||
what = ListRoutes
|
what = ListRoutes
|
||||||
@@ -34,38 +40,55 @@ func List(w http.ResponseWriter, r *http.Request) {
|
|||||||
switch what {
|
switch what {
|
||||||
case ListRoute:
|
case ListRoute:
|
||||||
if route := listRoute(which); route == nil {
|
if route := listRoute(which); route == nil {
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
U.RespondJSON(w, r, route)
|
U.RespondJSON(w, r, route)
|
||||||
}
|
}
|
||||||
case ListRoutes:
|
case ListRoutes:
|
||||||
U.RespondJSON(w, r, config.RoutesByAlias(route.RouteType(r.FormValue("type"))))
|
U.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type"))))
|
||||||
case ListConfigFiles:
|
case ListFiles:
|
||||||
listConfigFiles(w, r)
|
listFiles(w, r)
|
||||||
case ListMiddlewares:
|
case ListMiddlewares:
|
||||||
U.RespondJSON(w, r, middleware.All())
|
U.RespondJSON(w, r, middleware.All())
|
||||||
case ListMiddlewareTraces:
|
case ListMiddlewareTraces:
|
||||||
U.RespondJSON(w, r, middleware.GetAllTrace())
|
U.RespondJSON(w, r, middleware.GetAllTrace())
|
||||||
case ListMatchDomains:
|
case ListMatchDomains:
|
||||||
U.RespondJSON(w, r, config.Value().MatchDomains)
|
U.RespondJSON(w, r, cfg.Value().MatchDomains)
|
||||||
case ListHomepageConfig:
|
case ListHomepageConfig:
|
||||||
U.RespondJSON(w, r, config.HomepageConfig())
|
U.RespondJSON(w, r, routequery.HomepageConfig(cfg.Value().Homepage.UseDefaultCategories, r.FormValue("category"), r.FormValue("provider")))
|
||||||
|
case ListRouteProviders:
|
||||||
|
U.RespondJSON(w, r, cfg.RouteProviderList())
|
||||||
|
case ListHomepageCategories:
|
||||||
|
U.RespondJSON(w, r, routequery.HomepageCategories())
|
||||||
|
case ListIcons:
|
||||||
|
limit, err := strconv.Atoi(r.FormValue("limit"))
|
||||||
|
if err != nil {
|
||||||
|
limit = 0
|
||||||
|
}
|
||||||
|
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
|
||||||
|
if err != nil {
|
||||||
|
U.RespondError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if icons == nil {
|
||||||
|
icons = []string{}
|
||||||
|
}
|
||||||
|
U.RespondJSON(w, r, icons)
|
||||||
case ListTasks:
|
case ListTasks:
|
||||||
U.RespondJSON(w, r, task.DebugTaskMap())
|
U.RespondJSON(w, r, task.DebugTaskList())
|
||||||
default:
|
default:
|
||||||
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
|
U.HandleErr(w, r, U.ErrInvalidKey("what"), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if which is "all" or empty, return map[string]Route of all routes
|
||||||
|
// otherwise, return a single Route with alias which or nil if not found.
|
||||||
func listRoute(which string) any {
|
func listRoute(which string) any {
|
||||||
if which == "" {
|
if which == "" || which == "all" {
|
||||||
which = "all"
|
return routequery.RoutesByAlias()
|
||||||
}
|
}
|
||||||
if which == "all" {
|
routes := routequery.RoutesByAlias()
|
||||||
return config.RoutesByAlias()
|
|
||||||
}
|
|
||||||
routes := config.RoutesByAlias()
|
|
||||||
route, ok := routes[which]
|
route, ok := routes[which]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
@@ -73,14 +96,32 @@ func listRoute(which string) any {
|
|||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
func listConfigFiles(w http.ResponseWriter, r *http.Request) {
|
func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
files, err := utils.ListFiles(common.ConfigBasePath, 1)
|
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := range files {
|
resp := map[FileType][]string{
|
||||||
files[i] = strings.TrimPrefix(files[i], common.ConfigBasePath+"/")
|
FileTypeConfig: make([]string, 0),
|
||||||
|
FileTypeProvider: make([]string, 0),
|
||||||
|
FileTypeMiddleware: make([]string, 0),
|
||||||
}
|
}
|
||||||
U.RespondJSON(w, r, files)
|
|
||||||
|
for _, file := range files {
|
||||||
|
t := fileType(file)
|
||||||
|
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
||||||
|
resp[t] = append(resp[t], file)
|
||||||
|
}
|
||||||
|
|
||||||
|
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
U.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, mid := range mids {
|
||||||
|
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
||||||
|
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
||||||
|
}
|
||||||
|
U.RespondJSON(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
233
internal/api/v1/mem_logger.go
Normal file
233
internal/api/v1/mem_logger.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logEntryRange struct {
|
||||||
|
Start, End int
|
||||||
|
}
|
||||||
|
|
||||||
|
type memLogger struct {
|
||||||
|
bytes.Buffer
|
||||||
|
sync.RWMutex
|
||||||
|
notifyLock sync.RWMutex
|
||||||
|
connChans F.Map[chan *logEntryRange, struct{}]
|
||||||
|
|
||||||
|
bufPool sync.Pool // used in hook mode
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemLogger interface {
|
||||||
|
io.Writer
|
||||||
|
// TODO: hook does not pass in fields, looking for a workaround to do server side log rendering
|
||||||
|
zerolog.Hook
|
||||||
|
}
|
||||||
|
|
||||||
|
type buffer struct {
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxMemLogSize = 16 * 1024
|
||||||
|
truncateSize = maxMemLogSize / 2
|
||||||
|
initialWriteChunkSize = 4 * 1024
|
||||||
|
hookModeBufSize = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
var memLoggerInstance = &memLogger{
|
||||||
|
connChans: F.NewMapOf[chan *logEntryRange, struct{}](),
|
||||||
|
bufPool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
return &buffer{
|
||||||
|
data: make([]byte, 0, hookModeBufSize),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if !common.EnableLogStreaming {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memLoggerInstance.Grow(maxMemLogSize)
|
||||||
|
|
||||||
|
if common.DebugMemLogger {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-task.RootContextCanceled():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
logging.Info().Msgf("mem logger size: %d, active conns: %d",
|
||||||
|
memLoggerInstance.Len(),
|
||||||
|
memLoggerInstance.connChans.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogsWS() func(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
return memLoggerInstance.ServeHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMemLogger() MemLogger {
|
||||||
|
return memLoggerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) truncateIfNeeded(n int) {
|
||||||
|
m.RLock()
|
||||||
|
needTruncate := m.Len()+n > maxMemLogSize
|
||||||
|
m.RUnlock()
|
||||||
|
|
||||||
|
if needTruncate {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
needTruncate = m.Len()+n > maxMemLogSize
|
||||||
|
if !needTruncate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Truncate(truncateSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) notifyWS(pos, n int) {
|
||||||
|
if m.connChans.Size() > 0 {
|
||||||
|
timeout := time.NewTimer(1 * time.Second)
|
||||||
|
defer timeout.Stop()
|
||||||
|
|
||||||
|
m.notifyLock.RLock()
|
||||||
|
defer m.notifyLock.RUnlock()
|
||||||
|
m.connChans.Range(func(ch chan *logEntryRange, _ struct{}) bool {
|
||||||
|
select {
|
||||||
|
case ch <- &logEntryRange{pos, pos + n}:
|
||||||
|
return true
|
||||||
|
case <-timeout.C:
|
||||||
|
logging.Warn().Msg("mem logger: timeout logging to channel")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) writeBuf(b []byte) (pos int, err error) {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
pos = m.Len()
|
||||||
|
_, err = m.Buffer.Write(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements zerolog.Hook.
|
||||||
|
func (m *memLogger) Run(e *zerolog.Event, level zerolog.Level, message string) {
|
||||||
|
bufStruct := m.bufPool.Get().(*buffer)
|
||||||
|
buf := bufStruct.data
|
||||||
|
defer func() {
|
||||||
|
bufStruct.data = bufStruct.data[:0]
|
||||||
|
m.bufPool.Put(bufStruct)
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf = logging.FormatLogEntryHTML(level, message, buf)
|
||||||
|
n := len(buf)
|
||||||
|
|
||||||
|
m.truncateIfNeeded(n)
|
||||||
|
|
||||||
|
pos, err := m.writeBuf(buf)
|
||||||
|
if err != nil {
|
||||||
|
// not logging the error here, it will cause Run to be called again = infinite loop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.notifyWS(pos, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (m *memLogger) Write(p []byte) (n int, err error) {
|
||||||
|
n = len(p)
|
||||||
|
m.truncateIfNeeded(n)
|
||||||
|
|
||||||
|
pos, err := m.writeBuf(p)
|
||||||
|
if err != nil {
|
||||||
|
// not logging the error here, it will cause Run to be called again = infinite loop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.notifyWS(pos, n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) ServeHTTP(config config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := utils.InitiateWS(config, w, r)
|
||||||
|
if err != nil {
|
||||||
|
utils.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logCh := make(chan *logEntryRange)
|
||||||
|
m.connChans.Store(logCh, struct{}{})
|
||||||
|
|
||||||
|
/* trunk-ignore(golangci-lint/errcheck) */
|
||||||
|
defer func() {
|
||||||
|
_ = conn.CloseNow()
|
||||||
|
|
||||||
|
m.notifyLock.Lock()
|
||||||
|
m.connChans.Delete(logCh)
|
||||||
|
close(logCh)
|
||||||
|
m.notifyLock.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := m.wsInitial(r.Context(), conn); err != nil {
|
||||||
|
utils.HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.wsStreamLog(r.Context(), conn, logCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) writeBytes(ctx context.Context, conn *websocket.Conn, b []byte) error {
|
||||||
|
return conn.Write(ctx, websocket.MessageText, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) wsInitial(ctx context.Context, conn *websocket.Conn) error {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
return m.writeBytes(ctx, conn, m.Buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memLogger) wsStreamLog(ctx context.Context, conn *websocket.Conn, ch <-chan *logEntryRange) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case logRange := <-ch:
|
||||||
|
m.RLock()
|
||||||
|
msg := m.Buffer.Bytes()[logRange.Start:logRange.End]
|
||||||
|
err := m.writeBytes(ctx, conn, msg)
|
||||||
|
m.RUnlock()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ReloadServer() E.Error {
|
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 {
|
if err != nil {
|
||||||
return E.From(err)
|
return E.From(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Reload(w http.ResponseWriter, r *http.Request) {
|
func Reload(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
if err := config.Reload(); err != nil {
|
if err := cfg.Reload(); err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,31 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/coder/websocket/wsjson"
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
"github.com/yusing/go-proxy/internal/config"
|
|
||||||
"github.com/yusing/go-proxy/internal/server"
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Stats(w http.ResponseWriter, r *http.Request) {
|
func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
U.RespondJSON(w, r, getStats())
|
U.RespondJSON(w, r, getStats(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func StatsWS(w http.ResponseWriter, r *http.Request) {
|
func StatsWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
U.PeriodicWS(cfg, w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||||
originPats := make([]string, len(config.Value().MatchDomains)+len(localAddresses))
|
return wsjson.Write(r.Context(), conn, getStats(cfg))
|
||||||
|
|
||||||
if len(originPats) == 0 {
|
|
||||||
U.LogWarn(r).Msg("no match domains configured, accepting websocket API request from all origins")
|
|
||||||
originPats = []string{"*"}
|
|
||||||
} else {
|
|
||||||
for i, domain := range config.Value().MatchDomains {
|
|
||||||
originPats[i] = "*." + domain
|
|
||||||
}
|
|
||||||
originPats = append(originPats, localAddresses...)
|
|
||||||
}
|
|
||||||
if common.IsDebug {
|
|
||||||
originPats = []string{"*"}
|
|
||||||
}
|
|
||||||
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
|
||||||
OriginPatterns: originPats,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
U.LogError(r).Err(err).Msg("failed to upgrade websocket")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
/* trunk-ignore(golangci-lint/errcheck) */
|
|
||||||
defer conn.CloseNow()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for range ticker.C {
|
|
||||||
stats := getStats()
|
|
||||||
if err := wsjson.Write(ctx, conn, stats); err != nil {
|
|
||||||
U.LogError(r).Msg("failed to write JSON")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStats() map[string]any {
|
var startTime = time.Now()
|
||||||
|
|
||||||
|
func getStats(cfg config.ConfigInstance) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"proxies": config.Statistics(),
|
"proxies": cfg.Statistics(),
|
||||||
"uptime": strutils.FormatDuration(server.GetProxyServer().Uptime()),
|
"uptime": strutils.FormatDuration(time.Since(startTime)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleErr logs the error and returns an HTTP error response to the client.
|
// HandleErr logs the error and returns an error code to the client.
|
||||||
// If code is specified, it will be used as the HTTP status code; otherwise,
|
// If code is specified, it will be used as the HTTP status code; otherwise,
|
||||||
// http.StatusInternalServerError is used.
|
// http.StatusInternalServerError is used.
|
||||||
//
|
//
|
||||||
// The error is only logged but not returned to the client.
|
// The error is only logged but not returned to the client.
|
||||||
func HandleErr(w http.ResponseWriter, r *http.Request, origErr error, code ...int) {
|
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||||
if origErr == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
LogError(r).Msg(origErr.Error())
|
LogError(r).Msg(err.Error())
|
||||||
statusCode := http.StatusInternalServerError
|
if len(code) == 0 {
|
||||||
if len(code) > 0 {
|
code = []int{http.StatusInternalServerError}
|
||||||
statusCode = code[0]
|
|
||||||
}
|
}
|
||||||
http.Error(w, http.StatusText(statusCode), statusCode)
|
http.Error(w, http.StatusText(code[0]), code[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondError returns error details to the client.
|
||||||
|
// If code is specified, it will be used as the HTTP status code; otherwise,
|
||||||
|
// http.StatusBadRequest is used.
|
||||||
|
func RespondError(w http.ResponseWriter, err error, code ...int) {
|
||||||
|
if len(code) == 0 {
|
||||||
|
code = []int{http.StatusBadRequest}
|
||||||
|
}
|
||||||
|
buf, err := json.Marshal(err)
|
||||||
|
if err != nil { // just in case
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
http.Error(w, ansi.StripANSI(err.Error()), code[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(code[0])
|
||||||
|
_, _ = w.Write(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrMissingKey(k string) error {
|
func ErrMissingKey(k string) error {
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
|
func reqLogger(r *http.Request, level zerolog.Level) *zerolog.Event {
|
||||||
return logging.WithLevel(level).Str("module", "api").
|
return logging.WithLevel(level).
|
||||||
Str("method", r.Method).
|
Str("module", "api").
|
||||||
Str("path", r.RequestURI)
|
Str("remote", r.RemoteAddr).
|
||||||
|
Str("host", r.Host).
|
||||||
|
Str("uri", r.Method+" "+r.RequestURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func LogError(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.ErrorLevel) }
|
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 LogWarn(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.WarnLevel) }
|
||||||
func LogInfo(r *http.Request) *zerolog.Event { return reqLogger(r, zerolog.InfoLevel) }
|
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,14 +2,16 @@ package utils
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WriteBody(w http.ResponseWriter, body []byte) {
|
func WriteBody(w http.ResponseWriter, body []byte) {
|
||||||
if _, err := w.Write(body); err != nil {
|
if _, err := w.Write(body); err != nil {
|
||||||
HandleErr(w, nil, err)
|
logging.Err(err).Msg("failed to write body")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,16 +25,20 @@ func RespondJSON(w http.ResponseWriter, r *http.Request, data any, code ...int)
|
|||||||
|
|
||||||
switch data := data.(type) {
|
switch data := data.(type) {
|
||||||
case string:
|
case string:
|
||||||
j = []byte(`"` + data + `"`)
|
j = []byte(fmt.Sprintf("%q", data))
|
||||||
case []byte:
|
case []byte:
|
||||||
j = data
|
j = data
|
||||||
|
case error:
|
||||||
|
j, err = json.Marshal(ansi.StripANSI(data.Error()))
|
||||||
default:
|
default:
|
||||||
j, err = json.MarshalIndent(data, "", " ")
|
j, err = json.MarshalIndent(data, "", " ")
|
||||||
if err != nil {
|
|
||||||
logging.Panic().Err(err).Msg("failed to marshal json")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logging.Panic().Err(err).Msg("failed to marshal json")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
_, err = w.Write(j)
|
_, err = w.Write(j)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleErr(w, r, err)
|
HandleErr(w, r, err)
|
||||||
|
|||||||
68
internal/api/v1/utils/ws.go
Normal file
68
internal/api/v1/utils/ws.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coder/websocket"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func warnNoMatchDomains() {
|
||||||
|
logging.Warn().Msg("no match domains configured, accepting websocket API request from all origins")
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnNoMatchDomainOnce sync.Once
|
||||||
|
|
||||||
|
func InitiateWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||||
|
var originPats []string
|
||||||
|
|
||||||
|
localAddresses := []string{"127.0.0.1", "10.0.*.*", "172.16.*.*", "192.168.*.*"}
|
||||||
|
|
||||||
|
if len(cfg.Value().MatchDomains) == 0 {
|
||||||
|
warnNoMatchDomainOnce.Do(warnNoMatchDomains)
|
||||||
|
originPats = []string{"*"}
|
||||||
|
} else {
|
||||||
|
originPats = make([]string, len(cfg.Value().MatchDomains))
|
||||||
|
for i, domain := range cfg.Value().MatchDomains {
|
||||||
|
originPats[i] = "*" + domain
|
||||||
|
}
|
||||||
|
originPats = append(originPats, localAddresses...)
|
||||||
|
}
|
||||||
|
if common.IsDebug {
|
||||||
|
originPats = []string{"*"}
|
||||||
|
}
|
||||||
|
return websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
|
OriginPatterns: originPats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PeriodicWS(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request, interval time.Duration, do func(conn *websocket.Conn) error) {
|
||||||
|
conn, err := InitiateWS(cfg, w, r)
|
||||||
|
if err != nil {
|
||||||
|
HandleErr(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
/* trunk-ignore(golangci-lint/errcheck) */
|
||||||
|
defer conn.CloseNow()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cfg.Context().Done():
|
||||||
|
return
|
||||||
|
case <-r.Context().Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := do(conn); err != nil {
|
||||||
|
LogError(r).Msg(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,68 +4,115 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/certcrypto"
|
"github.com/go-acme/lego/v4/certcrypto"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config types.AutoCertConfig
|
type (
|
||||||
|
AutocertConfig struct {
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Domains []string `json:"domains,omitempty"`
|
||||||
|
CertPath string `json:"cert_path,omitempty"`
|
||||||
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
|
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Options ProviderOpt `json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
ProviderOpt map[string]any
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingDomain = E.New("missing field 'domains'")
|
ErrMissingDomain = E.New("missing field 'domains'")
|
||||||
ErrMissingEmail = E.New("missing field 'email'")
|
ErrMissingEmail = E.New("missing field 'email'")
|
||||||
ErrMissingProvider = E.New("missing field 'provider'")
|
ErrMissingProvider = E.New("missing field 'provider'")
|
||||||
|
ErrInvalidDomain = E.New("invalid domain")
|
||||||
ErrUnknownProvider = E.New("unknown provider")
|
ErrUnknownProvider = E.New("unknown provider")
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewConfig(cfg *types.AutoCertConfig) *Config {
|
var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||||
|
|
||||||
|
// Validate implements the utils.CustomValidator interface.
|
||||||
|
func (cfg *AutocertConfig) Validate() E.Error {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Provider == "" {
|
||||||
|
cfg.Provider = ProviderLocal
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := E.NewBuilder("autocert errors")
|
||||||
|
if cfg.Provider != ProviderLocal {
|
||||||
|
if len(cfg.Domains) == 0 {
|
||||||
|
b.Add(ErrMissingDomain)
|
||||||
|
}
|
||||||
|
if cfg.Email == "" {
|
||||||
|
b.Add(ErrMissingEmail)
|
||||||
|
}
|
||||||
|
for i, d := range cfg.Domains {
|
||||||
|
if !domainOrWildcardRE.MatchString(d) {
|
||||||
|
b.Add(ErrInvalidDomain.Subjectf("domains[%d]", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if provider is implemented
|
||||||
|
providerConstructor, ok := providersGenMap[cfg.Provider]
|
||||||
|
if !ok {
|
||||||
|
b.Add(ErrUnknownProvider.
|
||||||
|
Subject(cfg.Provider).
|
||||||
|
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
|
||||||
|
} else {
|
||||||
|
_, err := providerConstructor(cfg.Options)
|
||||||
|
if err != nil {
|
||||||
|
b.Add(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AutocertConfig) GetProvider() (*Provider, E.Error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = new(AutocertConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.CertPath == "" {
|
if cfg.CertPath == "" {
|
||||||
cfg.CertPath = CertFileDefault
|
cfg.CertPath = CertFileDefault
|
||||||
}
|
}
|
||||||
if cfg.KeyPath == "" {
|
if cfg.KeyPath == "" {
|
||||||
cfg.KeyPath = KeyFileDefault
|
cfg.KeyPath = KeyFileDefault
|
||||||
}
|
}
|
||||||
if cfg.Provider == "" {
|
if cfg.ACMEKeyPath == "" {
|
||||||
cfg.Provider = ProviderLocal
|
cfg.ACMEKeyPath = ACMEKeyFileDefault
|
||||||
}
|
}
|
||||||
return (*Config)(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) GetProvider() (*Provider, E.Error) {
|
var privKey *ecdsa.PrivateKey
|
||||||
b := E.NewBuilder("autocert errors")
|
var err error
|
||||||
|
|
||||||
if cfg.Provider != ProviderLocal {
|
if cfg.Provider != ProviderLocal {
|
||||||
if len(cfg.Domains) == 0 {
|
if privKey, err = cfg.loadACMEKey(); err != nil {
|
||||||
b.Add(ErrMissingDomain)
|
logging.Info().Err(err).Msg("load ACME private key failed")
|
||||||
|
logging.Info().Msg("generate new ACME private key")
|
||||||
|
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.New("generate ACME private key").With(err)
|
||||||
|
}
|
||||||
|
if err = cfg.saveACMEKey(privKey); err != nil {
|
||||||
|
return nil, E.New("save ACME private key").With(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if cfg.Provider == "" {
|
|
||||||
b.Add(ErrMissingProvider)
|
|
||||||
}
|
|
||||||
if cfg.Email == "" {
|
|
||||||
b.Add(ErrMissingEmail)
|
|
||||||
}
|
|
||||||
// check if provider is implemented
|
|
||||||
_, ok := providersGenMap[cfg.Provider]
|
|
||||||
if !ok {
|
|
||||||
b.Add(ErrUnknownProvider.
|
|
||||||
Subject(cfg.Provider).
|
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.HasError() {
|
|
||||||
return nil, b.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
b.Addf("generate private key: %w", err)
|
|
||||||
return nil, b.Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &User{
|
user := &User{
|
||||||
@@ -82,3 +129,19 @@ func (cfg *Config) GetProvider() (*Provider, E.Error) {
|
|||||||
legoCfg: legoCfg,
|
legoCfg: legoCfg,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *AutocertConfig) loadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||||
|
data, err := os.ReadFile(cfg.ACMEKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x509.ParseECPrivateKey(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *AutocertConfig) saveACMEKey(key *ecdsa.PrivateKey) error {
|
||||||
|
data, err := x509.MarshalECPrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(cfg.ACMEKeyPath, data, 0o600)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
"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/cloudflare"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||||
@@ -10,10 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
certBasePath = "certs/"
|
certBasePath = "certs/"
|
||||||
CertFileDefault = certBasePath + "cert.crt"
|
CertFileDefault = certBasePath + "cert.crt"
|
||||||
KeyFileDefault = certBasePath + "priv.key"
|
KeyFileDefault = certBasePath + "priv.key"
|
||||||
RegistrationFile = certBasePath + "registration.json"
|
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,7 +29,3 @@ var providersGenMap = map[string]ProviderGenerator{
|
|||||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
ErrGetCertFailure = errors.New("get certificate failed")
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package autocert
|
|
||||||
|
|
||||||
import "github.com/yusing/go-proxy/internal/logging"
|
|
||||||
|
|
||||||
var logger = logging.With().Str("module", "autocert").Logger()
|
|
||||||
@@ -3,6 +3,7 @@ package autocert
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -13,8 +14,8 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/challenge"
|
"github.com/go-acme/lego/v4/challenge"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
"github.com/yusing/go-proxy/internal/config/types"
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
@@ -22,19 +23,22 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Provider struct {
|
Provider struct {
|
||||||
cfg *Config
|
cfg *AutocertConfig
|
||||||
user *User
|
user *User
|
||||||
legoCfg *lego.Config
|
legoCfg *lego.Config
|
||||||
client *lego.Client
|
client *lego.Client
|
||||||
|
|
||||||
|
legoCert *certificate.Resource
|
||||||
tlsCert *tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
certExpiries CertExpiries
|
certExpiries CertExpiries
|
||||||
}
|
}
|
||||||
ProviderGenerator func(types.AutocertProviderOpt) (challenge.Provider, E.Error)
|
ProviderGenerator func(ProviderOpt) (challenge.Provider, E.Error)
|
||||||
|
|
||||||
CertExpiries map[string]time.Time
|
CertExpiries map[string]time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrGetCertFailure = errors.New("get certificate failed")
|
||||||
|
|
||||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if p.tlsCert == nil {
|
if p.tlsCert == nil {
|
||||||
return nil, ErrGetCertFailure
|
return nil, ErrGetCertFailure
|
||||||
@@ -75,14 +79,29 @@ func (p *Provider) ObtainCert() E.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := p.client
|
var cert *certificate.Resource
|
||||||
req := certificate.ObtainRequest{
|
var err error
|
||||||
Domains: p.cfg.Domains,
|
|
||||||
Bundle: true,
|
if p.legoCert != nil {
|
||||||
|
cert, err = p.client.Certificate.RenewWithOptions(*p.legoCert, &certificate.RenewOptions{
|
||||||
|
Bundle: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.legoCert = nil
|
||||||
|
logging.Err(err).Msg("cert renew failed, fallback to obtain")
|
||||||
|
} else {
|
||||||
|
p.legoCert = cert
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cert, err := client.Certificate.Obtain(req)
|
|
||||||
if err != nil {
|
if cert == nil {
|
||||||
return E.From(err)
|
cert, err = p.client.Certificate.Obtain(certificate.ObtainRequest{
|
||||||
|
Domains: p.cfg.Domains,
|
||||||
|
Bundle: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return E.From(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = p.saveCert(cert); err != nil {
|
if err = p.saveCert(cert); err != nil {
|
||||||
@@ -116,7 +135,7 @@ func (p *Provider) LoadCert() E.Error {
|
|||||||
p.tlsCert = &cert
|
p.tlsCert = &cert
|
||||||
p.certExpiries = expiries
|
p.certExpiries = expiries
|
||||||
|
|
||||||
logger.Info().Msgf("next renewal in %v", strutils.FormatDuration(time.Until(p.ShouldRenewOn())))
|
logging.Info().Msgf("next renewal in %v", strutils.FormatDuration(time.Until(p.ShouldRenewOn())))
|
||||||
return p.renewIfNeeded()
|
return p.renewIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,23 +148,37 @@ func (p *Provider) ShouldRenewOn() time.Time {
|
|||||||
panic("no certificate available")
|
panic("no certificate available")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ScheduleRenewal() {
|
func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
||||||
if p.GetName() == ProviderLocal {
|
if p.GetName() == ProviderLocal {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
task := task.GlobalTask("cert renew scheduler")
|
lastErrOn := time.Time{}
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
renewalTime := p.ShouldRenewOn()
|
||||||
defer ticker.Stop()
|
timer := time.NewTimer(time.Until(renewalTime))
|
||||||
defer task.Finish("cert renew scheduler stopped")
|
defer timer.Stop()
|
||||||
|
|
||||||
|
task := parent.Subtask("cert-renew-scheduler")
|
||||||
|
defer task.Finish(nil)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-task.Context().Done():
|
case <-task.Context().Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C: // check every 5 seconds
|
case <-timer.C:
|
||||||
if err := p.renewIfNeeded(); err != nil {
|
// Retry after 1 hour on failure
|
||||||
E.LogWarn("cert renew failed", err, &logger)
|
if !lastErrOn.IsZero() && time.Now().Before(lastErrOn.Add(time.Hour)) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if err := p.renewIfNeeded(); err != nil {
|
||||||
|
E.LogWarn("cert renew failed", err)
|
||||||
|
lastErrOn = time.Now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Reset on success
|
||||||
|
lastErrOn = time.Time{}
|
||||||
|
renewalTime = p.ShouldRenewOn()
|
||||||
|
timer.Reset(time.Until(renewalTime))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -176,12 +209,18 @@ func (p *Provider) registerACME() error {
|
|||||||
if p.user.Registration != nil {
|
if p.user.Registration != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if reg, err := p.client.Registration.ResolveAccountByKey(); err == nil {
|
||||||
|
p.user.Registration = reg
|
||||||
|
logging.Info().Msg("reused acme registration from private key")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.user.Registration = reg
|
p.user.Registration = reg
|
||||||
|
logging.Info().Interface("reg", reg).Msg("acme registered")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +266,7 @@ func (p *Provider) certState() CertState {
|
|||||||
sort.Strings(certDomains)
|
sort.Strings(certDomains)
|
||||||
|
|
||||||
if !reflect.DeepEqual(certDomains, wantedDomains) {
|
if !reflect.DeepEqual(certDomains, wantedDomains) {
|
||||||
logger.Info().Msgf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
|
logging.Info().Msgf("cert domains mismatch: %v != %v", certDomains, p.cfg.Domains)
|
||||||
return CertStateMismatch
|
return CertStateMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,17 +280,14 @@ func (p *Provider) renewIfNeeded() E.Error {
|
|||||||
|
|
||||||
switch p.certState() {
|
switch p.certState() {
|
||||||
case CertStateExpired:
|
case CertStateExpired:
|
||||||
logger.Info().Msg("certs expired, renewing")
|
logging.Info().Msg("certs expired, renewing")
|
||||||
case CertStateMismatch:
|
case CertStateMismatch:
|
||||||
logger.Info().Msg("cert domains mismatch with config, renewing")
|
logging.Info().Msg("cert domains mismatch with config, renewing")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.ObtainCert(); err != nil {
|
return p.ObtainCert()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
||||||
@@ -276,9 +312,9 @@ func providerGenerator[CT any, PT challenge.Provider](
|
|||||||
defaultCfg func() *CT,
|
defaultCfg func() *CT,
|
||||||
newProvider func(*CT) (PT, error),
|
newProvider func(*CT) (PT, error),
|
||||||
) ProviderGenerator {
|
) ProviderGenerator {
|
||||||
return func(opt types.AutocertProviderOpt) (challenge.Provider, E.Error) {
|
return func(opt ProviderOpt) (challenge.Provider, E.Error) {
|
||||||
cfg := defaultCfg()
|
cfg := defaultCfg()
|
||||||
err := U.Deserialize(opt, cfg)
|
err := U.Deserialize(opt, &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Provider) Setup() (err E.Error) {
|
func (p *Provider) Setup() (err E.Error) {
|
||||||
@@ -11,16 +13,14 @@ func (p *Provider) Setup() (err E.Error) {
|
|||||||
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
if !err.Is(os.ErrNotExist) { // ignore if cert doesn't exist
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Debug().Msg("obtaining cert due to error loading cert")
|
logging.Debug().Msg("obtaining cert due to error loading cert")
|
||||||
if err = p.ObtainCert(); err != nil {
|
if err = p.ObtainCert(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ScheduleRenewal()
|
|
||||||
|
|
||||||
for _, expiry := range p.GetExpiries() {
|
for _, expiry := range p.GetExpiries() {
|
||||||
logger.Info().Msg("certificate expire on " + expiry.String())
|
logging.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ type User struct {
|
|||||||
func (u *User) GetEmail() string {
|
func (u *User) GetEmail() string {
|
||||||
return u.Email
|
return u.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetRegistration() *registration.Resource {
|
func (u *User) GetRegistration() *registration.Resource {
|
||||||
return u.Registration
|
return u.Registration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
func (u *User) GetPrivateKey() crypto.PrivateKey {
|
||||||
return u.key
|
return u.key
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const (
|
|||||||
CommandDebugListEntries = "debug-ls-entries"
|
CommandDebugListEntries = "debug-ls-entries"
|
||||||
CommandDebugListProviders = "debug-ls-providers"
|
CommandDebugListProviders = "debug-ls-providers"
|
||||||
CommandDebugListMTrace = "debug-ls-mtrace"
|
CommandDebugListMTrace = "debug-ls-mtrace"
|
||||||
CommandDebugListTasks = "debug-ls-tasks"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var ValidCommands = []string{
|
var ValidCommands = []string{
|
||||||
@@ -35,7 +34,6 @@ var ValidCommands = []string{
|
|||||||
CommandDebugListEntries,
|
CommandDebugListEntries,
|
||||||
CommandDebugListProviders,
|
CommandDebugListProviders,
|
||||||
CommandDebugListMTrace,
|
CommandDebugListMTrace,
|
||||||
CommandDebugListTasks,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetArgs() Args {
|
func GetArgs() Args {
|
||||||
|
|||||||
@@ -13,21 +13,19 @@ const (
|
|||||||
// file, folder structure
|
// file, folder structure
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DotEnvPath = ".env"
|
DotEnvPath = ".env"
|
||||||
|
DotEnvExamplePath = ".env.example"
|
||||||
|
|
||||||
ConfigBasePath = "config"
|
ConfigBasePath = "config"
|
||||||
ConfigFileName = "config.yml"
|
ConfigFileName = "config.yml"
|
||||||
ConfigExampleFileName = "config.example.yml"
|
ConfigExampleFileName = "config.example.yml"
|
||||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||||
|
HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json"
|
||||||
JWTKeyPath = ConfigBasePath + "/jwt.key"
|
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
||||||
|
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
||||||
|
|
||||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||||
|
|
||||||
SchemaBasePath = "schema"
|
|
||||||
ConfigSchemaPath = SchemaBasePath + "/config.schema.json"
|
|
||||||
FileProviderSchemaPath = SchemaBasePath + "/providers.schema.json"
|
|
||||||
|
|
||||||
ComposeFileName = "compose.yml"
|
ComposeFileName = "compose.yml"
|
||||||
ComposeExampleFileName = "compose.example.yml"
|
ComposeExampleFileName = "compose.example.yml"
|
||||||
|
|
||||||
@@ -36,7 +34,6 @@ const (
|
|||||||
|
|
||||||
var RequiredDirectories = []string{
|
var RequiredDirectories = []string{
|
||||||
ConfigBasePath,
|
ConfigBasePath,
|
||||||
SchemaBasePath,
|
|
||||||
ErrorPagesBasePath,
|
ErrorPagesBasePath,
|
||||||
MiddlewareComposeBasePath,
|
MiddlewareComposeBasePath,
|
||||||
}
|
}
|
||||||
@@ -48,7 +45,7 @@ const (
|
|||||||
HealthCheckTimeoutDefault = 5 * time.Second
|
HealthCheckTimeoutDefault = 5 * time.Second
|
||||||
|
|
||||||
WakeTimeoutDefault = "30s"
|
WakeTimeoutDefault = "30s"
|
||||||
StopTimeoutDefault = "10s"
|
StopTimeoutDefault = "30s"
|
||||||
StopMethodDefault = "stop"
|
StopMethodDefault = "stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HashPassword(pwd string) []byte {
|
|
||||||
h := sha512.New()
|
|
||||||
h.Write([]byte(pwd))
|
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateJWTKey(size int) string {
|
|
||||||
bytes := make([]byte, size)
|
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
|
||||||
log.Panic().Err(err).Msg("failed to generate jwt key")
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeJWTKey(key string) []byte {
|
func decodeJWTKey(key string) []byte {
|
||||||
|
if key == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
bytes, err := base64.StdEncoding.DecodeString(key)
|
bytes, err := base64.StdEncoding.DecodeString(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic().Err(err).Msg("failed to decode jwt key")
|
log.Panic().Err(err).Msg("failed to decode jwt key")
|
||||||
|
|||||||
@@ -6,59 +6,91 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION", true)
|
prefixes = []string{"GODOXY_", "GOPROXY_", ""}
|
||||||
IsTest = GetEnvBool("GOPROXY_TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
|
||||||
IsDebug = GetEnvBool("GOPROXY_DEBUG", IsTest)
|
IsTest = GetEnvBool("TEST", false) || strings.HasSuffix(os.Args[0], ".test")
|
||||||
IsDebugSkipAuth = GetEnvBool("GOPROXY_DEBUG_SKIP_AUTH", false)
|
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||||
IsTrace = GetEnvBool("GOPROXY_TRACE", false) && IsDebug
|
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||||
|
IsProduction = !IsTest && !IsDebug
|
||||||
|
|
||||||
|
EnableLogStreaming = GetEnvBool("LOG_STREAMING", true)
|
||||||
|
DebugMemLogger = GetEnvBool("DEBUG_MEM_LOGGER", false) && EnableLogStreaming
|
||||||
|
|
||||||
ProxyHTTPAddr,
|
ProxyHTTPAddr,
|
||||||
ProxyHTTPHost,
|
ProxyHTTPHost,
|
||||||
ProxyHTTPPort,
|
ProxyHTTPPort,
|
||||||
ProxyHTTPURL = GetAddrEnv("GOPROXY_HTTP_ADDR", ":80", "http")
|
ProxyHTTPURL = GetAddrEnv("HTTP_ADDR", ":80", "http")
|
||||||
|
|
||||||
ProxyHTTPSAddr,
|
ProxyHTTPSAddr,
|
||||||
ProxyHTTPSHost,
|
ProxyHTTPSHost,
|
||||||
ProxyHTTPSPort,
|
ProxyHTTPSPort,
|
||||||
ProxyHTTPSURL = GetAddrEnv("GOPROXY_HTTPS_ADDR", ":443", "https")
|
ProxyHTTPSURL = GetAddrEnv("HTTPS_ADDR", ":443", "https")
|
||||||
|
|
||||||
APIHTTPAddr,
|
APIHTTPAddr,
|
||||||
APIHTTPHost,
|
APIHTTPHost,
|
||||||
APIHTTPPort,
|
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)))
|
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
||||||
APIUser = GetEnv("GOPROXY_API_USER", "admin")
|
|
||||||
APIPasswordHash = HashPassword(GetEnv("GOPROXY_API_PASSWORD", "password"))
|
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||||
|
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
||||||
|
APIUser = GetEnvString("API_USER", "admin")
|
||||||
|
APIPassword = GetEnvString("API_PASSWORD", "password")
|
||||||
|
|
||||||
|
// OIDC Configuration.
|
||||||
|
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
||||||
|
OIDCLogoutURL = GetEnvString("OIDC_LOGOUT_URL", "")
|
||||||
|
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
||||||
|
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||||
|
OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "")
|
||||||
|
OIDCScopes = GetEnvString("OIDC_SCOPES", "openid, profile, email")
|
||||||
|
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
||||||
|
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetEnvBool(key string, defaultValue bool) bool {
|
func GetEnv[T any](key string, defaultValue T, parser func(string) (T, error)) T {
|
||||||
value, ok := os.LookupEnv(key)
|
var value string
|
||||||
|
var ok bool
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
value, ok = os.LookupEnv(prefix + key)
|
||||||
|
if ok && value != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if !ok || value == "" {
|
if !ok || value == "" {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
b, err := strconv.ParseBool(value)
|
parsed, err := parser(value)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Fatal().Msgf("env %s: invalid boolean value: %s", key, value)
|
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 {
|
func GetEnvString(key string, defaultValue string) string {
|
||||||
value, ok := os.LookupEnv(key)
|
return GetEnv(key, defaultValue, func(s string) (string, error) {
|
||||||
if !ok || value == "" {
|
return s, nil
|
||||||
value = defaultValue
|
})
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
|
func GetEnvBool(key string, defaultValue bool) bool {
|
||||||
|
return GetEnv(key, defaultValue, strconv.ParseBool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) {
|
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)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
|
log.Fatal().Msgf("env %s: invalid address: %s", key, addr)
|
||||||
@@ -69,3 +101,11 @@ func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL str
|
|||||||
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
||||||
|
return GetEnv(key, defaultValue, time.ParseDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCommaSepEnv(key string, defaultValue string) []string {
|
||||||
|
return strutils.CommaSeperatedList(GetEnvString(key, defaultValue))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/api"
|
||||||
"github.com/yusing/go-proxy/internal/autocert"
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/config/types"
|
"github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/entrypoint"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/server"
|
||||||
"github.com/yusing/go-proxy/internal/notif"
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
|
||||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
"github.com/yusing/go-proxy/internal/watcher"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
value *types.Config
|
value *types.Config
|
||||||
providers F.Map[string, *proxy.Provider]
|
providers F.Map[string, *proxy.Provider]
|
||||||
autocertProvider *autocert.Provider
|
autocertProvider *autocert.Provider
|
||||||
task task.Task
|
entrypoint *entrypoint.Entrypoint
|
||||||
|
|
||||||
|
task *task.Task
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
instance *Config
|
instance *Config
|
||||||
cfgWatcher watcher.Watcher
|
cfgWatcher watcher.Watcher
|
||||||
logger = logging.With().Str("module", "config").Logger()
|
|
||||||
reloadMu sync.Mutex
|
reloadMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,15 +49,18 @@ Make sure you rename it back before next time you start.`
|
|||||||
You may run "ls-config" to show or dump the current config.`
|
You may run "ls-config" to show or dump the current config.`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Validate = types.Validate
|
||||||
|
|
||||||
func GetInstance() *Config {
|
func GetInstance() *Config {
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConfig() *Config {
|
func newConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
value: types.DefaultConfig(),
|
value: types.DefaultConfig(),
|
||||||
providers: F.NewMapOf[string, *proxy.Provider](),
|
providers: F.NewMapOf[string, *proxy.Provider](),
|
||||||
task: task.GlobalTask("config"),
|
entrypoint: entrypoint.NewEntrypoint(),
|
||||||
|
task: task.RootTask("config", false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,38 +73,32 @@ func Load() (*Config, E.Error) {
|
|||||||
return instance, instance.load()
|
return instance, instance.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Validate(data []byte) E.Error {
|
|
||||||
return U.ValidateYaml(U.GetSchema(common.ConfigSchemaPath), data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MatchDomains() []string {
|
func MatchDomains() []string {
|
||||||
return instance.value.MatchDomains
|
return instance.value.MatchDomains
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchChanges() {
|
func WatchChanges() {
|
||||||
task := task.GlobalTask("Config watcher")
|
t := task.RootTask("config_watcher", true)
|
||||||
eventQueue := events.NewEventQueue(
|
eventQueue := events.NewEventQueue(
|
||||||
task,
|
t,
|
||||||
configEventFlushInterval,
|
configEventFlushInterval,
|
||||||
OnConfigChange,
|
OnConfigChange,
|
||||||
func(err E.Error) {
|
func(err E.Error) {
|
||||||
E.LogError("config reload error", err, &logger)
|
E.LogError("config reload error", err)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
eventQueue.Start(cfgWatcher.Events(task.Context()))
|
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnConfigChange(flushTask task.Task, ev []events.Event) {
|
func OnConfigChange(ev []events.Event) {
|
||||||
defer flushTask.Finish("config reload complete")
|
|
||||||
|
|
||||||
// no matter how many events during the interval
|
// no matter how many events during the interval
|
||||||
// just reload once and check the last event
|
// just reload once and check the last event
|
||||||
switch ev[len(ev)-1].Action {
|
switch ev[len(ev)-1].Action {
|
||||||
case events.ActionFileRenamed:
|
case events.ActionFileRenamed:
|
||||||
logger.Warn().Msg(cfgRenameWarn)
|
logging.Warn().Msg(cfgRenameWarn)
|
||||||
return
|
return
|
||||||
case events.ActionFileDeleted:
|
case events.ActionFileDeleted:
|
||||||
logger.Warn().Msg(cfgDeleteWarn)
|
logging.Warn().Msg(cfgDeleteWarn)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,39 +116,96 @@ func Reload() E.Error {
|
|||||||
newCfg := newConfig()
|
newCfg := newConfig()
|
||||||
err := newCfg.load()
|
err := newCfg.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
newCfg.task.Finish(err)
|
||||||
|
return E.New("using last config").With(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel all current subtasks -> wait
|
// cancel all current subtasks -> wait
|
||||||
// -> replace config -> start new subtasks
|
// -> replace config -> start new subtasks
|
||||||
instance.task.Finish("config changed")
|
instance.task.Finish("config changed")
|
||||||
instance.task.Wait()
|
instance = newCfg
|
||||||
*instance = *newCfg
|
instance.Start(StartAllServers)
|
||||||
instance.StartProxyProviders()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Value() types.Config {
|
func (cfg *Config) Value() *types.Config {
|
||||||
return *instance.value
|
return instance.value
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAutoCertProvider() *autocert.Provider {
|
func (cfg *Config) Reload() E.Error {
|
||||||
|
return Reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) AutoCertProvider() *autocert.Provider {
|
||||||
return instance.autocertProvider
|
return instance.autocertProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) Task() task.Task {
|
func (cfg *Config) Task() *task.Task {
|
||||||
return cfg.task
|
return cfg.task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Context() context.Context {
|
||||||
|
return cfg.task.Context()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) Start(opts ...*StartServersOptions) {
|
||||||
|
cfg.StartAutoCert()
|
||||||
|
cfg.StartProxyProviders()
|
||||||
|
cfg.StartServers(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) StartAutoCert() {
|
||||||
|
autocert := cfg.autocertProvider
|
||||||
|
if autocert == nil {
|
||||||
|
logging.Info().Msg("autocert not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := autocert.Setup(); err != nil {
|
||||||
|
E.LogFatal("autocert setup error", err)
|
||||||
|
} else {
|
||||||
|
autocert.ScheduleRenewal(cfg.task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Config) StartProxyProviders() {
|
func (cfg *Config) StartProxyProviders() {
|
||||||
errs := cfg.providers.CollectErrorsParallel(
|
errs := cfg.providers.CollectErrorsParallel(
|
||||||
func(_ string, p *proxy.Provider) error {
|
func(_ string, p *proxy.Provider) error {
|
||||||
subtask := cfg.task.Subtask(p.String())
|
return p.Start(cfg.task)
|
||||||
return p.Start(subtask)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := E.Join(errs...); err != nil {
|
if err := E.Join(errs...); err != nil {
|
||||||
E.LogError("route provider errors", err, &logger)
|
E.LogError("route provider errors", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartServersOptions struct {
|
||||||
|
Proxy, API bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var StartAllServers = &StartServersOptions{true, true}
|
||||||
|
|
||||||
|
func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||||
|
if len(opts) == 0 {
|
||||||
|
opts = append(opts, &StartServersOptions{})
|
||||||
|
}
|
||||||
|
opt := opts[0]
|
||||||
|
if opt.Proxy {
|
||||||
|
server.StartServer(cfg.task, server.Options{
|
||||||
|
Name: "proxy",
|
||||||
|
CertProvider: cfg.AutoCertProvider(),
|
||||||
|
HTTPAddr: common.ProxyHTTPAddr,
|
||||||
|
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||||
|
Handler: cfg.entrypoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if opt.API {
|
||||||
|
server.StartServer(cfg.task, server.Options{
|
||||||
|
Name: "api",
|
||||||
|
CertProvider: cfg.AutoCertProvider(),
|
||||||
|
HTTPAddr: common.APIHTTPAddr,
|
||||||
|
Handler: api.NewHandler(cfg),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,56 +214,53 @@ func (cfg *Config) load() E.Error {
|
|||||||
|
|
||||||
data, err := os.ReadFile(common.ConfigPath)
|
data, err := os.ReadFile(common.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
E.LogFatal(errMsg, err, &logger)
|
E.LogFatal(errMsg, err)
|
||||||
}
|
|
||||||
|
|
||||||
if !common.NoSchemaValidation {
|
|
||||||
if err := Validate(data); err != nil {
|
|
||||||
E.LogFatal(errMsg, err, &logger)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model := types.DefaultConfig()
|
model := types.DefaultConfig()
|
||||||
if err := E.From(yaml.Unmarshal(data, model)); err != nil {
|
if err := utils.DeserializeYAML(data, model); err != nil {
|
||||||
E.LogFatal(errMsg, err, &logger)
|
E.LogFatal(errMsg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// errors are non fatal below
|
// errors are non fatal below
|
||||||
errs := E.NewBuilder(errMsg)
|
errs := E.NewBuilder(errMsg)
|
||||||
errs.Add(cfg.initNotification(model.Providers.Notification))
|
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
||||||
errs.Add(cfg.initAutoCert(&model.AutoCert))
|
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||||
|
cfg.initNotification(model.Providers.Notification)
|
||||||
|
errs.Add(cfg.initAutoCert(model.AutoCert))
|
||||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||||
|
|
||||||
cfg.value = model
|
cfg.value = model
|
||||||
route.SetFindMuxDomains(model.MatchDomains)
|
for i, domain := range model.MatchDomains {
|
||||||
|
if !strings.HasPrefix(domain, ".") {
|
||||||
|
model.MatchDomains[i] = "." + domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
||||||
|
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) initNotification(notifCfgMap types.NotificationConfigMap) (err E.Error) {
|
func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
||||||
if len(notifCfgMap) == 0 {
|
if len(notifCfg) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errs := E.NewBuilder("notification providers load errors")
|
dispatcher := notif.StartNotifDispatcher(cfg.task)
|
||||||
for name, notifCfg := range notifCfgMap {
|
for _, notifier := range notifCfg {
|
||||||
_, err := notif.RegisterProvider(cfg.task.Subtask(name), notifCfg)
|
dispatcher.RegisterProvider(¬ifier)
|
||||||
errs.Add(err)
|
|
||||||
}
|
}
|
||||||
return errs.Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) initAutoCert(autocertCfg *types.AutoCertConfig) (err E.Error) {
|
func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err E.Error) {
|
||||||
if cfg.autocertProvider != nil {
|
if cfg.autocertProvider != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.autocertProvider, err = autocert.NewConfig(autocertCfg).GetProvider()
|
cfg.autocertProvider, err = autocertCfg.GetProvider()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
|
func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
|
||||||
subtask := cfg.task.Subtask("load route providers")
|
|
||||||
defer subtask.Finish("done")
|
|
||||||
|
|
||||||
errs := E.NewBuilder("route provider errors")
|
errs := E.NewBuilder("route provider errors")
|
||||||
results := E.NewBuilder("loaded route providers")
|
results := E.NewBuilder("loaded route providers")
|
||||||
|
|
||||||
@@ -216,9 +271,9 @@ func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
|
|||||||
errs.Add(E.PrependSubject(filename, err))
|
errs.Add(E.PrependSubject(filename, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cfg.providers.Store(p.GetName(), p)
|
cfg.providers.Store(p.String(), p)
|
||||||
if len(p.GetName()) > lenLongestName {
|
if len(p.String()) > lenLongestName {
|
||||||
lenLongestName = len(p.GetName())
|
lenLongestName = len(p.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name, dockerHost := range providers.Docker {
|
for name, dockerHost := range providers.Docker {
|
||||||
@@ -227,17 +282,17 @@ func (cfg *Config) loadRouteProviders(providers *types.Providers) E.Error {
|
|||||||
errs.Add(E.PrependSubject(name, err))
|
errs.Add(E.PrependSubject(name, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cfg.providers.Store(p.GetName(), p)
|
cfg.providers.Store(p.String(), p)
|
||||||
if len(p.GetName()) > lenLongestName {
|
if len(p.String()) > lenLongestName {
|
||||||
lenLongestName = len(p.GetName())
|
lenLongestName = len(p.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||||
if err := p.LoadRoutes(); err != nil {
|
if err := p.LoadRoutes(); err != nil {
|
||||||
errs.Add(err.Subject(p.String()))
|
errs.Add(err.Subject(p.String()))
|
||||||
}
|
}
|
||||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.GetName(), p.NumRoutes())
|
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||||
})
|
})
|
||||||
logger.Info().Msg(results.String())
|
logging.Info().Msg(results.String())
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,151 +1,53 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
|
||||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
|
||||||
"github.com/yusing/go-proxy/internal/route"
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func DumpEntries() map[string]*entry.RawEntry {
|
func (cfg *Config) DumpRoutes() map[string]*route.Route {
|
||||||
entries := make(map[string]*entry.RawEntry)
|
entries := make(map[string]*route.Route)
|
||||||
instance.providers.RangeAll(func(_ string, p *proxy.Provider) {
|
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||||
p.RangeRoutes(func(alias string, r *route.Route) {
|
p.RangeRoutes(func(alias string, r *route.Route) {
|
||||||
entries[alias] = r.Entry
|
entries[alias] = r
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func DumpProviders() map[string]*proxy.Provider {
|
func (cfg *Config) DumpRouteProviders() map[string]*provider.Provider {
|
||||||
entries := make(map[string]*proxy.Provider)
|
entries := make(map[string]*provider.Provider)
|
||||||
instance.providers.RangeAll(func(name string, p *proxy.Provider) {
|
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||||
entries[name] = p
|
entries[p.ShortName()] = p
|
||||||
})
|
})
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomepageConfig() homepage.Config {
|
func (cfg *Config) RouteProviderList() []string {
|
||||||
var proto, port string
|
var list []string
|
||||||
domains := instance.value.MatchDomains
|
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||||
cert, _ := instance.autocertProvider.GetCert(nil)
|
list = append(list, p.ShortName())
|
||||||
if cert != nil {
|
|
||||||
proto = "https"
|
|
||||||
port = common.ProxyHTTPSPort
|
|
||||||
} else {
|
|
||||||
proto = "http"
|
|
||||||
port = common.ProxyHTTPPort
|
|
||||||
}
|
|
||||||
|
|
||||||
hpCfg := homepage.NewHomePageConfig()
|
|
||||||
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) {
|
|
||||||
en := r.Raw
|
|
||||||
item := en.Homepage
|
|
||||||
if item == nil {
|
|
||||||
item = new(homepage.Item)
|
|
||||||
item.Show = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !item.IsEmpty() {
|
|
||||||
item.Show = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !item.Show {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.Name == "" {
|
|
||||||
item.Name = strutils.Title(
|
|
||||||
strings.ReplaceAll(
|
|
||||||
strings.ReplaceAll(alias, "-", " "),
|
|
||||||
"_", " ",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.IsDocker(r) {
|
|
||||||
if item.Category == "" {
|
|
||||||
item.Category = "Docker"
|
|
||||||
}
|
|
||||||
item.SourceType = string(proxy.ProviderTypeDocker)
|
|
||||||
} else if entry.UseLoadBalance(r) {
|
|
||||||
if item.Category == "" {
|
|
||||||
item.Category = "Load-balanced"
|
|
||||||
}
|
|
||||||
item.SourceType = "loadbalancer"
|
|
||||||
} else {
|
|
||||||
if item.Category == "" {
|
|
||||||
item.Category = "Others"
|
|
||||||
}
|
|
||||||
item.SourceType = string(proxy.ProviderTypeFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.URL == "" {
|
|
||||||
if len(domains) > 0 {
|
|
||||||
item.URL = fmt.Sprintf("%s://%s.%s:%s", proto, strings.ToLower(alias), domains[0], port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.AltURL = r.TargetURL().String()
|
|
||||||
|
|
||||||
hpCfg.Add(item)
|
|
||||||
})
|
})
|
||||||
return hpCfg
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func RoutesByAlias(typeFilter ...route.RouteType) map[string]any {
|
func (cfg *Config) Statistics() map[string]any {
|
||||||
routes := make(map[string]any)
|
var rps, streams provider.RouteStats
|
||||||
if len(typeFilter) == 0 || typeFilter[0] == "" {
|
var total uint16
|
||||||
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
|
providerStats := make(map[string]provider.ProviderStats)
|
||||||
}
|
|
||||||
for _, t := range typeFilter {
|
|
||||||
switch t {
|
|
||||||
case route.RouteTypeReverseProxy:
|
|
||||||
route.GetReverseProxies().RangeAll(func(alias string, r *route.HTTPRoute) {
|
|
||||||
routes[alias] = r
|
|
||||||
})
|
|
||||||
case route.RouteTypeStream:
|
|
||||||
route.GetStreamProxies().RangeAll(func(alias string, r *route.StreamRoute) {
|
|
||||||
routes[alias] = r
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return routes
|
|
||||||
}
|
|
||||||
|
|
||||||
func Statistics() map[string]any {
|
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||||
nTotalStreams := 0
|
stats := p.Statistics()
|
||||||
nTotalRPs := 0
|
providerStats[p.ShortName()] = stats
|
||||||
providerStats := make(map[string]proxy.ProviderStats)
|
rps.AddOther(stats.RPs)
|
||||||
|
streams.AddOther(stats.Streams)
|
||||||
instance.providers.RangeAll(func(name string, p *proxy.Provider) {
|
total += stats.RPs.Total + stats.Streams.Total
|
||||||
providerStats[name] = p.Statistics()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, stats := range providerStats {
|
|
||||||
nTotalRPs += stats.NumRPs
|
|
||||||
nTotalStreams += stats.NumStreams
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"num_total_streams": nTotalStreams,
|
"total": total,
|
||||||
"num_total_reverse_proxies": nTotalRPs,
|
"reverse_proxies": rps,
|
||||||
"providers": providerStats,
|
"streams": streams,
|
||||||
|
"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
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
type (
|
|
||||||
AutoCertConfig struct {
|
|
||||||
Email string `json:"email,omitempty" yaml:"email"`
|
|
||||||
Domains []string `json:"domains,omitempty" yaml:",flow"`
|
|
||||||
CertPath string `json:"cert_path,omitempty" yaml:"cert_path"`
|
|
||||||
KeyPath string `json:"key_path,omitempty" yaml:"key_path"`
|
|
||||||
Provider string `json:"provider,omitempty" yaml:"provider"`
|
|
||||||
Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"`
|
|
||||||
}
|
|
||||||
AutocertProviderOpt map[string]any
|
|
||||||
)
|
|
||||||
@@ -1,25 +1,71 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/yusing/go-proxy/internal/autocert"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/notif"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
Providers Providers `json:"providers" yaml:",flow"`
|
AutoCert *autocert.AutocertConfig `json:"autocert"`
|
||||||
AutoCert AutoCertConfig `json:"autocert" yaml:",flow"`
|
Entrypoint Entrypoint `json:"entrypoint"`
|
||||||
ExplicitOnly bool `json:"explicit_only" yaml:"explicit_only"`
|
Providers Providers `json:"providers"`
|
||||||
MatchDomains []string `json:"match_domains" yaml:"match_domains"`
|
MatchDomains []string `json:"match_domains" validate:"domain_name"`
|
||||||
TimeoutShutdown int `json:"timeout_shutdown" yaml:"timeout_shutdown"`
|
Homepage HomepageConfig `json:"homepage"`
|
||||||
RedirectToHTTPS bool `json:"redirect_to_https" yaml:"redirect_to_https"`
|
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
Providers struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include"`
|
Files []string `json:"include" validate:"dive,filepath"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker"`
|
Docker map[string]string `json:"docker" validate:"dive,unix_addr|url"`
|
||||||
Notification NotificationConfigMap `json:"notification" yaml:"notification"`
|
Notification []notif.NotificationConfig `json:"notification"`
|
||||||
|
}
|
||||||
|
Entrypoint struct {
|
||||||
|
Middlewares []map[string]any `json:"middlewares"`
|
||||||
|
AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigInstance interface {
|
||||||
|
Value() *Config
|
||||||
|
Reload() E.Error
|
||||||
|
Statistics() map[string]any
|
||||||
|
RouteProviderList() []string
|
||||||
|
Context() context.Context
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Providers: Providers{},
|
|
||||||
TimeoutShutdown: 3,
|
TimeoutShutdown: 3,
|
||||||
RedirectToHTTPS: false,
|
Homepage: HomepageConfig{
|
||||||
|
UseDefaultCategories: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Validate(data []byte) E.Error {
|
||||||
|
var model Config
|
||||||
|
return utils.DeserializeYAML(data, &model)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utils.RegisterDefaultValueFactory(DefaultConfig)
|
||||||
|
utils.MustRegisterValidation("domain_name", func(fl validator.FieldLevel) bool {
|
||||||
|
domains := fl.Field().Interface().([]string)
|
||||||
|
for _, domain := range domains {
|
||||||
|
if !matchDomainsRegex.MatchString(domain) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
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"`
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import "github.com/yusing/go-proxy/internal/notif"
|
|
||||||
|
|
||||||
type NotificationConfigMap map[string]notif.ProviderConfig
|
|
||||||
@@ -12,11 +12,9 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Client = *SharedClient
|
|
||||||
SharedClient struct {
|
SharedClient struct {
|
||||||
*client.Client
|
*client.Client
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
clientMap F.Map[string, Client] = F.NewMapOf[string, Client]()
|
clientMap = make(map[string]*SharedClient, 5)
|
||||||
clientMapMu sync.Mutex
|
clientMapMu sync.Mutex
|
||||||
|
|
||||||
clientOptEnvHost = []client.Opt{
|
clientOptEnvHost = []client.Opt{
|
||||||
@@ -38,12 +36,15 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
task.GlobalTask("close docker clients").OnFinished("", func() {
|
task.OnProgramExit("docker_clients_cleanup", func() {
|
||||||
clientMap.RangeAllParallel(func(_ string, c Client) {
|
clientMapMu.Lock()
|
||||||
|
defer clientMapMu.Unlock()
|
||||||
|
|
||||||
|
for _, c := range clientMap {
|
||||||
if c.Connected() {
|
if c.Connected() {
|
||||||
c.Client.Close()
|
c.Client.Close()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,13 +53,10 @@ func (c *SharedClient) Connected() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the client is still referenced, this is no-op.
|
// if the client is still referenced, this is no-op.
|
||||||
func (c *SharedClient) Close() error {
|
func (c *SharedClient) Close() {
|
||||||
if !c.Connected() {
|
if c.Connected() {
|
||||||
return nil
|
c.refCount.Sub()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.refCount.Sub()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectClient creates a new Docker client connection to the specified host.
|
// ConnectClient creates a new Docker client connection to the specified host.
|
||||||
@@ -71,12 +69,11 @@ func (c *SharedClient) Close() error {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - Client: the Docker client connection.
|
// - Client: the Docker client connection.
|
||||||
// - error: an error if the connection failed.
|
// - error: an error if the connection failed.
|
||||||
func ConnectClient(host string) (Client, error) {
|
func ConnectClient(host string) (*SharedClient, error) {
|
||||||
clientMapMu.Lock()
|
clientMapMu.Lock()
|
||||||
defer clientMapMu.Unlock()
|
defer clientMapMu.Unlock()
|
||||||
|
|
||||||
// check if client exists
|
if client, ok := clientMap[host]; ok {
|
||||||
if client, ok := clientMap.Load(host); ok {
|
|
||||||
client.refCount.Add()
|
client.refCount.Add()
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
@@ -115,7 +112,6 @@ func ConnectClient(host string) (Client, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client, err := client.NewClientWithOpts(opt...)
|
client, err := client.NewClientWithOpts(opt...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -124,15 +120,17 @@ func ConnectClient(host string) (Client, error) {
|
|||||||
Client: client,
|
Client: client,
|
||||||
key: host,
|
key: host,
|
||||||
refCount: U.NewRefCounter(),
|
refCount: U.NewRefCounter(),
|
||||||
l: logger.With().Str("address", client.DaemonHost()).Logger(),
|
l: logging.With().Str("address", client.DaemonHost()).Logger(),
|
||||||
}
|
}
|
||||||
c.l.Trace().Msg("client connected")
|
c.l.Trace().Msg("client connected")
|
||||||
|
|
||||||
clientMap.Store(host, c)
|
clientMap[host] = c
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-c.refCount.Zero()
|
<-c.refCount.Zero()
|
||||||
clientMap.Delete(c.key)
|
clientMapMu.Lock()
|
||||||
|
delete(clientMap, c.key)
|
||||||
|
clientMapMu.Unlock()
|
||||||
|
|
||||||
if c.Connected() {
|
if c.Connected() {
|
||||||
c.Client.Close()
|
c.Client.Close()
|
||||||
|
|||||||
@@ -6,46 +6,54 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
PortMapping = map[string]types.Port
|
PortMapping = map[int]types.Port
|
||||||
Container struct {
|
Container struct {
|
||||||
_ U.NoCopy
|
_ U.NoCopy
|
||||||
|
|
||||||
DockerHost string `json:"docker_host" yaml:"-"`
|
DockerHost string `json:"docker_host"`
|
||||||
ContainerName string `json:"container_name" yaml:"-"`
|
ContainerName string `json:"container_name"`
|
||||||
ContainerID string `json:"container_id" yaml:"-"`
|
ContainerID string `json:"container_id"`
|
||||||
ImageName string `json:"image_name" yaml:"-"`
|
ImageName string `json:"image_name"`
|
||||||
|
|
||||||
Labels map[string]string `json:"labels" yaml:"-"`
|
Labels map[string]string `json:"-"`
|
||||||
|
|
||||||
PublicPortMapping PortMapping `json:"public_ports" yaml:"-"` // non-zero publicPort:types.Port
|
PublicPortMapping PortMapping `json:"public_ports"` // non-zero publicPort:types.Port
|
||||||
PrivatePortMapping PortMapping `json:"private_ports" yaml:"-"` // privatePort:types.Port
|
PrivatePortMapping PortMapping `json:"private_ports"` // privatePort:types.Port
|
||||||
PublicIP string `json:"public_ip" yaml:"-"`
|
PublicIP string `json:"public_ip"`
|
||||||
PrivateIP string `json:"private_ip" yaml:"-"`
|
PrivateIP string `json:"private_ip"`
|
||||||
NetworkMode string `json:"network_mode" yaml:"-"`
|
NetworkMode string `json:"network_mode"`
|
||||||
|
|
||||||
Aliases []string `json:"aliases" yaml:"-"`
|
Aliases []string `json:"aliases"`
|
||||||
IsExcluded bool `json:"is_excluded" yaml:"-"`
|
IsExcluded bool `json:"is_excluded"`
|
||||||
IsExplicit bool `json:"is_explicit" yaml:"-"`
|
IsExplicit bool `json:"is_explicit"`
|
||||||
IsDatabase bool `json:"is_database" yaml:"-"`
|
IsDatabase bool `json:"is_database"`
|
||||||
IdleTimeout string `json:"idle_timeout,omitempty" yaml:"-"`
|
IdleTimeout string `json:"idle_timeout,omitempty"`
|
||||||
WakeTimeout string `json:"wake_timeout,omitempty" yaml:"-"`
|
WakeTimeout string `json:"wake_timeout,omitempty"`
|
||||||
StopMethod string `json:"stop_method,omitempty" yaml:"-"`
|
StopMethod string `json:"stop_method,omitempty"`
|
||||||
StopTimeout string `json:"stop_timeout,omitempty" yaml:"-"` // stop_method = "stop" only
|
StopTimeout string `json:"stop_timeout,omitempty"` // stop_method = "stop" only
|
||||||
StopSignal string `json:"stop_signal,omitempty" yaml:"-"` // stop_method = "stop" | "kill" only
|
StopSignal string `json:"stop_signal,omitempty"` // stop_method = "stop" | "kill" only
|
||||||
Running bool `json:"running" yaml:"-"`
|
StartEndpoint string `json:"start_endpoint,omitempty"`
|
||||||
|
Running bool `json:"running"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var DummyContainer = new(Container)
|
var DummyContainer = new(Container)
|
||||||
|
|
||||||
func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
||||||
isExplicit := c.Labels[LabelAliases] != ""
|
isExplicit := false
|
||||||
helper := containerHelper{c}
|
helper := containerHelper{c}
|
||||||
|
for lbl := range c.Labels {
|
||||||
|
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||||
|
isExplicit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
res = &Container{
|
res = &Container{
|
||||||
DockerHost: dockerHost,
|
DockerHost: dockerHost,
|
||||||
ContainerName: helper.getName(),
|
ContainerName: helper.getName(),
|
||||||
@@ -58,16 +66,17 @@ func FromDocker(c *types.Container, dockerHost string) (res *Container) {
|
|||||||
PrivatePortMapping: helper.getPrivatePortMapping(),
|
PrivatePortMapping: helper.getPrivatePortMapping(),
|
||||||
NetworkMode: c.HostConfig.NetworkMode,
|
NetworkMode: c.HostConfig.NetworkMode,
|
||||||
|
|
||||||
Aliases: helper.getAliases(),
|
Aliases: helper.getAliases(),
|
||||||
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
||||||
IsExplicit: isExplicit,
|
IsExplicit: isExplicit,
|
||||||
IsDatabase: helper.isDatabase(),
|
IsDatabase: helper.isDatabase(),
|
||||||
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
|
IdleTimeout: helper.getDeleteLabel(LabelIdleTimeout),
|
||||||
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
|
WakeTimeout: helper.getDeleteLabel(LabelWakeTimeout),
|
||||||
StopMethod: helper.getDeleteLabel(LabelStopMethod),
|
StopMethod: helper.getDeleteLabel(LabelStopMethod),
|
||||||
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
|
StopTimeout: helper.getDeleteLabel(LabelStopTimeout),
|
||||||
StopSignal: helper.getDeleteLabel(LabelStopSignal),
|
StopSignal: helper.getDeleteLabel(LabelStopSignal),
|
||||||
Running: c.Status == "running" || c.State == "running",
|
StartEndpoint: helper.getDeleteLabel(LabelStartEndpoint),
|
||||||
|
Running: c.Status == "running" || c.State == "running",
|
||||||
}
|
}
|
||||||
res.setPrivateIP(helper)
|
res.setPrivateIP(helper)
|
||||||
res.setPublicIP()
|
res.setPublicIP()
|
||||||
@@ -120,7 +129,7 @@ func (c *Container) setPublicIP() {
|
|||||||
}
|
}
|
||||||
url, err := url.Parse(c.DockerHost)
|
url, err := url.Parse(c.DockerHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost)
|
logging.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost)
|
||||||
c.PublicIP = "127.0.0.1"
|
c.PublicIP = "127.0.0.1"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ func (c containerHelper) getName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c containerHelper) getImageName() string {
|
func (c containerHelper) getImageName() string {
|
||||||
colonSep := strings.Split(c.Image, ":")
|
colonSep := strutils.SplitRune(c.Image, ':')
|
||||||
slashSep := strings.Split(colonSep[0], "/")
|
slashSep := strutils.SplitRune(colonSep[0], '/')
|
||||||
return slashSep[len(slashSep)-1]
|
return slashSep[len(slashSep)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
|
|||||||
if v.PublicPort == 0 {
|
if v.PublicPort == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res[strutils.PortString(v.PublicPort)] = v
|
res[int(v.PublicPort)] = v
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
|
|||||||
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
||||||
res := make(PortMapping)
|
res := make(PortMapping)
|
||||||
for _, v := range c.Ports {
|
for _, v := range c.Ports {
|
||||||
res[strutils.PortString(v.PrivatePort)] = v
|
res[int(v.PrivatePort)] = v
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -66,14 +66,6 @@ var databaseMPs = map[string]struct{}{
|
|||||||
"/var/lib/rabbitmq": {},
|
"/var/lib/rabbitmq": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
var databasePrivPorts = map[uint16]struct{}{
|
|
||||||
5432: {}, // postgres
|
|
||||||
3306: {}, // mysql, mariadb
|
|
||||||
6379: {}, // redis
|
|
||||||
11211: {}, // memcached
|
|
||||||
27017: {}, // mongodb
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c containerHelper) isDatabase() bool {
|
func (c containerHelper) isDatabase() bool {
|
||||||
for _, m := range c.Mounts {
|
for _, m := range c.Mounts {
|
||||||
if _, ok := databaseMPs[m.Destination]; ok {
|
if _, ok := databaseMPs[m.Destination]; ok {
|
||||||
@@ -82,7 +74,9 @@ func (c containerHelper) isDatabase() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range c.Ports {
|
for _, v := range c.Ports {
|
||||||
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
|
switch v.PrivatePort {
|
||||||
|
// postgres, mysql or mariadb, redis, memcached, mongodb
|
||||||
|
case 5432, 3306, 6379, 11211, 27017:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
internal/docker/container_test.go
Normal file
43
internal/docker/container_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainerExplicit(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
labels map[string]string
|
||||||
|
isExplicit bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "explicit",
|
||||||
|
labels: map[string]string{
|
||||||
|
"proxy.aliases": "foo",
|
||||||
|
},
|
||||||
|
isExplicit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit2",
|
||||||
|
labels: map[string]string{
|
||||||
|
"proxy.idle_timeout": "1s",
|
||||||
|
},
|
||||||
|
isExplicit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not explicit",
|
||||||
|
labels: map[string]string{},
|
||||||
|
isExplicit: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := FromDocker(&types.Container{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
||||||
|
ExpectEqual(t, c.IsExplicit, tt.isExplicit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package idlewatcher
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ var loadingPage []byte
|
|||||||
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage)))
|
||||||
|
|
||||||
func (w *Watcher) makeLoadingPageBody() []byte {
|
func (w *Watcher) makeLoadingPageBody() []byte {
|
||||||
msg := fmt.Sprintf("%s is starting...", w.ContainerName)
|
msg := w.ContainerName + " is starting..."
|
||||||
|
|
||||||
data := new(templateData)
|
data := new(templateData)
|
||||||
data.CheckRedirectHeader = common.HeaderCheckRedirect
|
data.CheckRedirectHeader = common.HeaderCheckRedirect
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/docker"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
@@ -10,11 +12,12 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Config struct {
|
Config struct {
|
||||||
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
|
IdleTimeout time.Duration `json:"idle_timeout,omitempty"`
|
||||||
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
|
WakeTimeout time.Duration `json:"wake_timeout,omitempty"`
|
||||||
StopTimeout int `json:"stop_timeout,omitempty"` // docker api takes integer seconds for timeout argument
|
StopTimeout int `json:"stop_timeout,omitempty"` // docker api takes integer seconds for timeout argument
|
||||||
StopMethod StopMethod `json:"stop_method,omitempty"`
|
StopMethod StopMethod `json:"stop_method,omitempty"`
|
||||||
StopSignal Signal `json:"stop_signal,omitempty"`
|
StopSignal Signal `json:"stop_signal,omitempty"`
|
||||||
|
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
|
||||||
|
|
||||||
DockerHost string `json:"docker_host,omitempty"`
|
DockerHost string `json:"docker_host,omitempty"`
|
||||||
ContainerName string `json:"container_name,omitempty"`
|
ContainerName string `json:"container_name,omitempty"`
|
||||||
@@ -31,6 +34,12 @@ const (
|
|||||||
StopMethodKill StopMethod = "kill"
|
StopMethodKill StopMethod = "kill"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var validSignals = map[string]struct{}{
|
||||||
|
"": {},
|
||||||
|
"SIGINT": {}, "SIGTERM": {}, "SIGHUP": {}, "SIGQUIT": {},
|
||||||
|
"INT": {}, "TERM": {}, "HUP": {}, "QUIT": {},
|
||||||
|
}
|
||||||
|
|
||||||
func ValidateConfig(cont *docker.Container) (*Config, E.Error) {
|
func ValidateConfig(cont *docker.Container) (*Config, E.Error) {
|
||||||
if cont == nil {
|
if cont == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -52,17 +61,19 @@ func ValidateConfig(cont *docker.Container) (*Config, E.Error) {
|
|||||||
stopTimeout := E.Collect(errs, validateDurationPostitive, cont.StopTimeout)
|
stopTimeout := E.Collect(errs, validateDurationPostitive, cont.StopTimeout)
|
||||||
stopMethod := E.Collect(errs, validateStopMethod, cont.StopMethod)
|
stopMethod := E.Collect(errs, validateStopMethod, cont.StopMethod)
|
||||||
signal := E.Collect(errs, validateSignal, cont.StopSignal)
|
signal := E.Collect(errs, validateSignal, cont.StopSignal)
|
||||||
|
startEndpoint := E.Collect(errs, validateStartEndpoint, cont.StartEndpoint)
|
||||||
|
|
||||||
if errs.HasError() {
|
if errs.HasError() {
|
||||||
return nil, errs.Error()
|
return nil, errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
IdleTimeout: idleTimeout,
|
IdleTimeout: idleTimeout,
|
||||||
WakeTimeout: wakeTimeout,
|
WakeTimeout: wakeTimeout,
|
||||||
StopTimeout: int(stopTimeout.Seconds()),
|
StopTimeout: int(stopTimeout.Seconds()),
|
||||||
StopMethod: stopMethod,
|
StopMethod: stopMethod,
|
||||||
StopSignal: signal,
|
StopSignal: signal,
|
||||||
|
StartEndpoint: startEndpoint,
|
||||||
|
|
||||||
DockerHost: cont.DockerHost,
|
DockerHost: cont.DockerHost,
|
||||||
ContainerName: cont.ContainerName,
|
ContainerName: cont.ContainerName,
|
||||||
@@ -83,12 +94,9 @@ func validateDurationPostitive(value string) (time.Duration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateSignal(s string) (Signal, error) {
|
func validateSignal(s string) (Signal, error) {
|
||||||
switch s {
|
if _, ok := validSignals[s]; ok {
|
||||||
case "", "SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT",
|
|
||||||
"INT", "TERM", "HUP", "QUIT":
|
|
||||||
return Signal(s), nil
|
return Signal(s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("invalid signal " + s)
|
return "", errors.New("invalid signal " + s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,3 +109,21 @@ func validateStopMethod(s string) (StopMethod, error) {
|
|||||||
return "", errors.New("invalid stop method " + s)
|
return "", errors.New("invalid stop method " + s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateStartEndpoint(s string) (string, error) {
|
||||||
|
if s == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// checks needed as of Go 1.6 because of change https://github.com/golang/go/commit/617c93ce740c3c3cc28cdd1a0d712be183d0b328#diff-6c2d018290e298803c0c9419d8739885L195
|
||||||
|
// emulate browser and strip the '#' suffix prior to validation. see issue-#237
|
||||||
|
if i := strings.Index(s, "#"); i > -1 {
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
if len(s) == 0 {
|
||||||
|
return "", errors.New("start endpoint must not be empty if defined")
|
||||||
|
}
|
||||||
|
if _, err := url.ParseRequestURI(s); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|||||||
47
internal/docker/idlewatcher/types/config_test.go
Normal file
47
internal/docker/idlewatcher/types/config_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateStartEndpoint(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
input: "/start",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
input: "../foo",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single fragment",
|
||||||
|
input: "#",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
s, err := validateStartEndpoint(tc.input)
|
||||||
|
if err == nil {
|
||||||
|
ExpectEqual(t, s, tc.input)
|
||||||
|
}
|
||||||
|
if (err != nil) != tc.wantErr {
|
||||||
|
t.Errorf("validateStartEndpoint() error = %v, wantErr %t", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ type Waker interface {
|
|||||||
health.HealthMonitor
|
health.HealthMonitor
|
||||||
http.Handler
|
http.Handler
|
||||||
net.Stream
|
net.Stream
|
||||||
|
Wake() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,32 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
"github.com/yusing/go-proxy/internal/metrics"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/reverseproxy"
|
||||||
net "github.com/yusing/go-proxy/internal/net/types"
|
net "github.com/yusing/go-proxy/internal/net/types"
|
||||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
|
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
type waker struct {
|
type (
|
||||||
_ U.NoCopy
|
Waker = types.Waker
|
||||||
|
waker struct {
|
||||||
|
_ U.NoCopy
|
||||||
|
|
||||||
rp *gphttp.ReverseProxy
|
rp *reverseproxy.ReverseProxy
|
||||||
stream net.Stream
|
stream net.Stream
|
||||||
hc health.HealthChecker
|
hc health.HealthChecker
|
||||||
|
metric *metrics.Gauge
|
||||||
|
|
||||||
ready atomic.Bool
|
ready atomic.Bool
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
idleWakerCheckInterval = 100 * time.Millisecond
|
idleWakerCheckInterval = 100 * time.Millisecond
|
||||||
@@ -31,48 +38,63 @@ const (
|
|||||||
|
|
||||||
// TODO: support stream
|
// TODO: support stream
|
||||||
|
|
||||||
func newWaker(providerSubTask task.Task, entry entry.Entry, rp *gphttp.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
||||||
hcCfg := entry.HealthCheckConfig()
|
hcCfg := route.HealthCheckConfig()
|
||||||
hcCfg.Timeout = idleWakerCheckTimeout
|
hcCfg.Timeout = idleWakerCheckTimeout
|
||||||
|
|
||||||
waker := &waker{
|
waker := &waker{
|
||||||
rp: rp,
|
rp: rp,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
}
|
}
|
||||||
|
task := parent.Subtask("idlewatcher." + route.TargetName())
|
||||||
watcher, err := registerWatcher(providerSubTask, entry, waker)
|
watcher, err := registerWatcher(task, route, waker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Errorf("register watcher: %w", err)
|
return nil, E.Errorf("register watcher: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rp != nil {
|
switch {
|
||||||
waker.hc = health.NewHTTPHealthChecker(entry.TargetURL(), hcCfg, rp.Transport)
|
case rp != nil:
|
||||||
} else if stream != nil {
|
waker.hc = monitor.NewHTTPHealthChecker(route.TargetURL(), hcCfg)
|
||||||
waker.hc = health.NewRawHealthChecker(entry.TargetURL(), hcCfg)
|
case stream != nil:
|
||||||
} else {
|
waker.hc = monitor.NewRawHealthChecker(route.TargetURL(), hcCfg)
|
||||||
|
default:
|
||||||
panic("both nil")
|
panic("both nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if common.PrometheusEnabled {
|
||||||
|
m := metrics.GetServiceMetrics()
|
||||||
|
fqn := parent.Name() + "/" + route.TargetName()
|
||||||
|
waker.metric = m.HealthStatus.With(metrics.HealthMetricLabels(fqn))
|
||||||
|
waker.metric.Set(float64(watcher.Status()))
|
||||||
|
}
|
||||||
return watcher, nil
|
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) {
|
func NewHTTPWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy) (Waker, E.Error) {
|
||||||
return newWaker(providerSubTask, entry, rp, nil)
|
return newWaker(parent, route, rp, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStreamWaker(providerSubTask task.Task, entry entry.Entry, stream net.Stream) (Waker, E.Error) {
|
func NewStreamWaker(parent task.Parent, route route.Route, stream net.Stream) (Waker, E.Error) {
|
||||||
return newWaker(providerSubTask, entry, nil, stream)
|
return newWaker(parent, route, nil, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start implements health.HealthMonitor.
|
// Start implements health.HealthMonitor.
|
||||||
func (w *Watcher) Start(routeSubTask task.Task) E.Error {
|
func (w *Watcher) Start(parent task.Parent) E.Error {
|
||||||
routeSubTask.Finish("ignored")
|
w.task.OnCancel("route_cleanup", func() {
|
||||||
w.task.OnCancel("stop route", func() {
|
parent.Finish(w.task.FinishCause())
|
||||||
routeSubTask.Parent().Finish(w.task.FinishCause())
|
if w.metric != nil {
|
||||||
|
w.metric.Reset()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Task implements health.HealthMonitor.
|
||||||
|
func (w *Watcher) Task() *task.Task {
|
||||||
|
return w.task
|
||||||
|
}
|
||||||
|
|
||||||
// Finish implements health.HealthMonitor.
|
// Finish implements health.HealthMonitor.
|
||||||
func (w *Watcher) Finish(reason any) {
|
func (w *Watcher) Finish(reason any) {
|
||||||
if w.stream != nil {
|
if w.stream != nil {
|
||||||
@@ -95,8 +117,21 @@ func (w *Watcher) Uptime() time.Duration {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Latency implements health.HealthMonitor.
|
||||||
|
func (w *Watcher) Latency() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Status implements health.HealthMonitor.
|
// Status implements health.HealthMonitor.
|
||||||
func (w *Watcher) Status() health.Status {
|
func (w *Watcher) Status() health.Status {
|
||||||
|
status := w.getStatusUpdateReady()
|
||||||
|
if w.metric != nil {
|
||||||
|
w.metric.Set(float64(status))
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) getStatusUpdateReady() health.Status {
|
||||||
if !w.ContainerRunning {
|
if !w.ContainerRunning {
|
||||||
return health.StatusNapping
|
return health.StatusNapping
|
||||||
}
|
}
|
||||||
@@ -105,12 +140,12 @@ func (w *Watcher) Status() health.Status {
|
|||||||
return health.StatusHealthy
|
return health.StatusHealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
healthy, _, err := w.hc.CheckHealth()
|
result, err := w.hc.CheckHealth()
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
w.ready.Store(false)
|
w.ready.Store(false)
|
||||||
return health.StatusError
|
return health.StatusError
|
||||||
case healthy:
|
case result.Healthy:
|
||||||
w.ready.Store(true)
|
w.ready.Store(true)
|
||||||
return health.StatusHealthy
|
return health.StatusHealthy
|
||||||
default:
|
default:
|
||||||
@@ -120,11 +155,11 @@ func (w *Watcher) Status() health.Status {
|
|||||||
|
|
||||||
// MarshalJSON implements health.HealthMonitor.
|
// MarshalJSON implements health.HealthMonitor.
|
||||||
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
||||||
var url net.URL
|
var url *net.URL
|
||||||
if w.hc.URL().Port() != "0" {
|
if w.hc.URL().Port() != "0" {
|
||||||
url = w.hc.URL()
|
url = w.hc.URL()
|
||||||
}
|
}
|
||||||
return (&health.JSONRepresentation{
|
return (&monitor.JSONRepresentation{
|
||||||
Name: w.Name(),
|
Name: w.Name(),
|
||||||
Status: w.Status(),
|
Status: w.Status(),
|
||||||
Config: w.hc.Config(),
|
Config: w.hc.Config(),
|
||||||
|
|||||||
@@ -12,7 +12,22 @@ import (
|
|||||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler
|
type ForceCacheControl struct {
|
||||||
|
expires string
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ForceCacheControl) WriteHeader(code int) {
|
||||||
|
f.ResponseWriter.Header().Set("Cache-Control", "must-revalidate")
|
||||||
|
f.ResponseWriter.Header().Set("Expires", f.expires)
|
||||||
|
f.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ForceCacheControl) Unwrap() http.ResponseWriter {
|
||||||
|
return f.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements http.Handler.
|
||||||
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||||
shouldNext := w.wakeFromHTTP(rw, r)
|
shouldNext := w.wakeFromHTTP(rw, r)
|
||||||
if !shouldNext {
|
if !shouldNext {
|
||||||
@@ -22,7 +37,8 @@ func (w *Watcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
|||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
w.rp.ServeHTTP(rw, r)
|
f := &ForceCacheControl{expires: w.expires().Format(http.TimeFormat), ResponseWriter: rw}
|
||||||
|
w.rp.ServeHTTP(f, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +50,12 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if start endpoint is configured and request path matches
|
||||||
|
if w.StartEndpoint != "" && r.URL.Path != w.StartEndpoint {
|
||||||
|
http.Error(rw, "Forbidden: Container can only be started via configured start endpoint", http.StatusForbidden)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if r.Body != nil {
|
if r.Body != nil {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
}
|
}
|
||||||
@@ -47,7 +69,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
|||||||
body := w.makeLoadingPageBody()
|
body := w.makeLoadingPageBody()
|
||||||
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
rw.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||||
rw.Header().Add("Cache-Control", "no-cache")
|
rw.Header().Set("Cache-Control", "no-cache")
|
||||||
rw.Header().Add("Cache-Control", "no-store")
|
rw.Header().Add("Cache-Control", "no-store")
|
||||||
rw.Header().Add("Cache-Control", "must-revalidate")
|
rw.Header().Add("Cache-Control", "must-revalidate")
|
||||||
rw.Header().Add("Connection", "close")
|
rw.Header().Add("Connection", "close")
|
||||||
@@ -81,7 +103,7 @@ func (w *Watcher) wakeFromHTTP(rw http.ResponseWriter, r *http.Request) (shouldN
|
|||||||
w.WakeTrace().Msg("signal received")
|
w.WakeTrace().Msg("signal received")
|
||||||
err := w.wakeIfStopped()
|
err := w.wakeIfStopped()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WakeError(err).Send()
|
w.WakeError(err)
|
||||||
http.Error(rw, "Error waking container", http.StatusInternalServerError)
|
http.Error(rw, "Error waking container", http.StatusInternalServerError)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (w *Watcher) Accept() (conn types.StreamConn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if wakeErr := w.wakeFromStream(); wakeErr != nil {
|
if wakeErr := w.wakeFromStream(); wakeErr != nil {
|
||||||
w.WakeError(wakeErr).Msg("error waking from stream")
|
w.WakeError(wakeErr)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ func (w *Watcher) wakeFromStream() error {
|
|||||||
wakeErr := w.wakeIfStopped()
|
wakeErr := w.wakeIfStopped()
|
||||||
if wakeErr != nil {
|
if wakeErr != nil {
|
||||||
wakeErr = fmt.Errorf("%s failed: %w", w.String(), wakeErr)
|
wakeErr = fmt.Errorf("%s failed: %w", w.String(), wakeErr)
|
||||||
w.WakeError(wakeErr).Msg("wake failed")
|
w.WakeError(wakeErr)
|
||||||
return wakeErr
|
return wakeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import (
|
|||||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/proxy/entry"
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||||
"github.com/yusing/go-proxy/internal/watcher"
|
"github.com/yusing/go-proxy/internal/watcher"
|
||||||
W "github.com/yusing/go-proxy/internal/watcher"
|
|
||||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,10 +29,11 @@ type (
|
|||||||
*idlewatcher.Config
|
*idlewatcher.Config
|
||||||
*waker
|
*waker
|
||||||
|
|
||||||
client D.Client
|
client *D.SharedClient
|
||||||
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
|
stopByMethod StopCallback // send a docker command w.r.t. `stop_method`
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
task task.Task
|
lastReset time.Time
|
||||||
|
task *task.Task
|
||||||
}
|
}
|
||||||
|
|
||||||
WakeDone <-chan error
|
WakeDone <-chan error
|
||||||
@@ -45,16 +45,16 @@ var (
|
|||||||
watcherMap = F.NewMapOf[string, *Watcher]()
|
watcherMap = F.NewMapOf[string, *Watcher]()
|
||||||
watcherMapMu sync.Mutex
|
watcherMapMu sync.Mutex
|
||||||
|
|
||||||
logger = logging.With().Str("module", "idle_watcher").Logger()
|
errShouldNotReachHere = errors.New("should not reach here")
|
||||||
)
|
)
|
||||||
|
|
||||||
const dockerReqTimeout = 3 * time.Second
|
const dockerReqTimeout = 3 * time.Second
|
||||||
|
|
||||||
func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker) (*Watcher, error) {
|
func registerWatcher(watcherTask *task.Task, route route.Route, waker *waker) (*Watcher, error) {
|
||||||
cfg := entry.IdlewatcherConfig()
|
cfg := route.IdlewatcherConfig()
|
||||||
|
|
||||||
if cfg.IdleTimeout == 0 {
|
if cfg.IdleTimeout == 0 {
|
||||||
panic("should not reach here")
|
panic(errShouldNotReachHere)
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherMapMu.Lock()
|
watcherMapMu.Lock()
|
||||||
@@ -66,7 +66,7 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
|||||||
w.Config = cfg
|
w.Config = cfg
|
||||||
w.waker = waker
|
w.waker = waker
|
||||||
w.resetIdleTimer()
|
w.resetIdleTimer()
|
||||||
providerSubtask.Finish("used existing watcher")
|
watcherTask.Finish("used existing watcher")
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,11 +76,11 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
|||||||
}
|
}
|
||||||
|
|
||||||
w := &Watcher{
|
w := &Watcher{
|
||||||
Logger: logger.With().Str("name", cfg.ContainerName).Logger(),
|
Logger: logging.With().Str("name", cfg.ContainerName).Logger(),
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
waker: waker,
|
waker: waker,
|
||||||
client: client,
|
client: client,
|
||||||
task: providerSubtask,
|
task: watcherTask,
|
||||||
ticker: time.NewTicker(cfg.IdleTimeout),
|
ticker: time.NewTicker(cfg.IdleTimeout),
|
||||||
}
|
}
|
||||||
w.stopByMethod = w.getStopCallback()
|
w.stopByMethod = w.getStopCallback()
|
||||||
@@ -99,17 +99,23 @@ func registerWatcher(providerSubtask task.Task, entry entry.Entry, waker *waker)
|
|||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) Wake() error {
|
||||||
|
return w.wakeIfStopped()
|
||||||
|
}
|
||||||
|
|
||||||
// WakeDebug logs a debug message related to waking the container.
|
// WakeDebug logs a debug message related to waking the container.
|
||||||
func (w *Watcher) WakeDebug() *zerolog.Event {
|
func (w *Watcher) WakeDebug() *zerolog.Event {
|
||||||
|
//nolint:zerologlint
|
||||||
return w.Debug().Str("action", "wake")
|
return w.Debug().Str("action", "wake")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) WakeTrace() *zerolog.Event {
|
func (w *Watcher) WakeTrace() *zerolog.Event {
|
||||||
|
//nolint:zerologlint
|
||||||
return w.Trace().Str("action", "wake")
|
return w.Trace().Str("action", "wake")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) WakeError(err error) *zerolog.Event {
|
func (w *Watcher) WakeError(err error) {
|
||||||
return w.Err(err).Str("action", "wake")
|
w.Err(err).Str("action", "wake").Msg("error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) LogReason(action, reason string) {
|
func (w *Watcher) LogReason(action, reason string) {
|
||||||
@@ -174,7 +180,7 @@ func (w *Watcher) wakeIfStopped() error {
|
|||||||
case "running":
|
case "running":
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
panic("should not reach here")
|
return E.Errorf("unexpected container status: %s", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +194,7 @@ func (w *Watcher) getStopCallback() StopCallback {
|
|||||||
case idlewatcher.StopMethodKill:
|
case idlewatcher.StopMethodKill:
|
||||||
cb = w.containerKill
|
cb = w.containerKill
|
||||||
default:
|
default:
|
||||||
panic("should not reach here")
|
panic(errShouldNotReachHere)
|
||||||
}
|
}
|
||||||
return func() error {
|
return func() error {
|
||||||
ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.StopTimeout)*time.Second)
|
ctx, cancel := context.WithTimeout(w.task.Context(), time.Duration(w.StopTimeout)*time.Second)
|
||||||
@@ -200,21 +206,25 @@ func (w *Watcher) getStopCallback() StopCallback {
|
|||||||
func (w *Watcher) resetIdleTimer() {
|
func (w *Watcher) resetIdleTimer() {
|
||||||
w.Trace().Msg("reset idle timer")
|
w.Trace().Msg("reset idle timer")
|
||||||
w.ticker.Reset(w.IdleTimeout)
|
w.ticker.Reset(w.IdleTimeout)
|
||||||
|
w.lastReset = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask task.Task, eventCh <-chan events.Event, errCh <-chan E.Error) {
|
func (w *Watcher) expires() time.Time {
|
||||||
eventTask = w.task.Subtask("docker event watcher")
|
return w.lastReset.Add(w.IdleTimeout)
|
||||||
eventCh, errCh = dockerWatcher.EventsWithOptions(eventTask.Context(), W.DockerListOptions{
|
}
|
||||||
Filters: W.NewDockerFilter(
|
|
||||||
W.DockerFilterContainer,
|
func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventCh <-chan events.Event, errCh <-chan E.Error) {
|
||||||
W.DockerFilterContainerNameID(w.ContainerID),
|
eventCh, errCh = dockerWatcher.EventsWithOptions(w.Task().Context(), watcher.DockerListOptions{
|
||||||
W.DockerFilterStart,
|
Filters: watcher.NewDockerFilter(
|
||||||
W.DockerFilterStop,
|
watcher.DockerFilterContainer,
|
||||||
W.DockerFilterDie,
|
watcher.DockerFilterContainerNameID(w.ContainerID),
|
||||||
W.DockerFilterKill,
|
watcher.DockerFilterStart,
|
||||||
W.DockerFilterDestroy,
|
watcher.DockerFilterStop,
|
||||||
W.DockerFilterPause,
|
watcher.DockerFilterDie,
|
||||||
W.DockerFilterUnpause,
|
watcher.DockerFilterKill,
|
||||||
|
watcher.DockerFilterDestroy,
|
||||||
|
watcher.DockerFilterPause,
|
||||||
|
watcher.DockerFilterUnpause,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -230,11 +240,10 @@ func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventTask tas
|
|||||||
// stop method.
|
// stop method.
|
||||||
//
|
//
|
||||||
// it exits only if the context is canceled, the container is destroyed,
|
// 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) {
|
func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||||
dockerWatcher := W.NewDockerWatcherWithClient(w.client)
|
dockerWatcher := watcher.NewDockerWatcherWithClient(w.client)
|
||||||
eventTask, dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
|
dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
|
||||||
defer eventTask.Finish("stopped")
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -273,15 +282,21 @@ func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
|||||||
w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID)
|
w.Debug().Msgf("id changed %s -> %s", w.ContainerID, e.ActorID)
|
||||||
w.ContainerID = e.ActorID
|
w.ContainerID = e.ActorID
|
||||||
// recreate event stream
|
// recreate event stream
|
||||||
eventTask.Finish("recreate event stream")
|
dockerEventCh, dockerEventErrCh = w.getEventCh(dockerWatcher)
|
||||||
eventTask, dockerEventCh, dockerEventErrCh = w.getEventCh(dockerWatcher)
|
|
||||||
}
|
}
|
||||||
case <-w.ticker.C:
|
case <-w.ticker.C:
|
||||||
w.ticker.Stop()
|
w.ticker.Stop()
|
||||||
if w.ContainerRunning {
|
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:
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
err = errors.New("timeout waiting for container to stop, please set a higher value for `stop_timeout`")
|
||||||
|
}
|
||||||
w.Err(err).Msgf("container stop with method %q failed", w.StopMethod)
|
w.Err(err).Msgf("container stop with method %q failed", w.StopMethod)
|
||||||
} else {
|
default:
|
||||||
w.LogReason("container stopped", "idle timeout")
|
w.LogReason("container stopped", "idle timeout")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ import (
|
|||||||
|
|
||||||
func Inspect(dockerHost string, containerID string) (*Container, error) {
|
func Inspect(dockerHost string, containerID string) (*Container, error) {
|
||||||
client, err := ConnectClient(dockerHost)
|
client, err := ConnectClient(dockerHost)
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer client.Close()
|
||||||
return client.Inspect(containerID)
|
return client.Inspect(containerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) Inspect(containerID string) (*Container, error) {
|
func (c *SharedClient) Inspect(containerID string) (*Container, error) {
|
||||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
|
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +1,52 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
type LabelMap = map[string]any
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var ErrInvalidLabel = E.New("invalid label")
|
||||||
ErrApplyToNil = E.New("label value is nil")
|
|
||||||
ErrFieldNotExist = E.New("field does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (l *Label) String() string {
|
func ParseLabels(labels map[string]string) (LabelMap, E.Error) {
|
||||||
if l.Attribute == "" {
|
nestedMap := make(LabelMap)
|
||||||
return l.Namespace + "." + l.Target
|
errs := E.NewBuilder("labels error")
|
||||||
}
|
|
||||||
return l.Namespace + "." + l.Target + "." + l.Attribute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply applies the value of a Label to the corresponding field in the given object.
|
for lbl, value := range labels {
|
||||||
//
|
parts := strutils.SplitRune(lbl, '.')
|
||||||
// Parameters:
|
if parts[0] != NSProxy {
|
||||||
// - obj: a pointer to the object to which the Label value will be applied.
|
continue
|
||||||
// - 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !field.IsValid() {
|
if len(parts) == 1 {
|
||||||
return ErrFieldNotExist.Subject(l.Attribute).Subject(l.String())
|
errs.Add(ErrInvalidLabel.Subject(lbl))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
dst, ok := field.Interface().(NestedLabelMap)
|
parts = parts[1:]
|
||||||
if !ok {
|
currentMap := nestedMap
|
||||||
if field.Kind() == reflect.Ptr {
|
|
||||||
if field.IsNil() {
|
for i, k := range parts {
|
||||||
field.Set(reflect.New(field.Type().Elem()))
|
if i == len(parts)-1 {
|
||||||
}
|
// Last element, set the value
|
||||||
|
currentMap[k] = value
|
||||||
} else {
|
} 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
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return nestedMap, errs.Error()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,18 @@
|
|||||||
package docker
|
package docker_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/docker"
|
||||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func BenchmarkParseLabels(b *testing.B) {
|
||||||
mName = "middleware1"
|
for range b.N {
|
||||||
mAttr = "prop1"
|
_, _ = docker.ParseLabels(map[string]string{
|
||||||
v = "value1"
|
"proxy.a.host": "localhost",
|
||||||
)
|
"proxy.a.port": "4444",
|
||||||
|
"proxy.a.scheme": "http",
|
||||||
func makeLabel(ns, name, attr string) string {
|
"proxy.a.middlewares.request.hide_headers": "X-Header1,X-Header2",
|
||||||
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,14 +3,14 @@ package docker
|
|||||||
const (
|
const (
|
||||||
WildcardAlias = "*"
|
WildcardAlias = "*"
|
||||||
|
|
||||||
NSProxy = "proxy"
|
NSProxy = "proxy"
|
||||||
NSHomePage = "homepage"
|
|
||||||
|
|
||||||
LabelAliases = NSProxy + ".aliases"
|
LabelAliases = NSProxy + ".aliases"
|
||||||
LabelExclude = NSProxy + ".exclude"
|
LabelExclude = NSProxy + ".exclude"
|
||||||
LabelIdleTimeout = NSProxy + ".idle_timeout"
|
LabelIdleTimeout = NSProxy + ".idle_timeout"
|
||||||
LabelWakeTimeout = NSProxy + ".wake_timeout"
|
LabelWakeTimeout = NSProxy + ".wake_timeout"
|
||||||
LabelStopMethod = NSProxy + ".stop_method"
|
LabelStopMethod = NSProxy + ".stop_method"
|
||||||
LabelStopTimeout = NSProxy + ".stop_timeout"
|
LabelStopTimeout = NSProxy + ".stop_timeout"
|
||||||
LabelStopSignal = NSProxy + ".stop_signal"
|
LabelStopSignal = NSProxy + ".stop_signal"
|
||||||
|
LabelStartEndpoint = NSProxy + ".start_endpoint"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logger = logging.With().Str("module", "docker").Logger()
|
|
||||||
138
internal/entrypoint/entrypoint.go
Normal file
138
internal/entrypoint/entrypoint.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||||
|
"github.com/yusing/go-proxy/internal/net/http/middleware/errorpage"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
route "github.com/yusing/go-proxy/internal/route/types"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entrypoint struct {
|
||||||
|
middleware *middleware.Middleware
|
||||||
|
accessLogger *accesslog.AccessLogger
|
||||||
|
findRouteFunc func(host string) (route.HTTPRoute, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoSuchRoute = errors.New("no such route")
|
||||||
|
|
||||||
|
func NewEntrypoint() *Entrypoint {
|
||||||
|
return &Entrypoint{
|
||||||
|
findRouteFunc: findRouteAnyDomain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) SetFindRouteDomains(domains []string) {
|
||||||
|
if len(domains) == 0 {
|
||||||
|
ep.findRouteFunc = findRouteAnyDomain
|
||||||
|
} else {
|
||||||
|
ep.findRouteFunc = findRouteByDomains(domains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
|
||||||
|
if len(mws) == 0 {
|
||||||
|
ep.middleware = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mid, err := middleware.BuildMiddlewareFromChainRaw("entrypoint", mws)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ep.middleware = mid
|
||||||
|
|
||||||
|
logging.Debug().Msg("entrypoint middleware loaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) {
|
||||||
|
if cfg == nil {
|
||||||
|
ep.accessLogger = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.accessLogger, err = accesslog.NewFileAccessLogger(parent, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logging.Debug().Msg("entrypoint access logger created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mux, err := ep.findRouteFunc(r.Host)
|
||||||
|
if err == nil {
|
||||||
|
if ep.accessLogger != nil {
|
||||||
|
w = gphttp.NewModifyResponseWriter(w, r, func(resp *http.Response) error {
|
||||||
|
ep.accessLogger.Log(r, resp)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if ep.middleware != nil {
|
||||||
|
ep.middleware.ServeHTTP(mux.ServeHTTP, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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 served := middleware.ServeStaticErrorPageFile(w, r); !served {
|
||||||
|
logging.Err(err).
|
||||||
|
Str("method", r.Method).
|
||||||
|
Str("url", r.URL.String()).
|
||||||
|
Str("remote", r.RemoteAddr).
|
||||||
|
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 {
|
||||||
|
logging.Err(err).Msg("failed to write error page")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRouteAnyDomain(host string) (route.HTTPRoute, error) {
|
||||||
|
hostSplit := strutils.SplitRune(host, '.')
|
||||||
|
target := hostSplit[0]
|
||||||
|
|
||||||
|
if r, ok := routes.GetHTTPRouteOrExact(target, host); ok {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findRouteByDomains(domains []string) func(host string) (route.HTTPRoute, error) {
|
||||||
|
return func(host string) (route.HTTPRoute, error) {
|
||||||
|
for _, domain := range domains {
|
||||||
|
if strings.HasSuffix(host, domain) {
|
||||||
|
target := strings.TrimSuffix(host, domain)
|
||||||
|
if r, ok := routes.GetHTTPRoute(target); ok {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to exact match
|
||||||
|
if r, ok := routes.GetHTTPRoute(host); ok {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/entrypoint/entrypoint_test.go
Normal file
121
internal/entrypoint/entrypoint_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/route"
|
||||||
|
"github.com/yusing/go-proxy/internal/route/routes"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
r route.ReveseProxyRoute
|
||||||
|
ep = NewEntrypoint()
|
||||||
|
)
|
||||||
|
|
||||||
|
func run(t *testing.T, match []string, noMatch []string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Cleanup(routes.TestClear)
|
||||||
|
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
||||||
|
|
||||||
|
for _, test := range match {
|
||||||
|
t.Run(test, func(t *testing.T) {
|
||||||
|
found, err := ep.findRouteFunc(test)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
ExpectTrue(t, found == &r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range noMatch {
|
||||||
|
t.Run(test, func(t *testing.T) {
|
||||||
|
_, err := ep.findRouteFunc(test)
|
||||||
|
ExpectError(t, ErrNoSuchRoute, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRouteAnyDomain(t *testing.T) {
|
||||||
|
routes.SetHTTPRoute("app1", &r)
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"app1.com",
|
||||||
|
"app1.domain.com",
|
||||||
|
"app1.sub.domain.com",
|
||||||
|
}
|
||||||
|
testsNoMatch := []string{
|
||||||
|
"sub.app1.com",
|
||||||
|
"app2.com",
|
||||||
|
"app2.domain.com",
|
||||||
|
"app2.sub.domain.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
run(t, tests, testsNoMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRouteExactHostMatch(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"app2.com",
|
||||||
|
"app2.domain.com",
|
||||||
|
"app2.sub.domain.com",
|
||||||
|
}
|
||||||
|
testsNoMatch := []string{
|
||||||
|
"sub.app2.com",
|
||||||
|
"app1.com",
|
||||||
|
"app1.domain.com",
|
||||||
|
"app1.sub.domain.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
routes.SetHTTPRoute(test, &r)
|
||||||
|
}
|
||||||
|
|
||||||
|
run(t, tests, testsNoMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRouteByDomains(t *testing.T) {
|
||||||
|
ep.SetFindRouteDomains([]string{
|
||||||
|
".domain.com",
|
||||||
|
".sub.domain.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
routes.SetHTTPRoute("app1", &r)
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"app1.domain.com",
|
||||||
|
"app1.sub.domain.com",
|
||||||
|
}
|
||||||
|
testsNoMatch := []string{
|
||||||
|
"sub.app1.com",
|
||||||
|
"app1.com",
|
||||||
|
"app1.domain.co",
|
||||||
|
"app1.domain.com.hk",
|
||||||
|
"app1.sub.domain.co",
|
||||||
|
"app2.domain.com",
|
||||||
|
"app2.sub.domain.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
run(t, tests, testsNoMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||||
|
ep.SetFindRouteDomains([]string{
|
||||||
|
".domain.com",
|
||||||
|
".sub.domain.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
routes.SetHTTPRoute("app1.foo.bar", &r)
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"app1.foo.bar", // exact match
|
||||||
|
"app1.foo.bar.domain.com",
|
||||||
|
"app1.foo.bar.sub.domain.com",
|
||||||
|
}
|
||||||
|
testsNoMatch := []string{
|
||||||
|
"sub.app1.foo.bar",
|
||||||
|
"sub.app1.foo.bar.com",
|
||||||
|
"app1.domain.com",
|
||||||
|
"app1.sub.domain.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
run(t, tests, testsNoMatch)
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// baseError is an immutable wrapper around an error.
|
// baseError is an immutable wrapper around an error.
|
||||||
|
//
|
||||||
|
//nolint:recvcheck
|
||||||
type baseError struct {
|
type baseError struct {
|
||||||
Err error `json:"err"`
|
Err error `json:"err"`
|
||||||
}
|
}
|
||||||
@@ -44,3 +47,18 @@ func (err baseError) Withf(format string, args ...any) Error {
|
|||||||
func (err *baseError) Error() string {
|
func (err *baseError) Error() string {
|
||||||
return err.Err.Error()
|
return err.Err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (err *baseError) MarshalJSON() ([]byte, error) {
|
||||||
|
//nolint:errorlint
|
||||||
|
switch err := err.Err.(type) {
|
||||||
|
case Error, *withSubject:
|
||||||
|
return json.Marshal(err)
|
||||||
|
case json.Marshaler:
|
||||||
|
return err.MarshalJSON()
|
||||||
|
case interface{ MarshalText() ([]byte, error) }:
|
||||||
|
return err.MarshalText()
|
||||||
|
default:
|
||||||
|
return json.Marshal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -27,26 +27,31 @@ func (b *Builder) HasError() bool {
|
|||||||
return len(b.errs) > 0
|
return len(b.errs) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Error() Error {
|
func (b *Builder) error() Error {
|
||||||
if !b.HasError() {
|
if !b.HasError() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if len(b.errs) == 1 {
|
|
||||||
return From(b.errs[0])
|
|
||||||
}
|
|
||||||
return &nestedError{Err: New(b.about), Extras: b.errs}
|
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 {
|
func (b *Builder) String() string {
|
||||||
if !b.HasError() {
|
err := b.error()
|
||||||
|
if err == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return (&nestedError{Err: New(b.about), Extras: b.errs}).Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds an error to the Builder.
|
// Add adds an error to the Builder.
|
||||||
//
|
//
|
||||||
// adding nil is no-op,
|
// adding nil is no-op.
|
||||||
func (b *Builder) Add(err error) *Builder {
|
func (b *Builder) Add(err error) *Builder {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return b
|
return b
|
||||||
@@ -55,7 +60,7 @@ func (b *Builder) Add(err error) *Builder {
|
|||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
switch err := err.(type) {
|
switch err := From(err).(type) {
|
||||||
case *baseError:
|
case *baseError:
|
||||||
b.errs = append(b.errs, err.Err)
|
b.errs = append(b.errs, err.Err)
|
||||||
case *nestedError:
|
case *nestedError:
|
||||||
@@ -65,7 +70,7 @@ func (b *Builder) Add(err error) *Builder {
|
|||||||
b.errs = append(b.errs, err)
|
b.errs = append(b.errs, err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
b.errs = append(b.errs, err)
|
panic("bug: should not reach here")
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
return b
|
||||||
@@ -90,6 +95,21 @@ func (b *Builder) Addf(format string, args ...any) *Builder {
|
|||||||
return b
|
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 {
|
func (b *Builder) AddRange(errs ...error) *Builder {
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package error_test
|
package err_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
type Error interface {
|
type Error interface {
|
||||||
error
|
error
|
||||||
@@ -22,8 +22,10 @@ type Error interface {
|
|||||||
Subjectf(format string, args ...any) Error
|
Subjectf(format string, args ...any) Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// this makes JSON marshalling work,
|
// this makes JSON marshaling work,
|
||||||
// as the builtin one doesn't.
|
// as the builtin one doesn't.
|
||||||
|
//
|
||||||
|
//nolint:errname
|
||||||
type errStr string
|
type errStr string
|
||||||
|
|
||||||
func (err errStr) Error() string {
|
func (err errStr) Error() string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -18,11 +18,11 @@ func TestBaseWithSubject(t *testing.T) {
|
|||||||
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
|
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
|
||||||
|
|
||||||
ExpectError(t, err, withSubject)
|
ExpectError(t, err, withSubject)
|
||||||
ExpectStrEqual(t, withSubject.Error(), "foo: error")
|
ExpectEqual(t, withSubject.Error(), "foo: error")
|
||||||
ExpectTrue(t, withSubject.Is(err))
|
ExpectTrue(t, withSubject.Is(err))
|
||||||
|
|
||||||
ExpectError(t, err, withSubjectf)
|
ExpectError(t, err, withSubjectf)
|
||||||
ExpectStrEqual(t, withSubjectf.Error(), "foo bar: error")
|
ExpectEqual(t, withSubjectf.Error(), "foo bar: error")
|
||||||
ExpectTrue(t, withSubjectf.Is(err))
|
ExpectTrue(t, withSubjectf.Is(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +81,10 @@ func TestErrorImmutability(t *testing.T) {
|
|||||||
|
|
||||||
for range 3 {
|
for range 3 {
|
||||||
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
|
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
|
||||||
err.Subject("foo")
|
_ = err.Subject("foo")
|
||||||
ExpectFalse(t, strings.Contains(err.Error(), "foo"))
|
ExpectFalse(t, strings.Contains(err.Error(), "foo"))
|
||||||
|
|
||||||
err.With(err2)
|
_ = err.With(err2)
|
||||||
ExpectFalse(t, strings.Contains(err.Error(), "extra"))
|
ExpectFalse(t, strings.Contains(err.Error(), "extra"))
|
||||||
ExpectFalse(t, err.Is(err2))
|
ExpectFalse(t, err.Is(err2))
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ func TestErrorWith(t *testing.T) {
|
|||||||
ExpectTrue(t, err3.Is(err1))
|
ExpectTrue(t, err3.Is(err1))
|
||||||
ExpectTrue(t, err3.Is(err2))
|
ExpectTrue(t, err3.Is(err2))
|
||||||
|
|
||||||
err2.Subject("foo")
|
_ = err2.Subject("foo")
|
||||||
|
|
||||||
ExpectTrue(t, err3.Is(err1))
|
ExpectTrue(t, err3.Is(err1))
|
||||||
ExpectTrue(t, err3.Is(err2))
|
ExpectTrue(t, err3.Is(err2))
|
||||||
@@ -114,9 +114,9 @@ func TestErrorWith(t *testing.T) {
|
|||||||
func TestErrorStringSimple(t *testing.T) {
|
func TestErrorStringSimple(t *testing.T) {
|
||||||
errFailure := New("generic failure")
|
errFailure := New("generic failure")
|
||||||
ne := errFailure.Subject("foo bar")
|
ne := errFailure.Subject("foo bar")
|
||||||
ExpectStrEqual(t, ne.Error(), "foo bar: generic failure")
|
ExpectEqual(t, ne.Error(), "foo bar: generic failure")
|
||||||
ne = ne.Subject("baz")
|
ne = ne.Subject("baz")
|
||||||
ExpectStrEqual(t, ne.Error(), "baz > foo bar: generic failure")
|
ExpectEqual(t, ne.Error(), "baz > foo bar: generic failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorStringNested(t *testing.T) {
|
func TestErrorStringNested(t *testing.T) {
|
||||||
@@ -153,5 +153,5 @@ func TestErrorStringNested(t *testing.T) {
|
|||||||
• action 3 > inner3: generic failure
|
• action 3 > inner3: generic failure
|
||||||
• 3
|
• 3
|
||||||
• 3`
|
• 3`
|
||||||
ExpectStrEqual(t, ne.Error(), want)
|
ExpectEqual(t, ne.Error(), want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:recvcheck
|
||||||
type nestedError struct {
|
type nestedError struct {
|
||||||
Err error `json:"err"`
|
Err error `json:"err"`
|
||||||
Extras []error `json:"extras"`
|
Extras []error `json:"extras"`
|
||||||
@@ -66,7 +68,18 @@ func (err *nestedError) Is(other error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (err *nestedError) Error() string {
|
func (err *nestedError) Error() string {
|
||||||
return buildError(err, 0)
|
if err == nil {
|
||||||
|
return makeLine("<nil>", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, 0, 1+len(err.Extras))
|
||||||
|
if err.Err != nil {
|
||||||
|
lines = append(lines, makeLine(err.Err.Error(), 0))
|
||||||
|
lines = append(lines, makeLines(err.Extras, 1)...)
|
||||||
|
} else {
|
||||||
|
lines = append(lines, makeLines(err.Extras, 0)...)
|
||||||
|
}
|
||||||
|
return strutils.JoinLines(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
@@ -86,35 +99,15 @@ func makeLines(errs []error, level int) []string {
|
|||||||
}
|
}
|
||||||
lines := make([]string, 0, len(errs))
|
lines := make([]string, 0, len(errs))
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
switch err := err.(type) {
|
switch err := From(err).(type) {
|
||||||
case *nestedError:
|
case *nestedError:
|
||||||
if err.Err != nil {
|
if err.Err != nil {
|
||||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
lines = append(lines, makeLine(err.Err.Error(), level))
|
||||||
}
|
}
|
||||||
if extras := makeLines(err.Extras, level+1); len(extras) > 0 {
|
lines = append(lines, makeLines(err.Extras, level+1)...)
|
||||||
lines = append(lines, extras...)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
lines = append(lines, makeLine(err.Error(), level))
|
lines = append(lines, makeLine(err.Error(), level))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildError(err error, level int) string {
|
|
||||||
switch err := err.(type) {
|
|
||||||
case nil:
|
|
||||||
return makeLine("<nil>", level)
|
|
||||||
case *nestedError:
|
|
||||||
lines := make([]string, 0, 1+len(err.Extras))
|
|
||||||
if err.Err != nil {
|
|
||||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
|
||||||
}
|
|
||||||
if extras := makeLines(err.Extras, level+1); len(extras) > 0 {
|
|
||||||
lines = append(lines, extras...)
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
default:
|
|
||||||
return makeLine(err.Error(), level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:errname
|
||||||
type withSubject struct {
|
type withSubject struct {
|
||||||
Subject string `json:"subject"`
|
Subjects []string
|
||||||
Err error `json:"err"`
|
Err error
|
||||||
|
|
||||||
|
pendingSubject string
|
||||||
}
|
}
|
||||||
|
|
||||||
const subjectSep = " > "
|
const subjectSep = " > "
|
||||||
@@ -18,23 +22,40 @@ func highlight(subject string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PrependSubject(subject string, err error) error {
|
func PrependSubject(subject string, err error) error {
|
||||||
switch err := err.(type) {
|
if err == nil {
|
||||||
case nil:
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:errorlint
|
||||||
|
switch err := err.(type) {
|
||||||
case *withSubject:
|
case *withSubject:
|
||||||
return err.Prepend(subject)
|
return err.Prepend(subject)
|
||||||
case Error:
|
case Error:
|
||||||
return err.Subject(subject)
|
return err.Subject(subject)
|
||||||
default:
|
|
||||||
return &withSubject{subject, err}
|
|
||||||
}
|
}
|
||||||
|
return &withSubject{[]string{subject}, err, ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err withSubject) Prepend(subject string) *withSubject {
|
func (err *withSubject) Prepend(subject string) *withSubject {
|
||||||
if subject != "" {
|
if subject == "" {
|
||||||
err.Subject = subject + subjectSep + err.Subject
|
return err
|
||||||
}
|
}
|
||||||
return &err
|
|
||||||
|
clone := *err
|
||||||
|
switch subject[0] {
|
||||||
|
case '[', '(', '{':
|
||||||
|
// since prepend is called in depth-first order,
|
||||||
|
// the subject of the index is not yet seen
|
||||||
|
// add it when the next subject is seen
|
||||||
|
clone.pendingSubject += subject
|
||||||
|
default:
|
||||||
|
clone.Subjects = append(clone.Subjects, subject)
|
||||||
|
if clone.pendingSubject != "" {
|
||||||
|
clone.Subjects[len(clone.Subjects)-1] = subject + clone.pendingSubject
|
||||||
|
clone.pendingSubject = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *withSubject) Is(other error) bool {
|
func (err *withSubject) Is(other error) bool {
|
||||||
@@ -46,7 +67,39 @@ func (err *withSubject) Unwrap() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (err *withSubject) Error() string {
|
func (err *withSubject) Error() string {
|
||||||
subjects := strings.Split(err.Subject, subjectSep)
|
// subject is in reversed order
|
||||||
subjects[len(subjects)-1] = highlight(subjects[len(subjects)-1])
|
n := len(err.Subjects)
|
||||||
return strings.Join(subjects, subjectSep) + ": " + err.Err.Error()
|
size := 0
|
||||||
|
errStr := err.Err.Error()
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, s := range err.Subjects {
|
||||||
|
size += len(s)
|
||||||
|
}
|
||||||
|
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||||
|
|
||||||
|
for i := n - 1; i > 0; i-- {
|
||||||
|
sb.WriteString(err.Subjects[i])
|
||||||
|
sb.WriteString(subjectSep)
|
||||||
|
}
|
||||||
|
sb.WriteString(highlight(err.Subjects[0]))
|
||||||
|
sb.WriteString(": ")
|
||||||
|
sb.WriteString(errStr)
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (err *withSubject) MarshalJSON() ([]byte, error) {
|
||||||
|
subjects := make([]string, len(err.Subjects))
|
||||||
|
for i, s := range err.Subjects {
|
||||||
|
subjects[len(err.Subjects)-i-1] = s
|
||||||
|
}
|
||||||
|
reversed := struct {
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
|
Err error `json:"err"`
|
||||||
|
}{
|
||||||
|
Subjects: subjects,
|
||||||
|
Err: err.Err,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(reversed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package error
|
package err
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidErrorJson = errors.New("invalid error json")
|
|
||||||
|
|
||||||
func newError(message string) error {
|
func newError(message string) error {
|
||||||
return errStr(message)
|
return errStr(message)
|
||||||
}
|
}
|
||||||
@@ -22,23 +19,27 @@ func Errorf(format string, args ...any) Error {
|
|||||||
return &baseError{fmt.Errorf(format, args...)}
|
return &baseError{fmt.Errorf(format, args...)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Wrap(err error, message ...string) Error {
|
||||||
|
if len(message) == 0 || message[0] == "" {
|
||||||
|
return From(err)
|
||||||
|
}
|
||||||
|
return Errorf("%w: %s", err, message[0])
|
||||||
|
}
|
||||||
|
|
||||||
func From(err error) Error {
|
func From(err error) Error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err, ok := err.(Error); ok {
|
//nolint:errorlint
|
||||||
|
switch err := err.(type) {
|
||||||
|
case *baseError:
|
||||||
|
return err
|
||||||
|
case *nestedError:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return &baseError{err}
|
return &baseError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Must[T any](v T, err error) T {
|
|
||||||
if err != nil {
|
|
||||||
LogPanic("must failed", err)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func Join(errors ...error) Error {
|
func Join(errors ...error) Error {
|
||||||
n := 0
|
n := 0
|
||||||
for _, err := range errors {
|
for _, err := range errors {
|
||||||
@@ -49,10 +50,12 @@ func Join(errors ...error) Error {
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
errs := make([]error, 0, n)
|
errs := make([]error, n)
|
||||||
|
i := 0
|
||||||
for _, err := range errors {
|
for _, err := range errors {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs[i] = err
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &nestedError{Extras: errs}
|
return &nestedError{Extras: errs}
|
||||||
|
|||||||
66
internal/homepage/categories.go
Normal file
66
internal/homepage/categories.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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",
|
||||||
|
|
||||||
|
"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",
|
||||||
|
"logs": "Container Management",
|
||||||
|
"dozzle": "Container Management",
|
||||||
|
|
||||||
|
"rss": "RSS",
|
||||||
|
"rsshub": "RSS",
|
||||||
|
"rss-bridge": "RSS",
|
||||||
|
"miniflux": "RSS",
|
||||||
|
"freshrss": "RSS",
|
||||||
|
|
||||||
|
"paperless": "Documents",
|
||||||
|
"paperless-ngx": "Documents",
|
||||||
|
"s-pdf": "Documents",
|
||||||
|
"stirling-pdf": "Documents",
|
||||||
|
|
||||||
|
"minio": "Storage",
|
||||||
|
"filebrowser": "Storage",
|
||||||
|
"rclone": "Storage",
|
||||||
|
}
|
||||||
@@ -1,41 +1,72 @@
|
|||||||
package homepage
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Config map[string]Category
|
//nolint:recvcheck
|
||||||
Category []*Item
|
Categories map[string]Category
|
||||||
|
Category []*Item
|
||||||
|
|
||||||
|
ItemConfig struct {
|
||||||
|
Show bool `json:"show"`
|
||||||
|
Name string `json:"name"` // display name
|
||||||
|
Icon *IconURL `json:"icon"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Description string `json:"description" aliases:"desc"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
WidgetConfig map[string]any `json:"widget_config" aliases:"widget"`
|
||||||
|
URL string `json:"url"` // alias + domain
|
||||||
|
}
|
||||||
|
|
||||||
Item struct {
|
Item struct {
|
||||||
Show bool `json:"show" yaml:"show"`
|
*ItemConfig
|
||||||
Name string `json:"name" yaml:"name"`
|
|
||||||
Icon string `json:"icon" yaml:"icon"`
|
|
||||||
URL string `json:"url" yaml:"url"` // alias + domain
|
|
||||||
Category string `json:"category" yaml:"category"`
|
|
||||||
Description string `json:"description" yaml:"description"`
|
|
||||||
WidgetConfig map[string]any `json:"widget_config" yaml:",flow"`
|
|
||||||
|
|
||||||
SourceType string `json:"source_type" yaml:"-"`
|
Alias string `json:"alias"` // proxy alias
|
||||||
AltURL string `json:"alt_url" yaml:"-"` // original proxy target
|
SourceType string `json:"source_type"`
|
||||||
|
AltURL string `json:"alt_url"` // original proxy target
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
|
||||||
|
IsUnset bool `json:"-"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utils.RegisterDefaultValueFactory(func() *ItemConfig {
|
||||||
|
return &ItemConfig{
|
||||||
|
Show: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewItem(alias string) *Item {
|
||||||
|
return &Item{
|
||||||
|
ItemConfig: &ItemConfig{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Alias: alias,
|
||||||
|
IsUnset: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHomePageConfig() Categories {
|
||||||
|
return Categories(make(map[string]Category))
|
||||||
|
}
|
||||||
|
|
||||||
func (item *Item) IsEmpty() bool {
|
func (item *Item) IsEmpty() bool {
|
||||||
return item == nil || (item.Name == "" &&
|
return item == nil || item.IsUnset || item.ItemConfig == nil
|
||||||
item.Icon == "" &&
|
|
||||||
item.URL == "" &&
|
|
||||||
item.Category == "" &&
|
|
||||||
item.Description == "" &&
|
|
||||||
len(item.WidgetConfig) == 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHomePageConfig() Config {
|
func (item *Item) GetOverride() *Item {
|
||||||
return Config(make(map[string]Category))
|
return overrideConfigInstance.GetOverride(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Clear() {
|
func (c *Categories) Clear() {
|
||||||
*c = make(Config)
|
*c = make(Categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Add(item *Item) {
|
func (c Categories) Add(item *Item) {
|
||||||
if c[item.Category] == nil {
|
if c[item.Category] == nil {
|
||||||
c[item.Category] = make(Category, 0)
|
c[item.Category] = make(Category, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
36
internal/homepage/homepage_test.go
Normal file
36
internal/homepage/homepage_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOverrideItem(t *testing.T) {
|
||||||
|
InitOverridesConfig()
|
||||||
|
a := &Item{
|
||||||
|
Alias: "foo",
|
||||||
|
ItemConfig: &ItemConfig{
|
||||||
|
Show: false,
|
||||||
|
Name: "Foo",
|
||||||
|
Icon: &IconURL{
|
||||||
|
Value: "/favicon.ico",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
Category: "App",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
override := &ItemConfig{
|
||||||
|
Show: true,
|
||||||
|
Name: "Bar",
|
||||||
|
Category: "Test",
|
||||||
|
Icon: &IconURL{
|
||||||
|
Value: "@walkxcode/example.png",
|
||||||
|
IconSource: IconSourceWalkXCode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
overrides := GetOverrideConfig()
|
||||||
|
overrides.OverrideItem(a.Alias, override)
|
||||||
|
overridden := a.GetOverride()
|
||||||
|
ExpectDeepEqual(t, overridden.ItemConfig, override)
|
||||||
|
}
|
||||||
164
internal/homepage/icon_url.go
Normal file
164
internal/homepage/icon_url.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal"
|
||||||
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
IconURL struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
FullValue string `json:"full_value"`
|
||||||
|
IconSource `json:"source"`
|
||||||
|
Extra *IconExtra `json:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IconExtra struct {
|
||||||
|
FileType string `json:"file_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
IconSource int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IconSourceAbsolute IconSource = iota
|
||||||
|
IconSourceRelative
|
||||||
|
IconSourceWalkXCode
|
||||||
|
IconSourceSelfhSt
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidIconURL = E.New("invalid icon url")
|
||||||
|
|
||||||
|
func NewSelfhStIconURL(reference, format string) *IconURL {
|
||||||
|
return &IconURL{
|
||||||
|
Value: reference + "." + format,
|
||||||
|
FullValue: fmt.Sprintf("@selfhst/%s.%s", reference, format),
|
||||||
|
IconSource: IconSourceSelfhSt,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: format,
|
||||||
|
Name: reference,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWalkXCodeIconURL(name, format string) *IconURL {
|
||||||
|
return &IconURL{
|
||||||
|
Value: name + "." + format,
|
||||||
|
FullValue: fmt.Sprintf("@walkxcode/%s.%s", name, format),
|
||||||
|
IconSource: IconSourceWalkXCode,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: format,
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasIcon checks if the icon referenced by the IconURL exists in the cache based on its source.
|
||||||
|
// Returns false if the icon does not exist for IconSourceSelfhSt or IconSourceWalkXCode,
|
||||||
|
// otherwise returns true.
|
||||||
|
func (u *IconURL) HasIcon() bool {
|
||||||
|
if u.IconSource == IconSourceSelfhSt {
|
||||||
|
return internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType)
|
||||||
|
}
|
||||||
|
if u.IconSource == IconSourceWalkXCode {
|
||||||
|
return internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse implements strutils.Parser.
|
||||||
|
func (u *IconURL) Parse(v string) error {
|
||||||
|
if v == "" {
|
||||||
|
return ErrInvalidIconURL
|
||||||
|
}
|
||||||
|
slashIndex := strings.Index(v, "/")
|
||||||
|
if slashIndex == -1 {
|
||||||
|
return ErrInvalidIconURL
|
||||||
|
}
|
||||||
|
u.FullValue = v
|
||||||
|
beforeSlash := v[:slashIndex]
|
||||||
|
switch beforeSlash {
|
||||||
|
case "http:", "https:":
|
||||||
|
u.Value = v
|
||||||
|
u.IconSource = IconSourceAbsolute
|
||||||
|
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||||
|
u.Value = v[slashIndex:]
|
||||||
|
u.IconSource = IconSourceRelative
|
||||||
|
if u.Value == "/" {
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "empty path")
|
||||||
|
}
|
||||||
|
case "png", "svg", "webp": // walkxcode Icons
|
||||||
|
u.Value = v
|
||||||
|
u.IconSource = IconSourceWalkXCode
|
||||||
|
u.Extra = &IconExtra{
|
||||||
|
FileType: beforeSlash,
|
||||||
|
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
|
||||||
|
}
|
||||||
|
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
|
||||||
|
u.Value = v[slashIndex+1:]
|
||||||
|
if beforeSlash == "@selfhst" {
|
||||||
|
u.IconSource = IconSourceSelfhSt
|
||||||
|
} else {
|
||||||
|
u.IconSource = IconSourceWalkXCode
|
||||||
|
}
|
||||||
|
parts := strings.Split(u.Value, ".")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return ErrInvalidIconURL.Withf("expect @%s/<reference>.<format>, e.g. @%s/adguard-home.webp", beforeSlash, beforeSlash)
|
||||||
|
}
|
||||||
|
reference, format := parts[0], strings.ToLower(parts[1])
|
||||||
|
if reference == "" || format == "" {
|
||||||
|
return ErrInvalidIconURL
|
||||||
|
}
|
||||||
|
switch format {
|
||||||
|
case "svg", "png", "webp":
|
||||||
|
default:
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "invalid image format, expect svg/png/webp")
|
||||||
|
}
|
||||||
|
u.Extra = &IconExtra{
|
||||||
|
FileType: format,
|
||||||
|
Name: reference,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrInvalidIconURL.Withf("%s", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Value == "" {
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.HasIcon() {
|
||||||
|
return ErrInvalidIconURL.Withf("no such icon %s from %s", u.Value, beforeSlash)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *IconURL) URL() string {
|
||||||
|
switch u.IconSource {
|
||||||
|
case IconSourceAbsolute:
|
||||||
|
return u.Value
|
||||||
|
case IconSourceRelative:
|
||||||
|
return "/" + u.Value
|
||||||
|
case IconSourceWalkXCode:
|
||||||
|
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
|
||||||
|
case IconSourceSelfhSt:
|
||||||
|
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, u.Extra.Name, u.Extra.FileType)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *IconURL) String() string {
|
||||||
|
return u.FullValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *IconURL) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(u.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (u *IconURL) UnmarshalText(data []byte) error {
|
||||||
|
return u.Parse(string(data))
|
||||||
|
}
|
||||||
125
internal/homepage/icon_url_test.go
Normal file
125
internal/homepage/icon_url_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIconURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantValue *IconURL
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "absolute",
|
||||||
|
input: "http://example.com/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "http://example.com/icon.png",
|
||||||
|
IconSource: IconSourceAbsolute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative",
|
||||||
|
input: "@target/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "/icon.png",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative2",
|
||||||
|
input: "/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "/icon.png",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative_empty_path",
|
||||||
|
input: "@target/",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative_empty_path2",
|
||||||
|
input: "/",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "walkxcode",
|
||||||
|
input: "png/adguard-home.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "png/adguard-home.png",
|
||||||
|
IconSource: IconSourceWalkXCode,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: "png",
|
||||||
|
Name: "adguard-home",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "walkxcode_alt",
|
||||||
|
input: "@walkxcode/adguard-home.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "adguard-home.png",
|
||||||
|
IconSource: IconSourceWalkXCode,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: "png",
|
||||||
|
Name: "adguard-home",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "walkxcode_invalid_format",
|
||||||
|
input: "foo/walkxcode.png",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_valid",
|
||||||
|
input: "@selfhst/adguard-home.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "adguard-home.png",
|
||||||
|
IconSource: IconSourceSelfhSt,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: "png",
|
||||||
|
Name: "adguard-home",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_invalid",
|
||||||
|
input: "@selfhst/foo",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_invalid_format",
|
||||||
|
input: "@selfhst/foo.bar",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
input: "invalid",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
u := &IconURL{}
|
||||||
|
err := u.Parse(tc.input)
|
||||||
|
if tc.wantErr {
|
||||||
|
ExpectError(t, ErrInvalidIconURL, err)
|
||||||
|
} else {
|
||||||
|
tc.wantValue.FullValue = tc.input
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
ExpectDeepEqual(t, u, tc.wantValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
107
internal/homepage/override_config.go
Normal file
107
internal/homepage/override_config.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OverrideConfig struct {
|
||||||
|
ItemOverrides map[string]*ItemConfig `json:"item_overrides"`
|
||||||
|
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
|
||||||
|
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
|
||||||
|
ItemVisibility map[string]bool `json:"item_visibility"`
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var overrideConfigInstance = &OverrideConfig{
|
||||||
|
ItemOverrides: make(map[string]*ItemConfig),
|
||||||
|
DisplayOrder: make(map[string]int),
|
||||||
|
CategoryOrder: make(map[string]int),
|
||||||
|
ItemVisibility: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitOverridesConfig() {
|
||||||
|
overrideConfigInstance.mu.Lock()
|
||||||
|
defer overrideConfigInstance.mu.Unlock()
|
||||||
|
|
||||||
|
err := utils.LoadJSONIfExist(common.HomepageJSONConfigPath, overrideConfigInstance)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to load homepage overrides config")
|
||||||
|
} else {
|
||||||
|
logging.Info().Msgf("homepage overrides config loaded, %d items", len(overrideConfigInstance.ItemOverrides))
|
||||||
|
}
|
||||||
|
task.OnProgramExit("save_homepage_json_config", func() {
|
||||||
|
if len(overrideConfigInstance.ItemOverrides) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := utils.SaveJSON(common.HomepageJSONConfigPath, overrideConfigInstance, 0o644); err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to save homepage overrides config")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOverrideConfig() *OverrideConfig {
|
||||||
|
return overrideConfigInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.ItemOverrides[alias] = override
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for key, value := range items {
|
||||||
|
c.ItemOverrides[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) GetOverride(item *Item) *Item {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
itemOverride, hasOverride := c.ItemOverrides[item.Alias]
|
||||||
|
if hasOverride {
|
||||||
|
clone := *item
|
||||||
|
clone.ItemConfig = itemOverride
|
||||||
|
clone.IsUnset = false
|
||||||
|
item = &clone
|
||||||
|
}
|
||||||
|
if show, ok := c.ItemVisibility[item.Alias]; ok {
|
||||||
|
if !hasOverride {
|
||||||
|
clone := *item
|
||||||
|
clone.Show = show
|
||||||
|
item = &clone
|
||||||
|
} else {
|
||||||
|
item.Show = show
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) SetCategoryOrder(key string, value int) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.CategoryOrder[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) UnhideItems(keys ...string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for _, key := range keys {
|
||||||
|
c.ItemVisibility[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OverrideConfig) HideItems(keys ...string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for _, key := range keys {
|
||||||
|
c.ItemVisibility[key] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,17 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/utils"
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitHubContents struct { //! keep this, may reuse in future
|
type GitHubContents struct { //! keep this, may reuse in future
|
||||||
@@ -20,82 +23,275 @@ type GitHubContents struct { //! keep this, may reuse in future
|
|||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
type (
|
||||||
iconsCachePath = "/tmp/icons_cache.json"
|
IconsMap map[string]map[string]struct{}
|
||||||
updateInterval = 1 * time.Hour
|
IconList []string
|
||||||
|
Cache struct {
|
||||||
|
WalkxCode, Selfhst IconsMap
|
||||||
|
DisplayNames ReferenceDisplayNameMap
|
||||||
|
IconList IconList // combined into a single list
|
||||||
|
}
|
||||||
|
ReferenceDisplayNameMap map[string]string
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListAvailableIcons() ([]string, error) {
|
func (icons *Cache) needUpdate() bool {
|
||||||
owner := "walkxcode"
|
return len(icons.WalkxCode) == 0 || len(icons.Selfhst) == 0 || len(icons.IconList) == 0 || len(icons.DisplayNames) == 0
|
||||||
repo := "dashboard-icons"
|
}
|
||||||
ref := "main"
|
|
||||||
|
|
||||||
var lastUpdate time.Time
|
const updateInterval = 2 * time.Hour
|
||||||
|
|
||||||
icons := make([]string, 0)
|
var (
|
||||||
info, err := os.Stat(iconsCachePath)
|
iconsCache *Cache
|
||||||
if err == nil {
|
iconsCahceMu sync.RWMutex
|
||||||
lastUpdate = info.ModTime().Local()
|
lastUpdate time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
walkxcodeIcons = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/tree.json"
|
||||||
|
selfhstIcons = "https://cdn.selfh.st/directory/icons.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitIconListCache() {
|
||||||
|
iconsCahceMu.Lock()
|
||||||
|
defer iconsCahceMu.Unlock()
|
||||||
|
|
||||||
|
iconsCache = &Cache{
|
||||||
|
WalkxCode: make(IconsMap),
|
||||||
|
Selfhst: make(IconsMap),
|
||||||
|
DisplayNames: make(ReferenceDisplayNameMap),
|
||||||
|
IconList: []string{},
|
||||||
}
|
}
|
||||||
|
err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to load icon list cache config")
|
||||||
|
} else if stats, err := os.Stat(common.IconListCachePath); err == nil {
|
||||||
|
lastUpdate = stats.ModTime()
|
||||||
|
logging.Info().Msgf("icon list cache loaded (%d icons, %d display names), last updated at %s",
|
||||||
|
len(iconsCache.IconList),
|
||||||
|
len(iconsCache.DisplayNames),
|
||||||
|
strutils.FormatTime(lastUpdate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListAvailableIcons() (*Cache, error) {
|
||||||
|
iconsCahceMu.RLock()
|
||||||
if time.Since(lastUpdate) < updateInterval {
|
if time.Since(lastUpdate) < updateInterval {
|
||||||
err := utils.LoadJSON(iconsCachePath, &icons)
|
if !iconsCache.needUpdate() {
|
||||||
if err == nil {
|
iconsCahceMu.RUnlock()
|
||||||
return icons, nil
|
return iconsCache, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iconsCahceMu.RUnlock()
|
||||||
|
|
||||||
contents, err := getRepoContents(http.DefaultClient, owner, repo, ref, "")
|
iconsCahceMu.Lock()
|
||||||
|
defer iconsCahceMu.Unlock()
|
||||||
|
|
||||||
|
icons, err := fetchIconData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, content := range contents {
|
|
||||||
if content.Type != "dir" {
|
logging.Info().Msg("icons list updated")
|
||||||
icons = append(icons, content.Path)
|
|
||||||
}
|
iconsCache = icons
|
||||||
}
|
lastUpdate = time.Now()
|
||||||
err = utils.SaveJSON(iconsCachePath, &icons, 0o644)
|
|
||||||
|
err = utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print("error saving cache", err)
|
logging.Warn().Err(err).Msg("failed to save icon list cache")
|
||||||
}
|
}
|
||||||
return icons, nil
|
return icons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRepoContents(client *http.Client, owner string, repo string, ref string, path string) ([]GitHubContents, error) {
|
func SearchIcons(keyword string, limit int) ([]string, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s?ref=%s", owner, repo, path, ref), nil)
|
icons, err := ListAvailableIcons()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept", "application/json")
|
if keyword == "" {
|
||||||
|
return utils.Slice(icons.IconList, limit), nil
|
||||||
|
}
|
||||||
|
return utils.Slice(fuzzy.Find(keyword, icons.IconList), limit), nil
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
func HasWalkxCodeIcon(name string, filetype string) bool {
|
||||||
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := icons.WalkxCode[filetype]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := icons.WalkxCode[filetype][name+"."+filetype]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasSelfhstIcon(name string, filetype string) bool {
|
||||||
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := icons.Selfhst[filetype]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := icons.Selfhst[filetype][name+"."+filetype]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDisplayName(reference string) (string, bool) {
|
||||||
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
displayName, ok := icons.DisplayNames[reference]
|
||||||
|
return displayName, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIconData() (*Cache, error) {
|
||||||
|
walkxCodeIconMap, walkxCodeIconList, err := fetchWalkxCodeIcons()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for _, items := range walkxCodeIconMap {
|
||||||
|
n += len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
selfhstIconMap, selfhstIconList, referenceToNames, err := fetchSelfhstIcons()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Cache{
|
||||||
|
WalkxCode: walkxCodeIconMap,
|
||||||
|
Selfhst: selfhstIconMap,
|
||||||
|
DisplayNames: referenceToNames,
|
||||||
|
IconList: append(walkxCodeIconList, selfhstIconList...),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"png": [
|
||||||
|
"*.png",
|
||||||
|
],
|
||||||
|
"svg": [
|
||||||
|
"*.svg",
|
||||||
|
],
|
||||||
|
"webp": [
|
||||||
|
"*.webp",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func fetchWalkxCodeIcons() (IconsMap, IconList, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var contents []GitHubContents
|
data := make(map[string][]string)
|
||||||
err = json.Unmarshal(body, &contents)
|
err = json.Unmarshal(body, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
icons := make(IconsMap, len(data))
|
||||||
filesAndDirs := make([]GitHubContents, 0)
|
iconList := make(IconList, 0, 2000)
|
||||||
for _, content := range contents {
|
for fileType, files := range data {
|
||||||
if content.Type == "dir" {
|
icons[fileType] = make(map[string]struct{}, len(files))
|
||||||
subContents, err := getRepoContents(client, owner, repo, ref, content.Path)
|
for _, icon := range files {
|
||||||
if err != nil {
|
icons[fileType][icon] = struct{}{}
|
||||||
return nil, err
|
iconList = append(iconList, "@walkxcode/"+icon)
|
||||||
}
|
|
||||||
filesAndDirs = append(filesAndDirs, subContents...)
|
|
||||||
} else {
|
|
||||||
filesAndDirs = append(filesAndDirs, content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return icons, iconList, nil
|
||||||
return filesAndDirs, nil
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"Name": "2FAuth",
|
||||||
|
"Reference": "2fauth",
|
||||||
|
"SVG": "Yes",
|
||||||
|
"PNG": "Yes",
|
||||||
|
"WebP": "Yes",
|
||||||
|
"Light": "Yes",
|
||||||
|
"Category": "Self-Hosted",
|
||||||
|
"CreatedAt": "2024-08-16 00:27:23+00:00"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func fetchSelfhstIcons() (IconsMap, IconList, ReferenceDisplayNameMap, error) {
|
||||||
|
type SelfhStIcon struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Reference string `json:"Reference"`
|
||||||
|
SVG string `json:"SVG"`
|
||||||
|
PNG string `json:"PNG"`
|
||||||
|
WebP string `json:"WebP"`
|
||||||
|
// Light string
|
||||||
|
// Category string
|
||||||
|
// CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, selfhstIcons, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]SelfhStIcon, 0, 2000)
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iconList := make(IconList, 0, len(data)*3)
|
||||||
|
icons := make(IconsMap)
|
||||||
|
icons["svg"] = make(map[string]struct{}, len(data))
|
||||||
|
icons["png"] = make(map[string]struct{}, len(data))
|
||||||
|
icons["webp"] = make(map[string]struct{}, len(data))
|
||||||
|
|
||||||
|
referenceToNames := make(ReferenceDisplayNameMap, len(data))
|
||||||
|
|
||||||
|
for _, item := range data {
|
||||||
|
if item.SVG == "Yes" {
|
||||||
|
icons["svg"][item.Reference+".svg"] = struct{}{}
|
||||||
|
iconList = append(iconList, "@selfhst/"+item.Reference+".svg")
|
||||||
|
}
|
||||||
|
if item.PNG == "Yes" {
|
||||||
|
icons["png"][item.Reference+".png"] = struct{}{}
|
||||||
|
iconList = append(iconList, "@selfhst/"+item.Reference+".png")
|
||||||
|
}
|
||||||
|
if item.WebP == "Yes" {
|
||||||
|
icons["webp"][item.Reference+".webp"] = struct{}{}
|
||||||
|
iconList = append(iconList, "@selfhst/"+item.Reference+".webp")
|
||||||
|
}
|
||||||
|
referenceToNames[item.Reference] = item.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons, iconList, referenceToNames, nil
|
||||||
}
|
}
|
||||||
|
|||||||
159
internal/logging/html.go
Normal file
159
internal/logging/html.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var levelHTMLFormats = [][]byte{
|
||||||
|
[]byte(` <span class="log-trace">TRC</span> `),
|
||||||
|
[]byte(` <span class="log-debug">DBG</span> `),
|
||||||
|
[]byte(` <span class="log-info">INF</span> `),
|
||||||
|
[]byte(` <span class="log-warn">WRN</span> `),
|
||||||
|
[]byte(` <span class="log-error">ERR</span> `),
|
||||||
|
[]byte(` <span class="log-fatal">FTL</span> `),
|
||||||
|
[]byte(` <span class="log-panic">PAN</span> `),
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorToClass = map[string]string{
|
||||||
|
"1": "log-bold",
|
||||||
|
"3": "log-italic",
|
||||||
|
"4": "log-underline",
|
||||||
|
"30": "log-black",
|
||||||
|
"31": "log-red",
|
||||||
|
"32": "log-green",
|
||||||
|
"33": "log-yellow",
|
||||||
|
"34": "log-blue",
|
||||||
|
"35": "log-magenta",
|
||||||
|
"36": "log-cyan",
|
||||||
|
"37": "log-white",
|
||||||
|
"90": "log-bright-black",
|
||||||
|
"91": "log-red",
|
||||||
|
"92": "log-bright-green",
|
||||||
|
"93": "log-bright-yellow",
|
||||||
|
"94": "log-bright-blue",
|
||||||
|
"95": "log-bright-magenta",
|
||||||
|
"96": "log-bright-cyan",
|
||||||
|
"97": "log-bright-white",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatMessageToHTMLBytes converts text with ANSI color codes to HTML with class names.
|
||||||
|
// ANSI codes are mapped to classes via a static map, and reset codes ([0m) close all spans.
|
||||||
|
// Time complexity is O(n) with minimal allocations.
|
||||||
|
func FormatMessageToHTMLBytes(msg string, buf []byte) ([]byte, error) {
|
||||||
|
buf = append(buf, "<span class=\"log-message\">"...)
|
||||||
|
var stack []string
|
||||||
|
lastPos := 0
|
||||||
|
|
||||||
|
for i := 0; i < len(msg); {
|
||||||
|
if msg[i] == '\x1b' && i+1 < len(msg) && msg[i+1] == '[' {
|
||||||
|
if lastPos < i {
|
||||||
|
escapeAndAppend(msg[lastPos:i], &buf)
|
||||||
|
}
|
||||||
|
i += 2 // Skip \x1b[
|
||||||
|
|
||||||
|
start := i
|
||||||
|
for ; i < len(msg) && msg[i] != 'm'; i++ {
|
||||||
|
if !isANSICodeChar(msg[i]) {
|
||||||
|
return nil, fmt.Errorf("invalid ANSI char: %c", msg[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= len(msg) {
|
||||||
|
return nil, errors.New("unterminated ANSI sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
codeStr := msg[start:i]
|
||||||
|
i++ // Skip 'm'
|
||||||
|
lastPos = i
|
||||||
|
|
||||||
|
startPart := 0
|
||||||
|
for j := 0; j <= len(codeStr); j++ {
|
||||||
|
if j == len(codeStr) || codeStr[j] == ';' {
|
||||||
|
part := codeStr[startPart:j]
|
||||||
|
if part == "" {
|
||||||
|
return nil, errors.New("empty code part")
|
||||||
|
}
|
||||||
|
|
||||||
|
if part == "0" {
|
||||||
|
for range stack {
|
||||||
|
buf = append(buf, "</span>"...)
|
||||||
|
}
|
||||||
|
stack = stack[:0]
|
||||||
|
} else {
|
||||||
|
className, ok := colorToClass[part]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid ANSI code: %s", part)
|
||||||
|
}
|
||||||
|
stack = append(stack, className)
|
||||||
|
buf = append(buf, `<span class="`...)
|
||||||
|
buf = append(buf, className...)
|
||||||
|
buf = append(buf, `">`...)
|
||||||
|
}
|
||||||
|
startPart = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastPos < len(msg) {
|
||||||
|
escapeAndAppend(msg[lastPos:], &buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range stack {
|
||||||
|
buf = append(buf, "</span>"...)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = append(buf, "</span>"...)
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isANSICodeChar(c byte) bool {
|
||||||
|
return (c >= '0' && c <= '9') || c == ';'
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeAndAppend(s string, buf *[]byte) {
|
||||||
|
for i, r := range s {
|
||||||
|
switch r {
|
||||||
|
case '•':
|
||||||
|
*buf = append(*buf, "·"...)
|
||||||
|
case '&':
|
||||||
|
*buf = append(*buf, "&"...)
|
||||||
|
case '<':
|
||||||
|
*buf = append(*buf, "<"...)
|
||||||
|
case '>':
|
||||||
|
*buf = append(*buf, ">"...)
|
||||||
|
case '\t':
|
||||||
|
*buf = append(*buf, "	"...)
|
||||||
|
case '\n':
|
||||||
|
*buf = append(*buf, "<br>"...)
|
||||||
|
*buf = append(*buf, prefixHTML...)
|
||||||
|
default:
|
||||||
|
*buf = append(*buf, s[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeNowHTML() []byte {
|
||||||
|
if !common.IsTest {
|
||||||
|
return []byte(time.Now().Format(timeFmt))
|
||||||
|
}
|
||||||
|
return []byte(time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(timeFmt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatLogEntryHTML(level zerolog.Level, message string, buf []byte) []byte {
|
||||||
|
buf = append(buf, []byte(`<pre class="log-entry">`)...)
|
||||||
|
buf = append(buf, timeNowHTML()...)
|
||||||
|
if level < zerolog.NoLevel {
|
||||||
|
buf = append(buf, levelHTMLFormats[level+1]...)
|
||||||
|
}
|
||||||
|
buf, _ = FormatMessageToHTMLBytes(message, buf)
|
||||||
|
buf = append(buf, []byte("</pre>")...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user