mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-15 16:13:32 +01:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7205bf47de | ||
|
|
b12999210f | ||
|
|
8b8969f033 | ||
|
|
025ebab1ce | ||
|
|
ea7bd0d19a | ||
|
|
f889f5c08d | ||
|
|
932c20f32d | ||
|
|
2a08c55e39 | ||
|
|
93e1d17090 | ||
|
|
d72d403e2c | ||
|
|
b5d70a0592 | ||
|
|
da71dcf058 | ||
|
|
6b17272347 | ||
|
|
98afb02e7f | ||
|
|
103fd3b904 | ||
|
|
59917f52d7 | ||
|
|
24fb2e07e6 | ||
|
|
8f1c02ca72 | ||
|
|
e359bc8fd9 | ||
|
|
7b028adaa9 | ||
|
|
f3913e1f6f | ||
|
|
b72f3bde53 | ||
|
|
6077a1d70b | ||
|
|
59cae0967a | ||
|
|
1e1999b0af | ||
|
|
b64725f2f8 | ||
|
|
124069aaa4 | ||
|
|
d56663d3f9 | ||
|
|
d1476edf91 | ||
|
|
4ed6c7c74d | ||
|
|
f31b1b5ed3 | ||
|
|
e0d25e475c | ||
|
|
ef65481394 | ||
|
|
1e9303b1ef | ||
|
|
2c290a3916 | ||
|
|
58a2dc73dd | ||
|
|
1c080e067d | ||
|
|
2717dc963a | ||
|
|
4509622dde | ||
|
|
60c13a797b | ||
|
|
5e1da915dc | ||
|
|
3288624cf2 | ||
|
|
190d5e1ece | ||
|
|
0d2229cca0 | ||
|
|
493c0afdfa | ||
|
|
99c1922342 | ||
|
|
a483e15a20 | ||
|
|
fbe82c3082 | ||
|
|
24bcc2d2d2 | ||
|
|
d8c8cff8b7 | ||
|
|
ef54d336a2 | ||
|
|
0a5df1bd7f | ||
|
|
205928a741 | ||
|
|
11d18091fd | ||
|
|
3be72e5c68 | ||
|
|
a9847b6f81 | ||
|
|
04d823d616 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,6 +29,7 @@ todo.md
|
||||
.aider*
|
||||
mtrace.json
|
||||
.env
|
||||
*.env
|
||||
.cursorrules
|
||||
.cursor/
|
||||
.windsurfrules
|
||||
|
||||
7
Makefile
7
Makefile
@@ -6,7 +6,7 @@ export GOOS = linux
|
||||
WEBUI_DIR ?= ../godoxy-frontend
|
||||
DOCS_DIR ?= ../godoxy-wiki
|
||||
|
||||
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION} -checklinkname=0
|
||||
|
||||
ifeq ($(agent), 1)
|
||||
NAME = godoxy-agent
|
||||
@@ -119,6 +119,9 @@ dev:
|
||||
dev-build: build
|
||||
docker compose -f dev.compose.yml up -t 0 -d --build
|
||||
|
||||
dev-logs:
|
||||
docker compose -f dev.compose.yml logs -f app
|
||||
|
||||
mtrace:
|
||||
${BIN_PATH} debug-ls-mtrace > mtrace.json
|
||||
|
||||
@@ -152,4 +155,4 @@ gen-swagger-markdown: gen-swagger
|
||||
gen-api-types: gen-swagger
|
||||
# --disable-throw-on-error
|
||||
pnpx swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||
--responses -o ${WEBUI_DIR}/src/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
29
README.md
29
README.md
@@ -21,8 +21,6 @@ A lightweight, simple, and performant reverse proxy with WebUI.
|
||||
|
||||
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
|
||||
|
||||
**New WebUI and is now available in nightly tag [(Demo)](https://nightly.demo.godoxy.dev), feedbacks are welcomed!**
|
||||
|
||||
</div>
|
||||
|
||||
## Table of content
|
||||
@@ -41,6 +39,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## Running demo
|
||||
|
||||
@@ -63,6 +62,9 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
|
||||
- Auto-configuration for Docker containers
|
||||
- Hot-reloading of configurations and container state changes
|
||||
- **Container Runtime Support**
|
||||
- Docker
|
||||
- Podman
|
||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||
- Docker containers
|
||||
- Proxmox LXCs
|
||||
@@ -70,6 +72,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
|
||||
- HTTP reserve proxy
|
||||
- TCP/UDP port forwarding
|
||||
- **OpenID Connect support**: SSO and secure your apps easily
|
||||
- **ForwardAuth support**: integrate with any auth provider (e.g. TinyAuth)
|
||||
- **Customization**
|
||||
- [HTTP middlewares](https://docs.godoxy.dev/Middlewares)
|
||||
- [Custom error pages support](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||
@@ -136,22 +139,12 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>Uptime Monitor</b></td>
|
||||
<td align="center"><b>Docker Logs</b></td>
|
||||
<td align="center"><b>Server Overview</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>System Monitor</b></td>
|
||||
<td align="center"><b>Graphs</b></td>
|
||||
<td align="center"><b>Routes</b></td>
|
||||
<td align="center"><b>Servers</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -203,4 +196,8 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||
|
||||
5. build binary with `make build`
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
- [閒置休眠](#閒置休眠)
|
||||
- [監控](#監控)
|
||||
- [自行編譯](#自行編譯)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## 運行示例
|
||||
|
||||
@@ -60,6 +61,9 @@
|
||||
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
|
||||
- Docker 容器自動配置
|
||||
- 設定檔與容器狀態變更時自動熱重載
|
||||
- **容器運行時支援**
|
||||
- Docker
|
||||
- Podman
|
||||
- **閒置休眠**:根據流量停止和喚醒容器 _(參見[截圖](#閒置休眠))_
|
||||
- Docker 容器
|
||||
- Proxmox LXC 容器
|
||||
@@ -67,6 +71,7 @@
|
||||
- HTTP 反向代理
|
||||
- TCP/UDP 連接埠轉送
|
||||
- **OpenID Connect 支援**:輕鬆實現單點登入 (SSO) 並保護您的應用程式
|
||||
- **ForwardAuth 支援**:整合任何 auth provider (例如 TinyAuth)
|
||||
- **客製化**
|
||||
- [HTTP 中介軟體](https://docs.godoxy.dev/Middlewares)
|
||||
- [支援自訂錯誤頁面](https://docs.godoxy.dev/Custom-Error-Pages)
|
||||
@@ -80,8 +85,6 @@
|
||||
- **高效能**
|
||||
- 以 **[Go](https://go.dev)** 語言編寫
|
||||
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
## 前置需求
|
||||
|
||||
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||
@@ -106,8 +109,6 @@
|
||||
|
||||
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
### 手動安裝
|
||||
|
||||
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||
@@ -149,29 +150,17 @@
|
||||
|
||||

|
||||
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
### 監控
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
|
||||
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>運行時間監控</b></td>
|
||||
<td align="center"><b>Docker 日誌</b></td>
|
||||
<td align="center"><b>伺服器概覽</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>系統監控</b></td>
|
||||
<td align="center"><b>圖表</b></td>
|
||||
<td align="center"><b>路由</b></td>
|
||||
<td align="center"><b>伺服器</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -188,4 +177,8 @@
|
||||
|
||||
5. 使用 `make build` 編譯二進制檔案
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#yusing/godoxy&Date)
|
||||
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
|
||||
)
|
||||
@@ -46,6 +47,7 @@ func main() {
|
||||
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
|
||||
log.Info().Msgf("Agent name: %s", env.AgentName)
|
||||
log.Info().Msgf("Agent port: %d", env.AgentPort)
|
||||
log.Info().Msgf("Agent runtime: %s", env.Runtime)
|
||||
|
||||
log.Info().Msg(`
|
||||
Tips:
|
||||
@@ -63,9 +65,11 @@ Tips:
|
||||
server.StartAgentServer(t, opts)
|
||||
|
||||
if socketproxy.ListenAddr != "" {
|
||||
log.Info().Msgf("Docker socket listening on: %s", socketproxy.ListenAddr)
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: "docker",
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
}
|
||||
|
||||
13
agent/go.mod
13
agent/go.mod
@@ -10,17 +10,21 @@ replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
|
||||
|
||||
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
|
||||
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/go-proxy v0.17.5
|
||||
github.com/yusing/go-proxy v0.17.6
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
@@ -50,7 +54,8 @@ require (
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||
github.com/gotify/server/v2 v2.7.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
@@ -60,7 +65,6 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -70,7 +74,6 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
@@ -86,7 +89,7 @@ require (
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/yusing/ds v0.1.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
|
||||
19
agent/go.sum
19
agent/go.sum
@@ -80,10 +80,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
|
||||
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/gotify/server/v2 v2.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
|
||||
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
@@ -180,8 +180,8 @@ github.com/yusing/ds v0.1.0 h1:aiZs7jPMN3MEChUsddMYjpZFHhhAmkxrwRyIUnGy5AU=
|
||||
github.com/yusing/ds v0.1.0/go.mod h1:KC785+mtt+Bau0LLR+slExDaUjeiqLT1k9Or6Rpryh4=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
@@ -246,6 +246,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20210616094352-59db8d763f22/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -291,9 +292,9 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a h1:V8Zj/61zlL7B+VH151iV5hJlUnYc3fUNTEhLtyr9Kzc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
var agentPool = functional.NewMapOf[string, *AgentConfig]()
|
||||
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
|
||||
|
||||
func init() {
|
||||
if common.IsTest {
|
||||
@@ -51,6 +53,14 @@ func ListAgents() []*AgentConfig {
|
||||
return agents
|
||||
}
|
||||
|
||||
func IterAgents() iter.Seq2[string, *AgentConfig] {
|
||||
return agentPool.Range
|
||||
}
|
||||
|
||||
func NumAgents() int {
|
||||
return agentPool.Size()
|
||||
}
|
||||
|
||||
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
|
||||
agent, ok = agentPool.Load(addr)
|
||||
return
|
||||
|
||||
@@ -10,6 +10,16 @@ var (
|
||||
AGENT_PORT="{{.Port}}" \
|
||||
AGENT_CA_CERT="{{.CACert}}" \
|
||||
AGENT_SSL_CERT="{{.SSLCert}}" \
|
||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||
DOCKER_SOCKET="/var/run/containerd/containerd.sock" \
|
||||
RUNTIME="nerdctl" \
|
||||
{{ else if eq .ContainerRuntime "podman" -}}
|
||||
DOCKER_SOCKET="/var/run/podman/podman.sock" \
|
||||
RUNTIME="podman" \
|
||||
{{ else -}}
|
||||
DOCKER_SOCKET="/var/run/docker.sock" \
|
||||
RUNTIME="docker" \
|
||||
{{ end -}}
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/install-agent.sh)"`
|
||||
installScriptTemplate = template.Must(template.New("install.sh").Parse(installScript))
|
||||
)
|
||||
|
||||
@@ -20,9 +20,10 @@ import (
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
|
||||
httpClient *http.Client
|
||||
tlsConfig *tls.Config
|
||||
@@ -32,6 +33,7 @@ type AgentConfig struct {
|
||||
const (
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
EndpointProxyHTTP = "/proxy/http"
|
||||
EndpointHealth = "/health"
|
||||
EndpointLogs = "/logs"
|
||||
@@ -122,6 +124,30 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
return err
|
||||
}
|
||||
|
||||
// check agent runtime
|
||||
runtimeBytes, status, err := cfg.Fetch(ctx, EndpointRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch string(runtimeBytes) {
|
||||
case "docker":
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
// case "nerdctl":
|
||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||
case "podman":
|
||||
cfg.Runtime = ContainerRuntimePodman
|
||||
default:
|
||||
return fmt.Errorf("invalid agent runtime: %s", runtimeBytes)
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
// backward compatibility, old agent does not have runtime endpoint
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
default:
|
||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtimeBytes)
|
||||
}
|
||||
|
||||
cfg.Version = string(agentVersionBytes)
|
||||
agentVersion := pkg.ParseVersion(cfg.Version)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates/agent.compose.yml
|
||||
//go:embed templates/agent.compose.yml.tmpl
|
||||
agentComposeYAML string
|
||||
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml").Parse(agentComposeYAML))
|
||||
agentComposeYAMLTemplate = template.Must(template.New("agent.compose.yml.tmpl").Parse(agentComposeYAML))
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,7 +20,8 @@ const (
|
||||
|
||||
func (c *AgentComposeConfig) Generate() (string, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
if err := agentComposeYAMLTemplate.Execute(buf, c); err != nil {
|
||||
err := agentComposeYAMLTemplate.Execute(buf, c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package agent
|
||||
|
||||
type (
|
||||
AgentEnvConfig struct {
|
||||
Name string
|
||||
Port int
|
||||
CACert string
|
||||
SSLCert string
|
||||
ContainerRuntime string
|
||||
AgentEnvConfig struct {
|
||||
Name string
|
||||
Port int
|
||||
CACert string
|
||||
SSLCert string
|
||||
ContainerRuntime ContainerRuntime
|
||||
}
|
||||
AgentComposeConfig struct {
|
||||
Image string
|
||||
@@ -15,3 +17,9 @@ type (
|
||||
Generate() (string, error)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||
// ContainerRuntimeNerdctl ContainerRuntime = "nerdctl"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
@@ -16,7 +18,7 @@ func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io
|
||||
return cfg.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) ([]byte, int, error) {
|
||||
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||
req = req.WithContext(req.Context())
|
||||
req.URL.Host = AgentHost
|
||||
req.URL.Scheme = "https"
|
||||
@@ -24,11 +26,9 @@ func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) ([]byte, int
|
||||
req.RequestURI = ""
|
||||
resp, err := cfg.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return data, resp.StatusCode, nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {
|
||||
@@ -51,3 +51,22 @@ func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websoc
|
||||
"Host": {AgentHost},
|
||||
})
|
||||
}
|
||||
|
||||
// ReverseProxy reverse proxies the request to the agent
|
||||
//
|
||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
||||
// If the request has a query, it will be added to the proxy request's URL
|
||||
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) error {
|
||||
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(AgentURL), cfg.Transport())
|
||||
uri := APIEndpointBase + endpoint
|
||||
if req.URL.RawQuery != "" {
|
||||
uri += "?" + req.URL.RawQuery
|
||||
}
|
||||
r, err := http.NewRequestWithContext(req.Context(), req.Method, uri, req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header = req.Header
|
||||
rp.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
66
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
66
agent/pkg/agent/templates/agent.compose.yml.tmpl
Normal file
@@ -0,0 +1,66 @@
|
||||
services:
|
||||
agent:
|
||||
image: "{{.Image}}"
|
||||
container_name: godoxy-agent
|
||||
restart: always
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
ports:
|
||||
- "{{.Port}}:{{.Port}}"
|
||||
{{ else -}}
|
||||
network_mode: host # do not change this
|
||||
{{ end -}}
|
||||
environment:
|
||||
{{ if eq .ContainerRuntime "nerdctl" -}}
|
||||
DOCKER_SOCKET: "/var/run/containerd/containerd.sock"
|
||||
RUNTIME: "nerdctl"
|
||||
{{ else if eq .ContainerRuntime "podman" -}}
|
||||
DOCKER_SOCKET: "/var/run/podman/podman.sock"
|
||||
RUNTIME: "podman"
|
||||
{{ else -}}
|
||||
DOCKER_SOCKET: "/var/run/docker.sock"
|
||||
RUNTIME: "docker"
|
||||
{{ end -}}
|
||||
AGENT_NAME: "{{.Name}}"
|
||||
AGENT_PORT: "{{.Port}}"
|
||||
AGENT_CA_CERT: "{{.CACert}}"
|
||||
AGENT_SSL_CERT: "{{.SSLCert}}"
|
||||
# use agent as a docker socket proxy: [host]:port
|
||||
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
|
||||
LISTEN_ADDR:
|
||||
POST: false
|
||||
ALLOW_RESTARTS: false
|
||||
ALLOW_START: false
|
||||
ALLOW_STOP: false
|
||||
AUTH: false
|
||||
BUILD: false
|
||||
COMMIT: false
|
||||
CONFIGS: false
|
||||
CONTAINERS: false
|
||||
DISTRIBUTION: false
|
||||
EVENTS: true
|
||||
EXEC: false
|
||||
GRPC: false
|
||||
IMAGES: false
|
||||
INFO: false
|
||||
NETWORKS: false
|
||||
NODES: false
|
||||
PING: true
|
||||
PLUGINS: false
|
||||
SECRETS: false
|
||||
SERVICES: false
|
||||
SESSION: false
|
||||
SWARM: false
|
||||
SYSTEM: false
|
||||
TASKS: false
|
||||
VERSION: true
|
||||
VOLUMES: false
|
||||
volumes:
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
- /var/run/podman/podman.sock:/var/run/podman/podman.sock
|
||||
{{ else if eq .ContainerRuntime "nerdctl" -}}
|
||||
- /var/run/containerd/containerd.sock:/var/run/containerd/containerd.sock
|
||||
- /var/lib/nerdctl:/var/lib/nerdctl:ro # required to read metadata like network info
|
||||
{{ else -}}
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
{{ end -}}
|
||||
- ./data:/app/data
|
||||
11
agent/pkg/env/env.go
vendored
11
agent/pkg/env/env.go
vendored
@@ -3,7 +3,10 @@ package env
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DefaultAgentName() string {
|
||||
@@ -21,6 +24,7 @@ var (
|
||||
AgentCACert string
|
||||
AgentSSLCert string
|
||||
DockerSocket string
|
||||
Runtime agent.ContainerRuntime
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -35,4 +39,11 @@ func Load() {
|
||||
|
||||
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
|
||||
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
|
||||
Runtime = agent.ContainerRuntime(common.GetEnvString("RUNTIME", "docker"))
|
||||
|
||||
switch Runtime {
|
||||
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:
|
||||
default:
|
||||
log.Fatal().Str("runtime", string(Runtime)).Msg("invalid runtime")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ func NewAgentHandler() http.Handler {
|
||||
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.AgentName)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.Runtime)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
mux.ServeMux.HandleFunc("/", socketproxy.DockerSocketHandler(env.DockerSocket))
|
||||
|
||||
@@ -28,6 +28,8 @@ services:
|
||||
env_file: .env
|
||||
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /app/.next/cache # next image caching
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
|
||||
@@ -8,13 +8,13 @@ services:
|
||||
- TARGET=godoxy
|
||||
container_name: godoxy-proxy-dev
|
||||
restart: unless-stopped
|
||||
env_file: dev.env
|
||||
environment:
|
||||
TZ: Asia/Hong_Kong
|
||||
API_ADDR: :8999
|
||||
API_ADDR: 127.0.0.1:8999
|
||||
API_USER: dev
|
||||
API_PASSWORD: 1234
|
||||
API_SKIP_ORIGIN_CHECK: true
|
||||
API_JWT_SECURE: false
|
||||
API_JWT_TTL: 24h
|
||||
DEBUG: true
|
||||
API_SECRET: 1234567891234567
|
||||
@@ -30,8 +30,7 @@ services:
|
||||
- ./dev-data/error_pages:/app/error_pages:ro
|
||||
- ./dev-data/data:/app/data
|
||||
- ./dev-data/logs:/app/logs
|
||||
depends_on:
|
||||
- tinyauth
|
||||
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
|
||||
tinyauth:
|
||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
||||
container_name: tinyauth
|
||||
|
||||
29
go.mod
29
go.mod
@@ -17,11 +17,11 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 // oidc authentication
|
||||
github.com/docker/docker v28.4.0+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/go-acme/lego/v4 v4.25.2 // acme client
|
||||
github.com/go-acme/lego/v4 v4.26.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.27.0 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.6.3 // reference the Message struct for json response
|
||||
github.com/gotify/server/v2 v2.7.2 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
@@ -44,8 +44,8 @@ require (
|
||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/yusing/go-proxy/agent v0.0.0-20250910152023-7770ce7025be
|
||||
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250910152023-7770ce7025be
|
||||
github.com/yusing/go-proxy/agent v0.0.0-20250913143824-493c0afdface
|
||||
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250913143824-493c0afdface
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ require (
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
@@ -63,7 +63,6 @@ require (
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
||||
@@ -152,7 +151,6 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
@@ -162,7 +160,6 @@ require (
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sacloud/api-client-go v0.3.3 // indirect
|
||||
github.com/sacloud/go-http v0.1.9 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.17.1 // indirect
|
||||
@@ -175,7 +172,7 @@ require (
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.2.0 // indirect
|
||||
github.com/softlayer/softlayer-go v1.2.1 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
@@ -192,7 +189,7 @@ require (
|
||||
github.com/vultr/govultr/v3 v3.23.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
@@ -220,7 +217,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
@@ -232,20 +229,26 @@ require (
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.100.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.100.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
|
||||
github.com/onsi/gomega v1.38.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||
)
|
||||
|
||||
72
go.sum
72
go.sum
@@ -604,8 +604,8 @@ git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3p
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE=
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -635,6 +635,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
@@ -652,8 +654,8 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -674,6 +676,9 @@ github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4
|
||||
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
@@ -869,8 +874,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-acme/lego/v4 v4.25.2 h1:+D1Q+VnZrD+WJdlkgUEGHFFTcDrwGlE7q24IFtMmHDI=
|
||||
github.com/go-acme/lego/v4 v4.25.2/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
|
||||
github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
|
||||
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
@@ -904,6 +909,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -1036,13 +1043,12 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -1085,8 +1091,8 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
|
||||
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
|
||||
github.com/gotify/server/v2 v2.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
|
||||
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
@@ -1094,8 +1100,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
@@ -1342,7 +1348,6 @@ github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/
|
||||
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
|
||||
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
|
||||
@@ -1380,16 +1385,16 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk=
|
||||
github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -1403,8 +1408,6 @@ github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
@@ -1417,11 +1420,10 @@ github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -1543,8 +1545,8 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA=
|
||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/softlayer/softlayer-go v1.2.0 h1:UgGd1ffcqoHUNxIohEp+Bh1My4sOiek9Yy8YWBY6KEg=
|
||||
github.com/softlayer/softlayer-go v1.2.0/go.mod h1:GP2Y0KJo7xokdJCyRPwUZ0xce9xQi5a6DLTR3+Nd1Yk=
|
||||
github.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw=
|
||||
github.com/softlayer/softlayer-go v1.2.1/go.mod h1:Gz9/ktcmB7Z8EJlu+QEJJpkv8lAmnhYdB9Tc6gedjmo=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
@@ -1634,9 +1636,6 @@ github.com/vultr/govultr/v3 v3.23.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXza
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
@@ -1672,8 +1671,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
@@ -1702,6 +1701,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -2412,10 +2413,10 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a h1:V8Zj/61zlL7B+VH151iV5hJlUnYc3fUNTEhLtyr9Kzc=
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a/go.mod h1:q9+ZJOXH/LcpbpkQSsvYReIH5lCcwvfc2xE8JBSER0Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -2492,10 +2493,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
routeApi "github.com/yusing/go-proxy/internal/api/v1/route"
|
||||
"github.com/yusing/go-proxy/internal/auth"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
// @title GoDoxy API
|
||||
@@ -38,20 +39,25 @@ import (
|
||||
// @externalDocs.description GoDoxy Docs
|
||||
// @externalDocs.url https://docs.godoxy.dev
|
||||
func NewHandler() *gin.Engine {
|
||||
gin.SetMode("release")
|
||||
if !common.IsDebug {
|
||||
gin.SetMode("release")
|
||||
}
|
||||
r := gin.New()
|
||||
r.Use(ErrorHandler())
|
||||
r.Use(ErrorLoggingMiddleware())
|
||||
|
||||
r.GET("/api/v1/version", apiV1.Version)
|
||||
|
||||
v1Auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
v1Auth.HEAD("/check", authApi.Check)
|
||||
v1Auth.POST("/login", authApi.Login)
|
||||
v1Auth.GET("/callback", authApi.Callback)
|
||||
v1Auth.POST("/callback", authApi.Callback)
|
||||
v1Auth.POST("/logout", authApi.Logout)
|
||||
if auth.IsEnabled() {
|
||||
v1Auth := r.Group("/api/v1/auth")
|
||||
{
|
||||
v1Auth.HEAD("/check", authApi.Check)
|
||||
v1Auth.POST("/login", authApi.Login)
|
||||
v1Auth.GET("/callback", authApi.Callback)
|
||||
v1Auth.POST("/callback", authApi.Callback)
|
||||
v1Auth.POST("/logout", authApi.Logout)
|
||||
v1Auth.GET("/logout", authApi.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
@@ -93,7 +99,12 @@ func NewHandler() *gin.Engine {
|
||||
homepage.POST("/set/item", homepageApi.SetItem)
|
||||
homepage.POST("/set/items_batch", homepageApi.SetItemsBatch)
|
||||
homepage.POST("/set/item_visible", homepageApi.SetItemVisible)
|
||||
homepage.POST("/set/item_favorite", homepageApi.SetItemFavorite)
|
||||
homepage.POST("/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||
homepage.POST("/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||
homepage.POST("/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||
homepage.POST("/set/category_order", homepageApi.SetCategoryOrder)
|
||||
homepage.POST("/item_click", homepageApi.ItemClick)
|
||||
}
|
||||
|
||||
cert := v1.Group("/cert")
|
||||
@@ -112,14 +123,19 @@ func NewHandler() *gin.Engine {
|
||||
metrics := v1.Group("/metrics")
|
||||
{
|
||||
metrics.GET("/system_info", metricsApi.SystemInfo)
|
||||
metrics.GET("/all_system_info", metricsApi.AllSystemInfo)
|
||||
metrics.GET("/uptime", metricsApi.Uptime)
|
||||
}
|
||||
|
||||
docker := v1.Group("/docker")
|
||||
{
|
||||
docker.GET("/container/:id", dockerApi.GetContainer)
|
||||
docker.GET("/containers", dockerApi.Containers)
|
||||
docker.GET("/info", dockerApi.Info)
|
||||
docker.GET("/logs/:server/:container", dockerApi.Logs)
|
||||
docker.GET("/logs/:id", dockerApi.Logs)
|
||||
docker.POST("/start", dockerApi.Start)
|
||||
docker.POST("/stop", dockerApi.Stop)
|
||||
docker.POST("/restart", dockerApi.Restart)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,10 +198,11 @@ func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
if len(c.Errors) > 0 {
|
||||
logger := log.With().Str("uri", c.Request.RequestURI).Logger()
|
||||
for _, err := range c.Errors {
|
||||
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
||||
gperr.LogError("Internal error", err.Err, &logger)
|
||||
}
|
||||
if !isWebSocketRequest(c) {
|
||||
if !c.IsWebsocket() {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||
}
|
||||
}
|
||||
@@ -195,12 +212,8 @@ func ErrorHandler() gin.HandlerFunc {
|
||||
func ErrorLoggingMiddleware() gin.HandlerFunc {
|
||||
return gin.CustomRecoveryWithWriter(nil, func(c *gin.Context, err any) {
|
||||
log.Error().Any("error", err).Str("uri", c.Request.RequestURI).Msg("Internal error")
|
||||
if !isWebSocketRequest(c) {
|
||||
if !c.IsWebsocket() {
|
||||
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func isWebSocketRequest(c *gin.Context) bool {
|
||||
return c.GetHeader("Upgrade") == "websocket"
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package apitypes
|
||||
|
||||
type ErrorCode int
|
||||
|
||||
const (
|
||||
ErrorCodeUnauthorized ErrorCode = iota + 1
|
||||
ErrorCodeNotFound
|
||||
ErrorCodeInternalServerError
|
||||
)
|
||||
|
||||
func (e ErrorCode) String() string {
|
||||
return []string{
|
||||
"Unauthorized",
|
||||
"Not Found",
|
||||
"Internal Server Error",
|
||||
}[e]
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type NewAgentRequest struct {
|
||||
Name string `form:"name" validate:"required"`
|
||||
Host string `form:"host" validate:"required"`
|
||||
Port int `form:"port" validate:"required,min=1,max=65535"`
|
||||
Type string `form:"type" validate:"required,oneof=docker system"`
|
||||
Nightly bool `form:"nightly" validate:"omitempty"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
Port int `json:"port" binding:"required,min=1,max=65535"`
|
||||
Type string `json:"type" binding:"required,oneof=docker system"`
|
||||
Nightly bool `json:"nightly" binding:"omitempty"`
|
||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime" binding:"omitempty,oneof=docker podman" default:"docker"`
|
||||
} // @name NewAgentRequest
|
||||
|
||||
type NewAgentResponse struct {
|
||||
@@ -47,6 +48,7 @@ func Create(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
||||
if _, ok := agent.GetAgent(hostport); ok {
|
||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
||||
@@ -67,10 +69,11 @@ func Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
var cfg agent.Generator = &agent.AgentEnvConfig{
|
||||
Name: request.Name,
|
||||
Port: request.Port,
|
||||
CACert: ca.String(),
|
||||
SSLCert: srv.String(),
|
||||
Name: request.Name,
|
||||
Port: request.Port,
|
||||
CACert: ca.String(),
|
||||
SSLCert: srv.String(),
|
||||
ContainerRuntime: request.ContainerRuntime,
|
||||
}
|
||||
if request.Type == "docker" {
|
||||
cfg = &agent.AgentComposeConfig{
|
||||
|
||||
@@ -6,15 +6,17 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||
. "github.com/yusing/go-proxy/internal/api/types"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
)
|
||||
|
||||
type VerifyNewAgentRequest struct {
|
||||
Host string `json:"host"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
Host string `json:"host"`
|
||||
CA PEMPairResponse `json:"ca"`
|
||||
Client PEMPairResponse `json:"client"`
|
||||
ContainerRuntime agent.ContainerRuntime `json:"container_runtime"`
|
||||
} // @name VerifyNewAgentRequest
|
||||
|
||||
// @x-id "verify"
|
||||
@@ -55,7 +57,7 @@ func Verify(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client)
|
||||
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, Error("invalid request", err))
|
||||
return
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
// @Failure 400 {string} string "OIDC: invalid request (missing state cookie or oauth state)"
|
||||
// @Failure 400 {string} string "Userpass: invalid request / credentials"
|
||||
// @Failure 500 {string} string "Internal server error"
|
||||
// @Router /auth/callback [get]
|
||||
// @Router /auth/callback [post]
|
||||
func Callback(c *gin.Context) {
|
||||
auth.GetDefaultAuth().PostAuthCallbackHandler(c.Writer, c.Request)
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/auth"
|
||||
)
|
||||
|
||||
// @x-id "check"
|
||||
// @x-id "check"
|
||||
// @Base /api/v1
|
||||
// @Summary Check authentication status
|
||||
// @Description Checks if the user is authenticated by validating their token
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Failure 403 {string} string "Forbidden: use X-Redirect-To header to redirect to login page"
|
||||
// @Failure 302 {string} string "Redirects to login page or IdP"
|
||||
// @Router /auth/check [head]
|
||||
func Check(c *gin.Context) {
|
||||
auth.AuthCheckHandler(c.Writer, c.Request)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
// @Tags auth
|
||||
// @Produce plain
|
||||
// @Success 302 {string} string "Redirects to login page or IdP"
|
||||
// @Failure 403 {string} string "Forbidden(webui): follow X-Redirect-To header"
|
||||
// @Failure 429 {string} string "Too Many Requests"
|
||||
// @Router /auth/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// @Produce plain
|
||||
// @Success 302 {string} string "Redirects to home page"
|
||||
// @Router /auth/logout [post]
|
||||
// @Router /auth/logout [get]
|
||||
func Logout(c *gin.Context) {
|
||||
auth.GetDefaultAuth().LogoutHandler(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
63
internal/api/v1/docker/container.go
Normal file
63
internal/api/v1/docker/container.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
)
|
||||
|
||||
// @x-id "container"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get container
|
||||
// @Description Get container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param id path string true "Container ID"
|
||||
// @Success 200 {object} Container
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "ID is required"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/container/{id} [get]
|
||||
func GetContainer(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
cont, err := client.ContainerInspect(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerHost,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type Container struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
State ContainerState `json:"state"`
|
||||
State ContainerState `json:"state,omitempty" extensions:"x-nullable"`
|
||||
} // @name ContainerResponse
|
||||
|
||||
// @x-id "containers"
|
||||
|
||||
@@ -3,6 +3,7 @@ package dockerapi
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -10,15 +11,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
type LogsPathParams struct {
|
||||
Server string `uri:"server" binding:"required"`
|
||||
ContainerID string `uri:"container" binding:"required"`
|
||||
} // @name LogsPathParams
|
||||
|
||||
type LogsQueryParams struct {
|
||||
Stdout bool `form:"stdout,default=true"`
|
||||
Stderr bool `form:"stderr,default=true"`
|
||||
@@ -30,12 +27,11 @@ type LogsQueryParams struct {
|
||||
// @x-id "logs"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get docker container logs
|
||||
// @Description Get docker container logs
|
||||
// @Description Get docker container logs by container id
|
||||
// @Tags docker,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param server path string true "server name"
|
||||
// @Param container path string true "container id"
|
||||
// @Param id path string true "container id"
|
||||
// @Param stdout query bool false "show stdout"
|
||||
// @Param stderr query bool false "show stderr"
|
||||
// @Param from query string false "from timestamp"
|
||||
@@ -44,31 +40,34 @@ type LogsQueryParams struct {
|
||||
// @Success 200
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "server not found or container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/logs/{server}/{container} [get]
|
||||
// @Router /docker/logs/{id} [get]
|
||||
func Logs(c *gin.Context) {
|
||||
var pathParams LogsPathParams
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("container id is required"))
|
||||
return
|
||||
}
|
||||
|
||||
var queryParams LogsQueryParams
|
||||
if err := c.ShouldBindQuery(&queryParams); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query params"))
|
||||
return
|
||||
}
|
||||
if err := c.ShouldBindUri(&pathParams); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid path params"))
|
||||
|
||||
// TODO: implement levels
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
||||
return
|
||||
}
|
||||
// TODO: implement levels
|
||||
|
||||
dockerClient, found, err := getDockerClient(pathParams.Server)
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("server not found"))
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := container.LogsOptions{
|
||||
@@ -84,7 +83,7 @@ func Logs(c *gin.Context) {
|
||||
opts.Details = true
|
||||
}
|
||||
|
||||
logs, err := dockerClient.ContainerLogs(c.Request.Context(), pathParams.ContainerID, opts)
|
||||
logs, err := dockerClient.ContainerLogs(c.Request.Context(), id, opts)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get container logs"))
|
||||
return
|
||||
@@ -106,8 +105,8 @@ func Logs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
log.Err(err).
|
||||
Str("server", pathParams.Server).
|
||||
Str("container", pathParams.ContainerID).
|
||||
Str("server", dockerHost).
|
||||
Str("container", id).
|
||||
Msg("failed to de-multiplex logs")
|
||||
}
|
||||
}
|
||||
|
||||
52
internal/api/v1/docker/restart.go
Normal file
52
internal/api/v1/docker/restart.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
)
|
||||
|
||||
// @x-id "restart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart container
|
||||
// @Description Restart container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container restarted"))
|
||||
}
|
||||
58
internal/api/v1/docker/start.go
Normal file
58
internal/api/v1/docker/start.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
)
|
||||
|
||||
type StartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
container.StartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Start container
|
||||
// @Description Start container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StartRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/start [post]
|
||||
func Start(c *gin.Context) {
|
||||
var req StartRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container started"))
|
||||
}
|
||||
58
internal/api/v1/docker/stop.go
Normal file
58
internal/api/v1/docker/stop.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
)
|
||||
|
||||
type StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
container.StopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Stop container
|
||||
// @Description Stop container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "Container not found"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/stop [post]
|
||||
func Stop(c *gin.Context) {
|
||||
var req StopRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apitypes.Success("container stopped"))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
)
|
||||
|
||||
@@ -18,5 +19,24 @@ import (
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/categories [get]
|
||||
func Categories(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, routes.HomepageCategories())
|
||||
c.JSON(http.StatusOK, HomepageCategories())
|
||||
}
|
||||
|
||||
func HomepageCategories() []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
categories = append(categories, homepage.CategoryAll)
|
||||
categories = append(categories, homepage.CategoryFavorites)
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
item := r.HomepageItem()
|
||||
if item.Category == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := check[item.Category]; ok {
|
||||
continue
|
||||
}
|
||||
check[item.Category] = struct{}{}
|
||||
categories = append(categories, item.Category)
|
||||
}
|
||||
return categories
|
||||
}
|
||||
|
||||
36
internal/api/v1/homepage/item_click.go
Normal file
36
internal/api/v1/homepage/item_click.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
)
|
||||
|
||||
type HomepageOverrideItemClickParams struct {
|
||||
Which string `form:"which" binding:"required"`
|
||||
} // @name HomepageOverrideItemClickParams
|
||||
|
||||
// @x-id "item-click"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Increment item click
|
||||
// @Description Increment item click.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request query HomepageOverrideItemClickParams true "Increment item click"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/item_click [post]
|
||||
func ItemClick(c *gin.Context) {
|
||||
var params HomepageOverrideItemClickParams
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.IncrementItemClicks(params.Which)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
@@ -1,27 +1,38 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
)
|
||||
|
||||
type HomepageItemsRequest struct {
|
||||
Category string `form:"category" validate:"omitempty"`
|
||||
Provider string `form:"provider" validate:"omitempty"`
|
||||
SearchQuery string `form:"search"` // Search query
|
||||
Category string `form:"category"` // Category filter
|
||||
Provider string `form:"provider"` // Provider filter
|
||||
// Sort method
|
||||
SortMethod homepage.SortMethod `form:"sort_method" default:"alphabetical" binding:"omitempty,oneof=clicks alphabetical custom"`
|
||||
} // @name HomepageItemsRequest
|
||||
|
||||
// @x-id "items"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Homepage items
|
||||
// @Description Homepage items
|
||||
// @Tags homepage
|
||||
// @Tags homepage,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param category query string false "Category filter"
|
||||
// @Param provider query string false "Provider filter"
|
||||
// @Param query query HomepageItemsRequest false "Query parameters"
|
||||
// @Success 200 {object} homepage.Homepage
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -42,5 +53,81 @@ func Items(c *gin.Context) {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, routes.HomepageItems(proto, hostname, request.Category, request.Provider))
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 2*time.Second, func() (any, error) {
|
||||
return HomepageItems(proto, hostname, &request), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, HomepageItems(proto, hostname, &request))
|
||||
}
|
||||
}
|
||||
|
||||
func HomepageItems(proto, hostname string, request *HomepageItemsRequest) homepage.Homepage {
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
default:
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
hp := homepage.NewHomepageMap(routes.HTTP.Size())
|
||||
|
||||
if strings.Count(hostname, ".") > 1 {
|
||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||
}
|
||||
|
||||
for _, r := range routes.HTTP.Iter {
|
||||
if request.Provider != "" && r.ProviderName() != request.Provider {
|
||||
continue
|
||||
}
|
||||
item := r.HomepageItem()
|
||||
if request.Category != "" && item.Category != request.Category {
|
||||
continue
|
||||
}
|
||||
if request.SearchQuery != "" && !fuzzy.MatchFold(request.SearchQuery, item.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// clear url if invalid
|
||||
_, err := url.Parse(item.URL)
|
||||
if err != nil {
|
||||
item.URL = ""
|
||||
}
|
||||
|
||||
// append hostname if provided and only if alias is not FQDN
|
||||
if hostname != "" && item.URL == "" {
|
||||
isFQDNAlias := strings.Contains(item.Alias, ".")
|
||||
if !isFQDNAlias {
|
||||
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
|
||||
} else {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// prepend protocol if not exists
|
||||
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
|
||||
}
|
||||
|
||||
hp.Add(&item)
|
||||
}
|
||||
|
||||
ret := hp.Values()
|
||||
// sort items in each category
|
||||
for _, category := range ret {
|
||||
category.Sort(request.SortMethod)
|
||||
}
|
||||
// sort categories
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
slices.SortStableFunc(ret, func(a, b *homepage.Category) int {
|
||||
// if category is "Hidden", move it to the end of the list
|
||||
if a.Name == homepage.CategoryHidden {
|
||||
return 1
|
||||
}
|
||||
if b.Name == homepage.CategoryHidden {
|
||||
return -1
|
||||
}
|
||||
// sort categories by order in config
|
||||
return overrides.CategoryOrder[a.Name] - overrides.CategoryOrder[b.Name]
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package homepageapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,16 +14,22 @@ type (
|
||||
Value homepage.ItemConfig `json:"value"`
|
||||
} // @name HomepageOverrideItemParams
|
||||
HomepageOverrideItemsBatchParams struct {
|
||||
Value map[string]*homepage.ItemConfig `json:"value"`
|
||||
Value map[string]homepage.ItemConfig `json:"value"`
|
||||
} // @name HomepageOverrideItemsBatchParams
|
||||
|
||||
HomepageOverrideCategoryOrderParams struct {
|
||||
Which string `json:"which"`
|
||||
Value int `json:"value"`
|
||||
} // @name HomepageOverrideCategoryOrderParams
|
||||
HomepageOverrideItemSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemSortOrderParams
|
||||
HomepageOverrideItemAllSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemAllSortOrderParams
|
||||
HomepageOverrideItemFavSortOrderParams HomepageOverrideCategoryOrderParams // @name HomepageOverrideItemFavSortOrderParams
|
||||
|
||||
HomepageOverrideItemVisibleParams struct {
|
||||
Which []string `json:"which"`
|
||||
Value bool `json:"value"`
|
||||
} // @name HomepageOverrideItemVisibleParams
|
||||
HomepageOverrideItemFavoriteParams HomepageOverrideItemVisibleParams // @name HomepageOverrideItemFavoriteParams
|
||||
)
|
||||
|
||||
// @x-id "set-item"
|
||||
@@ -46,7 +51,7 @@ func SetItem(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.OverrideItem(params.Which, ¶ms.Value)
|
||||
overrides.OverrideItem(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
@@ -65,15 +70,8 @@ func SetItem(c *gin.Context) {
|
||||
func SetItemsBatch(c *gin.Context) {
|
||||
var params HomepageOverrideItemsBatchParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
data, derr := c.GetRawData()
|
||||
if derr != nil {
|
||||
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
|
||||
return
|
||||
}
|
||||
if uerr := json.Unmarshal(data, ¶ms); uerr != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.OverrideItems(params.Value)
|
||||
@@ -95,22 +93,103 @@ func SetItemsBatch(c *gin.Context) {
|
||||
func SetItemVisible(c *gin.Context) {
|
||||
var params HomepageOverrideItemVisibleParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
data, derr := c.GetRawData()
|
||||
if derr != nil {
|
||||
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
|
||||
return
|
||||
}
|
||||
if uerr := json.Unmarshal(data, ¶ms); uerr != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
if params.Value {
|
||||
overrides.UnhideItems(params.Which)
|
||||
} else {
|
||||
overrides.HideItems(params.Which)
|
||||
overrides.SetItemsVisibility(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-favorite"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item favorite
|
||||
// @Description Set homepage item favorite.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemFavoriteParams true "Set item favorite"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_favorite [post]
|
||||
func SetItemFavorite(c *gin.Context) {
|
||||
var params HomepageOverrideItemFavoriteParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetItemsFavorite(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item sort order
|
||||
// @Description Set homepage item sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemSortOrderParams true "Set item sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_sort_order [post]
|
||||
func SetItemSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-all-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item all sort order
|
||||
// @Description Set homepage item all sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemAllSortOrderParams true "Set item all sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_all_sort_order [post]
|
||||
func SetItemAllSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemAllSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetAllSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
// @x-id "set-item-fav-sort-order"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Set homepage item fav sort order
|
||||
// @Description Set homepage item fav sort order.
|
||||
// @Tags homepage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body HomepageOverrideItemFavSortOrderParams true "Set item fav sort order"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /homepage/set/item_fav_sort_order [post]
|
||||
func SetItemFavSortOrder(c *gin.Context) {
|
||||
var params HomepageOverrideItemFavSortOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetFavSortOrder(params.Which, params.Value)
|
||||
c.JSON(http.StatusOK, apitypes.Success("success"))
|
||||
}
|
||||
|
||||
@@ -129,15 +208,8 @@ func SetItemVisible(c *gin.Context) {
|
||||
func SetCategoryOrder(c *gin.Context) {
|
||||
var params HomepageOverrideCategoryOrderParams
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
data, derr := c.GetRawData()
|
||||
if derr != nil {
|
||||
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
|
||||
return
|
||||
}
|
||||
if uerr := json.Unmarshal(data, ¶ms); uerr != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
overrides := homepage.GetOverrideConfig()
|
||||
overrides.SetCategoryOrder(params.Which, params.Value)
|
||||
|
||||
267
internal/api/v1/metrics/all_system_info.go
Normal file
267
internal/api/v1/metrics/all_system_info.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/metrics/period"
|
||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
||||
"github.com/yusing/go-proxy/internal/utils/synk"
|
||||
)
|
||||
|
||||
var (
|
||||
// for json marshaling (unknown size)
|
||||
allSystemInfoBytesPool = synk.GetBytesPoolWithUniqueMemory()
|
||||
// for storing http response body (known size)
|
||||
allSystemInfoFixedSizePool = synk.GetBytesPool()
|
||||
)
|
||||
|
||||
type AllSystemInfoRequest struct {
|
||||
Period period.Filter `query:"period"`
|
||||
Aggregate systeminfo.SystemInfoAggregateMode `query:"aggregate"`
|
||||
Interval time.Duration `query:"interval" swaggertype:"string" format:"duration"`
|
||||
} // @name AllSystemInfoRequest
|
||||
|
||||
type bytesFromPool struct {
|
||||
json.RawMessage
|
||||
}
|
||||
|
||||
// @x-id "all_system_info"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get system info
|
||||
// @Description Get system info
|
||||
// @Tags metrics,websocket
|
||||
// @Produce json
|
||||
// @Param request query AllSystemInfoRequest false "Request"
|
||||
// @Success 200 {object} map[string]systeminfo.SystemInfo "no period specified, system info by agent name"
|
||||
// @Success 200 {object} map[string]SystemInfoAggregate "period specified, aggregated system info by agent name"
|
||||
// @Failure 400 {object} apitypes.ErrorResponse
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /metrics/all_system_info [get]
|
||||
func AllSystemInfo(c *gin.Context) {
|
||||
var req AllSystemInfoRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid query", err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Interval < period.PollInterval {
|
||||
req.Interval = period.PollInterval
|
||||
}
|
||||
|
||||
if !httpheaders.IsWebsocket(c.Request.Header) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("bad request, websocket is required"))
|
||||
return
|
||||
}
|
||||
|
||||
manager, err := websocket.NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
|
||||
query := c.Request.URL.Query()
|
||||
queryEncoded := c.Request.URL.Query().Encode()
|
||||
|
||||
type SystemInfoData struct {
|
||||
AgentName string
|
||||
SystemInfo any
|
||||
}
|
||||
|
||||
// leave 5 extra slots for buffering in case new agents are added.
|
||||
dataCh := make(chan SystemInfoData, 1+agent.NumAgents()+5)
|
||||
defer close(dataCh)
|
||||
|
||||
ticker := time.NewTicker(req.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case data := <-dataCh:
|
||||
err := marshalSystemInfo(manager, data.AgentName, data.SystemInfo)
|
||||
if err != nil {
|
||||
manager.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// processing function for one round.
|
||||
doRound := func() (bool, error) {
|
||||
var roundWg sync.WaitGroup
|
||||
var numErrs atomic.Int32
|
||||
|
||||
totalAgents := int32(1) // myself
|
||||
|
||||
errs := gperr.NewBuilderWithConcurrency()
|
||||
// get system info for me and all agents in parallel.
|
||||
roundWg.Go(func() {
|
||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||
if err != nil {
|
||||
errs.Add(gperr.Wrap(err, "Main server"))
|
||||
numErrs.Add(1)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: "GoDoxy",
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
})
|
||||
|
||||
for _, a := range agent.IterAgents() {
|
||||
totalAgents++
|
||||
agentShallowCopy := *a
|
||||
|
||||
roundWg.Go(func() {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), &agentShallowCopy, queryEncoded)
|
||||
if err != nil {
|
||||
errs.Add(gperr.Wrap(err, "Agent "+agentShallowCopy.Name))
|
||||
numErrs.Add(1)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: agentShallowCopy.Name,
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
roundWg.Wait()
|
||||
return numErrs.Load() == totalAgents, errs.Error()
|
||||
}
|
||||
|
||||
// write system info immediately once.
|
||||
if shouldContinue, err := doRound(); err != nil {
|
||||
if !shouldContinue {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// then continue on the ticker.
|
||||
for {
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if shouldContinue, err := doRound(); err != nil {
|
||||
if !shouldContinue {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get all system info"))
|
||||
return
|
||||
}
|
||||
gperr.LogWarn("failed to get some system info", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
path := agent.EndpointSystemInfo + "?" + query
|
||||
resp, err := a.Do(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// NOTE: buffer will be released by marshalSystemInfo once marshaling is done.
|
||||
if resp.ContentLength >= 0 {
|
||||
bytesBuf := allSystemInfoFixedSizePool.GetSized(int(resp.ContentLength))
|
||||
_, err = io.ReadFull(resp.Body, bytesBuf)
|
||||
if err != nil {
|
||||
// prevent pool leak on error.
|
||||
allSystemInfoFixedSizePool.Put(bytesBuf)
|
||||
return nil, err
|
||||
}
|
||||
return bytesFromPool{json.RawMessage(bytesBuf)}, nil
|
||||
}
|
||||
|
||||
// Fallback when content length is unknown (should not happen but just in case).
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.RawMessage(data), nil
|
||||
}
|
||||
|
||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, query string) (json.Marshaler, error) {
|
||||
const maxRetries = 3
|
||||
var lastErr error
|
||||
|
||||
for attempt := range maxRetries {
|
||||
// Apply backoff delay for retries (not for first attempt)
|
||||
if attempt > 0 {
|
||||
delay := max((1<<attempt)*time.Second, 5*time.Second)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
|
||||
data, err := getAgentSystemInfo(ctx, a, query)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
log.Debug().Str("agent", a.Name).Int("attempt", attempt+1).Str("error", err.Error()).Msg("Agent request attempt failed")
|
||||
|
||||
// Don't retry on context cancellation
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func marshalSystemInfo(ws *websocket.Manager, agentName string, systemInfo any) error {
|
||||
bytesBuf := allSystemInfoBytesPool.Get()
|
||||
defer allSystemInfoBytesPool.Put(bytesBuf)
|
||||
|
||||
// release the buffer retrieved from getAgentSystemInfo
|
||||
if bufFromPool, ok := systemInfo.(bytesFromPool); ok {
|
||||
defer allSystemInfoFixedSizePool.Put(bufFromPool.RawMessage)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(bytesBuf)
|
||||
err := json.NewEncoder(buf).Encode(map[string]any{
|
||||
agentName: systemInfo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ws.WriteData(websocket.TextMessage, buf.Bytes(), 3*time.Second)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -9,17 +11,16 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/metrics/period"
|
||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
type SystemInfoRequest struct {
|
||||
AgentAddr string `query:"agent_addr"`
|
||||
AgentName string `query:"agent_name"`
|
||||
Aggregate systeminfo.SystemInfoAggregateMode `query:"aggregate"`
|
||||
Period period.Filter `query:"period"`
|
||||
} // @name SystemInfoRequest
|
||||
|
||||
type SystemInfoAggregate period.ResponseType[systeminfo.Aggregated] // @name SystemInfoAggregate
|
||||
type SystemInfoAggregate period.ResponseType[systeminfo.AggregatedJSON] // @name SystemInfoAggregate
|
||||
|
||||
// @x-id "system_info"
|
||||
// @BasePath /api/v1
|
||||
@@ -38,39 +39,40 @@ type SystemInfoAggregate period.ResponseType[systeminfo.Aggregated] // @name Sys
|
||||
func SystemInfo(c *gin.Context) {
|
||||
query := c.Request.URL.Query()
|
||||
agentAddr := query.Get("agent_addr")
|
||||
agentName := query.Get("agent_name")
|
||||
query.Del("agent_addr")
|
||||
if agentAddr == "" {
|
||||
query.Del("agent_name")
|
||||
if agentAddr == "" && agentName == "" {
|
||||
systeminfo.Poller.ServeHTTP(c)
|
||||
return
|
||||
}
|
||||
|
||||
agent, ok := agentPkg.GetAgent(agentAddr)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("agent_addr not found"))
|
||||
agent, ok = agentPkg.GetAgentByName(agentName)
|
||||
}
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("agent_addr or agent_name not found"))
|
||||
return
|
||||
}
|
||||
|
||||
isWS := httpheaders.IsWebsocket(c.Request.Header)
|
||||
if !isWS {
|
||||
respData, status, err := agent.Forward(c.Request, agentPkg.EndpointSystemInfo)
|
||||
resp, err := agent.Forward(c.Request, agentPkg.EndpointSystemInfo)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to forward request to agent"))
|
||||
return
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
c.JSON(status, apitypes.Error(string(respData)))
|
||||
return
|
||||
}
|
||||
c.JSON(status, respData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
maps.Copy(c.Writer.Header(), resp.Header)
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
} else {
|
||||
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(agentPkg.AgentURL), agent.Transport())
|
||||
header := c.Request.Header.Clone()
|
||||
r, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
|
||||
err := agent.ReverseProxy(c.Writer, c.Request, agentPkg.EndpointSystemInfo+"?"+query.Encode())
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create request"))
|
||||
c.Error(apitypes.InternalServerError(err, "failed to reverse proxy"))
|
||||
return
|
||||
}
|
||||
r.Header = header
|
||||
rp.ServeHTTP(c.Writer, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,14 @@ func Route(c *gin.Context) {
|
||||
route, ok := routes.Get(request.Which)
|
||||
if ok {
|
||||
c.JSON(http.StatusOK, route)
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// also search for excluded routes
|
||||
route = config.GetInstance().SearchRoute(request.Which)
|
||||
if route != nil {
|
||||
c.JSON(http.StatusOK, route)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, nil)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type StatsResponse struct {
|
||||
Proxies ProxyStats `json:"proxies"`
|
||||
Uptime string `json:"uptime"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
} // @name StatsResponse
|
||||
|
||||
type ProxyStats struct {
|
||||
@@ -40,7 +39,7 @@ func Stats(c *gin.Context) {
|
||||
getStats := func() (any, error) {
|
||||
return map[string]any{
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": strutils.FormatDuration(time.Since(startTime)),
|
||||
"uptime": int64(time.Since(startTime).Round(time.Second).Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -247,12 +247,7 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthState), state, 300*time.Second)
|
||||
// redirect user to Idp
|
||||
url := auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r))
|
||||
if IsFrontend(r) {
|
||||
w.Header().Set("X-Redirect-To", url)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
} else {
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||
|
||||
@@ -129,8 +129,7 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Redirect-To", "/login")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -27,6 +28,8 @@ type Config struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
|
||||
Resolvers []string `json:"resolvers,omitempty"`
|
||||
|
||||
// Custom ACME CA
|
||||
CADirURL string `json:"ca_dir_url,omitempty"`
|
||||
CACerts []string `json:"ca_certs,omitempty"`
|
||||
@@ -111,6 +114,12 @@ func (cfg *Config) Validate() gperr.Error {
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
func (cfg *Config) dns01Options() []dns01.ChallengeOption {
|
||||
return []dns01.ChallengeOption{
|
||||
dns01.CondOption(len(cfg.Resolvers) > 0, dns01.AddRecursiveNameservers(cfg.Resolvers)),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -286,7 +286,7 @@ func (p *Provider) initClient() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = legoClient.Challenge.SetDNS01Provider(p.cfg.challengeProvider)
|
||||
err = legoClient.Challenge.SetDNS01Provider(p.cfg.challengeProvider, p.cfg.dns01Options()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
)
|
||||
|
||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
|
||||
if slices.ContainsFunc(cfg.value.Providers.Agents, func(a *agent.AgentConfig) bool {
|
||||
return a.Addr == host
|
||||
}) {
|
||||
return 0, gperr.New("agent already exists")
|
||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
for _, a := range cfg.value.Providers.Agents {
|
||||
if a.Addr == host {
|
||||
return 0, gperr.New("agent already exists")
|
||||
}
|
||||
}
|
||||
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
agentCfg := agent.AgentConfig{
|
||||
Addr: host,
|
||||
Runtime: containerRuntime,
|
||||
}
|
||||
err := agentCfg.StartWithCerts(cfg.Task().Context(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to start agent")
|
||||
}
|
||||
agent.AddAgent(&agentCfg)
|
||||
|
||||
provider := provider.NewAgentProvider(&agentCfg)
|
||||
if err := cfg.errIfExists(provider); err != nil {
|
||||
agent.RemoveAgent(&agentCfg)
|
||||
return 0, err
|
||||
if _, loaded := cfg.providers.LoadOrStore(provider.String(), provider); loaded {
|
||||
return 0, gperr.Errorf("provider %s already exists", provider.String())
|
||||
}
|
||||
|
||||
// agent must be added before loading routes
|
||||
agent.AddAgent(&agentCfg)
|
||||
err = provider.LoadRoutes()
|
||||
if err != nil {
|
||||
cfg.providers.Delete(provider.String())
|
||||
agent.RemoveAgent(&agentCfg)
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
}
|
||||
|
||||
return provider.NumRoutes(), nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||
"github.com/yusing/go-proxy/internal/serialization"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
|
||||
type Config struct {
|
||||
value *config.Config
|
||||
providers F.Map[string, *proxy.Provider]
|
||||
providers *xsync.Map[string, *proxy.Provider]
|
||||
autocertProvider *autocert.Provider
|
||||
entrypoint *entrypoint.Entrypoint
|
||||
|
||||
@@ -59,7 +59,7 @@ var Validate = config.Validate
|
||||
func newConfig() *Config {
|
||||
return &Config{
|
||||
value: config.DefaultConfig(),
|
||||
providers: F.NewMapOf[string, *proxy.Provider](),
|
||||
providers: xsync.NewMap[string, *proxy.Provider](),
|
||||
entrypoint: entrypoint.NewEntrypoint(),
|
||||
task: task.RootTask("config", false),
|
||||
}
|
||||
@@ -174,12 +174,19 @@ func (cfg *Config) StartAutoCert() {
|
||||
}
|
||||
|
||||
func (cfg *Config) StartProxyProviders() {
|
||||
errs := cfg.providers.CollectErrors(
|
||||
func(_ string, p *proxy.Provider) error {
|
||||
return p.Start(cfg.task)
|
||||
})
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if err := gperr.Join(errs...); err != nil {
|
||||
errs := gperr.NewBuilderWithConcurrency()
|
||||
for _, p := range cfg.providers.Range {
|
||||
wg.Go(func() {
|
||||
if err := p.Start(cfg.task); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if err := errs.Error(); err != nil {
|
||||
gperr.LogError("route provider errors", err)
|
||||
}
|
||||
}
|
||||
@@ -315,72 +322,87 @@ func (cfg *Config) initProxmox(proxmoxCfg []proxmox.Config) gperr.Error {
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
|
||||
if _, ok := cfg.providers.Load(p.String()); ok {
|
||||
return gperr.Errorf("provider %s already exists", p.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) storeProvider(p *proxy.Provider) {
|
||||
cfg.providers.Store(p.String(), p)
|
||||
}
|
||||
|
||||
func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||
errs := gperr.NewBuilder("route provider errors")
|
||||
errs := gperr.NewBuilderWithConcurrency("route provider errors")
|
||||
results := gperr.NewBuilder("loaded route providers")
|
||||
|
||||
agentPkg.RemoveAllAgents()
|
||||
|
||||
numProviders := len(providers.Agents) + len(providers.Files) + len(providers.Docker)
|
||||
providersCh := make(chan *proxy.Provider, numProviders)
|
||||
|
||||
// start providers concurrently
|
||||
var providersConsumer sync.WaitGroup
|
||||
providersConsumer.Go(func() {
|
||||
for p := range providersCh {
|
||||
if actual, loaded := cfg.providers.LoadOrStore(p.String(), p); loaded {
|
||||
errs.Add(gperr.Errorf("provider %s already exists, first: %s, second: %s", p.String(), actual.GetType(), p.GetType()))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
})
|
||||
|
||||
var providersProducer sync.WaitGroup
|
||||
for _, agent := range providers.Agents {
|
||||
if err := agent.Start(cfg.task.Context()); err != nil {
|
||||
errs.Add(gperr.PrependSubject(agent.String(), err))
|
||||
continue
|
||||
}
|
||||
agentPkg.AddAgent(agent)
|
||||
p := proxy.NewAgentProvider(agent)
|
||||
if err := cfg.errIfExists(p); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
for _, filename := range providers.Files {
|
||||
p, err := proxy.NewFileProvider(filename)
|
||||
if err == nil {
|
||||
err = cfg.errIfExists(p)
|
||||
}
|
||||
if err != nil {
|
||||
errs.Add(gperr.PrependSubject(filename, err))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
for name, dockerHost := range providers.Docker {
|
||||
p := proxy.NewDockerProvider(name, dockerHost)
|
||||
if err := cfg.errIfExists(p); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
if cfg.providers.Size() == 0 {
|
||||
return nil
|
||||
providersProducer.Go(func() {
|
||||
if err := agent.Start(cfg.task.Context()); err != nil {
|
||||
errs.Add(gperr.PrependSubject(agent.String(), err))
|
||||
return
|
||||
}
|
||||
agentPkg.AddAgent(agent)
|
||||
p := proxy.NewAgentProvider(agent)
|
||||
providersCh <- p
|
||||
})
|
||||
}
|
||||
|
||||
for _, filename := range providers.Files {
|
||||
providersProducer.Go(func() {
|
||||
p, err := proxy.NewFileProvider(filename)
|
||||
if err != nil {
|
||||
errs.Add(gperr.PrependSubject(filename, err))
|
||||
} else {
|
||||
providersCh <- p
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for name, dockerHost := range providers.Docker {
|
||||
providersProducer.Go(func() {
|
||||
providersCh <- proxy.NewDockerProvider(name, dockerHost)
|
||||
})
|
||||
}
|
||||
|
||||
providersProducer.Wait()
|
||||
|
||||
close(providersCh)
|
||||
providersConsumer.Wait()
|
||||
|
||||
lenLongestName := 0
|
||||
cfg.providers.RangeAll(func(k string, _ *proxy.Provider) {
|
||||
for k := range cfg.providers.Range {
|
||||
if len(k) > lenLongestName {
|
||||
lenLongestName = len(k)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
results.EnableConcurrency()
|
||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||
if err := p.LoadRoutes(); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
}
|
||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||
})
|
||||
|
||||
// load routes concurrently
|
||||
var providersLoader sync.WaitGroup
|
||||
for _, p := range cfg.providers.Range {
|
||||
providersLoader.Go(func() {
|
||||
if err := p.LoadRoutes(); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
}
|
||||
results.Addf("%-"+strconv.Itoa(lenLongestName)+"s %d routes", p.String(), p.NumRoutes())
|
||||
})
|
||||
}
|
||||
providersLoader.Wait()
|
||||
|
||||
log.Info().Msg(results.String())
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ func (cfg *Config) RouteProviderList() []config.RouteProviderListResponse {
|
||||
return list
|
||||
}
|
||||
|
||||
func (cfg *Config) SearchRoute(alias string) types.Route {
|
||||
for _, p := range cfg.providers.Range {
|
||||
if r, ok := p.GetRoute(alias); ok {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) Statistics() map[string]any {
|
||||
var rps, streams types.RouteStats
|
||||
var total uint16
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
"github.com/yusing/go-proxy/internal/proxmox"
|
||||
"github.com/yusing/go-proxy/internal/serialization"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -51,8 +52,9 @@ type (
|
||||
Reload() gperr.Error
|
||||
Statistics() map[string]any
|
||||
RouteProviderList() []RouteProviderListResponse
|
||||
SearchRoute(alias string) types.Route
|
||||
Context() context.Context
|
||||
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
|
||||
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error)
|
||||
AutoCertProvider() *autocert.Provider
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ replace github.com/yusing/go-proxy => ../..
|
||||
replace github.com/yusing/go-proxy/internal/utils => ../utils
|
||||
|
||||
require (
|
||||
github.com/go-acme/lego/v4 v4.25.2
|
||||
github.com/yusing/go-proxy v0.17.5
|
||||
github.com/go-acme/lego/v4 v4.26.0
|
||||
github.com/yusing/go-proxy v0.17.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -16,15 +16,16 @@ require (
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.8.0 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
|
||||
@@ -44,6 +45,7 @@ require (
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.26 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
@@ -54,6 +56,7 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
@@ -69,7 +72,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||
github.com/gotify/server/v2 v2.6.3 // indirect
|
||||
github.com/gotify/server/v2 v2.7.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
@@ -107,11 +110,11 @@ require (
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect
|
||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||
@@ -128,7 +131,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/smartystreets/assertions v1.1.0 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.2.0 // indirect
|
||||
github.com/softlayer/softlayer-go v1.2.1 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
@@ -136,6 +139,8 @@ require (
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
|
||||
@@ -144,12 +149,13 @@ require (
|
||||
github.com/vultr/govultr/v3 v3.23.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusing/go-proxy/internal/utils v0.0.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
@@ -168,4 +174,5 @@ require (
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.15.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -604,8 +604,8 @@ git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3p
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE=
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -633,6 +633,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
|
||||
@@ -646,8 +648,8 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 h1:h/33OxYLqBk0BYmEbSUy7MlvgQR/m1w1/7OJFKoPL1I=
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0/go.mod h1:rvh3imDA6EaQi+oM/GQHkQAOHbXPKJ7EWJvfjuw141Q=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -664,6 +666,9 @@ github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4
|
||||
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
@@ -819,8 +824,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-acme/lego/v4 v4.25.2 h1:+D1Q+VnZrD+WJdlkgUEGHFFTcDrwGlE7q24IFtMmHDI=
|
||||
github.com/go-acme/lego/v4 v4.25.2/go.mod h1:OORYyVNZPaNdIdVYCGSBNRNZDIjhQbPuFxwGDgWj/yM=
|
||||
github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA=
|
||||
github.com/go-acme/lego/v4 v4.26.0/go.mod h1:BQVAWgcyzW4IT9eIKHY/RxYlVhoyKyOMXOkq7jK1eEQ=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
@@ -851,6 +856,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -978,7 +985,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
||||
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -1019,16 +1025,14 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.6.3 h1:2sLDRsQ/No1+hcFwFDvjNtwKepfCSIR8L3BkXl/Vz1I=
|
||||
github.com/gotify/server/v2 v2.6.3/go.mod h1:IyeQ/iL3vetcuqUAzkCMVObIMGGJx4zb13/mVatIwE8=
|
||||
github.com/gotify/server/v2 v2.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
|
||||
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
|
||||
github.com/hashicorp/consul/api v1.20.0/go.mod h1:nR64eD44KQ59Of/ECwt2vUmIK2DKsDzAwTmwmLl8Wpo=
|
||||
@@ -1247,7 +1251,6 @@ github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/
|
||||
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
|
||||
github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
|
||||
@@ -1285,16 +1288,16 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/onsi/gomega v1.38.1 h1:FaLA8GlcpXDwsb7m0h2A9ew2aTk3vnZMlzFgg5tz/pk=
|
||||
github.com/onsi/gomega v1.38.1/go.mod h1:LfcV8wZLvwcYRwPiJysphKAEsmcFnLMK/9c+PjvlX8g=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
|
||||
@@ -1302,8 +1305,6 @@ github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE=
|
||||
github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
@@ -1425,8 +1426,8 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA=
|
||||
github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||
github.com/softlayer/softlayer-go v1.2.0 h1:UgGd1ffcqoHUNxIohEp+Bh1My4sOiek9Yy8YWBY6KEg=
|
||||
github.com/softlayer/softlayer-go v1.2.0/go.mod h1:GP2Y0KJo7xokdJCyRPwUZ0xce9xQi5a6DLTR3+Nd1Yk=
|
||||
github.com/softlayer/softlayer-go v1.2.1 h1:8ucHxn5laVsVPb0/aMGnr6tOMt1I9BgEtU5mn70OGKw=
|
||||
github.com/softlayer/softlayer-go v1.2.1/go.mod h1:Gz9/ktcmB7Z8EJlu+QEJJpkv8lAmnhYdB9Tc6gedjmo=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
@@ -1504,9 +1505,6 @@ github.com/vultr/govultr/v3 v3.23.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXza
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
@@ -1538,8 +1536,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
|
||||
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
@@ -1562,6 +1560,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
@@ -2239,10 +2239,10 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl
|
||||
google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a h1:V8Zj/61zlL7B+VH151iV5hJlUnYc3fUNTEhLtyr9Kzc=
|
||||
google.golang.org/genproto v0.0.0-20250811230008-5f3141c8851a/go.mod h1:q9+ZJOXH/LcpbpkQSsvYReIH5lCcwvfc2xE8JBSER0Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
|
||||
google.golang.org/genproto v0.0.0-20250908214217-97024824d090/go.mod h1:zwJI9HzbJJlw2KXy0wX+lmT2JuZoaKK9JC4ppqmxxjk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -2319,10 +2319,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -66,6 +66,7 @@ func FromDocker(c *container.Summary, dockerHost string) (res *types.Container)
|
||||
IsExplicit: isExplicit,
|
||||
IsHostNetworkMode: c.HostConfig.NetworkMode == "host",
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
State: c.State,
|
||||
}
|
||||
|
||||
if agent.IsDockerHostAgent(dockerHost) {
|
||||
@@ -143,9 +144,11 @@ var databaseMPs = map[string]struct{}{
|
||||
}
|
||||
|
||||
func isDatabase(c *types.Container) bool {
|
||||
for _, m := range c.Mounts.Iter {
|
||||
if _, ok := databaseMPs[m]; ok {
|
||||
return true
|
||||
if c.Mounts != nil { // only happens in test
|
||||
for _, m := range c.Mounts.Iter {
|
||||
if _, ok := databaseMPs[m]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
internal/docker/id_lookup.go
Normal file
21
internal/docker/id_lookup.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var idDockerHostMap = xsync.NewMap[string, string](xsync.WithPresize(100))
|
||||
|
||||
func GetDockerHostByContainerID(id string) (string, bool) {
|
||||
return idDockerHostMap.Load(id)
|
||||
}
|
||||
|
||||
func SetDockerHostByContainerID(id, host string) {
|
||||
log.Debug().Str("id", id).Str("host", host).Int("size", idDockerHostMap.Size()).Msg("setting docker host by container id")
|
||||
idDockerHostMap.Store(id, host)
|
||||
}
|
||||
|
||||
func DeleteDockerHostByContainerID(id string) {
|
||||
idDockerHostMap.Delete(id)
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -21,16 +19,13 @@ var listOptions = container.ListOptions{
|
||||
All: true,
|
||||
}
|
||||
|
||||
func ListContainers(clientHost string) ([]container.Summary, error) {
|
||||
func ListContainers(ctx context.Context, clientHost string) ([]container.Summary, error) {
|
||||
dockerClient, err := NewClient(clientHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("list containers timeout"))
|
||||
defer cancel()
|
||||
|
||||
containers, err := dockerClient.ContainerList(ctx, listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type Entrypoint struct {
|
||||
@@ -73,12 +72,13 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w = accesslog.NewResponseRecorder(w)
|
||||
defer ep.accessLogger.Log(r, w.(*accesslog.ResponseRecorder).Response())
|
||||
}
|
||||
mux, err := ep.findRouteFunc(r.Host)
|
||||
route, err := ep.findRouteFunc(r.Host)
|
||||
if err == nil {
|
||||
r = routes.WithRouteContext(r, route)
|
||||
if ep.middleware != nil {
|
||||
ep.middleware.ServeHTTP(mux.ServeHTTP, w, routes.WithRouteContext(r, mux))
|
||||
ep.middleware.ServeHTTP(route.ServeHTTP, w, r)
|
||||
} else {
|
||||
mux.ServeHTTP(w, r)
|
||||
route.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -106,20 +106,23 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func findRouteAnyDomain(host string) (types.HTTPRoute, error) {
|
||||
hostSplit := strutils.SplitRune(host, '.')
|
||||
target := hostSplit[0]
|
||||
|
||||
if r, ok := routes.GetHTTPRouteOrExact(target, host); ok {
|
||||
idx := strings.IndexByte(host, '.')
|
||||
if idx != -1 {
|
||||
target := host[:idx]
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
if r, ok := routes.HTTP.Get(host); ok {
|
||||
return r, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, target)
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
||||
}
|
||||
|
||||
func findRouteByDomains(domains []string) func(host string) (types.HTTPRoute, error) {
|
||||
return func(host string) (types.HTTPRoute, error) {
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(host, domain) {
|
||||
target := strings.TrimSuffix(host, domain)
|
||||
if target, ok := strings.CutSuffix(host, domain); ok {
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
154
internal/entrypoint/entrypoint_benchmark_test.go
Normal file
154
internal/entrypoint/entrypoint_benchmark_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type noopResponseWriter struct {
|
||||
statusCode int
|
||||
written []byte
|
||||
}
|
||||
|
||||
func (w *noopResponseWriter) Header() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
func (w *noopResponseWriter) Write(b []byte) (int, error) {
|
||||
w.written = b
|
||||
return len(b), nil
|
||||
}
|
||||
func (w *noopResponseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
type noopTransport struct{}
|
||||
|
||||
func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("1")),
|
||||
Request: req,
|
||||
Header: http.Header{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BenchmarkEntrypointReal(b *testing.B) {
|
||||
var ep Entrypoint
|
||||
var req = http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||
Host: "test.domain.tld",
|
||||
}
|
||||
ep.SetFindRouteDomains([]string{})
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", "1")
|
||||
w.Write([]byte("1"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
url, err := url.Parse(srv.URL)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(url.Host)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
portInt, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
r := &route.Route{
|
||||
Alias: "test",
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
Port: route.Port{Proxy: portInt},
|
||||
HealthCheck: &types.HealthCheckConfig{Disable: true},
|
||||
}
|
||||
|
||||
err = r.Validate()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
err = r.Start(task.RootTask("test", false))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var w noopResponseWriter
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
ep.ServeHTTP(&w, &req)
|
||||
// if w.statusCode != http.StatusOK {
|
||||
// b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||
// }
|
||||
// if string(w.written) != "1" {
|
||||
// b.Fatalf("written is not 1: %s", string(w.written))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEntrypoint(b *testing.B) {
|
||||
var ep Entrypoint
|
||||
var req = http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/", RawPath: "/"},
|
||||
Host: "test.domain.tld",
|
||||
}
|
||||
ep.SetFindRouteDomains([]string{})
|
||||
|
||||
r := &route.Route{
|
||||
Alias: "test",
|
||||
Scheme: "http",
|
||||
Host: "localhost",
|
||||
Port: route.Port{
|
||||
Proxy: 8080,
|
||||
},
|
||||
HealthCheck: &types.HealthCheckConfig{
|
||||
Disable: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := r.Validate()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
err = r.Start(task.RootTask("test", false))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
rev, ok := routes.HTTP.Get("test")
|
||||
if !ok {
|
||||
b.Fatal("route not found")
|
||||
}
|
||||
rev.(types.ReverseProxyRoute).ReverseProxy().Transport = noopTransport{}
|
||||
|
||||
var w noopResponseWriter
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
ep.ServeHTTP(&w, &req)
|
||||
if w.statusCode != http.StatusOK {
|
||||
b.Fatalf("status code is not 200: %d", w.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ func addRoute(alias string) {
|
||||
routes.HTTP.Add(&route.ReveseProxyRoute{
|
||||
Route: &route.Route{
|
||||
Alias: alias,
|
||||
Port: route.Port{
|
||||
Proxy: 80,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,34 +2,72 @@ package homepage
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/go-proxy/internal/homepage/widgets"
|
||||
"github.com/yusing/go-proxy/internal/serialization"
|
||||
)
|
||||
|
||||
type (
|
||||
Homepage map[string]Category // @name HomepageItems
|
||||
Category []*Item // @name HomepageCategory
|
||||
HomepageMap struct {
|
||||
ordered.Map[string, *Category]
|
||||
} // @name HomepageItemsMap
|
||||
|
||||
Homepage []*Category // @name HomepageItems
|
||||
Category struct {
|
||||
Items []*Item `json:"items"`
|
||||
Name string `json:"name"`
|
||||
} // @name HomepageCategory
|
||||
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Name string `json:"name"` // display name
|
||||
Icon *IconURL `json:"icon" swaggertype:"string"`
|
||||
Category string `json:"category"`
|
||||
Category string `json:"category" validate:"omitempty"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
Item struct {
|
||||
*ItemConfig
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
|
||||
} // @name HomepageItemConfig
|
||||
|
||||
Alias string `json:"alias"`
|
||||
Provider string `json:"provider"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
}
|
||||
Widget struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
} // @name HomepageItemWidget
|
||||
|
||||
Item struct {
|
||||
ItemConfig
|
||||
|
||||
SortOrder int `json:"sort_order"` // sort order in category
|
||||
FavSortOrder int `json:"fav_sort_order"` // sort order in favorite
|
||||
AllSortOrder int `json:"all_sort_order"` // sort order in all
|
||||
|
||||
Clicks int `json:"clicks"`
|
||||
|
||||
Widgets []Widget `json:"widgets,omitempty"`
|
||||
|
||||
Alias string `json:"alias"`
|
||||
Provider string `json:"provider"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
ContainerID string `json:"container_id,omitempty" extensions:"x-nullable"`
|
||||
} // @name HomepageItem
|
||||
|
||||
SortMethod string // @name HomepageSortMethod
|
||||
)
|
||||
|
||||
const (
|
||||
CategoryAll = "All"
|
||||
CategoryFavorites = "Favorites"
|
||||
CategoryHidden = "Hidden"
|
||||
CategoryOthers = "Others"
|
||||
)
|
||||
|
||||
const (
|
||||
SortMethodClicks = "clicks" // @name HomepageSortMethodClicks
|
||||
SortMethodAlphabetical = "alphabetical" // @name HomepageSortMethodAlphabetical
|
||||
SortMethodCustom = "custom" // @name HomepageSortMethodCustom
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,22 +78,115 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
|
||||
return overrideConfigInstance.GetOverride(alias, cfg)
|
||||
func NewHomepageMap(total int) *HomepageMap {
|
||||
m := &HomepageMap{
|
||||
Map: *ordered.NewMap[string, *Category](ordered.WithCapacity(10)),
|
||||
}
|
||||
m.Set(CategoryFavorites, &Category{
|
||||
Items: make([]*Item, 0), // no capacity reserved for this category
|
||||
Name: CategoryFavorites,
|
||||
})
|
||||
m.Set(CategoryAll, &Category{
|
||||
Items: make([]*Item, 0, total),
|
||||
Name: CategoryAll,
|
||||
})
|
||||
m.Set(CategoryHidden, &Category{
|
||||
Items: make([]*Item, 0),
|
||||
Name: CategoryHidden,
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func (c Homepage) Add(item *Item) {
|
||||
if c[item.Category] == nil {
|
||||
c[item.Category] = make(Category, 0)
|
||||
func (cfg Item) GetOverride() Item {
|
||||
return overrideConfigInstance.GetOverride(cfg)
|
||||
}
|
||||
|
||||
func (c *HomepageMap) Add(item *Item) {
|
||||
c.add(item, item.Category)
|
||||
// add to all category even if item is hidden
|
||||
c.add(item, CategoryAll)
|
||||
if item.Show {
|
||||
if item.Favorite {
|
||||
c.add(item, CategoryFavorites)
|
||||
}
|
||||
} else {
|
||||
c.add(item, CategoryHidden)
|
||||
}
|
||||
c[item.Category] = append(c[item.Category], item)
|
||||
slices.SortStableFunc(c[item.Category], func(a, b *Item) int {
|
||||
if a.SortOrder < b.SortOrder {
|
||||
}
|
||||
|
||||
func (c *HomepageMap) add(item *Item, categoryName string) {
|
||||
category := c.Get(categoryName)
|
||||
if category == nil {
|
||||
category = &Category{
|
||||
Items: make([]*Item, 0),
|
||||
Name: categoryName,
|
||||
}
|
||||
c.Set(categoryName, category)
|
||||
}
|
||||
category.Items = append(category.Items, item)
|
||||
}
|
||||
|
||||
func (c *Category) Sort(method SortMethod) {
|
||||
switch method {
|
||||
case SortMethodClicks:
|
||||
c.sortByClicks()
|
||||
case SortMethodAlphabetical:
|
||||
c.sortByAlphabetical()
|
||||
case SortMethodCustom:
|
||||
c.sortByCustom()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Category) sortByClicks() {
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
if a.Clicks > b.Clicks {
|
||||
return -1
|
||||
}
|
||||
if a.SortOrder > b.SortOrder {
|
||||
if a.Clicks < b.Clicks {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
// fallback to alphabetical
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Category) sortByAlphabetical() {
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Category) sortByCustom() {
|
||||
switch c.Name {
|
||||
case CategoryFavorites:
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
if a.FavSortOrder < b.FavSortOrder {
|
||||
return -1
|
||||
}
|
||||
if a.FavSortOrder > b.FavSortOrder {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
case CategoryAll:
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
if a.AllSortOrder < b.AllSortOrder {
|
||||
return -1
|
||||
}
|
||||
if a.AllSortOrder > b.AllSortOrder {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
default:
|
||||
slices.SortStableFunc(c.Items, func(a, b *Item) int {
|
||||
if a.SortOrder < b.SortOrder {
|
||||
return -1
|
||||
}
|
||||
if a.SortOrder > b.SortOrder {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
ItemConfig: &ItemConfig{
|
||||
ItemConfig: ItemConfig{
|
||||
Show: false,
|
||||
Name: "Foo",
|
||||
Icon: &IconURL{
|
||||
@@ -20,7 +20,7 @@ func TestOverrideItem(t *testing.T) {
|
||||
Category: "App",
|
||||
},
|
||||
}
|
||||
want := &ItemConfig{
|
||||
want := ItemConfig{
|
||||
Show: true,
|
||||
Name: "Bar",
|
||||
Category: "Test",
|
||||
@@ -30,7 +30,107 @@ func TestOverrideItem(t *testing.T) {
|
||||
},
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
overrides.Initialize()
|
||||
overrides.OverrideItem(a.Alias, want)
|
||||
got := a.GetOverride(a.Alias)
|
||||
ExpectEqual(t, got, want)
|
||||
got := a.GetOverride()
|
||||
ExpectEqual(t, got, Item{
|
||||
ItemConfig: want,
|
||||
Alias: a.Alias,
|
||||
})
|
||||
}
|
||||
|
||||
func TestOverrideItem_PreservesURL(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "svc",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: true,
|
||||
Name: "Service",
|
||||
URL: "http://origin.local",
|
||||
},
|
||||
}
|
||||
wantCfg := ItemConfig{
|
||||
Show: true,
|
||||
Name: "Overridden",
|
||||
URL: "http://should-not-apply",
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
overrides.Initialize()
|
||||
overrides.OverrideItem(a.Alias, wantCfg)
|
||||
|
||||
got := a.GetOverride()
|
||||
ExpectEqual(t, got.URL, "http://origin.local")
|
||||
ExpectEqual(t, got.Name, "Overridden")
|
||||
}
|
||||
|
||||
func TestVisibilityFavoriteAndSortOrders(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "alpha",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: true,
|
||||
Name: "Alpha",
|
||||
Category: "Apps",
|
||||
Favorite: false,
|
||||
},
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
overrides.Initialize()
|
||||
overrides.SetItemsVisibility([]string{a.Alias}, false)
|
||||
overrides.SetItemsFavorite([]string{a.Alias}, true)
|
||||
overrides.SetSortOrder(a.Alias, 5)
|
||||
overrides.SetAllSortOrder(a.Alias, 9)
|
||||
overrides.SetFavSortOrder(a.Alias, 2)
|
||||
|
||||
got := a.GetOverride()
|
||||
ExpectEqual(t, got.Show, false)
|
||||
ExpectEqual(t, got.Favorite, true)
|
||||
ExpectEqual(t, got.SortOrder, 5)
|
||||
ExpectEqual(t, got.AllSortOrder, 9)
|
||||
ExpectEqual(t, got.FavSortOrder, 2)
|
||||
}
|
||||
|
||||
func TestCategoryDefaultedWhenEmpty(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "no-cat",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: true,
|
||||
Name: "NoCat",
|
||||
},
|
||||
}
|
||||
got := a.GetOverride()
|
||||
ExpectEqual(t, got.Category, CategoryOthers)
|
||||
}
|
||||
|
||||
func TestOverrideItems_Bulk(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "bulk-1",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: true,
|
||||
Name: "Bulk1",
|
||||
Category: "X",
|
||||
},
|
||||
}
|
||||
b := &Item{
|
||||
Alias: "bulk-2",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: true,
|
||||
Name: "Bulk2",
|
||||
Category: "Y",
|
||||
},
|
||||
}
|
||||
|
||||
overrides := GetOverrideConfig()
|
||||
overrides.Initialize()
|
||||
overrides.OverrideItems(map[string]ItemConfig{
|
||||
a.Alias: {Show: true, Name: "A*", Category: "AX"},
|
||||
b.Alias: {Show: false, Name: "B*", Category: "BY"},
|
||||
})
|
||||
|
||||
ga := a.GetOverride()
|
||||
gb := b.GetOverride()
|
||||
|
||||
ExpectEqual(t, ga.Name, "A*")
|
||||
ExpectEqual(t, ga.Category, "AX")
|
||||
ExpectEqual(t, gb.Name, "B*")
|
||||
ExpectEqual(t, gb.Category, "BY")
|
||||
ExpectEqual(t, gb.Show, false)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -23,19 +24,20 @@ type (
|
||||
IconMap map[IconKey]*IconMeta
|
||||
IconList []string
|
||||
IconMeta struct {
|
||||
SVG, PNG, WebP bool
|
||||
Light, Dark bool
|
||||
DisplayName string
|
||||
Tag string
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
DisplayName string `json:"-"`
|
||||
Tag string `json:"-"`
|
||||
}
|
||||
IconMetaSearch struct {
|
||||
Source IconSource `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
SVG bool `json:"SVG"`
|
||||
PNG bool `json:"PNG"`
|
||||
WebP bool `json:"WebP"`
|
||||
Light bool `json:"Light"`
|
||||
Dark bool `json:"Dark"`
|
||||
*IconMeta
|
||||
|
||||
rank int
|
||||
}
|
||||
Cache struct {
|
||||
Icons IconMap
|
||||
@@ -92,8 +94,8 @@ func NewIconKey(source IconSource, reference string) IconKey {
|
||||
}
|
||||
|
||||
func (k IconKey) SourceRef() (IconSource, string) {
|
||||
parts := strings.Split(string(k), "/")
|
||||
return IconSource(parts[0]), parts[1]
|
||||
source, ref, _ := strings.Cut(string(k), "/")
|
||||
return IconSource(source), ref
|
||||
}
|
||||
|
||||
func InitIconListCache() {
|
||||
@@ -118,6 +120,10 @@ func InitIconListCache() {
|
||||
})
|
||||
}
|
||||
|
||||
func TestClearIconsCache() {
|
||||
clear(iconsCache.Icons)
|
||||
}
|
||||
|
||||
func ListAvailableIcons() (*Cache, error) {
|
||||
if common.IsTest {
|
||||
return iconsCache, nil
|
||||
@@ -150,31 +156,54 @@ func ListAvailableIcons() (*Cache, error) {
|
||||
return iconsCache, nil
|
||||
}
|
||||
|
||||
func SearchIcons(keyword string, limit int) []IconMetaSearch {
|
||||
func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
if keyword == "" {
|
||||
return make([]IconMetaSearch, 0)
|
||||
return []*IconMetaSearch{}
|
||||
}
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
iconsCache.RLock()
|
||||
defer iconsCache.RUnlock()
|
||||
result := make([]IconMetaSearch, 0)
|
||||
|
||||
searchLimit := min(limit*5, 50)
|
||||
|
||||
results := make([]*IconMetaSearch, 0, searchLimit)
|
||||
|
||||
sortByRank := func(a, b *IconMetaSearch) int {
|
||||
return a.rank - b.rank
|
||||
}
|
||||
|
||||
var rank int
|
||||
for k, icon := range iconsCache.Icons {
|
||||
if fuzzy.MatchFold(keyword, string(k)) {
|
||||
source, ref := k.SourceRef()
|
||||
result = append(result, IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
SVG: icon.SVG,
|
||||
PNG: icon.PNG,
|
||||
WebP: icon.WebP,
|
||||
Light: icon.Light,
|
||||
Dark: icon.Dark,
|
||||
})
|
||||
if strutils.ContainsFold(string(k), keyword) || strutils.ContainsFold(icon.DisplayName, keyword) {
|
||||
rank = 0
|
||||
} else {
|
||||
rank = fuzzy.RankMatchFold(keyword, string(k))
|
||||
if rank == -1 || rank > 3 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(result) >= limit {
|
||||
|
||||
source, ref := k.SourceRef()
|
||||
ranked := &IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
IconMeta: icon,
|
||||
rank: rank,
|
||||
}
|
||||
// Sorted insert based on rank (lower rank = better match)
|
||||
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
|
||||
results = slices.Insert(results, insertPos, ranked)
|
||||
if len(results) == searchLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
// Extract results and limit to the requested count
|
||||
return results[:min(len(results), limit)]
|
||||
}
|
||||
|
||||
func HasIcon(icon *IconURL) bool {
|
||||
@@ -359,7 +388,8 @@ func UpdateSelfhstIcons() error {
|
||||
for _, item := range data {
|
||||
var tag string
|
||||
if item.Tags != "" {
|
||||
tag = strutils.CommaSeperatedList(item.Tags)[0]
|
||||
tag, _, _ = strings.Cut(item.Tags, ",")
|
||||
tag = strings.TrimSpace(tag)
|
||||
}
|
||||
icon := &IconMeta{
|
||||
DisplayName: item.Name,
|
||||
|
||||
@@ -91,6 +91,8 @@ func runTests(t *testing.T, iconsCache *Cache, test []testCases) {
|
||||
}
|
||||
|
||||
func TestListWalkxCodeIcons(t *testing.T) {
|
||||
t.Cleanup(TestClearIconsCache)
|
||||
|
||||
MockHTTPGet([]byte(walkxcodeIcons))
|
||||
if err := UpdateWalkxCodeIcons(); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -124,6 +126,7 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListSelfhstIcons(t *testing.T) {
|
||||
t.Cleanup(TestClearIconsCache)
|
||||
MockHTTPGet([]byte(selfhstIcons))
|
||||
if err := UpdateSelfhstIcons(); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -135,9 +138,6 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
if len(iconsCache.Icons) != 3 {
|
||||
t.Fatalf("expect 3 icons, got %d", len(iconsCache.Icons))
|
||||
}
|
||||
// if len(iconsCache.IconList) != 8 {
|
||||
// t.Fatalf("expect 8 icons, got %d", len(iconsCache.IconList))
|
||||
// }
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "2fauth"),
|
||||
|
||||
@@ -9,10 +9,14 @@ import (
|
||||
)
|
||||
|
||||
type OverrideConfig struct {
|
||||
ItemOverrides map[string]*ItemConfig `json:"item_overrides"`
|
||||
DisplayOrder map[string]int `json:"display_order"`
|
||||
CategoryOrder map[string]int `json:"category_order"`
|
||||
ItemVisibility map[string]bool `json:"item_visibility"`
|
||||
ItemOverrides map[string]ItemConfig `json:"item_overrides"`
|
||||
DisplayOrder map[string]int `json:"display_order"`
|
||||
CategoryOrder map[string]int `json:"category_order"`
|
||||
AllSortOrder map[string]int `json:"all_sort_order"`
|
||||
FavSortOrder map[string]int `json:"fav_sort_order"`
|
||||
ItemClicks map[string]int `json:"item_clicks"`
|
||||
ItemVisibility map[string]bool `json:"item_visibility"`
|
||||
ItemFavorite map[string]bool `json:"item_favorite"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -23,57 +27,104 @@ func GetOverrideConfig() *OverrideConfig {
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) Initialize() {
|
||||
c.ItemOverrides = make(map[string]*ItemConfig)
|
||||
c.ItemOverrides = make(map[string]ItemConfig)
|
||||
c.DisplayOrder = make(map[string]int)
|
||||
c.CategoryOrder = make(map[string]int)
|
||||
c.AllSortOrder = make(map[string]int)
|
||||
c.FavSortOrder = make(map[string]int)
|
||||
c.ItemClicks = make(map[string]int)
|
||||
c.ItemVisibility = make(map[string]bool)
|
||||
c.ItemFavorite = make(map[string]bool)
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
||||
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) {
|
||||
func (c *OverrideConfig) OverrideItems(items map[string]ItemConfig) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
maps.Copy(c.ItemOverrides, items)
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig {
|
||||
func (c *OverrideConfig) GetOverride(item Item) Item {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if itemOverride, hasOverride := c.ItemOverrides[alias]; hasOverride {
|
||||
itemOverride.URL = item.URL // NOTE: we don't want to override the URL
|
||||
item = itemOverride
|
||||
|
||||
if overrides, hasOverride := c.ItemOverrides[item.Alias]; hasOverride {
|
||||
overrides.URL = item.URL // NOTE: we don't want to override the URL
|
||||
item.ItemConfig = overrides
|
||||
}
|
||||
if show, ok := c.ItemVisibility[alias]; ok {
|
||||
clone := *item
|
||||
clone.Show = show
|
||||
return &clone
|
||||
|
||||
if show, ok := c.ItemVisibility[item.Alias]; ok {
|
||||
item.Show = show
|
||||
}
|
||||
if fav, ok := c.ItemFavorite[item.Alias]; ok {
|
||||
item.Favorite = fav
|
||||
}
|
||||
if displayOrder, ok := c.DisplayOrder[item.Alias]; ok {
|
||||
item.SortOrder = displayOrder
|
||||
}
|
||||
if allSortOrder, ok := c.AllSortOrder[item.Alias]; ok {
|
||||
item.AllSortOrder = allSortOrder
|
||||
}
|
||||
if favSortOrder, ok := c.FavSortOrder[item.Alias]; ok {
|
||||
item.FavSortOrder = favSortOrder
|
||||
}
|
||||
if clicks, ok := c.ItemClicks[item.Alias]; ok {
|
||||
item.Clicks = clicks
|
||||
}
|
||||
|
||||
if item.Category == "" {
|
||||
item.Category = CategoryOthers
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) SetSortOrder(key string, value int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.DisplayOrder[key] = value
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) SetAllSortOrder(key string, value int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.AllSortOrder[key] = value
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) SetFavSortOrder(key string, value int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.FavSortOrder[key] = value
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) SetItemsVisibility(keys []string, value bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for _, key := range keys {
|
||||
c.ItemVisibility[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) SetItemsFavorite(keys []string, value bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for _, key := range keys {
|
||||
c.ItemFavorite[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
func (c *OverrideConfig) IncrementItemClicks(key 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
|
||||
}
|
||||
c.ItemClicks[key]++
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ const (
|
||||
errBurst = 5
|
||||
)
|
||||
|
||||
var lineBufPool = synk.GetBytesPool()
|
||||
var lineBufPool = synk.GetBytesPoolWithUniqueMemory()
|
||||
|
||||
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (*AccessLogger, error) {
|
||||
io, err := cfg.IO()
|
||||
|
||||
@@ -66,7 +66,7 @@ type lineInfo struct {
|
||||
Size int64 // Size of this line
|
||||
}
|
||||
|
||||
var rotateBytePool = synk.GetBytesPool()
|
||||
var rotateBytePool = synk.GetBytesPoolWithUniqueMemory()
|
||||
|
||||
// rotateLogFile rotates the log file based on the retention policy.
|
||||
// It returns the result of the rotation and an error if any.
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type Entries[T any] struct {
|
||||
entries [maxEntries]*T
|
||||
entries [maxEntries]T
|
||||
index int
|
||||
count int
|
||||
interval time.Duration
|
||||
@@ -16,33 +16,48 @@ type Entries[T any] struct {
|
||||
const maxEntries = 100
|
||||
|
||||
func newEntries[T any](duration time.Duration) *Entries[T] {
|
||||
interval := duration / maxEntries
|
||||
if interval < time.Second {
|
||||
interval = time.Second
|
||||
}
|
||||
interval := max(duration/maxEntries, time.Second)
|
||||
return &Entries[T]{
|
||||
interval: interval,
|
||||
lastAdd: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Entries[T]) Add(now time.Time, info *T) {
|
||||
func (e *Entries[T]) Add(now time.Time, info T) {
|
||||
if now.Sub(e.lastAdd) < e.interval {
|
||||
return
|
||||
}
|
||||
e.addWithTime(now, info)
|
||||
}
|
||||
|
||||
// addWithTime adds an entry with a specific timestamp without interval checking.
|
||||
// This is used internally for reconstructing historical data.
|
||||
func (e *Entries[T]) addWithTime(timestamp time.Time, info T) {
|
||||
e.entries[e.index] = info
|
||||
e.index = (e.index + 1) % maxEntries
|
||||
if e.count < maxEntries {
|
||||
e.count++
|
||||
}
|
||||
e.lastAdd = now
|
||||
e.lastAdd = timestamp
|
||||
}
|
||||
|
||||
func (e *Entries[T]) Get() []*T {
|
||||
// validateInterval checks if the current interval matches the expected interval for the duration.
|
||||
// Returns true if valid, false if the interval needs to be recalculated.
|
||||
func (e *Entries[T]) validateInterval(expectedDuration time.Duration) bool {
|
||||
expectedInterval := max(expectedDuration/maxEntries, time.Second)
|
||||
return e.interval == expectedInterval
|
||||
}
|
||||
|
||||
// fixInterval recalculates and sets the correct interval based on the expected duration.
|
||||
func (e *Entries[T]) fixInterval(expectedDuration time.Duration) {
|
||||
e.interval = max(expectedDuration/maxEntries, time.Second)
|
||||
}
|
||||
|
||||
func (e *Entries[T]) Get() []T {
|
||||
if e.count < maxEntries {
|
||||
return e.entries[:e.count]
|
||||
}
|
||||
res := make([]*T, maxEntries)
|
||||
res := make([]T, maxEntries)
|
||||
copy(res, e.entries[e.index:])
|
||||
copy(res[maxEntries-e.index:], e.entries[:e.index])
|
||||
return res
|
||||
@@ -57,7 +72,7 @@ func (e *Entries[T]) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (e *Entries[T]) UnmarshalJSON(data []byte) error {
|
||||
var v struct {
|
||||
Entries []*T `json:"entries"`
|
||||
Entries []T `json:"entries"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
@@ -70,10 +85,17 @@ func (e *Entries[T]) UnmarshalJSON(data []byte) error {
|
||||
if len(entries) > maxEntries {
|
||||
entries = entries[:maxEntries]
|
||||
}
|
||||
now := time.Now()
|
||||
for _, info := range entries {
|
||||
e.Add(now, info)
|
||||
}
|
||||
|
||||
// Set the interval first before adding entries.
|
||||
e.interval = v.Interval
|
||||
|
||||
// Add entries with proper time spacing to respect the interval.
|
||||
now := time.Now()
|
||||
for i, info := range entries {
|
||||
// Calculate timestamp based on entry position and interval.
|
||||
// Most recent entry gets current time, older entries get earlier times.
|
||||
entryTime := now.Add(-time.Duration(len(entries)-1-i) * e.interval)
|
||||
e.addWithTime(entryTime, info)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package period
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
@@ -29,25 +29,22 @@ type ResponseType[AggregateT any] struct {
|
||||
//
|
||||
// If the request is a websocket request, it serves the data for the given period for every interval.
|
||||
func (p *Poller[T, AggregateT]) ServeHTTP(c *gin.Context) {
|
||||
period := Filter(c.Query("period"))
|
||||
query := c.Request.URL.Query()
|
||||
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
interval := metricsutils.QueryDuration(query, "interval", 0)
|
||||
|
||||
minInterval := 1 * time.Second
|
||||
if interval == 0 {
|
||||
interval = pollInterval
|
||||
}
|
||||
if interval < minInterval {
|
||||
interval = minInterval
|
||||
if interval < PollInterval {
|
||||
interval = PollInterval
|
||||
}
|
||||
websocket.PeriodicWrite(c, interval, func() (any, error) {
|
||||
return p.getRespData(c.Request)
|
||||
return p.GetRespData(period, query)
|
||||
})
|
||||
} else {
|
||||
data, err := p.getRespData(c.Request)
|
||||
data, err := p.GetRespData(period, query)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get response data"))
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("bad request", err))
|
||||
return
|
||||
}
|
||||
if data == nil {
|
||||
@@ -58,13 +55,22 @@ func (p *Poller[T, AggregateT]) ServeHTTP(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) getRespData(r *http.Request) (any, error) {
|
||||
query := r.URL.Query()
|
||||
period := query.Get("period")
|
||||
// GetRespData returns the aggregated data for the given period and query.
|
||||
//
|
||||
// When period is specified:
|
||||
//
|
||||
// It returns a map with the total and the data.
|
||||
// It returns an error if the period or query is invalid.
|
||||
//
|
||||
// When period is not specified:
|
||||
//
|
||||
// It returns the last result.
|
||||
// It returns nil if no last result is found.
|
||||
func (p *Poller[T, AggregateT]) GetRespData(period Filter, query url.Values) (any, error) {
|
||||
if period == "" {
|
||||
return p.GetLastResult(), nil
|
||||
}
|
||||
rangeData, ok := p.Get(Filter(period))
|
||||
rangeData, ok := p.Get(period)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid period")
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func NewPeriod[T any]() *Period[T] {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Period[T]) Add(info *T) {
|
||||
func (p *Period[T]) Add(info T) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
now := time.Now()
|
||||
@@ -41,13 +41,13 @@ func (p *Period[T]) Add(info *T) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Period[T]) Get(filter Filter) ([]*T, bool) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
func (p *Period[T]) Get(filter Filter) ([]T, bool) {
|
||||
period, ok := p.Entries[filter]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return period.Get(), true
|
||||
}
|
||||
|
||||
@@ -60,3 +60,26 @@ func (p *Period[T]) Total() int {
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ValidateAndFixIntervals checks all period intervals and fixes them if they're incorrect.
|
||||
// This should be called after loading data from JSON to ensure data integrity.
|
||||
func (p *Period[T]) ValidateAndFixIntervals() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
durations := map[Filter]time.Duration{
|
||||
MetricsPeriod5m: 5 * time.Minute,
|
||||
MetricsPeriod15m: 15 * time.Minute,
|
||||
MetricsPeriod1h: 1 * time.Hour,
|
||||
MetricsPeriod1d: 24 * time.Hour,
|
||||
MetricsPeriod1mo: 30 * 24 * time.Hour,
|
||||
}
|
||||
|
||||
for filter, entries := range p.Entries {
|
||||
if expectedDuration, exists := durations[filter]; exists {
|
||||
if !entries.validateInterval(expectedDuration) {
|
||||
entries.fixInterval(expectedDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,16 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
PollFunc[T any] func(ctx context.Context, lastResult *T) (*T, error)
|
||||
AggregateFunc[T any, AggregateT json.Marshaler] func(entries []*T, query url.Values) (total int, result AggregateT)
|
||||
FilterFunc[T any] func(entries []*T, keyword string) (filtered []*T)
|
||||
PollFunc[T any] func(ctx context.Context, lastResult T) (T, error)
|
||||
AggregateFunc[T any, AggregateT json.Marshaler] func(entries []T, query url.Values) (total int, result AggregateT)
|
||||
FilterFunc[T any] func(entries []T, keyword string) (filtered []T)
|
||||
Poller[T any, AggregateT json.Marshaler] struct {
|
||||
name string
|
||||
poll PollFunc[T]
|
||||
aggregate AggregateFunc[T, AggregateT]
|
||||
resultFilter FilterFunc[T]
|
||||
period *Period[T]
|
||||
lastResult atomic.Value[*T]
|
||||
lastResult atomic.Value[T]
|
||||
errs []pollErr
|
||||
}
|
||||
pollErr struct {
|
||||
@@ -36,7 +36,7 @@ type (
|
||||
)
|
||||
|
||||
const (
|
||||
pollInterval = 1 * time.Second
|
||||
PollInterval = 1 * time.Second
|
||||
gatherErrsInterval = 30 * time.Second
|
||||
saveInterval = 5 * time.Minute
|
||||
|
||||
@@ -73,7 +73,12 @@ func (p *Poller[T, AggregateT]) load() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(entries, &p.period)
|
||||
if err := json.Unmarshal(entries, &p.period); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate and fix intervals after loading to ensure data integrity.
|
||||
p.period.ValidateAndFixIntervals()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) save() error {
|
||||
@@ -122,7 +127,7 @@ func (p *Poller[T, AggregateT]) clearErrs() {
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) pollWithTimeout(ctx context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, pollInterval)
|
||||
ctx, cancel := context.WithTimeout(ctx, PollInterval)
|
||||
defer cancel()
|
||||
data, err := p.poll(ctx, p.lastResult.Load())
|
||||
if err != nil {
|
||||
@@ -146,7 +151,7 @@ func (p *Poller[T, AggregateT]) Start() {
|
||||
}
|
||||
|
||||
go func() {
|
||||
pollTicker := time.NewTicker(pollInterval)
|
||||
pollTicker := time.NewTicker(PollInterval)
|
||||
gatherErrsTicker := time.NewTicker(gatherErrsInterval)
|
||||
saveTicker := time.NewTicker(saveInterval)
|
||||
|
||||
@@ -162,7 +167,7 @@ func (p *Poller[T, AggregateT]) Start() {
|
||||
t.Finish(err)
|
||||
}()
|
||||
|
||||
l.Debug().Dur("interval", pollInterval).Msg("Starting poller")
|
||||
l.Debug().Dur("interval", PollInterval).Msg("Starting poller")
|
||||
|
||||
p.pollWithTimeout(t.Context())
|
||||
|
||||
@@ -188,10 +193,10 @@ func (p *Poller[T, AggregateT]) Start() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) Get(filter Filter) ([]*T, bool) {
|
||||
func (p *Poller[T, AggregateT]) Get(filter Filter) ([]T, bool) {
|
||||
return p.period.Get(filter)
|
||||
}
|
||||
|
||||
func (p *Poller[T, AggregateT]) GetLastResult() *T {
|
||||
func (p *Poller[T, AggregateT]) GetLastResult() T {
|
||||
return p.lastResult.Load()
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
package systeminfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/shirou/gopsutil/v4/sensors"
|
||||
"github.com/yusing/go-proxy/internal/utils/synk"
|
||||
)
|
||||
|
||||
var bufPool = synk.GetBytesPool()
|
||||
|
||||
// explicitly implement MarshalJSON to avoid reflection.
|
||||
func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
b := bufPool.Get()
|
||||
defer bufPool.Put(b)
|
||||
b := make([]byte, 0, 4096)
|
||||
|
||||
b = append(b, '{')
|
||||
|
||||
@@ -54,7 +47,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = fmt.Appendf(b,
|
||||
`"%s":{"device":"%s","path":"%s","fstype":"%s","total":%d,"free":%d,"used":%d,"used_percent":%.2f}`,
|
||||
`%q:{"device":%q,"path":%q,"fstype":%q,"total":%d,"free":%d,"used":%d,"used_percent":%.2f}`,
|
||||
device,
|
||||
device,
|
||||
disk.Path,
|
||||
@@ -81,7 +74,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = fmt.Appendf(b,
|
||||
`"%s":{"name":"%s","read_bytes":%d,"write_bytes":%d,"read_speed":%.2f,"write_speed":%.2f,"iops":%d}`,
|
||||
`%q:{"name":%q,"read_bytes":%d,"write_bytes":%d,"read_speed":%.2f,"write_speed":%.2f,"iops":%d}`,
|
||||
name,
|
||||
name,
|
||||
usage.ReadBytes,
|
||||
@@ -114,15 +107,14 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
// sensors
|
||||
b = append(b, `,"sensors":`...)
|
||||
if len(s.Sensors) > 0 {
|
||||
b = append(b, '{')
|
||||
b = append(b, '[')
|
||||
first := true
|
||||
for _, sensor := range s.Sensors {
|
||||
if !first {
|
||||
b = append(b, ',')
|
||||
}
|
||||
b = fmt.Appendf(b,
|
||||
`"%s":{"name":"%s","temperature":%.2f,"high":%.2f,"critical":%.2f}`,
|
||||
sensor.SensorKey,
|
||||
`{"name":%q,"temperature":%.2f,"high":%.2f,"critical":%.2f}`,
|
||||
sensor.SensorKey,
|
||||
sensor.Temperature,
|
||||
sensor.High,
|
||||
@@ -130,7 +122,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
)
|
||||
first = false
|
||||
}
|
||||
b = append(b, '}')
|
||||
b = append(b, ']')
|
||||
} else {
|
||||
b = append(b, "null"...)
|
||||
}
|
||||
@@ -139,34 +131,22 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (s *Sensors) UnmarshalJSON(data []byte) error {
|
||||
var v map[string]map[string]any
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
*s = make(Sensors, 0, len(v))
|
||||
for k, v := range v {
|
||||
*s = append(*s, sensors.TemperatureStat{
|
||||
SensorKey: k,
|
||||
Temperature: v["temperature"].(float64),
|
||||
High: v["high"].(float64),
|
||||
Critical: v["critical"].(float64),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (result Aggregated) MarshalJSON() ([]byte, error) {
|
||||
buf := bufPool.Get()
|
||||
defer bufPool.Put(buf)
|
||||
if len(result.Entries) == 0 {
|
||||
return []byte("[]"), nil
|
||||
}
|
||||
|
||||
capacity := 10 * 1024
|
||||
if result.Mode == SystemInfoAggregateModeSensorTemperature {
|
||||
// give each sensor key 30 bytes per entry per sensor key
|
||||
capacity = 30 * len(result.Entries) * len(result.Entries[0])
|
||||
}
|
||||
buf := make([]byte, 0, capacity)
|
||||
|
||||
buf = append(buf, '[')
|
||||
i := 0
|
||||
n := len(result)
|
||||
for _, entry := range result {
|
||||
n := len(result.Entries)
|
||||
for _, entry := range result.Entries {
|
||||
buf = append(buf, '{')
|
||||
j := 0
|
||||
m := len(entry)
|
||||
@@ -178,10 +158,12 @@ func (result Aggregated) MarshalJSON() ([]byte, error) {
|
||||
switch v := v.(type) {
|
||||
case float64:
|
||||
buf = strconv.AppendFloat(buf, v, 'f', 2, 64)
|
||||
case uint64:
|
||||
buf = strconv.AppendUint(buf, v, 10)
|
||||
case int32:
|
||||
buf = strconv.AppendInt(buf, int64(v), 10)
|
||||
case int64:
|
||||
buf = strconv.AppendInt(buf, v, 10)
|
||||
case uint64:
|
||||
buf = strconv.AppendUint(buf, v, 10)
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected type: %T", v))
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ import (
|
||||
|
||||
type (
|
||||
Sensors []sensors.TemperatureStat // @name Sensors
|
||||
Aggregated []map[string]any
|
||||
Aggregated struct {
|
||||
Entries []map[string]any
|
||||
Mode SystemInfoAggregateMode
|
||||
}
|
||||
AggregatedJSON []map[string]any
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
@@ -218,12 +222,15 @@ func (s *SystemInfo) collectSensorsInfo(ctx context.Context) error {
|
||||
// recharts friendly.
|
||||
func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggregated) {
|
||||
n := len(entries)
|
||||
aggregated := make(Aggregated, 0, n)
|
||||
switch SystemInfoAggregateMode(query.Get("aggregate")) {
|
||||
aggregated := Aggregated{
|
||||
Entries: make([]map[string]any, 0, n),
|
||||
Mode: SystemInfoAggregateMode(query.Get("aggregate")),
|
||||
}
|
||||
switch aggregated.Mode {
|
||||
case SystemInfoAggregateModeCPUAverage:
|
||||
for _, entry := range entries {
|
||||
if entry.CPUAverage != nil {
|
||||
aggregated = append(aggregated, map[string]any{
|
||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
||||
"timestamp": entry.Timestamp,
|
||||
"cpu_average": *entry.CPUAverage,
|
||||
})
|
||||
@@ -232,7 +239,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
case SystemInfoAggregateModeMemoryUsage:
|
||||
for _, entry := range entries {
|
||||
if entry.Memory != nil {
|
||||
aggregated = append(aggregated, map[string]any{
|
||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
||||
"timestamp": entry.Timestamp,
|
||||
"memory_usage": entry.Memory.Used,
|
||||
})
|
||||
@@ -241,7 +248,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
case SystemInfoAggregateModeMemoryUsagePercent:
|
||||
for _, entry := range entries {
|
||||
if entry.Memory != nil {
|
||||
aggregated = append(aggregated, map[string]any{
|
||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
||||
"timestamp": entry.Timestamp,
|
||||
"memory_usage_percent": entry.Memory.UsedPercent,
|
||||
})
|
||||
@@ -257,7 +264,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
m[name] = usage.ReadSpeed
|
||||
}
|
||||
m["timestamp"] = entry.Timestamp
|
||||
aggregated = append(aggregated, m)
|
||||
aggregated.Entries = append(aggregated.Entries, m)
|
||||
}
|
||||
case SystemInfoAggregateModeDisksWriteSpeed:
|
||||
for _, entry := range entries {
|
||||
@@ -269,7 +276,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
m[name] = usage.WriteSpeed
|
||||
}
|
||||
m["timestamp"] = entry.Timestamp
|
||||
aggregated = append(aggregated, m)
|
||||
aggregated.Entries = append(aggregated.Entries, m)
|
||||
}
|
||||
case SystemInfoAggregateModeDisksIOPS:
|
||||
for _, entry := range entries {
|
||||
@@ -281,7 +288,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
m[name] = usage.Iops
|
||||
}
|
||||
m["timestamp"] = entry.Timestamp
|
||||
aggregated = append(aggregated, m)
|
||||
aggregated.Entries = append(aggregated.Entries, m)
|
||||
}
|
||||
case SystemInfoAggregateModeDiskUsage:
|
||||
for _, entry := range entries {
|
||||
@@ -293,14 +300,14 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
m[name] = disk.Used
|
||||
}
|
||||
m["timestamp"] = entry.Timestamp
|
||||
aggregated = append(aggregated, m)
|
||||
aggregated.Entries = append(aggregated.Entries, m)
|
||||
}
|
||||
case SystemInfoAggregateModeNetworkSpeed:
|
||||
for _, entry := range entries {
|
||||
if entry.Network == nil {
|
||||
continue
|
||||
}
|
||||
aggregated = append(aggregated, map[string]any{
|
||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
||||
"timestamp": entry.Timestamp,
|
||||
"upload": entry.Network.UploadSpeed,
|
||||
"download": entry.Network.DownloadSpeed,
|
||||
@@ -311,7 +318,7 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
if entry.Network == nil {
|
||||
continue
|
||||
}
|
||||
aggregated = append(aggregated, map[string]any{
|
||||
aggregated.Entries = append(aggregated.Entries, map[string]any{
|
||||
"timestamp": entry.Timestamp,
|
||||
"upload": entry.Network.BytesSent,
|
||||
"download": entry.Network.BytesRecv,
|
||||
@@ -327,12 +334,12 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
|
||||
m[sensor.SensorKey] = sensor.Temperature
|
||||
}
|
||||
m["timestamp"] = entry.Timestamp
|
||||
aggregated = append(aggregated, m)
|
||||
aggregated.Entries = append(aggregated.Entries, m)
|
||||
}
|
||||
default:
|
||||
return -1, nil
|
||||
return -1, Aggregated{}
|
||||
}
|
||||
return len(aggregated), aggregated
|
||||
return len(aggregated.Entries), aggregated
|
||||
}
|
||||
|
||||
func diff(x, y uint64) uint64 {
|
||||
|
||||
@@ -133,11 +133,11 @@ func TestSerialize(t *testing.T) {
|
||||
ExpectNoError(t, err)
|
||||
var v []map[string]any
|
||||
ExpectNoError(t, json.Unmarshal(s, &v))
|
||||
ExpectEqual(t, len(v), len(result))
|
||||
ExpectEqual(t, len(v), len(result.Entries))
|
||||
for i, m := range v {
|
||||
for k, v := range m {
|
||||
// some int64 values are converted to float64 on json.Unmarshal
|
||||
vv := reflect.ValueOf(result[i][k])
|
||||
vv := reflect.ValueOf(result.Entries[i][k])
|
||||
ExpectEqual(t, reflect.ValueOf(v).Convert(vv.Type()).Interface(), vv.Interface())
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func BenchmarkSerialize(b *testing.B) {
|
||||
b.Run("json", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for _, query := range allQueries {
|
||||
_, _ = json.Marshal([]map[string]any(queries[string(query)]))
|
||||
_, _ = json.Marshal([]map[string]any(queries[string(query)].Entries))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/metrics/period"
|
||||
metricsutils "github.com/yusing/go-proxy/internal/metrics/utils"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
@@ -23,18 +23,20 @@ type (
|
||||
} // @name RouteStatusesByAlias
|
||||
Status struct {
|
||||
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
Latency int64 `json:"latency"`
|
||||
Latency int32 `json:"latency"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
} // @name RouteStatus
|
||||
RouteStatuses map[string][]*Status // @name RouteStatuses
|
||||
RouteStatuses map[string][]Status // @name RouteStatuses
|
||||
RouteAggregate struct {
|
||||
Alias string `json:"alias"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
Downtime float64 `json:"downtime"`
|
||||
Idle float64 `json:"idle"`
|
||||
AvgLatency float64 `json:"avg_latency"`
|
||||
Statuses []*Status `json:"statuses"`
|
||||
Alias string `json:"alias"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Uptime float32 `json:"uptime"`
|
||||
Downtime float32 `json:"downtime"`
|
||||
Idle float32 `json:"idle"`
|
||||
AvgLatency float32 `json:"avg_latency"`
|
||||
IsDocker bool `json:"is_docker"`
|
||||
CurrentStatus types.HealthStatus `json:"current_status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
|
||||
Statuses []Status `json:"statuses"`
|
||||
} // @name RouteUptimeAggregate
|
||||
Aggregated []RouteAggregate
|
||||
)
|
||||
@@ -64,9 +66,9 @@ func aggregateStatuses(entries []*StatusByAlias, query url.Values) (int, Aggrega
|
||||
statuses := make(RouteStatuses)
|
||||
for _, entry := range entries {
|
||||
for alias, status := range entry.Map {
|
||||
statuses[alias] = append(statuses[alias], &Status{
|
||||
statuses[alias] = append(statuses[alias], Status{
|
||||
Status: status.Status,
|
||||
Latency: status.Latency.Milliseconds(),
|
||||
Latency: int32(status.Latency.Milliseconds()),
|
||||
Timestamp: entry.Timestamp,
|
||||
})
|
||||
}
|
||||
@@ -81,12 +83,12 @@ func aggregateStatuses(entries []*StatusByAlias, query url.Values) (int, Aggrega
|
||||
return len(statuses), statuses.aggregate(limit, offset)
|
||||
}
|
||||
|
||||
func (rs RouteStatuses) calculateInfo(statuses []*Status) (up float64, down float64, idle float64, _ float64) {
|
||||
func (rs RouteStatuses) calculateInfo(statuses []Status) (up float32, down float32, idle float32, _ float32) {
|
||||
if len(statuses) == 0 {
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
total := float64(0)
|
||||
latency := float64(0)
|
||||
total := float32(0)
|
||||
latency := float32(0)
|
||||
for _, status := range statuses {
|
||||
// ignoring unknown; treating napping and starting as downtime
|
||||
if status.Status == types.StatusUnknown {
|
||||
@@ -101,7 +103,7 @@ func (rs RouteStatuses) calculateInfo(statuses []*Status) (up float64, down floa
|
||||
down++
|
||||
}
|
||||
total++
|
||||
latency += float64(status.Latency)
|
||||
latency += float32(status.Latency)
|
||||
}
|
||||
if total == 0 {
|
||||
return 0, 0, 0, 0
|
||||
@@ -121,34 +123,41 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
|
||||
sortedAliases[i] = alias
|
||||
i++
|
||||
}
|
||||
// unknown statuses are at the end, then sort by alias
|
||||
slices.SortFunc(sortedAliases, func(a, b string) int {
|
||||
if rs[a][len(rs[a])-1].Status == types.StatusUnknown {
|
||||
return 1
|
||||
}
|
||||
if rs[b][len(rs[b])-1].Status == types.StatusUnknown {
|
||||
return -1
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
slices.Sort(sortedAliases)
|
||||
sortedAliases = sortedAliases[beg:end]
|
||||
result := make(Aggregated, len(sortedAliases))
|
||||
for i, alias := range sortedAliases {
|
||||
statuses := rs[alias]
|
||||
up, down, idle, latency := rs.calculateInfo(statuses)
|
||||
result[i] = RouteAggregate{
|
||||
Alias: alias,
|
||||
Uptime: up,
|
||||
Downtime: down,
|
||||
Idle: idle,
|
||||
AvgLatency: latency,
|
||||
Statuses: statuses,
|
||||
}
|
||||
|
||||
displayName := alias
|
||||
r, ok := routes.Get(alias)
|
||||
if ok {
|
||||
result[i].DisplayName = r.HomepageConfig().Name
|
||||
} else {
|
||||
result[i].DisplayName = alias
|
||||
if !ok {
|
||||
// also search for excluded routes
|
||||
r = config.GetInstance().SearchRoute(alias)
|
||||
}
|
||||
if r != nil {
|
||||
displayName = r.DisplayName()
|
||||
}
|
||||
|
||||
status := types.StatusUnknown
|
||||
if r != nil {
|
||||
mon := r.HealthMonitor()
|
||||
if mon != nil {
|
||||
status = mon.Status()
|
||||
}
|
||||
}
|
||||
|
||||
result[i] = RouteAggregate{
|
||||
Alias: alias,
|
||||
DisplayName: displayName,
|
||||
Uptime: up,
|
||||
Downtime: down,
|
||||
Idle: idle,
|
||||
AvgLatency: latency,
|
||||
CurrentStatus: status,
|
||||
Statuses: statuses,
|
||||
IsDocker: r != nil && r.IsDocker(),
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -2,8 +2,11 @@ package middleware_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -135,17 +138,31 @@ func TestReverseProxyBypass(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEntrypointBypassRoute(t *testing.T) {
|
||||
go http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("test"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
url, err := url.Parse(srv.URL)
|
||||
expect.NoError(t, err)
|
||||
|
||||
host, port, err := net.SplitHostPort(url.Host)
|
||||
expect.NoError(t, err)
|
||||
|
||||
portInt, err := strconv.Atoi(port)
|
||||
expect.NoError(t, err)
|
||||
|
||||
expect.NoError(t, err)
|
||||
entry := entrypoint.NewEntrypoint()
|
||||
r := &route.Route{
|
||||
Alias: "test-route",
|
||||
Host: host,
|
||||
Port: routeTypes.Port{
|
||||
Proxy: 8080,
|
||||
Proxy: portInt,
|
||||
},
|
||||
}
|
||||
err := entry.SetMiddlewares([]map[string]any{
|
||||
|
||||
err = entry.SetMiddlewares([]map[string]any{
|
||||
{
|
||||
"use": "redirectHTTP",
|
||||
"bypass": []string{"route test-route"},
|
||||
|
||||
@@ -5,16 +5,16 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/serialization"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
type (
|
||||
cidrWhitelist struct {
|
||||
CIDRWhitelistOpts
|
||||
cachedAddr F.Map[string, bool] // cache for trusted IPs
|
||||
cachedAddr *xsync.Map[string, bool] // cache for trusted IPs
|
||||
}
|
||||
CIDRWhitelistOpts struct {
|
||||
Allow []*nettypes.CIDR `validate:"min=1"`
|
||||
@@ -42,7 +42,7 @@ func init() {
|
||||
// setup implements MiddlewareWithSetup.
|
||||
func (wl *cidrWhitelist) setup() {
|
||||
wl.CIDRWhitelistOpts = cidrWhitelistDefaults
|
||||
wl.cachedAddr = F.NewMapOf[string, bool]()
|
||||
wl.cachedAddr = xsync.NewMap[string, bool](xsync.WithPresize(100))
|
||||
}
|
||||
|
||||
// before implements RequestModifier.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -115,11 +116,12 @@ func fetchUpdateCFIPRange(endpoint string, cfCIDRs *[]*nettypes.CIDR) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range strutils.SplitLine(string(body)) {
|
||||
if line == "" {
|
||||
for line := range bytes.Lines(body) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
_, cidr, err := net.ParseCIDR(line)
|
||||
_, cidr, err := net.ParseCIDR(string(line))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare responeded an invalid CIDR: %s", line)
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
@@ -20,13 +20,13 @@ const errPagesBasePath = common.ErrorPagesBasePath
|
||||
|
||||
var (
|
||||
setupOnce sync.Once
|
||||
dirWatcher W.Watcher
|
||||
fileContentMap = F.NewMapOf[string, []byte]()
|
||||
dirWatcher watcher.Watcher
|
||||
fileContentMap = xsync.NewMap[string, []byte](xsync.WithGrowOnly())
|
||||
)
|
||||
|
||||
func setup() {
|
||||
t := task.RootTask("error_page", false)
|
||||
dirWatcher = W.NewDirectoryWatcher(t, errPagesBasePath)
|
||||
dirWatcher = watcher.NewDirectoryWatcher(t, errPagesBasePath)
|
||||
loadContent()
|
||||
go watchDir()
|
||||
}
|
||||
@@ -46,13 +46,13 @@ func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) {
|
||||
}
|
||||
|
||||
func loadContent() {
|
||||
files, err := U.ListFiles(errPagesBasePath, 0)
|
||||
files, err := utils.ListFiles(errPagesBasePath, 0)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to list error page resources")
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if fileContentMap.Has(file) {
|
||||
if _, ok := fileContentMap.Load(file); ok {
|
||||
continue
|
||||
}
|
||||
content, err := os.ReadFile(file)
|
||||
|
||||
@@ -59,6 +59,17 @@ func (ri *realIP) isInCIDRList(ip net.IP) bool {
|
||||
}
|
||||
|
||||
func (ri *realIP) setRealIP(req *http.Request) {
|
||||
// skip first if header is not present
|
||||
realIPs := req.Header.Values(ri.Header)
|
||||
if len(realIPs) == 0 {
|
||||
// try non-canonical key
|
||||
realIPs = req.Header[ri.Header]
|
||||
}
|
||||
|
||||
if len(realIPs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
clientIPStr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
clientIPStr = req.RemoteAddr
|
||||
@@ -77,18 +88,8 @@ func (ri *realIP) setRealIP(req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
realIPs := req.Header.Values(ri.Header)
|
||||
lastNonTrustedIP := ""
|
||||
|
||||
if len(realIPs) == 0 {
|
||||
// try non-canonical key
|
||||
realIPs = req.Header[ri.Header]
|
||||
}
|
||||
|
||||
if len(realIPs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !ri.Recursive {
|
||||
lastNonTrustedIP = realIPs[len(realIPs)-1]
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,8 @@ import (
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
// A ProxyRequest contains a request to be rewritten by a [ReverseProxy].
|
||||
@@ -169,6 +171,9 @@ func copyHeader(dst, src http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
//go:linkname errStreamClosed golang.org/x/net/http2.errStreamClosed
|
||||
var errStreamClosed error
|
||||
|
||||
func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) {
|
||||
reqURL := r.Host + r.URL.Path
|
||||
switch {
|
||||
@@ -186,6 +191,9 @@ func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err
|
||||
log.Err(err).Msg("underlying error")
|
||||
goto logged
|
||||
}
|
||||
if errors.Is(err, errStreamClosed) {
|
||||
goto logged
|
||||
}
|
||||
var h2Err http2.StreamError
|
||||
if errors.As(err, &h2Err) {
|
||||
// ignore these errors
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package reverseproxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
nettypes "github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
type noopTransport struct{}
|
||||
|
||||
func (t noopTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("Hello, world!")),
|
||||
Request: req,
|
||||
ContentLength: int64(len("Hello, world!")),
|
||||
Header: http.Header{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type noopResponseWriter struct{}
|
||||
|
||||
func (w noopResponseWriter) Header() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (w noopResponseWriter) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w noopResponseWriter) WriteHeader(statusCode int) {
|
||||
}
|
||||
|
||||
func BenchmarkReverseProxy(b *testing.B) {
|
||||
var w noopResponseWriter
|
||||
var req = http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Scheme: "http", Host: "test"},
|
||||
Body: io.NopCloser(strings.NewReader("Hello, world!")),
|
||||
}
|
||||
proxy := NewReverseProxy("test", nettypes.MustParseURL("http://localhost:8080"), noopTransport{})
|
||||
for b.Loop() {
|
||||
proxy.ServeHTTP(w, &req)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
@@ -121,10 +122,21 @@ func NewManagerWithUpgrade(c *gin.Context) (*Manager, error) {
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// Periodic writes data to the connection periodically.
|
||||
func (cm *Manager) Context() context.Context {
|
||||
return cm.ctx
|
||||
}
|
||||
|
||||
// Periodic writes data to the connection periodically, with deduplication.
|
||||
// If the connection is closed, the error is returned.
|
||||
// If the write timeout is reached, ErrWriteTimeout is returned.
|
||||
func (cm *Manager) PeriodicWrite(interval time.Duration, getData func() (any, error)) error {
|
||||
func (cm *Manager) PeriodicWrite(interval time.Duration, getData func() (any, error), deduplicate ...DeduplicateFunc) error {
|
||||
var lastData any
|
||||
|
||||
var equals DeduplicateFunc
|
||||
if len(deduplicate) > 0 {
|
||||
equals = deduplicate[0]
|
||||
}
|
||||
|
||||
write := func() {
|
||||
data, err := getData()
|
||||
if err != nil {
|
||||
@@ -133,6 +145,13 @@ func (cm *Manager) PeriodicWrite(interval time.Duration, getData func() (any, er
|
||||
return
|
||||
}
|
||||
|
||||
// skip if the data is the same as the last data
|
||||
if equals != nil && equals(data, lastData) {
|
||||
return
|
||||
}
|
||||
|
||||
lastData = data
|
||||
|
||||
if err := cm.WriteJSON(data, interval); err != nil {
|
||||
cm.err = err
|
||||
cm.Close()
|
||||
@@ -214,6 +233,17 @@ func (cm *Manager) ReadJSON(out any, timeout time.Duration) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *Manager) ReadBinary(timeout time.Duration) ([]byte, error) {
|
||||
select {
|
||||
case <-cm.ctx.Done():
|
||||
return nil, cm.err
|
||||
case data := <-cm.readCh:
|
||||
return data, nil
|
||||
case <-time.After(timeout):
|
||||
return nil, ErrReadTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection and cancels the context
|
||||
func (cm *Manager) Close() {
|
||||
cm.closeOnce.Do(cm.close)
|
||||
@@ -230,6 +260,12 @@ func (cm *Manager) close() {
|
||||
cm.conn.Close()
|
||||
|
||||
cm.pingCheckTicker.Stop()
|
||||
|
||||
if cm.err != nil {
|
||||
log.Debug().Caller(4).Msg("Closing WebSocket connection: " + cm.err.Error())
|
||||
} else {
|
||||
log.Debug().Caller(4).Msg("Closing WebSocket connection")
|
||||
}
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the context is done or the connection is closed
|
||||
|
||||
25
internal/net/gphttp/websocket/reader.go
Normal file
25
internal/net/gphttp/websocket/reader.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
func (m *Manager) NewReader() io.Reader {
|
||||
return &Reader{
|
||||
manager: m,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Read(p []byte) (int, error) {
|
||||
data, err := r.manager.ReadBinary(10 * time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
copy(p, data)
|
||||
return len(data), nil
|
||||
}
|
||||
@@ -7,14 +7,16 @@ import (
|
||||
apitypes "github.com/yusing/go-proxy/internal/api/types"
|
||||
)
|
||||
|
||||
func PeriodicWrite(c *gin.Context, interval time.Duration, get func() (any, error)) {
|
||||
type DeduplicateFunc func(last, current any) bool
|
||||
|
||||
func PeriodicWrite(c *gin.Context, interval time.Duration, get func() (any, error), deduplicate ...DeduplicateFunc) {
|
||||
manager, err := NewManagerWithUpgrade(c)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to upgrade to websocket"))
|
||||
return
|
||||
}
|
||||
defer manager.Close()
|
||||
err = manager.PeriodicWrite(interval, get)
|
||||
err = manager.PeriodicWrite(interval, get, deduplicate...)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to write to websocket"))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,3 @@ func (cm *Manager) NewWriter(msgType int) io.Writer {
|
||||
func (w *Writer) Write(p []byte) (int, error) {
|
||||
return len(p), w.manager.WriteData(w.msgType, p, 10*time.Second)
|
||||
}
|
||||
|
||||
func (w *Writer) Close() error {
|
||||
return w.manager.conn.Close()
|
||||
}
|
||||
|
||||
@@ -97,10 +97,6 @@ func (s *FileServer) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
routes.HTTP.Add(s)
|
||||
s.task.OnFinished("remove_route_from_http", func() {
|
||||
routes.HTTP.Del(s)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
@@ -31,9 +31,6 @@ const (
|
||||
var ErrAliasRefIndexOutOfRange = gperr.New("index out of range")
|
||||
|
||||
func DockerProviderImpl(name, dockerHost string) ProviderImpl {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
return &DockerProvider{
|
||||
name,
|
||||
dockerHost,
|
||||
@@ -62,7 +59,10 @@ func (p *DockerProvider) NewWatcher() watcher.Watcher {
|
||||
}
|
||||
|
||||
func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) {
|
||||
containers, err := docker.ListContainers(p.dockerHost)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
containers, err := docker.ListContainers(ctx, p.dockerHost)
|
||||
if err != nil {
|
||||
return nil, gperr.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
@@ -67,6 +69,10 @@ func NewFileProvider(filename string) (p *Provider, err error) {
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
if dockerHost == common.DockerHostFromEnv {
|
||||
dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost)
|
||||
}
|
||||
|
||||
p := newProvider(provider.ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(name, dockerHost)
|
||||
p.watcher = p.NewWatcher()
|
||||
|
||||
@@ -136,10 +136,6 @@ func (r *ReveseProxyRoute) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.UseLoadBalance() {
|
||||
r.addToLoadBalancer(parent)
|
||||
} else {
|
||||
@@ -171,7 +167,7 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
linked = l.(*ReveseProxyRoute)
|
||||
lb = linked.loadBalancer
|
||||
lb.UpdateConfigIfNeeded(cfg)
|
||||
if linked.Homepage == nil {
|
||||
if linked.Homepage.Name == "" {
|
||||
linked.Homepage = r.Homepage
|
||||
}
|
||||
} else {
|
||||
@@ -187,10 +183,8 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
}
|
||||
linked.SetHealthMonitor(lb)
|
||||
routes.HTTP.AddKey(cfg.Link, linked)
|
||||
routes.All.AddKey(cfg.Link, linked)
|
||||
r.task.OnFinished("remove_loadbalancer_route", func() {
|
||||
routes.HTTP.DelKey(cfg.Link)
|
||||
routes.All.DelKey(cfg.Link)
|
||||
})
|
||||
lbLock.Unlock()
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/route/rules"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
@@ -65,7 +64,8 @@ type (
|
||||
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
|
||||
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
|
||||
|
||||
Excluded *bool `json:"excluded"`
|
||||
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
|
||||
ExcludedReason string `json:"excluded_reason,omitempty" extensions:"x-nullable"`
|
||||
|
||||
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
|
||||
// for swagger
|
||||
@@ -84,6 +84,7 @@ type (
|
||||
once sync.Once
|
||||
}
|
||||
Routes map[string]*Route
|
||||
Port = route.Port
|
||||
)
|
||||
|
||||
const DefaultHost = "localhost"
|
||||
@@ -259,8 +260,10 @@ func (r *Route) Validate() gperr.Error {
|
||||
}
|
||||
|
||||
r.impl = impl
|
||||
excluded := r.ShouldExclude()
|
||||
r.Excluded = &excluded
|
||||
r.Excluded = r.ShouldExclude()
|
||||
if r.Excluded {
|
||||
r.ExcludedReason = r.GetExcludedReason()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -288,25 +291,27 @@ func (r *Route) start(parent task.Parent) gperr.Error {
|
||||
}
|
||||
defer close(r.started)
|
||||
|
||||
if err := r.impl.Start(parent); err != nil {
|
||||
return err
|
||||
// skip checking for excluded routes
|
||||
if !r.ShouldExclude() {
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if conflict, added := routes.All.AddIfNotExists(r.impl); !added {
|
||||
err := gperr.Errorf("route %s already exists: from %s and %s", r.Alias, r.ProviderName(), conflict.ProviderName())
|
||||
r.task.FinishAndWait(err)
|
||||
if cont := r.ContainerInfo(); cont != nil {
|
||||
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost)
|
||||
}
|
||||
|
||||
if err := r.impl.Start(parent); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// reference here because r.impl will be nil after Finish() is called.
|
||||
impl := r.impl
|
||||
r.task.OnCancel("remove_routes_from_all", func() {
|
||||
routes.All.Del(impl)
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Route) Finish(reason any) {
|
||||
if cont := r.ContainerInfo(); cont != nil {
|
||||
docker.DeleteDockerHostByContainerID(cont.ContainerID)
|
||||
}
|
||||
r.FinishAndWait(reason)
|
||||
}
|
||||
|
||||
@@ -411,16 +416,21 @@ func (r *Route) LoadBalanceConfig() *types.LoadBalancerConfig {
|
||||
return r.LoadBalance
|
||||
}
|
||||
|
||||
func (r *Route) HomepageConfig() *homepage.ItemConfig {
|
||||
return r.Homepage.GetOverride(r.Alias)
|
||||
func (r *Route) HomepageItem() homepage.Item {
|
||||
containerID := ""
|
||||
if r.Container != nil {
|
||||
containerID = r.Container.ContainerID
|
||||
}
|
||||
return homepage.Item{
|
||||
Alias: r.Alias,
|
||||
Provider: r.Provider,
|
||||
ItemConfig: *r.Homepage,
|
||||
ContainerID: containerID,
|
||||
}.GetOverride()
|
||||
}
|
||||
|
||||
func (r *Route) HomepageItem() *homepage.Item {
|
||||
return &homepage.Item{
|
||||
Alias: r.Alias,
|
||||
Provider: r.Provider,
|
||||
ItemConfig: r.HomepageConfig(),
|
||||
}
|
||||
func (r *Route) DisplayName() string {
|
||||
return r.Homepage.Name
|
||||
}
|
||||
|
||||
func (r *Route) ContainerInfo() *types.Container {
|
||||
@@ -442,8 +452,8 @@ func (r *Route) ShouldExclude() bool {
|
||||
if r.lastError != nil {
|
||||
return true
|
||||
}
|
||||
if r.Excluded != nil {
|
||||
return *r.Excluded
|
||||
if r.Excluded {
|
||||
return true
|
||||
}
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
@@ -465,6 +475,33 @@ func (r *Route) ShouldExclude() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Route) GetExcludedReason() string {
|
||||
if r.lastError != nil {
|
||||
return string(gperr.Plain(r.lastError))
|
||||
}
|
||||
if r.ExcludedReason != "" {
|
||||
return r.ExcludedReason
|
||||
}
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
case r.Container.IsExcluded:
|
||||
return "Manual exclusion"
|
||||
case r.IsZeroPort() && !r.UseIdleWatcher():
|
||||
return "No port exposed in container"
|
||||
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
|
||||
return "Blacklisted (backend service or database)"
|
||||
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
||||
return "Buildx"
|
||||
}
|
||||
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
|
||||
return "No port specified"
|
||||
}
|
||||
if strings.HasSuffix(r.Alias, "-old") {
|
||||
return "Container renaming intermediate state"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *Route) UseLoadBalance() bool {
|
||||
return r.LoadBalance != nil && r.LoadBalance.Link != ""
|
||||
}
|
||||
@@ -602,9 +639,11 @@ func (r *Route) FinalizeHomepageConfig() {
|
||||
isDocker := r.Container != nil
|
||||
|
||||
if r.Homepage == nil {
|
||||
r.Homepage = &homepage.ItemConfig{Show: true}
|
||||
r.Homepage = &homepage.ItemConfig{
|
||||
Show: true,
|
||||
Name: r.Alias,
|
||||
}
|
||||
}
|
||||
r.Homepage = r.Homepage.GetOverride(r.Alias)
|
||||
|
||||
if r.ShouldExclude() && isDocker {
|
||||
r.Homepage.Show = false
|
||||
|
||||
@@ -2,18 +2,42 @@ package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
type RouteContext struct{}
|
||||
type RouteContextKey struct{}
|
||||
|
||||
var routeContextKey = RouteContext{}
|
||||
type RouteContext struct {
|
||||
context.Context
|
||||
Route types.HTTPRoute
|
||||
}
|
||||
|
||||
var routeContextKey = RouteContextKey{}
|
||||
|
||||
func (r *RouteContext) Value(key any) any {
|
||||
if key == routeContextKey {
|
||||
return r.Route
|
||||
}
|
||||
return r.Context.Value(key)
|
||||
}
|
||||
|
||||
func WithRouteContext(r *http.Request, route types.HTTPRoute) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
|
||||
// we don't want to copy the request object every fucking requests
|
||||
// return r.WithContext(context.WithValue(r.Context(), routeContextKey, route))
|
||||
(*requestInternal)(unsafe.Pointer(r)).ctx = &RouteContext{
|
||||
Context: r.Context(),
|
||||
Route: route,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TryGetRoute(r *http.Request) types.HTTPRoute {
|
||||
@@ -74,3 +98,44 @@ func TryGetUpstreamURL(r *http.Request) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type requestInternal struct {
|
||||
Method string
|
||||
URL *url.URL
|
||||
Proto string
|
||||
ProtoMajor int
|
||||
ProtoMinor int
|
||||
Header http.Header
|
||||
Body io.ReadCloser
|
||||
GetBody func() (io.ReadCloser, error)
|
||||
ContentLength int64
|
||||
TransferEncoding []string
|
||||
Close bool
|
||||
Host string
|
||||
Form url.Values
|
||||
PostForm url.Values
|
||||
MultipartForm *multipart.Form
|
||||
Trailer http.Header
|
||||
RemoteAddr string
|
||||
RequestURI string
|
||||
TLS *tls.ConnectionState
|
||||
Cancel <-chan struct{}
|
||||
Response *http.Response
|
||||
Pattern string
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func init() {
|
||||
// make sure ctx has the same offset as http.Request
|
||||
f, ok := reflect.TypeFor[requestInternal]().FieldByName("ctx")
|
||||
if !ok {
|
||||
panic("ctx field not found")
|
||||
}
|
||||
f2, ok := reflect.TypeFor[http.Request]().FieldByName("ctx")
|
||||
if !ok {
|
||||
panic("ctx field not found")
|
||||
}
|
||||
if f.Offset != f2.Offset {
|
||||
panic(fmt.Sprintf("ctx has different offset than http.Request: %d != %d", f.Offset, f2.Offset))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/types"
|
||||
)
|
||||
|
||||
@@ -78,71 +75,6 @@ func getHealthInfo(r types.Route) *HealthInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func HomepageCategories() []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
for _, r := range HTTP.Iter {
|
||||
item := r.HomepageConfig()
|
||||
if item == nil || item.Category == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := check[item.Category]; ok {
|
||||
continue
|
||||
}
|
||||
check[item.Category] = struct{}{}
|
||||
categories = append(categories, item.Category)
|
||||
}
|
||||
return categories
|
||||
}
|
||||
|
||||
func HomepageItems(proto, hostname, categoryFilter, providerFilter string) homepage.Homepage {
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
default:
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
hp := make(homepage.Homepage)
|
||||
|
||||
if strings.Count(hostname, ".") > 1 {
|
||||
_, hostname, _ = strings.Cut(hostname, ".") // remove the subdomain
|
||||
}
|
||||
|
||||
for _, r := range HTTP.Iter {
|
||||
if providerFilter != "" && r.ProviderName() != providerFilter {
|
||||
continue
|
||||
}
|
||||
item := *r.HomepageItem()
|
||||
if categoryFilter != "" && item.Category != categoryFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
// clear url if invalid
|
||||
_, err := url.Parse(item.URL)
|
||||
if err != nil {
|
||||
item.URL = ""
|
||||
}
|
||||
|
||||
// append hostname if provided and only if alias is not FQDN
|
||||
if hostname != "" && item.URL == "" {
|
||||
isFQDNAlias := strings.Contains(item.Alias, ".")
|
||||
if !isFQDNAlias {
|
||||
item.URL = fmt.Sprintf("%s://%s.%s", proto, item.Alias, hostname)
|
||||
} else {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// prepend protocol if not exists
|
||||
if !strings.HasPrefix(item.URL, "http://") && !strings.HasPrefix(item.URL, "https://") {
|
||||
item.URL = fmt.Sprintf("%s://%s", proto, item.URL)
|
||||
}
|
||||
|
||||
hp.Add(&item)
|
||||
}
|
||||
return hp
|
||||
}
|
||||
|
||||
func ByProvider() map[string][]types.Route {
|
||||
rts := make(map[string][]types.Route)
|
||||
for r := range Iter {
|
||||
|
||||
@@ -8,16 +8,15 @@ import (
|
||||
var (
|
||||
HTTP = pool.New[types.HTTPRoute]("http_routes")
|
||||
Stream = pool.New[types.StreamRoute]("stream_routes")
|
||||
// All is a pool of all routes, including HTTP, Stream routes and also excluded routes.
|
||||
All = pool.New[types.Route]("all_routes")
|
||||
)
|
||||
|
||||
func init() {
|
||||
All.DisableLog()
|
||||
}
|
||||
|
||||
func Iter(yield func(r types.Route) bool) {
|
||||
for _, r := range All.Iter {
|
||||
for _, r := range HTTP.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, r := range Stream.Iter {
|
||||
if !yield(r) {
|
||||
break
|
||||
}
|
||||
@@ -25,7 +24,12 @@ func Iter(yield func(r types.Route) bool) {
|
||||
}
|
||||
|
||||
func IterKV(yield func(alias string, r types.Route) bool) {
|
||||
for k, r := range All.Iter {
|
||||
for k, r := range HTTP.Iter {
|
||||
if !yield(k, r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for k, r := range Stream.Iter {
|
||||
if !yield(k, r) {
|
||||
break
|
||||
}
|
||||
@@ -33,13 +37,12 @@ func IterKV(yield func(alias string, r types.Route) bool) {
|
||||
}
|
||||
|
||||
func NumRoutes() int {
|
||||
return All.Size()
|
||||
return HTTP.Size() + Stream.Size()
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
HTTP.Clear()
|
||||
Stream.Clear()
|
||||
All.Clear()
|
||||
}
|
||||
|
||||
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
||||
@@ -52,5 +55,11 @@ func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
|
||||
}
|
||||
|
||||
func Get(alias string) (types.Route, bool) {
|
||||
return All.Get(alias)
|
||||
if r, ok := HTTP.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
if r, ok := Stream.Get(alias); ok {
|
||||
return r, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestParseCommands(t *testing.T) {
|
||||
// serve tests
|
||||
{
|
||||
name: "serve_valid",
|
||||
input: "serve /var/www",
|
||||
input: "serve /",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -50,6 +50,11 @@ func TestParseCommands(t *testing.T) {
|
||||
input: "serve ",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "serve_non_exist_path",
|
||||
input: "serve /non-exist-path",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "serve_too_many_args",
|
||||
input: "serve / / /",
|
||||
|
||||
@@ -40,8 +40,8 @@ type (
|
||||
*/
|
||||
Rule struct {
|
||||
Name string `json:"name"`
|
||||
On RuleOn `json:"on"`
|
||||
Do Command `json:"do"`
|
||||
On RuleOn `json:"on" swaggertype:"string"`
|
||||
Do Command `json:"do" swaggertype:"string"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -73,10 +73,6 @@ func (r *StreamRoute) Start(parent task.Parent) gperr.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkExists(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.ListenAndServe(r.task.Context(), nil, nil)
|
||||
r.l = r.l.With().Stringer("rurl", r.ProxyURL).Stringer("laddr", r.LocalAddr()).Logger()
|
||||
r.l.Info().Msg("stream started")
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -190,15 +189,7 @@ func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool)
|
||||
dstV := reflect.ValueOf(dst)
|
||||
dstT := dstV.Type()
|
||||
|
||||
if src == nil {
|
||||
if dstV.CanSet() {
|
||||
dstV.Set(reflect.Zero(dstT))
|
||||
return nil
|
||||
}
|
||||
return gperr.Errorf("deserialize: src is %w and dst is not settable", ErrNilValue)
|
||||
}
|
||||
|
||||
if dstT.Implements(mapUnmarshalerType) {
|
||||
if src != nil && dstT.Implements(mapUnmarshalerType) {
|
||||
dstV, _, err = dive(dstV)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,6 +202,14 @@ func mapUnmarshalValidate(src SerializedObject, dst any, checkValidateTag bool)
|
||||
return err
|
||||
}
|
||||
|
||||
if src == nil {
|
||||
if dstV.CanSet() {
|
||||
dstV.Set(reflect.Zero(dstT))
|
||||
return nil
|
||||
}
|
||||
return gperr.Errorf("deserialize: src is %w and dst is not settable", ErrNilValue)
|
||||
}
|
||||
|
||||
// convert data fields to lower no-snake
|
||||
// convert target fields to lower no-snake
|
||||
// then check if the field of data is in the target
|
||||
@@ -565,7 +564,7 @@ func UnmarshalValidateYAMLIntercept[T any](data []byte, target *T, intercept fun
|
||||
return MapUnmarshalValidate(m, target)
|
||||
}
|
||||
|
||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ functional.Map[string, V], err gperr.Error) {
|
||||
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], err gperr.Error) {
|
||||
data, err = substituteEnv(data)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -579,7 +578,11 @@ func UnmarshalValidateYAMLXSync[V any](data []byte) (_ functional.Map[string, V]
|
||||
if err = MapUnmarshalValidate(m, m2); err != nil {
|
||||
return
|
||||
}
|
||||
return functional.NewMapFrom(m2), nil
|
||||
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
|
||||
for k, v := range m2 {
|
||||
ret.Store(k, v)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func loadSerialized[T any](path string, dst *T, deserialize func(data []byte, dst any) error) error {
|
||||
|
||||
21
internal/serialization/time.go
Normal file
21
internal/serialization/time.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"time"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname unitMap time.unitMap
|
||||
var unitMap map[string]uint64
|
||||
|
||||
const (
|
||||
unitDay uint64 = 24 * uint64(time.Hour)
|
||||
unitWeek uint64 = 7 * unitDay
|
||||
unitMonth uint64 = 30 * unitDay
|
||||
)
|
||||
|
||||
func init() {
|
||||
unitMap["d"] = unitDay
|
||||
unitMap["w"] = unitWeek
|
||||
unitMap["M"] = unitMonth
|
||||
}
|
||||
16
internal/serialization/time_test.go
Normal file
16
internal/serialization/time_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
// NOTE: -ldflags=-checklinkname=0 is required to test this function
|
||||
func TestParseDuration(t *testing.T) {
|
||||
require.Equal(t, 24*time.Hour, expect.Must(time.ParseDuration("1d")))
|
||||
require.Equal(t, 7*24*time.Hour, expect.Must(time.ParseDuration("1w")))
|
||||
require.Equal(t, 30*24*time.Hour, expect.Must(time.ParseDuration("1M")))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user