Compare commits

..

18 Commits

Author SHA1 Message Date
yusing
aec937a114 fix(makefile): remove GOARCH 2025-09-10 09:01:54 +08:00
yusing
bab9471bde feat(config): implement environment variable substitution in configuration file reading 2025-09-09 23:33:05 +08:00
yusing
4ebd1dbf32 feat(setup): enhance setup script for rootless Docker support and network configuration 2025-09-09 23:13:38 +08:00
yusing
82a4a61df0 feat(docker): add example configuration files for rootless Docker setup 2025-09-09 22:48:26 +08:00
yusing
9e56ea5db1 fix(docker): add healthcheck label to Dockerfile to prevent self checking 2025-09-09 22:36:26 +08:00
yusing
719682c99f refactor(websocket): enhance connection management by ensuring resources are released on context cancellation 2025-09-09 22:25:02 +08:00
yusing
f81a2b6607 fix(docker): treat containers from $DOCKER_HOST as local 2025-09-09 22:23:50 +08:00
yusing
f47ba0a9b5 feat(docs): update README files to include logo and improve table of contents formatting 2025-09-09 14:40:09 +08:00
yusing
52e949de85 feat: Add development environment configuration with Docker Compose and Dockerfile 2025-09-08 09:15:24 +08:00
yusing
abeb26b556 fix(monitor): prevent nil pointer dereference in Finish method 2025-09-08 09:02:19 +08:00
yusing
23d392d88b fix(route): improve error handling in route.Start method 2025-09-08 09:02:19 +08:00
yusing
d588664bfa fix: prevent panicking on misconfigurations 2025-09-08 09:02:19 +08:00
DeAndre Harris
41ce784a7f feat: Add per-route OIDC client ID and secret support (#145) 2025-09-08 08:16:30 +08:00
yusing
577169d03c refactor(idlewatcher): improve container readiness handling and health check logic
- Simplified the wakeFromHTTP and wakeFromStream methods by removing unnecessary loops and integrating direct checks for container readiness.
- Introduced a waitForReady method to streamline the waiting process for container readiness notifications.
- Enhanced the checkUpdateState method to include timeout detection for container startup.
- Added health check retries and logging for better monitoring of container state transitions.
2025-09-06 07:51:28 +08:00
yusing
b43274e9e6 refactor(idlewatcher): replace map with ordered.Map for deduplicating dependencies 2025-09-06 07:49:50 +08:00
yusing
d83c367e7f chore: update Go version to 1.25.1 in Dockerfile and module files 2025-09-06 07:48:57 +08:00
yusing
d9fbd53870 refactor(api): remove unused Swagger docs.go and clean up dependencies; Makefile update 2025-09-06 07:48:23 +08:00
yusing
7f54f50af8 docs(README): add announcement for new WebUI availability in nightly tag 2025-09-06 07:46:09 +08:00
93 changed files with 1738 additions and 8183 deletions

1
.gitignore vendored
View File

@@ -38,5 +38,4 @@ node_modules/
tsconfig.tsbuildinfo
!agent.compose.yml
!dev.compose.yml
!agent/pkg/**

View File

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.25.0-alpine AS deps
FROM golang:1.25.1-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -47,6 +47,7 @@ FROM scratch
LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1
LABEL proxy.#1.healthcheck.disable=true
# copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

View File

@@ -2,7 +2,6 @@ shell := /bin/sh
export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
export GOARCH ?= amd64
WEBUI_DIR ?= ../godoxy-frontend
DOCS_DIR ?= ../godoxy-wiki
@@ -144,6 +143,8 @@ push-github:
gen-swagger:
swag init --parseDependency --parseInternal -g handler.go -d internal/api -o internal/api/v1/docs
python3 scripts/fix-swagger-json.py
# we don't need this
rm internal/api/v1/docs/docs.go
gen-swagger-markdown: gen-swagger
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md

View File

@@ -1,10 +1,11 @@
<div align="center">
# GoDoxy
<img src="assets/godoxy.png" width="200">
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=go-proxy)
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
@@ -16,10 +17,10 @@ A lightweight, simple, and performant reverse proxy with WebUI.
<h5>EN | <a href="README_CHT.md">中文</a></h5>
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
<img src="screenshots/webui.jpg" style="max-width: 650">
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>
@@ -28,19 +29,18 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
<!-- TOC -->
- [GoDoxy](#godoxy)
- [Table of content](#table-of-content)
- [Running demo](#running-demo)
- [Key Features](#key-features)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [How does GoDoxy work](#how-does-godoxy-work)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
- [Metrics and Logs](#metrics-and-logs)
- [Manual Setup](#manual-setup)
- [Folder structrue](#folder-structrue)
- [Build it yourself](#build-it-yourself)
- [Table of content](#table-of-content)
- [Running demo](#running-demo)
- [Key Features](#key-features)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [How does GoDoxy work](#how-does-godoxy-work)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
- [Metrics and Logs](#metrics-and-logs)
- [Manual Setup](#manual-setup)
- [Folder structrue](#folder-structrue)
- [Build it yourself](#build-it-yourself)
## Running demo

View File

@@ -1,10 +1,11 @@
<div align="center">
# GoDoxy
<img src="assets/godoxy.png" width="200">
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![Demo](https://img.shields.io/website?url=https%3A%2F%2Fdemo.godoxy.dev&label=Demo&link=https%3A%2F%2Fdemo.godoxy.dev)
[![Discord](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](https://discord.gg/umReR62nRd)
@@ -16,28 +17,27 @@
<h5><a href="README.md">EN</a> | 中文</h5>
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh)
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
有疑問? 問 [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)!(鳴謝 [@ismesid](https://github.com/arevindh)
</div>
## 目錄
<!-- TOC -->
- [GoDoxy](#godoxy)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [目錄](#目錄)
- [運行示例](#運行示例)
- [主要特點](#主要特點)
- [前置需求](#前置需求)
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
## 運行示例

View File

@@ -1,6 +1,6 @@
module github.com/yusing/go-proxy/agent
go 1.25.0
go 1.25.1
replace github.com/yusing/go-proxy => ..
@@ -15,7 +15,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy v0.17.2
github.com/yusing/go-proxy v0.17.1
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
)
@@ -24,8 +24,7 @@ require (
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
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -33,8 +32,8 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v28.4.0+incompatible // indirect
github.com/docker/docker v28.4.0+incompatible // indirect
github.com/docker/cli v28.3.3+incompatible // indirect
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
@@ -49,6 +48,7 @@ require (
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.6.3 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
@@ -59,7 +59,6 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
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
@@ -76,7 +75,7 @@ require (
github.com/samber/lo v1.51.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
github.com/shirou/gopsutil/v4 v4.25.7 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
@@ -87,11 +86,11 @@ require (
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/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect

View File

@@ -6,10 +6,8 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -29,10 +27,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -70,6 +68,8 @@ github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d h1:bNqtnmyhGDxpBSaFYIo7ferYRIc/QzlaGfIhh/JmMPk=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d/go.mod h1:7iQ/w4jyGYJCZ56dZLNztwM4atNxj5C2HNTBxhLvV8A=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -86,6 +86,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -175,6 +177,8 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.1.0 h1:aiZs7jPMN3MEChUsddMYjpZFHhhAmkxrwRyIUnGy5AU=
github.com/yusing/ds v0.1.0/go.mod h1:KC785+mtt+Bau0LLR+slExDaUjeiqLT1k9Or6Rpryh4=
@@ -182,22 +186,22 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
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/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=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -207,6 +211,8 @@ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
@@ -214,6 +220,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -221,7 +229,10 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -233,6 +244,8 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -242,7 +255,9 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -284,6 +299,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
@@ -291,6 +308,9 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-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=

View File

@@ -1,13 +1,11 @@
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 = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
var agentPool = functional.NewMapOf[string, *AgentConfig]()
func init() {
if common.IsTest {
@@ -53,14 +51,6 @@ 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

View File

@@ -16,7 +16,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) (*http.Response, error) {
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) ([]byte, int, error) {
req = req.WithContext(req.Context())
req.URL.Host = AgentHost
req.URL.Scheme = "https"
@@ -24,9 +24,11 @@ func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Respo
req.RequestURI = ""
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, err
return nil, 0, err
}
return resp, nil
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return data, resp.StatusCode, nil
}
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {

BIN
assets/godoxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -1,22 +1,6 @@
services:
socket-proxy:
container_name: socket-proxy-dev
image: ghcr.io/yusing/socket-proxy:latest
environment:
- CONTAINERS=1
- EVENTS=1
- INFO=1
- PING=1
- POST=0
- VERSION=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
tmpfs:
- /run
app:
image: godoxy-dev
user: 1000:1000
build:
context: .
dockerfile: dev.Dockerfile
@@ -24,9 +8,6 @@ services:
- TARGET=godoxy
container_name: godoxy-proxy-dev
restart: unless-stopped
depends_on:
socket-proxy:
condition: service_started
environment:
TZ: Asia/Hong_Kong
API_ADDR: :8999
@@ -36,18 +17,21 @@ services:
API_JWT_SECURE: false
API_JWT_TTL: 24h
DEBUG: true
DOCKER_HOST: tcp://socket-proxy:2375
API_SECRET: 1234567891234567
ports:
- 8999:8999
- 80:80
- 443:443
labels:
proxy.exclude: true
proxy.#1.healthcheck.disable: true
ipc: host
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./dev-data/config:/app/config
- ./dev-data/certs:/app/certs
- ./dev-data/error_pages:/app/error_pages:ro
- ./dev-data/data:/app/data
- ./dev-data/logs:/app/logs
depends_on:
- tinyauth
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:v3
container_name: tinyauth

80
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/yusing/go-proxy
go 1.25.0
go 1.25.1
replace github.com/yusing/go-proxy/agent => ./agent
@@ -15,7 +15,7 @@ replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.
require (
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.15.0 // oidc authentication
github.com/docker/docker v28.4.0+incompatible // docker daemon
github.com/docker/docker v28.3.3+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-playground/validator/v10 v10.27.0 // validator
@@ -25,7 +25,7 @@ require (
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
github.com/shirou/gopsutil/v4 v4.25.8 // system info metrics
github.com/shirou/gopsutil/v4 v4.25.7 // system info metrics
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.41.0 // encrypting password with bcrypt
golang.org/x/net v0.43.0 // HTTP header utilities
@@ -35,7 +35,7 @@ require (
)
require (
github.com/docker/cli v28.4.0+incompatible
github.com/docker/cli v28.3.3+incompatible
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/luthermonson/go-proxmox v0.2.2
@@ -44,8 +44,8 @@ require (
github.com/samber/slog-zerolog/v2 v2.7.3
github.com/spf13/afero v1.14.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy/agent v0.0.0-20250903143810-e1133a2daf72
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250903143810-e1133a2daf72
github.com/yusing/go-proxy/agent v0.0.0-20250819142638-5e15fd4bbef0
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250819142638-5e15fd4bbef0
github.com/yusing/go-proxy/internal/utils v0.0.0
)
@@ -65,21 +65,22 @@ require (
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.38.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.8 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.241 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
@@ -91,7 +92,7 @@ require (
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/exoscale/egoscale/v3 v3.1.26 // indirect
github.com/exoscale/egoscale/v3 v3.1.25 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
@@ -105,6 +106,7 @@ require (
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -147,6 +149,7 @@ require (
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
@@ -181,10 +184,10 @@ require (
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
@@ -197,10 +200,10 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/mock v0.6.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
@@ -220,13 +223,11 @@ require (
require (
github.com/gin-gonic/gin v1.10.1
github.com/swaggo/swag v1.16.6
github.com/yusing/ds v0.1.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.11 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
@@ -234,9 +235,7 @@ require (
github.com/alibabacloud-go/tea v1.3.11 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.7 // 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
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
@@ -248,31 +247,14 @@ require (
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.5.11 // indirect
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
github.com/go-openapi/jsonpointer v0.22.0 // indirect
github.com/go-openapi/jsonreference v0.21.1 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
github.com/go-openapi/swag/loading v0.24.0 // indirect
github.com/go-openapi/swag/mangling v0.24.0 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // 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.99.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect

155
go.sum
View File

@@ -635,8 +635,6 @@ 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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
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=
@@ -727,40 +725,40 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
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=
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
github.com/aws/aws-sdk-go-v2 v1.38.2 h1:QUkLO1aTW0yqW95pVzZS0LGFanL71hJ0a49w4TJLMyM=
github.com/aws/aws-sdk-go-v2 v1.38.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.4 h1:aY2IstXOfjdLtr1lDvxFBk5DpBnHgS5GS3jgR/0BmPw=
github.com/aws/aws-sdk-go-v2/config v1.31.4/go.mod h1:1IAykiegrTp6n+CbZoCpW6kks1I74fEDgl2BPQSkLSU=
github.com/aws/aws-sdk-go-v2/credentials v1.18.8 h1:0FfdP0I9gs/f1rwtEdkcEdsclTEkPB8o6zWUG2Z8+IM=
github.com/aws/aws-sdk-go-v2/credentials v1.18.8/go.mod h1:9UReQ1UmGooX93JKzHyr7PRF3F+p3r+PmRwR7+qHJYA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5 h1:ul7hICbZ5Z/Pp9VnLVGUVe7rqYLXCyIiPU7hQ0sRkow=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5/go.mod h1:5cIWJ0N6Gjj+72Q6l46DeaNtcxXHV42w/Uq3fIfeUl4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5 h1:d45S2DqHZOkHu0uLUW92VdBoT5v0hh3EyR+DzMEh3ag=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5/go.mod h1:G6e/dR2c2huh6JmIo9SXysjuLuDDGWMeYGibfW2ZrXg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5 h1:ENhnQOV3SxWHplOqNN1f+uuCNf9n4Y/PKpl6b1WRP0Q=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5/go.mod h1:csQLMI+odbC0/J+UecSTztG70Dc4aTCOu4GyPNDNpVo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2 h1:bbcKDYr5ivT4ghbcNmKPmLpH/42dn0CqZgE6c7SziQU=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2/go.mod h1:yYrzhBVvgD0aekhyjDij7gw1JVFHetfPUfxyyr0X3e8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0 h1:P7dm9TlRs6EEiXhwMn8DYQ92M/443GAzDk2q6GaPDNQ=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0/go.mod h1:j4q6vBiAJvH9oxFyFtZoV739zxVMsSn26XNFvFlorfU=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5 h1:Cx1M/UUgYu9UCQnIMKaOhkVaFvLy1HneD6T4sS/DlKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5/go.mod h1:fTRNLgrTvPpEzGqc9QkeO4hu/3ng+mdtUbL8shUwXz4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1 h1:szIozoXngSj0ibL/GNJbb5e2Y8WUmSqfk0BIiviO2qo=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1/go.mod h1:HBXDArKSFmUoPiHIatSFZP7RCOuSO86D1skZZPP0eLI=
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1 h1:t6CAhkQ5yVxPeDeAExUSDKRexiqIrOhUcQ/L3wXFnh8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1/go.mod h1:zvtb01R4yNazMQQlaDybZFGDJH13+zSp1psLzG0CUhQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3 h1:z6lajFT/qGlLRB/I8V5CCklqSuWZKUkdwRAn9leIkiQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3/go.mod h1:BnyjuIX0l+KXJVl2o9Ki3Zf0M4pA2hQYopFCRUj9ADU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 h1:8yI3jK5JZ310S8RpgdZdzwvlvBu3QbG8DP7Be/xJ6yo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1/go.mod h1:HPzXfFgrLd02lYpcFYdDz5xZs94LOb+lWlvbAGaeMsk=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 h1:3kWmIg5iiWPMBJyq/I55Fki5fyfoMtrn/SkUIpxPwHQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1/go.mod h1:yi0b3Qez6YamRVJ+Rbi19IgvjfjPODgVRhkWA6RTMUM=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aziontech/azionapi-go-sdk v0.142.0 h1:1NOHXlC0/7VgbfbTIGVpsYn1THCugM4/SCOXVdUHQ+A=
github.com/aziontech/azionapi-go-sdk v0.142.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA=
github.com/baidubce/bce-sdk-go v0.9.241 h1:8eRf+o1MCEh96MtP6eZF1ypFigtWNija02y2OZ+JK5k=
github.com/baidubce/bce-sdk-go v0.9.241/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -776,10 +774,8 @@ github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVk
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
@@ -853,10 +849,10 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dnsimple/dnsimple-go/v4 v4.0.0 h1:nUCICZSyZDiiqimAAL+E8XL+0sKGks5VRki5S8XotRo=
github.com/dnsimple/dnsimple-go/v4 v4.0.0/go.mod h1:AXT2yfAFOntJx6iMeo1J/zKBw0ggXFYBt4e97dqqPnc=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/cli v28.3.3+incompatible h1:fp9ZHAr1WWPGdIWBM1b3zLtgCF+83gRdVMTJsUeiyAo=
github.com/docker/cli v28.3.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -887,8 +883,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/exoscale/egoscale/v3 v3.1.26 h1:bXXT0zVLbE4QFm6tmt0bg6ZPk9pQgUA3Z8SJrctQ7b0=
github.com/exoscale/egoscale/v3 v3.1.26/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU=
github.com/exoscale/egoscale/v3 v3.1.25 h1:Xy4LdmElaUXdf72vCt8gv9DCivKUlmW5Ar5ATInwWU8=
github.com/exoscale/egoscale/v3 v3.1.25/go.mod h1:TJCI0OG3Lz2rnleRB0xwiOFg82uNCCytRqw7TxPoIvc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -961,36 +957,6 @@ 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-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -1036,6 +1002,7 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
@@ -1274,8 +1241,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -1357,8 +1322,6 @@ github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXq
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -1460,12 +1423,14 @@ github.com/nrdcg/goinwx v0.11.0 h1:GER0SE3POub7rxARt3Y3jRy1OON1hwF1LRxHz5xsFBw=
github.com/nrdcg/goinwx v0.11.0/go.mod h1:0BXSC0FxVtU4aTjX0Zw3x0DK32tjugLzeNIAGtwXvPQ=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.2 h1:FQvfUIV9JpCXHJ0fqZ+r/pAqSMh8AkQ94Ooz2WO9rwg=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.2/go.mod h1:sOWH1Rqtipy3kyrIER0JLge8O7n8pIr7G8UVEs6xaDY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2 h1:OgO02pgzn6JcrDSlHENAkQP4coftx6cEM6WnUOZlprk=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2/go.mod h1:CxuFDFnoi+G8wtUODauGxaRw/dHZ5IlUtz8Uwsw36Kk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1 h1:Q7EzGKkBg6Zw4HuqTNbK13ccuJK08eyStO0NIkCYyoU=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1/go.mod h1:Sls/ilbR5DFjMQEhFsO3gseG0xgJERRqqli/85xgkDo=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1 h1:wt9okvG0nJZJbLQluvjqpsyVSMipdg4tSt8I/5WRwaA=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1/go.mod h1:cN/nLDjwvq/+K29i2cYRsk8frENw09lwXjpEoSpiM14=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -1675,8 +1640,8 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
@@ -1710,11 +1675,9 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1208/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22 h1:1unTmvNXynDN0mOZSWh9tL5Wp9Rb5paMGwFvua+HHoI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18 h1:Pkyu9oYcYkp0GFKWa/Jyf8IeS6AADgUoUoRzP2eDq2Q=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -1730,8 +1693,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k=
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@@ -1795,22 +1758,22 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
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=
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=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=

View File

@@ -14,14 +14,12 @@ import (
authApi "github.com/yusing/go-proxy/internal/api/v1/auth"
certApi "github.com/yusing/go-proxy/internal/api/v1/cert"
dockerApi "github.com/yusing/go-proxy/internal/api/v1/docker"
"github.com/yusing/go-proxy/internal/api/v1/docs"
fileApi "github.com/yusing/go-proxy/internal/api/v1/file"
homepageApi "github.com/yusing/go-proxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/go-proxy/internal/api/v1/metrics"
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
@@ -40,27 +38,20 @@ import (
// @externalDocs.description GoDoxy Docs
// @externalDocs.url https://docs.godoxy.dev
func NewHandler() *gin.Engine {
if !common.IsDebug {
gin.SetMode("release")
}
gin.SetMode("release")
r := gin.New()
r.Use(ErrorHandler())
r.Use(ErrorLoggingMiddleware())
docs.SwaggerInfo.Title = "GoDoxy API"
docs.SwaggerInfo.BasePath = "/api/v1"
r.GET("/api/v1/version", apiV1.Version)
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 := 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)
}
v1 := r.Group("/api/v1")
@@ -129,9 +120,6 @@ func NewHandler() *gin.Engine {
docker.GET("/containers", dockerApi.Containers)
docker.GET("/info", dockerApi.Info)
docker.GET("/logs/:server/:container", dockerApi.Logs)
docker.POST("/start", dockerApi.Start)
docker.POST("/stop", dockerApi.Stop)
docker.POST("/restart", dockerApi.Restart)
}
}
@@ -194,9 +182,8 @@ 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 {
gperr.LogError("Internal error", err.Err, &logger)
log.Err(err.Err).Str("uri", c.Request.RequestURI).Msg("Internal error")
}
if !isWebSocketRequest(c) {
c.JSON(http.StatusInternalServerError, apitypes.Error("Internal server error"))

View File

@@ -18,6 +18,7 @@ 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)

View File

@@ -1,61 +0,0 @@
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 403 {object} apitypes.ErrorResponse
// @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,
})
}

View File

@@ -16,7 +16,7 @@ type Container struct {
Name string `json:"name"`
ID string `json:"id"`
Image string `json:"image"`
State ContainerState `json:"state,omitempty" extensions:"x-nullable"`
State ContainerState `json:"state"`
} // @name ContainerResponse
// @x-id "containers"

View File

@@ -10,11 +10,15 @@ 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"`
@@ -26,11 +30,12 @@ type LogsQueryParams struct {
// @x-id "logs"
// @BasePath /api/v1
// @Summary Get docker container logs
// @Description Get docker container logs by container id
// @Description Get docker container logs
// @Tags docker,websocket
// @Accept json
// @Produce json
// @Param id path string true "container id"
// @Param server path string true "server name"
// @Param container 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"
@@ -41,34 +46,29 @@ type LogsQueryParams struct {
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /docker/logs/{id} [get]
// @Router /docker/logs/{server}/{container} [get]
func Logs(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, apitypes.Error("container id is required"))
return
}
var pathParams LogsPathParams
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"))
return
}
// TODO: implement levels
dockerHost, ok := docker.GetDockerHostByContainerID(id)
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
return
}
dockerClient, err := docker.NewClient(dockerHost)
dockerClient, found, err := getDockerClient(pathParams.Server)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
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 +84,7 @@ func Logs(c *gin.Context) {
opts.Details = true
}
logs, err := dockerClient.ContainerLogs(c.Request.Context(), id, opts)
logs, err := dockerClient.ContainerLogs(c.Request.Context(), pathParams.ContainerID, opts)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to get container logs"))
return
@@ -106,8 +106,8 @@ func Logs(c *gin.Context) {
return
}
log.Err(err).
Str("server", dockerHost).
Str("container", id).
Str("server", pathParams.Server).
Str("container", pathParams.ContainerID).
Msg("failed to de-multiplex logs")
}
}

View File

@@ -1,50 +0,0 @@
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 403 {object} apitypes.ErrorResponse
// @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"))
}

View File

@@ -1,56 +0,0 @@
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 403 {object} apitypes.ErrorResponse
// @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"))
}

View File

@@ -1,56 +0,0 @@
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 403 {object} apitypes.ErrorResponse
// @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

View File

@@ -4,6 +4,8 @@ definitions:
properties:
addr:
type: string
is_nerdctl:
type: boolean
name:
type: string
version:
@@ -95,8 +97,6 @@ definitions:
description: non-zero publicPort:types.Port
running:
type: boolean
state:
$ref: '#/definitions/container.ContainerState'
type: object
ContainerImage:
properties:
@@ -122,9 +122,7 @@ definitions:
server:
type: string
state:
allOf:
- $ref: '#/definitions/ContainerState'
x-nullable: true
$ref: '#/definitions/ContainerState'
type: object
ContainerState:
enum:
@@ -283,84 +281,11 @@ definitions:
additionalProperties:
$ref: '#/definitions/routes.HealthInfo'
type: object
HomepageCategory:
properties:
HomepageItems:
additionalProperties:
items:
items:
$ref: '#/definitions/HomepageItem'
type: array
name:
type: string
type: object
HomepageItem:
properties:
alias:
type: string
all_sort_order:
description: sort order in all
type: integer
category:
type: string
description:
type: string
fav_sort_order:
description: sort order in favorite
type: integer
favorite:
type: boolean
icon:
type: string
name:
description: display name
type: string
origin_url:
type: string
provider:
type: string
show:
type: boolean
sort_order:
description: sort order in category
type: integer
url:
type: string
widget_config:
allOf:
- $ref: '#/definitions/widgets.Config'
x-nullable: true
widgets:
items:
$ref: '#/definitions/HomepageItemWidget'
type: array
type: object
HomepageItemConfig:
properties:
category:
type: string
description:
type: string
favorite:
type: boolean
icon:
type: string
name:
description: display name
type: string
show:
type: boolean
url:
type: string
widget_config:
allOf:
- $ref: '#/definitions/widgets.Config'
x-nullable: true
type: object
HomepageItemWidget:
properties:
label:
type: string
value:
type: string
$ref: '#/definitions/homepage.Item'
type: array
type: object
HomepageOverrideCategoryOrderParams:
properties:
@@ -369,40 +294,10 @@ definitions:
which:
type: string
type: object
HomepageOverrideItemAllSortOrderParams:
properties:
value:
type: integer
which:
type: string
type: object
HomepageOverrideItemFavSortOrderParams:
properties:
value:
type: integer
which:
type: string
type: object
HomepageOverrideItemFavoriteParams:
properties:
value:
type: boolean
which:
items:
type: string
type: array
type: object
HomepageOverrideItemParams:
properties:
value:
$ref: '#/definitions/HomepageItemConfig'
which:
type: string
type: object
HomepageOverrideItemSortOrderParams:
properties:
value:
type: integer
$ref: '#/definitions/homepage.ItemConfig'
which:
type: string
type: object
@@ -419,7 +314,7 @@ definitions:
properties:
value:
additionalProperties:
$ref: '#/definitions/HomepageItemConfig'
$ref: '#/definitions/homepage.ItemConfig'
type: object
type: object
IdlewatcherConfig:
@@ -559,6 +454,13 @@ definitions:
- MetricsPeriod1mo
NewAgentRequest:
properties:
container_runtime:
allOf:
- $ref: '#/definitions/agent.ContainerRuntime'
enum:
- docker
- podman
- nerdctl
host:
type: string
name:
@@ -685,10 +587,6 @@ definitions:
type: boolean
excluded:
type: boolean
x-nullable: true
excluded_reason:
type: string
x-nullable: true
health:
allOf:
- $ref: '#/definitions/HealthJSON'
@@ -696,7 +594,7 @@ definitions:
healthcheck:
$ref: '#/definitions/HealthCheckConfig'
homepage:
$ref: '#/definitions/HomepageItemConfig'
$ref: '#/definitions/homepage.ItemConfig'
host:
type: string
idlewatcher:
@@ -795,22 +693,12 @@ definitions:
type: string
avg_latency:
type: number
current_status:
enum:
- healthy
- unhealthy
- unknown
- napping
- starting
type: string
display_name:
type: string
downtime:
type: number
idle:
type: number
is_docker:
type: boolean
statuses:
items:
$ref: '#/definitions/RouteStatus'
@@ -931,6 +819,8 @@ definitions:
$ref: '#/definitions/PEMPairResponse'
client:
$ref: '#/definitions/PEMPairResponse'
container_runtime:
$ref: '#/definitions/agent.ContainerRuntime'
host:
type: string
type: object
@@ -982,6 +872,16 @@ definitions:
status_codes:
$ref: '#/definitions/LogFilter-StatusCodeRange'
type: object
agent.ContainerRuntime:
enum:
- docker
- podman
- nerdctl
type: string
x-enum-varnames:
- ContainerRuntimeDocker
- ContainerRuntimePodman
- ContainerRuntimeNerdctl
auth.UserPassAuthCallbackRequest:
properties:
password:
@@ -989,43 +889,6 @@ definitions:
username:
type: string
type: object
container.ContainerState:
enum:
- created
- running
- paused
- restarting
- removing
- exited
- dead
type: string
x-enum-comments:
StateCreated: StateCreated indicates the container is created, but not (yet)
started.
StateDead: StateDead indicates that the container failed to be deleted. Containers
in this state are attempted to be cleaned up when the daemon restarts.
StateExited: StateExited indicates that the container exited.
StatePaused: StatePaused indicates that the container's current state is paused.
StateRemoving: StateRemoving indicates that the container is being removed.
StateRestarting: StateRestarting indicates that the container is currently restarting.
StateRunning: StateRunning indicates that the container is running.
x-enum-descriptions:
- StateCreated indicates the container is created, but not (yet) started.
- StateRunning indicates that the container is running.
- StatePaused indicates that the container's current state is paused.
- StateRestarting indicates that the container is currently restarting.
- StateRemoving indicates that the container is being removed.
- StateExited indicates that the container exited.
- StateDead indicates that the container failed to be deleted. Containers in this
state are attempted to be cleaned up when the daemon restarts.
x-enum-varnames:
- StateCreated
- StateRunning
- StatePaused
- StateRestarting
- StateRemoving
- StateExited
- StateDead
container.Port:
properties:
IP:
@@ -1097,41 +960,6 @@ definitions:
used_percent:
type: number
type: object
dockerapi.StartRequest:
properties:
checkpointDir:
type: string
checkpointID:
type: string
id:
type: string
required:
- id
type: object
dockerapi.StopRequest:
properties:
id:
type: string
signal:
description: |-
Signal (optional) is the signal to send to the container to (gracefully)
stop it before forcibly terminating the container with SIGKILL after the
timeout expires. If not value is set, the default (SIGTERM) is used.
type: string
timeout:
description: |-
Timeout (optional) is the timeout (in seconds) to wait for the container
to stop gracefully before forcibly terminating it with SIGKILL.
- Use nil to use the default timeout (10 seconds).
- Use '-1' to wait indefinitely.
- Use '0' to not wait for the container to exit gracefully, and
immediately proceeds to forcibly terminating the container.
- Other positive values are used as timeout (in seconds).
type: integer
required:
- id
type: object
homepage.FetchResult:
properties:
errMsg:
@@ -1173,6 +1001,52 @@ definitions:
- IconSourceRelative
- IconSourceWalkXCode
- IconSourceSelfhSt
homepage.Item:
properties:
alias:
type: string
category:
type: string
description:
type: string
icon:
type: string
name:
description: display name
type: string
origin_url:
type: string
provider:
type: string
show:
type: boolean
sort_order:
type: integer
url:
type: string
widget_config:
allOf:
- $ref: '#/definitions/widgets.Config'
x-nullable: true
type: object
homepage.ItemConfig:
properties:
category:
type: string
description:
type: string
icon:
type: string
name:
description: display name
type: string
show:
type: boolean
sort_order:
type: integer
url:
type: string
type: object
mem.VirtualMemoryStat:
properties:
available:
@@ -1244,10 +1118,6 @@ definitions:
type: boolean
excluded:
type: boolean
x-nullable: true
excluded_reason:
type: string
x-nullable: true
health:
allOf:
- $ref: '#/definitions/HealthJSON'
@@ -1255,7 +1125,7 @@ definitions:
healthcheck:
$ref: '#/definitions/HealthCheckConfig'
homepage:
$ref: '#/definitions/HomepageItemConfig'
$ref: '#/definitions/homepage.ItemConfig'
host:
type: string
idlewatcher:
@@ -1342,14 +1212,18 @@ definitions:
description: uptime in milliseconds
type: number
type: object
rules.Command:
type: object
rules.Rule:
properties:
do:
type: string
$ref: '#/definitions/rules.Command'
name:
type: string
"on":
type: string
$ref: '#/definitions/rules.RuleOn'
type: object
rules.RuleOn:
type: object
sensors.TemperatureStat:
properties:
@@ -1515,6 +1389,38 @@ paths:
- agent
x-id: verify
/auth/callback:
get:
description: Handles the callback from the provider after successful authentication
parameters:
- description: Userpass only
in: body
name: body
required: true
schema:
$ref: '#/definitions/auth.UserPassAuthCallbackRequest'
produces:
- text/plain
responses:
"200":
description: 'Userpass: OK'
schema:
type: string
"302":
description: 'OIDC: Redirects to home page'
schema:
type: string
"400":
description: 'Userpass: invalid request / credentials'
schema:
type: string
"500":
description: Internal server error
schema:
type: string
summary: Auth Callback
tags:
- auth
x-id: callback
post:
description: Handles the callback from the provider after successful authentication
parameters:
@@ -1651,34 +1557,6 @@ paths:
- cert
- websocket
x-id: renew
/docker/container/{id}:
get:
description: Get container by container id
parameters:
- description: Container ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/ContainerResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Get container
tags:
- docker
x-id: container
/docker/containers:
get:
description: Get containers
@@ -1725,15 +1603,20 @@ paths:
tags:
- docker
x-id: info
/docker/logs/{id}:
/docker/logs/{server}/{container}:
get:
consumes:
- application/json
description: Get docker container logs by container id
description: Get docker container logs
parameters:
- description: server name
in: path
name: server
required: true
type: string
- description: container id
in: path
name: id
name: container
required: true
type: string
- description: show stdout
@@ -1782,93 +1665,6 @@ paths:
- docker
- websocket
x-id: logs
/docker/restart:
post:
description: Restart container by container id
parameters:
- description: Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dockerapi.StopRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Restart container
tags:
- docker
x-id: restart
/docker/start:
post:
description: Start container by container id
parameters:
- description: Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dockerapi.StartRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Start container
tags:
- docker
x-id: start
/docker/stop:
post:
description: Stop container by container id
parameters:
- description: Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dockerapi.StopRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Stop container
tags:
- docker
x-id: stop
/favicon:
get:
consumes:
@@ -2134,10 +1930,6 @@ paths:
- application/json
description: Homepage items
parameters:
- description: Search query
in: query
name: search
type: string
- description: Category filter
in: query
name: category
@@ -2152,9 +1944,7 @@ paths:
"200":
description: OK
schema:
items:
$ref: '#/definitions/HomepageCategory'
type: array
$ref: '#/definitions/HomepageItems'
"400":
description: Bad Request
schema:
@@ -2229,130 +2019,6 @@ paths:
tags:
- homepage
x-id: set-item
/homepage/set/item_all_sort_order:
post:
consumes:
- application/json
description: Set homepage item all sort order.
parameters:
- description: Set item all sort order
in: body
name: request
required: true
schema:
$ref: '#/definitions/HomepageOverrideItemAllSortOrderParams'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Set homepage item all sort order
tags:
- homepage
x-id: set-item-all-sort-order
/homepage/set/item_fav_sort_order:
post:
consumes:
- application/json
description: Set homepage item fav sort order.
parameters:
- description: Set item fav sort order
in: body
name: request
required: true
schema:
$ref: '#/definitions/HomepageOverrideItemFavSortOrderParams'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Set homepage item fav sort order
tags:
- homepage
x-id: set-item-fav-sort-order
/homepage/set/item_favorite:
post:
consumes:
- application/json
description: Set homepage item favorite.
parameters:
- description: Set item favorite
in: body
name: request
required: true
schema:
$ref: '#/definitions/HomepageOverrideItemFavoriteParams'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Set homepage item favorite
tags:
- homepage
x-id: set-item-favorite
/homepage/set/item_sort_order:
post:
consumes:
- application/json
description: Set homepage item sort order.
parameters:
- description: Set item sort order
in: body
name: request
required: true
schema:
$ref: '#/definitions/HomepageOverrideItemSortOrderParams'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/SuccessResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Set homepage item sort order
tags:
- homepage
x-id: set-item-sort-order
/homepage/set/item_visible:
post:
consumes:
@@ -2450,80 +2116,6 @@ paths:
tags:
- v1
x-id: icons
/metrics/all_system_info:
get:
description: Get system info
parameters:
- enum:
- cpu_average
- memory_usage
- memory_usage_percent
- disks_read_speed
- disks_write_speed
- disks_iops
- disk_usage
- network_speed
- network_transfer
- sensor_temperature
in: query
name: aggregate
type: string
x-enum-varnames:
- SystemInfoAggregateModeCPUAverage
- SystemInfoAggregateModeMemoryUsage
- SystemInfoAggregateModeMemoryUsagePercent
- SystemInfoAggregateModeDisksReadSpeed
- SystemInfoAggregateModeDisksWriteSpeed
- SystemInfoAggregateModeDisksIOPS
- SystemInfoAggregateModeDiskUsage
- SystemInfoAggregateModeNetworkSpeed
- SystemInfoAggregateModeNetworkTransfer
- SystemInfoAggregateModeSensorTemperature
- format: duration
in: query
name: interval
type: string
- enum:
- 5m
- 15m
- 1h
- 1d
- 1mo
in: query
name: period
type: string
x-enum-varnames:
- MetricsPeriod5m
- MetricsPeriod15m
- MetricsPeriod1h
- MetricsPeriod1d
- MetricsPeriod1mo
produces:
- application/json
responses:
"200":
description: period specified, aggregated system info by agent name
schema:
additionalProperties:
$ref: '#/definitions/SystemInfoAggregate'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/ErrorResponse'
summary: Get system info
tags:
- metrics
- websocket
x-id: all_system_info
/metrics/system_info:
get:
description: Get system info
@@ -2531,9 +2123,6 @@ paths:
- in: query
name: agentAddr
type: string
- in: query
name: agentName
type: string
- enum:
- cpu_average
- memory_usage
@@ -2589,6 +2178,10 @@ paths:
description: Forbidden
schema:
$ref: '#/definitions/ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/ErrorResponse'
"500":
description: Internal Server Error
schema:

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/route/routes"
)
@@ -19,24 +18,5 @@ import (
// @Failure 403 {object} apitypes.ErrorResponse
// @Router /homepage/categories [get]
func Categories(c *gin.Context) {
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
c.JSON(http.StatusOK, routes.HomepageCategories())
}

View File

@@ -1,23 +1,16 @@
package homepageapi
import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"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/route/routes"
)
type HomepageItemsRequest struct {
SearchQuery string `form:"search" validate:"omitempty"`
Category string `form:"category" validate:"omitempty"`
Provider string `form:"provider" validate:"omitempty"`
Category string `form:"category" validate:"omitempty"`
Provider string `form:"provider" validate:"omitempty"`
} // @name HomepageItemsRequest
// @x-id "items"
@@ -27,7 +20,6 @@ type HomepageItemsRequest struct {
// @Tags homepage
// @Accept json
// @Produce json
// @Param search query string false "Search query"
// @Param category query string false "Category filter"
// @Param provider query string false "Provider filter"
// @Success 200 {object} homepage.Homepage
@@ -50,75 +42,5 @@ func Items(c *gin.Context) {
hostname = host
}
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()
}
// 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
c.JSON(http.StatusOK, routes.HomepageItems(proto, hostname, request.Category, request.Provider))
}

View File

@@ -1,6 +1,7 @@
package homepageapi
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
@@ -14,22 +15,16 @@ 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"
@@ -51,7 +46,7 @@ func SetItem(c *gin.Context) {
return
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItem(params.Which, params.Value)
overrides.OverrideItem(params.Which, &params.Value)
c.JSON(http.StatusOK, apitypes.Success("success"))
}
@@ -70,8 +65,15 @@ func SetItem(c *gin.Context) {
func SetItemsBatch(c *gin.Context) {
var params HomepageOverrideItemsBatchParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
}
overrides := homepage.GetOverrideConfig()
overrides.OverrideItems(params.Value)
@@ -93,107 +95,22 @@ func SetItemsBatch(c *gin.Context) {
func SetItemVisible(c *gin.Context) {
var params HomepageOverrideItemVisibleParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
}
overrides := homepage.GetOverrideConfig()
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(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
if params.Value {
overrides.UnhideItems(params.Which)
} else {
overrides.HideItems(params.Which)
}
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(&params); 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"
// @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(&params); 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"
// @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(&params); 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"))
}
@@ -212,8 +129,15 @@ func SetItemFavSortOrder(c *gin.Context) {
func SetCategoryOrder(c *gin.Context) {
var params HomepageOverrideCategoryOrderParams
if err := c.ShouldBindJSON(&params); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
data, derr := c.GetRawData()
if derr != nil {
c.Error(apitypes.InternalServerError(derr, "failed to get raw data"))
return
}
if uerr := json.Unmarshal(data, &params); uerr != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", uerr))
return
}
}
overrides := homepage.GetOverrideConfig()
overrides.SetCategoryOrder(params.Which, params.Value)

View File

@@ -1,268 +0,0 @@
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
}
gperr.LogWarn("failed to get some system info", err)
}
// 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)
}

View File

@@ -1,8 +1,6 @@
package metrics
import (
"io"
"maps"
"net/http"
"github.com/gin-gonic/gin"
@@ -17,12 +15,11 @@ import (
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.AggregatedJSON] // @name SystemInfoAggregate
type SystemInfoAggregate period.ResponseType[systeminfo.Aggregated] // @name SystemInfoAggregate
// @x-id "system_info"
// @BasePath /api/v1
@@ -35,46 +32,45 @@ type SystemInfoAggregate period.ResponseType[systeminfo.AggregatedJSON] // @name
// @Success 200 {object} SystemInfoAggregate "period specified"
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /metrics/system_info [get]
func SystemInfo(c *gin.Context) {
query := c.Request.URL.Query()
agentAddr := query.Get("agent_addr")
agentName := query.Get("agent_name")
query.Del("agent_addr")
query.Del("agent_name")
if agentAddr == "" && agentName == "" {
if agentAddr == "" {
systeminfo.Poller.ServeHTTP(c)
return
}
agent, ok := agentPkg.GetAgent(agentAddr)
if !ok {
agent, ok = agentPkg.GetAgentByName(agentName)
}
if !ok {
c.JSON(http.StatusNotFound, apitypes.Error("agent_addr or agent_name not found"))
c.JSON(http.StatusNotFound, apitypes.Error("agent_addr not found"))
return
}
isWS := httpheaders.IsWebsocket(c.Request.Header)
if !isWS {
resp, err := agent.Forward(c.Request, agentPkg.EndpointSystemInfo)
respData, status, err := agent.Forward(c.Request, agentPkg.EndpointSystemInfo)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to forward request to agent"))
return
}
maps.Copy(c.Writer.Header(), resp.Header)
c.Status(resp.StatusCode)
io.Copy(c.Writer, resp.Body)
if status != http.StatusOK {
c.JSON(status, apitypes.Error(string(respData)))
return
}
c.JSON(status, respData)
} else {
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(agentPkg.AgentURL), agent.Transport())
r, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), c.Request.Body)
header := c.Request.Header.Clone()
r, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, agentPkg.EndpointSystemInfo+"?"+query.Encode(), nil)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to create request"))
return
}
r.Header = c.Request.Header
r.Header = header
rp.ServeHTTP(c.Writer, r)
}
}

View File

@@ -5,7 +5,6 @@ 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"
)
@@ -36,14 +35,7 @@ func Route(c *gin.Context) {
route, ok := routes.Get(request.Which)
if ok {
c.JSON(http.StatusOK, route)
return
} else {
c.JSON(http.StatusNotFound, nil)
}
// 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)
}

View File

@@ -130,7 +130,7 @@ func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.R
log.Err(err).Msg("failed to sign session token")
return
}
SetTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthSessionToken), signed, common.APIJWTTokenTTL)
}
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {

View File

@@ -3,6 +3,7 @@ package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
@@ -39,12 +40,27 @@ type (
var _ Provider = (*OIDCProvider)(nil)
// Cookie names for OIDC authentication
const (
CookieOauthState = "godoxy_oidc_state"
CookieOauthToken = "godoxy_oauth_token" //nolint:gosec
CookieOauthSessionToken = "godoxy_session_token" //nolint:gosec
)
// getAppScopedCookieName returns a cookie name scoped to the specific application
// to prevent conflicts between different OIDC clients
func (auth *OIDCProvider) getAppScopedCookieName(baseName string) string {
// Use the client ID to scope the cookie name
// This prevents conflicts when multiple apps use different client IDs
if auth.oauthConfig.ClientID != "" {
// Create a hash of the client ID to keep cookie names short
hash := sha256.Sum256([]byte(auth.oauthConfig.ClientID))
clientHash := base64.URLEncoding.EncodeToString(hash[:])[:8]
return fmt.Sprintf("%s_%s", baseName, clientHash)
}
return baseName
}
const (
OIDCAuthInitPath = "/"
OIDCPostAuthPath = "/auth/callback"
@@ -117,6 +133,37 @@ func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
)
}
// NewOIDCProviderWithCustomClient creates a new OIDCProvider with custom client credentials
// based on an existing provider (for issuer discovery)
func NewOIDCProviderWithCustomClient(baseProvider *OIDCProvider, clientID, clientSecret string) (*OIDCProvider, error) {
if clientID == "" || clientSecret == "" {
return nil, errors.New("client ID and client secret are required")
}
// Create a new OIDC verifier with the custom client ID
oidcVerifier := baseProvider.oidcProvider.Verifier(&oidc.Config{
ClientID: clientID,
})
// Create new OAuth config with custom credentials
oauthConfig := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: "",
Endpoint: baseProvider.oauthConfig.Endpoint,
Scopes: baseProvider.oauthConfig.Scopes,
}
return &OIDCProvider{
oauthConfig: oauthConfig,
oidcProvider: baseProvider.oidcProvider,
oidcVerifier: oidcVerifier,
endSessionURL: baseProvider.endSessionURL,
allowedUsers: baseProvider.allowedUsers,
allowedGroups: baseProvider.allowedGroups,
}, nil
}
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
auth.allowedUsers = users
}
@@ -125,6 +172,10 @@ func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
auth.allowedGroups = groups
}
func (auth *OIDCProvider) SetScopes(scopes []string) {
auth.oauthConfig.Scopes = scopes
}
// optRedirectPostAuth returns an oauth2 option that sets the "redirect_uri"
// parameter of the authorization URL to the post auth path of the current
// request host.
@@ -169,7 +220,7 @@ var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
// check for session token
sessionToken, err := r.Cookie(CookieOauthSessionToken)
sessionToken, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthSessionToken))
if err == nil { // session token exists
result, err := auth.TryRefreshToken(r.Context(), sessionToken.Value)
// redirect back to where they requested
@@ -193,7 +244,7 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
}
state := generateState()
SetTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthState), state, 300*time.Second)
// redirect user to Idp
url := auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r))
if IsFrontend(r) {
@@ -209,7 +260,8 @@ func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
if err := idToken.Claims(&claim); err != nil {
return nil, fmt.Errorf("failed to parse claims: %w", err)
}
if claim.Username == "" {
// Username is optional if groups are present
if claim.Username == "" && len(claim.Groups) == 0 {
return nil, errors.New("missing username in ID token")
}
return &claim, nil
@@ -228,7 +280,7 @@ func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
}
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
tokenCookie, err := r.Cookie(CookieOauthToken)
tokenCookie, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthToken))
if err != nil {
return ErrMissingOAuthToken
}
@@ -257,7 +309,7 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
}
// verify state
state, err := r.Cookie(CookieOauthState)
state, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthState))
if err != nil {
http.Error(w, "missing state cookie", http.StatusBadRequest)
return
@@ -297,8 +349,8 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
}
func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
oauthToken, _ := r.Cookie(CookieOauthToken)
sessionToken, _ := r.Cookie(CookieOauthSessionToken)
oauthToken, _ := r.Cookie(auth.getAppScopedCookieName(CookieOauthToken))
sessionToken, _ := r.Cookie(auth.getAppScopedCookieName(CookieOauthSessionToken))
auth.clearCookie(w, r)
if sessionToken != nil {
@@ -325,17 +377,17 @@ func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request)
}
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
SetTokenCookie(w, r, CookieOauthToken, jwt, ttl)
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthToken), jwt, ttl)
}
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
ClearTokenCookie(w, r, CookieOauthToken)
ClearTokenCookie(w, r, CookieOauthSessionToken)
ClearTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthToken))
ClearTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthSessionToken))
}
// handleTestCallback handles OIDC callback in test environment.
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
state, err := r.Cookie(CookieOauthState)
state, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthState))
if err != nil {
http.Error(w, "missing state cookie", http.StatusBadRequest)
return
@@ -347,7 +399,7 @@ func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Requ
}
// Create test JWT token
SetTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthToken), "test", time.Hour)
http.Redirect(w, r, "/", http.StatusFound)
}

View File

@@ -426,6 +426,9 @@ func TestCheckToken(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Create the Auth Provider.
auth := &OIDCProvider{
oauthConfig: &oauth2.Config{
ClientID: clientID,
},
oidcVerifier: provider.verifier,
allowedUsers: tc.allowedUsers,
allowedGroups: tc.allowedGroups,
@@ -435,7 +438,7 @@ func TestCheckToken(t *testing.T) {
// Craft a test HTTP request that includes the token as a cookie.
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{
Name: CookieOauthToken,
Name: auth.getAppScopedCookieName(CookieOauthToken),
Value: signedToken,
})

View File

@@ -125,7 +125,7 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
return
}
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
http.Redirect(w, r, "/", http.StatusFound)
w.WriteHeader(http.StatusOK)
}
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -15,24 +15,23 @@ func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PE
return 0, gperr.New("agent already exists")
}
agentCfg := agent.AgentConfig{
Addr: host,
}
var agentCfg agent.AgentConfig
agentCfg.Addr = host
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 _, loaded := cfg.providers.LoadOrStore(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
if err := cfg.errIfExists(provider); err != nil {
agent.RemoveAgent(&agentCfg)
return 0, err
}
err = provider.LoadRoutes()
if err != nil {
agent.RemoveAgent(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
}
agent.AddAgent(&agentCfg)
return provider.NumRoutes(), nil
}

View File

@@ -4,12 +4,12 @@ import (
"context"
"errors"
"os"
"regexp"
"strconv"
"strings"
"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"
@@ -26,6 +26,7 @@ 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 +34,7 @@ import (
type Config struct {
value *config.Config
providers *xsync.Map[string, *proxy.Provider]
providers F.Map[string, *proxy.Provider]
autocertProvider *autocert.Provider
entrypoint *entrypoint.Entrypoint
@@ -59,7 +60,7 @@ var Validate = config.Validate
func newConfig() *Config {
return &Config{
value: config.DefaultConfig(),
providers: xsync.NewMap[string, *proxy.Provider](),
providers: F.NewMapOf[string, *proxy.Provider](),
entrypoint: entrypoint.NewEntrypoint(),
task: task.RootTask("config", false),
}
@@ -174,19 +175,12 @@ func (cfg *Config) StartAutoCert() {
}
func (cfg *Config) StartProxyProviders() {
var wg sync.WaitGroup
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()))
}
errs := cfg.providers.CollectErrors(
func(_ string, p *proxy.Provider) error {
return p.Start(cfg.task)
})
}
wg.Wait()
if err := errs.Error(); err != nil {
if err := gperr.Join(errs...); err != nil {
gperr.LogError("route provider errors", err)
}
}
@@ -222,10 +216,23 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
}
}
var envRegex = regexp.MustCompile(`\$\{([^}]+)\}`) // e.g. ${CLOUDFLARE_API_KEY}
var readFile = os.ReadFile
func (cfg *Config) readConfigFile() ([]byte, error) {
data, err := readFile(common.ConfigPath)
if err != nil {
return nil, err
}
return envRegex.ReplaceAllFunc(data, func(match []byte) []byte {
return strconv.AppendQuote(nil, os.Getenv(string(match[2:len(match)-1])))
}), nil
}
func (cfg *Config) load() gperr.Error {
const errMsg = "config load error"
data, err := os.ReadFile(common.ConfigPath)
data, err := cfg.readConfigFile()
if err != nil {
if os.IsNotExist(err) {
log.Warn().Msg("config file not found, using default config")
@@ -322,87 +329,72 @@ 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.NewBuilderWithConcurrency("route provider errors")
errs := gperr.NewBuilder("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 {
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
})
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 {
providersProducer.Go(func() {
p, err := proxy.NewFileProvider(filename)
if err != nil {
errs.Add(gperr.PrependSubject(filename, err))
} else {
providersCh <- p
}
})
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 {
providersProducer.Go(func() {
providersCh <- proxy.NewDockerProvider(name, dockerHost)
})
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.Wait()
close(providersCh)
providersConsumer.Wait()
lenLongestName := 0
for k := range cfg.providers.Range {
cfg.providers.RangeAll(func(k string, _ *proxy.Provider) {
if len(k) > lenLongestName {
lenLongestName = len(k)
}
}
})
results.EnableConcurrency()
// 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()
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())
})
log.Info().Msg(results.String())
return errs.Error()
}

View File

@@ -0,0 +1,38 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestConfigEnvSubstitution(t *testing.T) {
os.Setenv("CLOUDFLARE_AUTH_TOKEN", "test")
readFile = func(_ string) ([]byte, error) {
return []byte(`
---
autocert:
email: "test@test.com"
domains:
- "*.test.com"
provider: cloudflare
options:
auth_token: ${CLOUDFLARE_AUTH_TOKEN}
`), nil
}
var cfg Config
out, err := cfg.readConfigFile()
require.NoError(t, err)
require.Equal(t, `
---
autocert:
email: "test@test.com"
domains:
- "*.test.com"
provider: cloudflare
options:
auth_token: "test"
`, string(out))
}

View File

@@ -25,15 +25,6 @@ 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

View File

@@ -15,7 +15,6 @@ 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 (
@@ -52,7 +51,6 @@ 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)
AutoCertProvider() *autocert.Provider

View File

@@ -31,12 +31,7 @@ blacklists = [
"cloudxns",
"dnspod",
"mythicbeasts",
"yandexcloud",
# dependencies issue
"namesilo",
"binarylane",
"edgeone",
"baiducloud",
"yandexcloud"
]
for item in data:

View File

@@ -1,6 +1,6 @@
module github.com/yusing/go-proxy/internal/dnsproviders
go 1.25.0
go 1.25.1
replace github.com/yusing/go-proxy => ../..
@@ -8,7 +8,7 @@ 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.2
github.com/yusing/go-proxy v0.17.1
)
require (
@@ -31,28 +31,28 @@ require (
github.com/alibabacloud-go/tea v1.3.11 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.38.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.4 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.8 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
github.com/baidubce/bce-sdk-go v0.9.241 // indirect
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/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/exoscale/egoscale/v3 v3.1.26 // indirect
github.com/exoscale/egoscale/v3 v3.1.25 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
@@ -111,9 +111,10 @@ require (
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1 // indirect
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
@@ -143,10 +144,10 @@ require (
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/pflag v1.0.7 // indirect
github.com/spf13/viper v1.20.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
@@ -157,10 +158,10 @@ require (
github.com/yusing/go-proxy/internal/utils v0.0.0 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/crypto v0.41.0 // indirect

View File

@@ -715,40 +715,40 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
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=
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
github.com/aws/aws-sdk-go-v2 v1.38.2 h1:QUkLO1aTW0yqW95pVzZS0LGFanL71hJ0a49w4TJLMyM=
github.com/aws/aws-sdk-go-v2 v1.38.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.4 h1:aY2IstXOfjdLtr1lDvxFBk5DpBnHgS5GS3jgR/0BmPw=
github.com/aws/aws-sdk-go-v2/config v1.31.4/go.mod h1:1IAykiegrTp6n+CbZoCpW6kks1I74fEDgl2BPQSkLSU=
github.com/aws/aws-sdk-go-v2/credentials v1.18.8 h1:0FfdP0I9gs/f1rwtEdkcEdsclTEkPB8o6zWUG2Z8+IM=
github.com/aws/aws-sdk-go-v2/credentials v1.18.8/go.mod h1:9UReQ1UmGooX93JKzHyr7PRF3F+p3r+PmRwR7+qHJYA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5 h1:ul7hICbZ5Z/Pp9VnLVGUVe7rqYLXCyIiPU7hQ0sRkow=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.5/go.mod h1:5cIWJ0N6Gjj+72Q6l46DeaNtcxXHV42w/Uq3fIfeUl4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5 h1:d45S2DqHZOkHu0uLUW92VdBoT5v0hh3EyR+DzMEh3ag=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.5/go.mod h1:G6e/dR2c2huh6JmIo9SXysjuLuDDGWMeYGibfW2ZrXg=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5 h1:ENhnQOV3SxWHplOqNN1f+uuCNf9n4Y/PKpl6b1WRP0Q=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.5/go.mod h1:csQLMI+odbC0/J+UecSTztG70Dc4aTCOu4GyPNDNpVo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2 h1:bbcKDYr5ivT4ghbcNmKPmLpH/42dn0CqZgE6c7SziQU=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.2/go.mod h1:yYrzhBVvgD0aekhyjDij7gw1JVFHetfPUfxyyr0X3e8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0 h1:P7dm9TlRs6EEiXhwMn8DYQ92M/443GAzDk2q6GaPDNQ=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.0/go.mod h1:j4q6vBiAJvH9oxFyFtZoV739zxVMsSn26XNFvFlorfU=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5 h1:Cx1M/UUgYu9UCQnIMKaOhkVaFvLy1HneD6T4sS/DlKg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.5/go.mod h1:fTRNLgrTvPpEzGqc9QkeO4hu/3ng+mdtUbL8shUwXz4=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1 h1:szIozoXngSj0ibL/GNJbb5e2Y8WUmSqfk0BIiviO2qo=
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.1/go.mod h1:HBXDArKSFmUoPiHIatSFZP7RCOuSO86D1skZZPP0eLI=
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1 h1:t6CAhkQ5yVxPeDeAExUSDKRexiqIrOhUcQ/L3wXFnh8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.57.1/go.mod h1:zvtb01R4yNazMQQlaDybZFGDJH13+zSp1psLzG0CUhQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3 h1:z6lajFT/qGlLRB/I8V5CCklqSuWZKUkdwRAn9leIkiQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.28.3/go.mod h1:BnyjuIX0l+KXJVl2o9Ki3Zf0M4pA2hQYopFCRUj9ADU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1 h1:8yI3jK5JZ310S8RpgdZdzwvlvBu3QbG8DP7Be/xJ6yo=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.1/go.mod h1:HPzXfFgrLd02lYpcFYdDz5xZs94LOb+lWlvbAGaeMsk=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 h1:3kWmIg5iiWPMBJyq/I55Fki5fyfoMtrn/SkUIpxPwHQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1/go.mod h1:yi0b3Qez6YamRVJ+Rbi19IgvjfjPODgVRhkWA6RTMUM=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aziontech/azionapi-go-sdk v0.142.0 h1:1NOHXlC0/7VgbfbTIGVpsYn1THCugM4/SCOXVdUHQ+A=
github.com/aziontech/azionapi-go-sdk v0.142.0/go.mod h1:cA5DY/VP4X5Eu11LpQNzNn83ziKjja7QVMIl4J45feA=
github.com/baidubce/bce-sdk-go v0.9.241 h1:8eRf+o1MCEh96MtP6eZF1ypFigtWNija02y2OZ+JK5k=
github.com/baidubce/bce-sdk-go v0.9.241/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -839,15 +839,15 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/exoscale/egoscale/v3 v3.1.26 h1:bXXT0zVLbE4QFm6tmt0bg6ZPk9pQgUA3Z8SJrctQ7b0=
github.com/exoscale/egoscale/v3 v3.1.26/go.mod h1:0iY8OxgHJCS5TKqDNhwOW95JBKCnBZl3YGU4Yt+NqkU=
github.com/exoscale/egoscale/v3 v3.1.25 h1:Xy4LdmElaUXdf72vCt8gv9DCivKUlmW5Ar5ATInwWU8=
github.com/exoscale/egoscale/v3 v3.1.25/go.mod h1:TJCI0OG3Lz2rnleRB0xwiOFg82uNCCytRqw7TxPoIvc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -1330,12 +1330,14 @@ github.com/nrdcg/goinwx v0.11.0 h1:GER0SE3POub7rxARt3Y3jRy1OON1hwF1LRxHz5xsFBw=
github.com/nrdcg/goinwx v0.11.0/go.mod h1:0BXSC0FxVtU4aTjX0Zw3x0DK32tjugLzeNIAGtwXvPQ=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.2 h1:FQvfUIV9JpCXHJ0fqZ+r/pAqSMh8AkQ94Ooz2WO9rwg=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.2/go.mod h1:sOWH1Rqtipy3kyrIER0JLge8O7n8pIr7G8UVEs6xaDY=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2 h1:OgO02pgzn6JcrDSlHENAkQP4coftx6cEM6WnUOZlprk=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.2/go.mod h1:CxuFDFnoi+G8wtUODauGxaRw/dHZ5IlUtz8Uwsw36Kk=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1 h1:Q7EzGKkBg6Zw4HuqTNbK13ccuJK08eyStO0NIkCYyoU=
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.99.1/go.mod h1:Sls/ilbR5DFjMQEhFsO3gseG0xgJERRqqli/85xgkDo=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1 h1:wt9okvG0nJZJbLQluvjqpsyVSMipdg4tSt8I/5WRwaA=
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.99.1/go.mod h1:cN/nLDjwvq/+K29i2cYRsk8frENw09lwXjpEoSpiM14=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -1522,8 +1524,8 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
@@ -1558,8 +1560,8 @@ github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1208/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22 h1:1unTmvNXynDN0mOZSWh9tL5Wp9Rb5paMGwFvua+HHoI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.22/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18 h1:Pkyu9oYcYkp0GFKWa/Jyf8IeS6AADgUoUoRzP2eDq2Q=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.18/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@@ -1624,18 +1626,18 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
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=
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=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=

View File

@@ -11,8 +11,8 @@ import (
"github.com/go-acme/lego/v4/providers/dns/auroradns"
"github.com/go-acme/lego/v4/providers/dns/autodns"
"github.com/go-acme/lego/v4/providers/dns/axelname"
"github.com/go-acme/lego/v4/providers/dns/azion"
"github.com/go-acme/lego/v4/providers/dns/azuredns"
"github.com/go-acme/lego/v4/providers/dns/baiducloud"
"github.com/go-acme/lego/v4/providers/dns/bindman"
"github.com/go-acme/lego/v4/providers/dns/bluecat"
"github.com/go-acme/lego/v4/providers/dns/bookmyname"
@@ -24,7 +24,6 @@ import (
"github.com/go-acme/lego/v4/providers/dns/cloudns"
"github.com/go-acme/lego/v4/providers/dns/cloudru"
"github.com/go-acme/lego/v4/providers/dns/conoha"
"github.com/go-acme/lego/v4/providers/dns/conohav3"
"github.com/go-acme/lego/v4/providers/dns/constellix"
"github.com/go-acme/lego/v4/providers/dns/corenetworks"
"github.com/go-acme/lego/v4/providers/dns/cpanel"
@@ -41,7 +40,6 @@ import (
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
"github.com/go-acme/lego/v4/providers/dns/duckdns"
"github.com/go-acme/lego/v4/providers/dns/dyn"
"github.com/go-acme/lego/v4/providers/dns/dyndnsfree"
"github.com/go-acme/lego/v4/providers/dns/dynu"
"github.com/go-acme/lego/v4/providers/dns/easydns"
"github.com/go-acme/lego/v4/providers/dns/edgedns"
@@ -94,11 +92,11 @@ import (
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
"github.com/go-acme/lego/v4/providers/dns/namecheap"
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
"github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
"github.com/go-acme/lego/v4/providers/dns/netcup"
"github.com/go-acme/lego/v4/providers/dns/netlify"
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
"github.com/go-acme/lego/v4/providers/dns/nicru"
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
"github.com/go-acme/lego/v4/providers/dns/njalla"
"github.com/go-acme/lego/v4/providers/dns/nodion"
@@ -149,7 +147,6 @@ import (
"github.com/go-acme/lego/v4/providers/dns/westcn"
"github.com/go-acme/lego/v4/providers/dns/yandex"
"github.com/go-acme/lego/v4/providers/dns/yandex360"
"github.com/go-acme/lego/v4/providers/dns/zoneedit"
"github.com/go-acme/lego/v4/providers/dns/zoneee"
"github.com/go-acme/lego/v4/providers/dns/zonomi"
"github.com/yusing/go-proxy/internal/autocert"
@@ -171,8 +168,8 @@ func InitProviders() {
autocert.Providers["auroradns"] = autocert.DNSProvider(auroradns.NewDefaultConfig, auroradns.NewDNSProviderConfig)
autocert.Providers["autodns"] = autocert.DNSProvider(autodns.NewDefaultConfig, autodns.NewDNSProviderConfig)
autocert.Providers["axelname"] = autocert.DNSProvider(axelname.NewDefaultConfig, axelname.NewDNSProviderConfig)
autocert.Providers["azion"] = autocert.DNSProvider(azion.NewDefaultConfig, azion.NewDNSProviderConfig)
autocert.Providers["azuredns"] = autocert.DNSProvider(azuredns.NewDefaultConfig, azuredns.NewDNSProviderConfig)
autocert.Providers["baiducloud"] = autocert.DNSProvider(baiducloud.NewDefaultConfig, baiducloud.NewDNSProviderConfig)
autocert.Providers["bindman"] = autocert.DNSProvider(bindman.NewDefaultConfig, bindman.NewDNSProviderConfig)
autocert.Providers["bluecat"] = autocert.DNSProvider(bluecat.NewDefaultConfig, bluecat.NewDNSProviderConfig)
autocert.Providers["bookmyname"] = autocert.DNSProvider(bookmyname.NewDefaultConfig, bookmyname.NewDNSProviderConfig)
@@ -184,7 +181,6 @@ func InitProviders() {
autocert.Providers["cloudns"] = autocert.DNSProvider(cloudns.NewDefaultConfig, cloudns.NewDNSProviderConfig)
autocert.Providers["cloudru"] = autocert.DNSProvider(cloudru.NewDefaultConfig, cloudru.NewDNSProviderConfig)
autocert.Providers["conoha"] = autocert.DNSProvider(conoha.NewDefaultConfig, conoha.NewDNSProviderConfig)
autocert.Providers["conohav3"] = autocert.DNSProvider(conohav3.NewDefaultConfig, conohav3.NewDNSProviderConfig)
autocert.Providers["constellix"] = autocert.DNSProvider(constellix.NewDefaultConfig, constellix.NewDNSProviderConfig)
autocert.Providers["corenetworks"] = autocert.DNSProvider(corenetworks.NewDefaultConfig, corenetworks.NewDNSProviderConfig)
autocert.Providers["cpanel"] = autocert.DNSProvider(cpanel.NewDefaultConfig, cpanel.NewDNSProviderConfig)
@@ -201,7 +197,6 @@ func InitProviders() {
autocert.Providers["dreamhost"] = autocert.DNSProvider(dreamhost.NewDefaultConfig, dreamhost.NewDNSProviderConfig)
autocert.Providers["duckdns"] = autocert.DNSProvider(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig)
autocert.Providers["dyn"] = autocert.DNSProvider(dyn.NewDefaultConfig, dyn.NewDNSProviderConfig)
autocert.Providers["dyndnsfree"] = autocert.DNSProvider(dyndnsfree.NewDefaultConfig, dyndnsfree.NewDNSProviderConfig)
autocert.Providers["dynu"] = autocert.DNSProvider(dynu.NewDefaultConfig, dynu.NewDNSProviderConfig)
autocert.Providers["easydns"] = autocert.DNSProvider(easydns.NewDefaultConfig, easydns.NewDNSProviderConfig)
autocert.Providers["edgedns"] = autocert.DNSProvider(edgedns.NewDefaultConfig, edgedns.NewDNSProviderConfig)
@@ -254,11 +249,11 @@ func InitProviders() {
autocert.Providers["mydnsjp"] = autocert.DNSProvider(mydnsjp.NewDefaultConfig, mydnsjp.NewDNSProviderConfig)
autocert.Providers["namecheap"] = autocert.DNSProvider(namecheap.NewDefaultConfig, namecheap.NewDNSProviderConfig)
autocert.Providers["namedotcom"] = autocert.DNSProvider(namedotcom.NewDefaultConfig, namedotcom.NewDNSProviderConfig)
autocert.Providers["namesilo"] = autocert.DNSProvider(namesilo.NewDefaultConfig, namesilo.NewDNSProviderConfig)
autocert.Providers["nearlyfreespeech"] = autocert.DNSProvider(nearlyfreespeech.NewDefaultConfig, nearlyfreespeech.NewDNSProviderConfig)
autocert.Providers["netcup"] = autocert.DNSProvider(netcup.NewDefaultConfig, netcup.NewDNSProviderConfig)
autocert.Providers["netlify"] = autocert.DNSProvider(netlify.NewDefaultConfig, netlify.NewDNSProviderConfig)
autocert.Providers["nicmanager"] = autocert.DNSProvider(nicmanager.NewDefaultConfig, nicmanager.NewDNSProviderConfig)
autocert.Providers["nicru"] = autocert.DNSProvider(nicru.NewDefaultConfig, nicru.NewDNSProviderConfig)
autocert.Providers["nifcloud"] = autocert.DNSProvider(nifcloud.NewDefaultConfig, nifcloud.NewDNSProviderConfig)
autocert.Providers["njalla"] = autocert.DNSProvider(njalla.NewDefaultConfig, njalla.NewDNSProviderConfig)
autocert.Providers["nodion"] = autocert.DNSProvider(nodion.NewDefaultConfig, nodion.NewDNSProviderConfig)
@@ -309,7 +304,6 @@ func InitProviders() {
autocert.Providers["westcn"] = autocert.DNSProvider(westcn.NewDefaultConfig, westcn.NewDNSProviderConfig)
autocert.Providers["yandex"] = autocert.DNSProvider(yandex.NewDefaultConfig, yandex.NewDNSProviderConfig)
autocert.Providers["yandex360"] = autocert.DNSProvider(yandex360.NewDefaultConfig, yandex360.NewDNSProviderConfig)
autocert.Providers["zoneedit"] = autocert.DNSProvider(zoneedit.NewDefaultConfig, zoneedit.NewDNSProviderConfig)
autocert.Providers["zoneee"] = autocert.DNSProvider(zoneee.NewDefaultConfig, zoneee.NewDNSProviderConfig)
autocert.Providers["zonomi"] = autocert.DNSProvider(zonomi.NewDefaultConfig, zonomi.NewDNSProviderConfig)
}

View File

@@ -66,7 +66,6 @@ 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) {

View File

@@ -1,19 +0,0 @@
package docker
import (
"github.com/puzpuzpuz/xsync/v4"
)
var idDockerHostMap = xsync.NewMap[string, string]()
func GetDockerHostByContainerID(id string) (string, bool) {
return idDockerHostMap.Load(id)
}
func SetDockerHostByContainerID(id, host string) {
idDockerHostMap.Store(id, host)
}
func DeleteDockerHostByContainerID(id string) {
idDockerHostMap.Delete(id)
}

View File

@@ -3,59 +3,33 @@ package homepage
import (
"slices"
"github.com/yusing/ds/ordered"
"github.com/yusing/go-proxy/internal/homepage/widgets"
"github.com/yusing/go-proxy/internal/serialization"
)
type (
HomepageMap struct {
*ordered.Map[string, *Category]
} // @name HomepageItemsMap
Homepage []*Category // @name HomepageItems
Category struct {
Items []*Item `json:"items"`
Name string `json:"name"`
} // @name HomepageCategory
Homepage map[string]Category // @name HomepageItems
Category []*Item // @name HomepageCategory
ItemConfig struct {
Show bool `json:"show"`
Name string `json:"name"` // display name
Icon *IconURL `json:"icon" swaggertype:"string"`
Category string `json:"category" validate:"omitempty"`
Category string `json:"category"`
Description string `json:"description" aliases:"desc"`
URL string `json:"url,omitempty"`
Favorite bool `json:"favorite"`
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
} // @name HomepageItemConfig
Widget struct {
Label string `json:"label"`
Value string `json:"value"`
} // @name HomepageItemWidget
SortOrder int `json:"sort_order"`
}
Item struct {
ItemConfig
*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
Widgets []Widget `json:"widgets,omitempty"`
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
Alias string `json:"alias"`
Provider string `json:"provider"`
OriginURL string `json:"origin_url"`
} // @name HomepageItem
)
const (
CategoryAll = "All"
CategoryFavorites = "Favorites"
CategoryHidden = "Hidden"
CategoryOthers = "Others"
}
)
func init() {
@@ -66,85 +40,22 @@ func init() {
})
}
func NewHomepageMap(total int) *HomepageMap {
m := &HomepageMap{
Map: ordered.NewMap[string, *Category](ordered.WithCapacity(10)),
func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
return overrideConfigInstance.GetOverride(alias, cfg)
}
func (c Homepage) Add(item *Item) {
if c[item.Category] == nil {
c[item.Category] = make(Category, 0)
}
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 (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)
c[item.Category] = append(c[item.Category], item)
slices.SortStableFunc(c[item.Category], func(a, b *Item) int {
if a.SortOrder < b.SortOrder {
return -1
}
} else {
c.add(item, CategoryHidden)
}
}
func (c HomepageMap) add(item *Item, categoryName string) {
category := c.Get(categoryName)
if category == nil {
category = &Category{
Items: make([]*Item, 0),
Name: categoryName,
if a.SortOrder > b.SortOrder {
return 1
}
c.Set(categoryName, category)
}
category.Items = append(category.Items, item)
}
func (c *Category) Sort() {
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
})
}
return 0
})
}

View File

@@ -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,107 +30,7 @@ func TestOverrideItem(t *testing.T) {
},
}
overrides := GetOverrideConfig()
overrides.Initialize()
overrides.OverrideItem(a.Alias, 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)
got := a.GetOverride(a.Alias)
ExpectEqual(t, got, want)
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"
"sync"
"time"
@@ -24,20 +23,19 @@ type (
IconMap map[IconKey]*IconMeta
IconList []string
IconMeta struct {
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:"-"`
SVG, PNG, WebP bool
Light, Dark bool
DisplayName string
Tag string
}
IconMetaSearch struct {
Source IconSource `json:"Source"`
Ref string `json:"Ref"`
*IconMeta
rank int
SVG bool `json:"SVG"`
PNG bool `json:"PNG"`
WebP bool `json:"WebP"`
Light bool `json:"Light"`
Dark bool `json:"Dark"`
}
Cache struct {
Icons IconMap
@@ -94,8 +92,8 @@ func NewIconKey(source IconSource, reference string) IconKey {
}
func (k IconKey) SourceRef() (IconSource, string) {
source, ref, _ := strings.Cut(string(k), "/")
return IconSource(source), ref
parts := strings.Split(string(k), "/")
return IconSource(parts[0]), parts[1]
}
func InitIconListCache() {
@@ -152,54 +150,31 @@ func ListAvailableIcons() (*Cache, error) {
return iconsCache, nil
}
func SearchIcons(keyword string, limit int) []*IconMetaSearch {
func SearchIcons(keyword string, limit int) []IconMetaSearch {
if keyword == "" {
return []*IconMetaSearch{}
return make([]IconMetaSearch, 0)
}
if limit == 0 {
limit = 10
}
iconsCache.RLock()
defer iconsCache.RUnlock()
searchLimit := min(limit*5, 50)
results := make([]*IconMetaSearch, 0, searchLimit)
sortByRank := func(a, b *IconMetaSearch) int {
return a.rank - b.rank
}
var rank int
result := make([]IconMetaSearch, 0)
for k, icon := range iconsCache.Icons {
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 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,
})
}
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 {
if len(result) >= limit {
break
}
}
// Extract results and limit to the requested count
return results[:min(len(results), limit)]
return result
}
func HasIcon(icon *IconURL) bool {
@@ -384,8 +359,7 @@ func UpdateSelfhstIcons() error {
for _, item := range data {
var tag string
if item.Tags != "" {
tag, _, _ = strings.Cut(item.Tags, ",")
tag = strings.TrimSpace(tag)
tag = strutils.CommaSeperatedList(item.Tags)[0]
}
icon := &IconMeta{
DisplayName: item.Name,

View File

@@ -9,13 +9,10 @@ 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"`
AllSortOrder map[string]int `json:"all_sort_order"`
FavSortOrder map[string]int `json:"fav_sort_order"`
ItemVisibility map[string]bool `json:"item_visibility"`
ItemFavorite map[string]bool `json:"item_favorite"`
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"`
mu sync.RWMutex
}
@@ -26,94 +23,57 @@ 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.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(item Item) Item {
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig {
c.mu.RLock()
defer c.mu.RUnlock()
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 itemOverride, hasOverride := c.ItemOverrides[alias]; hasOverride {
itemOverride.URL = item.URL // NOTE: we don't want to override the URL
item = itemOverride
}
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 item.Category == "" {
item.Category = CategoryOthers
if show, ok := c.ItemVisibility[alias]; ok {
clone := *item
clone.Show = show
return &clone
}
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) {
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
}
}

View File

@@ -76,7 +76,7 @@ const (
errBurst = 5
)
var lineBufPool = synk.GetBytesPoolWithUniqueMemory()
var lineBufPool = synk.GetBytesPool()
func NewAccessLogger(parent task.Parent, cfg AnyConfig) (*AccessLogger, error) {
io, err := cfg.IO()

View File

@@ -66,7 +66,7 @@ type lineInfo struct {
Size int64 // Size of this line
}
var rotateBytePool = synk.GetBytesPoolWithUniqueMemory()
var rotateBytePool = synk.GetBytesPool()
// rotateLogFile rotates the log file based on the retention policy.
// It returns the result of the rotation and an error if any.

View File

@@ -1,13 +1,12 @@
package period
import (
"bytes"
"encoding/json"
"time"
)
type Entries[T any] struct {
entries [maxEntries]T
entries [maxEntries]*T
index int
count int
interval time.Duration
@@ -17,90 +16,38 @@ type Entries[T any] struct {
const maxEntries = 100
func newEntries[T any](duration time.Duration) *Entries[T] {
interval := max(duration/maxEntries, time.Second)
interval := duration / maxEntries
if interval < time.Second {
interval = 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 = timestamp
e.lastAdd = now
}
// 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 {
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
}
func (e *Entries[T]) Iter(yield func(entry T) bool) {
if e.count < maxEntries {
for _, entry := range e.entries[:e.count] {
if !yield(entry) {
return
}
}
return
}
for _, entry := range e.entries[e.index:] {
if !yield(entry) {
return
}
}
for _, entry := range e.entries[:e.index] {
if !yield(entry) {
return
}
}
}
func (e *Entries[T]) GetJSON() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, maxEntries*1024))
je := json.NewEncoder(buf)
buf.WriteByte('[')
for entry := range e.Iter {
if err := je.Encode(entry); err != nil {
return nil, err
}
buf.Truncate(buf.Len() - 1) // remove the \n just added by Encode
buf.WriteByte(',')
}
buf.Truncate(buf.Len() - 1) // remove the last comma
buf.WriteByte(']')
return buf.Bytes(), nil
}
func (e *Entries[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"entries": e.Get(),
@@ -110,7 +57,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 {
@@ -123,17 +70,10 @@ func (e *Entries[T]) UnmarshalJSON(data []byte) error {
if len(entries) > maxEntries {
entries = entries[:maxEntries]
}
// 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)
for _, info := range entries {
e.Add(now, info)
}
e.interval = v.Interval
return nil
}

View File

@@ -3,7 +3,7 @@ package period
import (
"errors"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
@@ -29,22 +29,25 @@ 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)
if interval < PollInterval {
interval = PollInterval
minInterval := 1 * time.Second
if interval == 0 {
interval = pollInterval
}
if interval < minInterval {
interval = minInterval
}
websocket.PeriodicWrite(c, interval, func() (any, error) {
return p.GetRespData(period, query)
return p.getRespData(c.Request)
})
} else {
data, err := p.GetRespData(period, query)
data, err := p.getRespData(c.Request)
if err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("bad request", err))
c.Error(apitypes.InternalServerError(err, "failed to get response data"))
return
}
if data == nil {
@@ -55,22 +58,13 @@ func (p *Poller[T, AggregateT]) ServeHTTP(c *gin.Context) {
}
}
// 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) {
func (p *Poller[T, AggregateT]) getRespData(r *http.Request) (any, error) {
query := r.URL.Query()
period := query.Get("period")
if period == "" {
return p.GetLastResult(), nil
}
rangeData, ok := p.Get(period)
rangeData, ok := p.Get(Filter(period))
if !ok {
return nil, errors.New("invalid period")
}

View File

@@ -1,48 +0,0 @@
package period
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestEntries_GetJSON_NotFull(t *testing.T) {
e := newEntries[int](time.Second)
now := time.Now().Add(e.interval)
e.Add(now, 1)
e.Add(now.Add(time.Second), 2)
e.Add(now.Add(2*time.Second), 3)
jsonBytes, err := e.GetJSON()
require.NoError(t, err)
expectedJSON := `[1,2,3]`
require.Equal(t, expectedJSON, string(jsonBytes))
}
func TestEntries_GetJSON_Full(t *testing.T) {
e := newEntries[int](time.Second)
now := time.Now().Add(e.interval)
const exceed = 50
for i := range maxEntries + exceed {
e.Add(now.Add(time.Duration(i)*e.interval), i)
}
jsonBytes, err := e.GetJSON()
require.NoError(t, err)
var expectedJSON bytes.Buffer
expectedJSON.WriteByte('[')
// 50 ... 99
for i := range maxEntries - exceed {
expectedJSON.WriteString(fmt.Sprintf("%d,", e.entries[maxEntries-exceed+i]))
}
// 0 ... 49
for i := range exceed {
expectedJSON.WriteString(fmt.Sprintf("%d,", e.entries[i]))
}
expectedJSON.Truncate(expectedJSON.Len() - 1) // remove the last comma
expectedJSON.WriteByte(']')
require.Equal(t, expectedJSON.String(), string(jsonBytes))
}

View File

@@ -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) {
func (p *Period[T]) Get(filter Filter) ([]*T, bool) {
p.mu.RLock()
defer p.mu.RUnlock()
period, ok := p.Entries[filter]
if !ok {
return nil, false
}
p.mu.RLock()
defer p.mu.RUnlock()
return period.Get(), true
}
@@ -60,26 +60,3 @@ 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)
}
}
}
}

View File

@@ -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,12 +73,7 @@ func (p *Poller[T, AggregateT]) load() error {
if err != nil {
return err
}
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
return json.Unmarshal(entries, &p.period)
}
func (p *Poller[T, AggregateT]) save() error {
@@ -127,7 +122,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 {
@@ -151,7 +146,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)
@@ -167,7 +162,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())
@@ -193,10 +188,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()
}

View File

@@ -1,13 +1,20 @@
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 := make([]byte, 0, 4096)
b := bufPool.Get()
defer bufPool.Put(b)
b = append(b, '{')
@@ -107,14 +114,15 @@ 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,
`{"name":"%s","temperature":%.2f,"high":%.2f,"critical":%.2f}`,
`"%s":{"name":"%s","temperature":%.2f,"high":%.2f,"critical":%.2f}`,
sensor.SensorKey,
sensor.SensorKey,
sensor.Temperature,
sensor.High,
@@ -122,7 +130,7 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
)
first = false
}
b = append(b, ']')
b = append(b, '}')
} else {
b = append(b, "null"...)
}
@@ -131,22 +139,34 @@ func (s *SystemInfo) MarshalJSON() ([]byte, error) {
return b, nil
}
func (result Aggregated) MarshalJSON() ([]byte, error) {
if len(result.Entries) == 0 {
return []byte("[]"), 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
}
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)
func (result Aggregated) MarshalJSON() ([]byte, error) {
buf := bufPool.Get()
defer bufPool.Put(buf)
buf = append(buf, '[')
i := 0
n := len(result.Entries)
for _, entry := range result.Entries {
n := len(result)
for _, entry := range result {
buf = append(buf, '{')
j := 0
m := len(entry)
@@ -158,12 +178,10 @@ func (result Aggregated) MarshalJSON() ([]byte, error) {
switch v := v.(type) {
case float64:
buf = strconv.AppendFloat(buf, v, 'f', 2, 64)
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)
case int64:
buf = strconv.AppendInt(buf, v, 10)
default:
panic(fmt.Sprintf("unexpected type: %T", v))
}

View File

@@ -24,11 +24,7 @@ import (
type (
Sensors []sensors.TemperatureStat // @name Sensors
Aggregated struct {
Entries []map[string]any
Mode SystemInfoAggregateMode
}
AggregatedJSON []map[string]any
Aggregated []map[string]any
)
type SystemInfo struct {
@@ -222,40 +218,37 @@ 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 := Aggregated{
Entries: make([]map[string]any, n),
Mode: SystemInfoAggregateMode(query.Get("aggregate")),
}
switch aggregated.Mode {
aggregated := make(Aggregated, 0, n)
switch SystemInfoAggregateMode(query.Get("aggregate")) {
case SystemInfoAggregateModeCPUAverage:
for i, entry := range entries {
for _, entry := range entries {
if entry.CPUAverage != nil {
aggregated.Entries[i] = map[string]any{
aggregated = append(aggregated, map[string]any{
"timestamp": entry.Timestamp,
"cpu_average": *entry.CPUAverage,
}
})
}
}
case SystemInfoAggregateModeMemoryUsage:
for i, entry := range entries {
for _, entry := range entries {
if entry.Memory != nil {
aggregated.Entries[i] = map[string]any{
aggregated = append(aggregated, map[string]any{
"timestamp": entry.Timestamp,
"memory_usage": entry.Memory.Used,
}
})
}
}
case SystemInfoAggregateModeMemoryUsagePercent:
for i, entry := range entries {
for _, entry := range entries {
if entry.Memory != nil {
aggregated.Entries[i] = map[string]any{
aggregated = append(aggregated, map[string]any{
"timestamp": entry.Timestamp,
"memory_usage_percent": entry.Memory.UsedPercent,
}
})
}
}
case SystemInfoAggregateModeDisksReadSpeed:
for i, entry := range entries {
for _, entry := range entries {
if entry.DisksIO == nil {
continue
}
@@ -264,10 +257,10 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
m[name] = usage.ReadSpeed
}
m["timestamp"] = entry.Timestamp
aggregated.Entries[i] = m
aggregated = append(aggregated, m)
}
case SystemInfoAggregateModeDisksWriteSpeed:
for i, entry := range entries {
for _, entry := range entries {
if entry.DisksIO == nil {
continue
}
@@ -276,10 +269,10 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
m[name] = usage.WriteSpeed
}
m["timestamp"] = entry.Timestamp
aggregated.Entries[i] = m
aggregated = append(aggregated, m)
}
case SystemInfoAggregateModeDisksIOPS:
for i, entry := range entries {
for _, entry := range entries {
if entry.DisksIO == nil {
continue
}
@@ -288,10 +281,10 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
m[name] = usage.Iops
}
m["timestamp"] = entry.Timestamp
aggregated.Entries[i] = m
aggregated = append(aggregated, m)
}
case SystemInfoAggregateModeDiskUsage:
for i, entry := range entries {
for _, entry := range entries {
if entry.Disks == nil {
continue
}
@@ -300,32 +293,32 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
m[name] = disk.Used
}
m["timestamp"] = entry.Timestamp
aggregated.Entries[i] = m
aggregated = append(aggregated, m)
}
case SystemInfoAggregateModeNetworkSpeed:
for i, entry := range entries {
for _, entry := range entries {
if entry.Network == nil {
continue
}
aggregated.Entries[i] = map[string]any{
aggregated = append(aggregated, map[string]any{
"timestamp": entry.Timestamp,
"upload": entry.Network.UploadSpeed,
"download": entry.Network.DownloadSpeed,
}
})
}
case SystemInfoAggregateModeNetworkTransfer:
for i, entry := range entries {
for _, entry := range entries {
if entry.Network == nil {
continue
}
aggregated.Entries[i] = map[string]any{
aggregated = append(aggregated, map[string]any{
"timestamp": entry.Timestamp,
"upload": entry.Network.BytesSent,
"download": entry.Network.BytesRecv,
}
})
}
case SystemInfoAggregateModeSensorTemperature:
for i, entry := range entries {
for _, entry := range entries {
if entry.Sensors == nil {
continue
}
@@ -334,12 +327,12 @@ func aggregate(entries []*SystemInfo, query url.Values) (total int, result Aggre
m[sensor.SensorKey] = sensor.Temperature
}
m["timestamp"] = entry.Timestamp
aggregated.Entries[i] = m
aggregated = append(aggregated, m)
}
default:
return -1, Aggregated{}
return -1, nil
}
return len(aggregated.Entries), aggregated
return len(aggregated), aggregated
}
func diff(x, y uint64) uint64 {

View File

@@ -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,20 +23,18 @@ type (
} // @name RouteStatusesByAlias
Status struct {
Status types.HealthStatus `json:"status" swaggertype:"string" enums:"healthy,unhealthy,unknown,napping,starting"`
Latency int32 `json:"latency"`
Latency int64 `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 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"`
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"`
} // @name RouteUptimeAggregate
Aggregated []RouteAggregate
)
@@ -66,9 +64,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: int32(status.Latency.Milliseconds()),
Latency: status.Latency.Milliseconds(),
Timestamp: entry.Timestamp,
})
}
@@ -83,12 +81,12 @@ func aggregateStatuses(entries []*StatusByAlias, query url.Values) (int, Aggrega
return len(statuses), statuses.aggregate(limit, offset)
}
func (rs RouteStatuses) calculateInfo(statuses []Status) (up float32, down float32, idle float32, _ float32) {
func (rs RouteStatuses) calculateInfo(statuses []*Status) (up float64, down float64, idle float64, _ float64) {
if len(statuses) == 0 {
return 0, 0, 0, 0
}
total := float32(0)
latency := float32(0)
total := float64(0)
latency := float64(0)
for _, status := range statuses {
// ignoring unknown; treating napping and starting as downtime
if status.Status == types.StatusUnknown {
@@ -103,7 +101,7 @@ func (rs RouteStatuses) calculateInfo(statuses []Status) (up float32, down float
down++
}
total++
latency += float32(status.Latency)
latency += float64(status.Latency)
}
if total == 0 {
return 0, 0, 0, 0
@@ -123,41 +121,34 @@ func (rs RouteStatuses) aggregate(limit int, offset int) Aggregated {
sortedAliases[i] = alias
i++
}
slices.Sort(sortedAliases)
// 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)
})
sortedAliases = sortedAliases[beg:end]
result := make(Aggregated, len(sortedAliases))
for i, alias := range sortedAliases {
statuses := rs[alias]
up, down, idle, latency := rs.calculateInfo(statuses)
displayName := alias
r, ok := routes.Get(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.ContainerInfo() != nil,
Alias: alias,
Uptime: up,
Downtime: down,
Idle: idle,
AvgLatency: latency,
Statuses: statuses,
}
r, ok := routes.Get(alias)
if ok {
result[i].DisplayName = r.HomepageConfig().Name
} else {
result[i].DisplayName = alias
}
}
return result

View File

@@ -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 *xsync.Map[string, bool] // cache for trusted IPs
cachedAddr F.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 = xsync.NewMap[string, bool](xsync.WithPresize(100))
wl.cachedAddr = F.NewMapOf[string, bool]()
}
// before implements RequestModifier.

View File

@@ -1,7 +1,6 @@
package middleware
import (
"bytes"
"context"
"errors"
"fmt"
@@ -116,11 +115,11 @@ func fetchUpdateCFIPRange(endpoint string, cfCIDRs *[]*nettypes.CIDR) error {
return err
}
for line := range bytes.Lines(body) {
if len(line) == 0 {
for _, line := range strutils.SplitLine(string(body)) {
if line == "" {
continue
}
_, cidr, err := net.ParseCIDR(string(line))
_, cidr, err := net.ParseCIDR(line)
if err != nil {
return fmt.Errorf("cloudflare responeded an invalid CIDR: %s", line)
}

View File

@@ -6,12 +6,12 @@ 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/watcher/events"
)
@@ -21,7 +21,7 @@ const errPagesBasePath = common.ErrorPagesBasePath
var (
setupOnce sync.Once
dirWatcher W.Watcher
fileContentMap = xsync.NewMap[string, []byte](xsync.WithGrowOnly())
fileContentMap = F.NewMapOf[string, []byte]()
)
func setup() {
@@ -52,7 +52,7 @@ func loadContent() {
return
}
for _, file := range files {
if _, ok := fileContentMap.Load(file); ok {
if fileContentMap.Has(file) {
continue
}
content, err := os.ReadFile(file)

View File

@@ -3,6 +3,7 @@ package middleware
import (
"errors"
"net/http"
"strings"
"sync"
"sync/atomic"
@@ -13,6 +14,9 @@ import (
type oidcMiddleware struct {
AllowedUsers []string `json:"allowed_users"`
AllowedGroups []string `json:"allowed_groups"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Scopes string `json:"scopes"`
auth *auth.OIDCProvider
@@ -49,11 +53,28 @@ func (amw *oidcMiddleware) initSlow() error {
amw.initMu.Unlock()
}()
// Always start with the global OIDC provider (for issuer discovery)
authProvider, err := auth.NewOIDCProviderFromEnv()
if err != nil {
return err
}
// Check if custom client credentials are provided
if amw.ClientID != "" && amw.ClientSecret != "" {
// Use custom client credentials
customProvider, err := auth.NewOIDCProviderWithCustomClient(
authProvider,
amw.ClientID,
amw.ClientSecret,
)
if err != nil {
return err
}
authProvider = customProvider
}
// If no custom credentials, authProvider remains the global one
// Apply per-route user/group restrictions (these always override global)
if len(amw.AllowedUsers) > 0 {
authProvider.SetAllowedUsers(amw.AllowedUsers)
}
@@ -61,6 +82,11 @@ func (amw *oidcMiddleware) initSlow() error {
authProvider.SetAllowedGroups(amw.AllowedGroups)
}
// Apply custom scopes if provided
if amw.Scopes != "" {
authProvider.SetScopes(strings.Split(amw.Scopes, ","))
}
amw.auth = authProvider
return nil
}

View File

@@ -0,0 +1,35 @@
package middleware
import (
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestOIDCMiddlewarePerRouteConfig(t *testing.T) {
t.Run("middleware struct has correct fields", func(t *testing.T) {
middleware := &oidcMiddleware{
AllowedUsers: []string{"custom-user"},
AllowedGroups: []string{"custom-group"},
ClientID: "custom-client-id",
ClientSecret: "custom-client-secret",
Scopes: "openid,profile,email,groups",
}
ExpectEqual(t, middleware.AllowedUsers, []string{"custom-user"})
ExpectEqual(t, middleware.AllowedGroups, []string{"custom-group"})
ExpectEqual(t, middleware.ClientID, "custom-client-id")
ExpectEqual(t, middleware.ClientSecret, "custom-client-secret")
ExpectEqual(t, middleware.Scopes, "openid,profile,email,groups")
})
t.Run("middleware struct handles empty values", func(t *testing.T) {
middleware := &oidcMiddleware{}
ExpectEqual(t, middleware.AllowedUsers, nil)
ExpectEqual(t, middleware.AllowedGroups, nil)
ExpectEqual(t, middleware.ClientID, "")
ExpectEqual(t, middleware.ClientSecret, "")
ExpectEqual(t, middleware.Scopes, "")
})
}

View File

@@ -59,17 +59,6 @@ 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
@@ -88,8 +77,18 @@ 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 {

View File

@@ -16,7 +16,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/common"
)
@@ -122,21 +121,10 @@ func NewManagerWithUpgrade(c *gin.Context) (*Manager, error) {
return cm, nil
}
func (cm *Manager) Context() context.Context {
return cm.ctx
}
// Periodic writes data to the connection periodically, with deduplication.
// Periodic writes data to the connection periodically.
// 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), deduplicate ...DeduplicateFunc) error {
var lastData any
var equals DeduplicateFunc
if len(deduplicate) > 0 {
equals = deduplicate[0]
}
func (cm *Manager) PeriodicWrite(interval time.Duration, getData func() (any, error)) error {
write := func() {
data, err := getData()
if err != nil {
@@ -145,13 +133,6 @@ 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()
@@ -249,12 +230,6 @@ 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

View File

@@ -7,16 +7,14 @@ import (
apitypes "github.com/yusing/go-proxy/internal/api/types"
)
type DeduplicateFunc func(last, current any) bool
func PeriodicWrite(c *gin.Context, interval time.Duration, get func() (any, error), deduplicate ...DeduplicateFunc) {
func PeriodicWrite(c *gin.Context, interval time.Duration, get func() (any, error)) {
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, deduplicate...)
err = manager.PeriodicWrite(interval, get)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to write to websocket"))
}

View File

@@ -97,6 +97,10 @@ 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)

View File

@@ -136,6 +136,10 @@ 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 {
@@ -167,7 +171,7 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
linked = l.(*ReveseProxyRoute)
lb = linked.loadBalancer
lb.UpdateConfigIfNeeded(cfg)
if linked.Homepage.Name == "" {
if linked.Homepage == nil {
linked.Homepage = r.Homepage
}
} else {
@@ -175,16 +179,18 @@ func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
_ = lb.Start(parent) // always return nil
linked = &ReveseProxyRoute{
Route: &Route{
Alias: cfg.Link,
Homepage: r.Homepage,
HealthMon: lb,
Alias: cfg.Link,
Homepage: r.Homepage,
},
loadBalancer: lb,
handler: lb,
}
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()
}

View File

@@ -24,6 +24,7 @@ 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"
@@ -50,9 +51,6 @@ type (
Agent string `json:"agent,omitempty"`
Idlewatcher *types.IdlewatcherConfig `json:"idlewatcher,omitempty" extensions:"x-nullable"`
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
// for swagger
HealthJSON *types.HealthJSON `json:",omitempty" form:"health"`
Metadata `deserialize:"-"`
}
@@ -67,8 +65,11 @@ type (
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
ExcludedReason string `json:"excluded_reason,omitempty" extensions:"x-nullable"`
Excluded *bool `json:"excluded"`
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
// for swagger
HealthJSON *types.HealthJSON `json:",omitempty" form:"health"`
impl types.Route
task *task.Task
@@ -258,10 +259,8 @@ func (r *Route) Validate() gperr.Error {
}
r.impl = impl
r.Excluded = r.ShouldExclude()
if r.Excluded {
r.ExcludedReason = r.GetExcludedReason()
}
excluded := r.ShouldExclude()
r.Excluded = &excluded
return nil
}
@@ -273,11 +272,14 @@ func (r *Route) Task() *task.Task {
return r.task
}
func (r *Route) Start(parent task.Parent) (err gperr.Error) {
func (r *Route) Start(parent task.Parent) gperr.Error {
if r.lastError != nil {
return r.lastError
}
r.once.Do(func() {
err = r.start(parent)
r.lastError = r.start(parent)
})
return
return r.lastError
}
func (r *Route) start(parent task.Parent) gperr.Error {
@@ -286,27 +288,25 @@ func (r *Route) start(parent task.Parent) gperr.Error {
}
defer close(r.started)
// skip checking for excluded routes
if !r.ShouldExclude() {
if err := checkExists(r); err != nil {
return err
}
}
if cont := r.ContainerInfo(); cont != nil {
docker.SetDockerHostByContainerID(cont.ContainerID, cont.DockerHost)
}
if err := r.impl.Start(parent); 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)
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 +411,16 @@ func (r *Route) LoadBalanceConfig() *types.LoadBalancerConfig {
return r.LoadBalance
}
func (r *Route) HomepageItem() homepage.Item {
return homepage.Item{
Alias: r.Alias,
Provider: r.Provider,
ItemConfig: *r.Homepage,
}.GetOverride()
func (r *Route) HomepageConfig() *homepage.ItemConfig {
return r.Homepage.GetOverride(r.Alias)
}
func (r *Route) DisplayName() string {
return r.Homepage.Name
func (r *Route) HomepageItem() *homepage.Item {
return &homepage.Item{
Alias: r.Alias,
Provider: r.Provider,
ItemConfig: r.HomepageConfig(),
}
}
func (r *Route) ContainerInfo() *types.Container {
@@ -442,8 +442,8 @@ func (r *Route) ShouldExclude() bool {
if r.lastError != nil {
return true
}
if r.Excluded {
return true
if r.Excluded != nil {
return *r.Excluded
}
if r.Container != nil {
switch {
@@ -465,39 +465,12 @@ 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 != ""
}
func (r *Route) UseIdleWatcher() bool {
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0 && r.Idlewatcher.ValErr() == nil
}
func (r *Route) UseHealthCheck() bool {
@@ -613,13 +586,11 @@ func (r *Route) Finalize() {
r.HealthCheck = types.DefaultHealthConfig()
}
if !r.HealthCheck.Disable {
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
if r.HealthCheck.Interval == 0 {
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
}
if r.HealthCheck.Timeout == 0 {
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
}
}
@@ -630,13 +601,10 @@ func (r *Route) FinalizeHomepageConfig() {
isDocker := r.Container != nil
// apply override config
if r.Homepage == nil {
r.Homepage = &homepage.ItemConfig{
Show: true,
Name: r.Alias,
}
r.Homepage = &homepage.ItemConfig{Show: true}
}
r.Homepage = r.Homepage.GetOverride(r.Alias)
if r.ShouldExclude() && isDocker {
r.Homepage.Show = false

View File

@@ -4,8 +4,11 @@ import (
"encoding/json"
"fmt"
"math"
"net/url"
"strings"
"time"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/types"
)
@@ -75,6 +78,71 @@ 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 {

View File

@@ -8,15 +8,16 @@ 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 HTTP.Iter {
if !yield(r) {
break
}
}
for _, r := range Stream.Iter {
for _, r := range All.Iter {
if !yield(r) {
break
}
@@ -24,12 +25,7 @@ func Iter(yield func(r types.Route) bool) {
}
func IterKV(yield func(alias string, r types.Route) bool) {
for k, r := range HTTP.Iter {
if !yield(k, r) {
break
}
}
for k, r := range Stream.Iter {
for k, r := range All.Iter {
if !yield(k, r) {
break
}
@@ -37,12 +33,13 @@ func IterKV(yield func(alias string, r types.Route) bool) {
}
func NumRoutes() int {
return HTTP.Size() + Stream.Size()
return All.Size()
}
func Clear() {
HTTP.Clear()
Stream.Clear()
All.Clear()
}
func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
@@ -55,11 +52,5 @@ func GetHTTPRouteOrExact(alias, host string) (types.HTTPRoute, bool) {
}
func Get(alias string) (types.Route, bool) {
if r, ok := HTTP.Get(alias); ok {
return r, true
}
if r, ok := Stream.Get(alias); ok {
return r, true
}
return nil, false
return All.Get(alias)
}

View File

@@ -40,8 +40,8 @@ type (
*/
Rule struct {
Name string `json:"name"`
On RuleOn `json:"on" swaggertype:"string"`
Do Command `json:"do" swaggertype:"string"`
On RuleOn `json:"on"`
Do Command `json:"do"`
}
)

View File

@@ -73,6 +73,10 @@ 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")

View File

@@ -14,6 +14,7 @@ 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"
)
@@ -535,7 +536,7 @@ func UnmarshalValidateYAMLIntercept[T any](data []byte, target *T, intercept fun
return MapUnmarshalValidate(m, target)
}
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], err gperr.Error) {
func UnmarshalValidateYAMLXSync[V any](data []byte) (_ functional.Map[string, V], err gperr.Error) {
m := make(map[string]any)
if err = gperr.Wrap(yaml.Unmarshal(data, &m)); err != nil {
return
@@ -544,11 +545,7 @@ func UnmarshalValidateYAMLXSync[V any](data []byte) (_ *xsync.Map[string, V], er
if err = MapUnmarshalValidate(m, m2); err != nil {
return
}
ret := xsync.NewMap[string, V](xsync.WithPresize(len(m)))
for k, v := range m2 {
ret.Store(k, v)
}
return ret, nil
return functional.NewMapFrom(m2), nil
}
func loadSerialized[T any](path string, dst *T, deserialize func(data []byte, dst any) error) error {

View File

@@ -22,8 +22,6 @@ type (
ContainerName string `json:"container_name"`
ContainerID string `json:"container_id"`
State container.ContainerState `json:"state"`
Agent *agent.AgentConfig `json:"agent"`
Labels map[string]string `json:"-"` // for creating routes

View File

@@ -30,6 +30,8 @@ type (
StartEndpoint string `json:"start_endpoint,omitempty"` // Optional path that must be hit to start container
DependsOn []string `json:"depends_on,omitempty"`
valErr gperr.Error
} // @name IdlewatcherConfig
ContainerStopMethod string // @name ContainerStopMethod
ContainerSignal string // @name ContainerSignal
@@ -70,9 +72,10 @@ func (c *IdlewatcherConfig) ContainerName() string {
func (c *IdlewatcherConfig) Validate() gperr.Error {
if c.IdleTimeout == 0 { // zero idle timeout means no idle watcher
c.valErr = nil
return nil
}
errs := gperr.NewBuilder("idlewatcher config validation error")
errs := gperr.NewBuilder()
errs.AddRange(
c.validateProvider(),
c.validateTimeouts(),
@@ -80,7 +83,12 @@ func (c *IdlewatcherConfig) Validate() gperr.Error {
c.validateStopSignal(),
c.validateStartEndpoint(),
)
return errs.Error()
c.valErr = errs.Error()
return c.valErr
}
func (c *IdlewatcherConfig) ValErr() gperr.Error {
return c.valErr
}
func (c *IdlewatcherConfig) validateProvider() error {

View File

@@ -28,8 +28,8 @@ type (
IdlewatcherConfig() *IdlewatcherConfig
HealthCheckConfig() *HealthCheckConfig
LoadBalanceConfig() *LoadBalancerConfig
HomepageItem() homepage.Item
DisplayName() string
HomepageConfig() *homepage.ItemConfig
HomepageItem() *homepage.Item
ContainerInfo() *Container
GetAgent() *agent.AgentConfig

View File

@@ -1,243 +0,0 @@
package utils
import (
"reflect"
"unsafe"
)
// DeepEqual reports whether x and y are deeply equal.
// It supports numerics, strings, maps, slices, arrays, and structs (exported fields only).
// It's optimized for performance by avoiding reflection for common types and
// adaptively choosing between BFS and DFS traversal strategies.
func DeepEqual(x, y any) bool {
if x == nil || y == nil {
return x == y
}
v1 := reflect.ValueOf(x)
v2 := reflect.ValueOf(y)
if v1.Type() != v2.Type() {
return false
}
return deepEqual(v1, v2, make(map[visit]bool), 0)
}
// visit represents a visit to a pair of values during comparison
type visit struct {
a1, a2 unsafe.Pointer
typ reflect.Type
}
// deepEqual performs the actual deep comparison with cycle detection
func deepEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if !v1.IsValid() || !v2.IsValid() {
return v1.IsValid() == v2.IsValid()
}
if v1.Type() != v2.Type() {
return false
}
// Handle cycle detection for pointer-like types
if v1.CanAddr() && v2.CanAddr() {
addr1 := unsafe.Pointer(v1.UnsafeAddr())
addr2 := unsafe.Pointer(v2.UnsafeAddr())
typ := v1.Type()
v := visit{addr1, addr2, typ}
if visited[v] {
return true // already visiting, assume equal
}
visited[v] = true
defer delete(visited, v)
}
switch v1.Kind() {
case reflect.Bool:
return v1.Bool() == v2.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v1.Int() == v2.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v1.Uint() == v2.Uint()
case reflect.Float32, reflect.Float64:
return floatEqual(v1.Float(), v2.Float())
case reflect.Complex64, reflect.Complex128:
c1, c2 := v1.Complex(), v2.Complex()
return floatEqual(real(c1), real(c2)) && floatEqual(imag(c1), imag(c2))
case reflect.String:
return v1.String() == v2.String()
case reflect.Array:
return deepEqualArray(v1, v2, visited, depth)
case reflect.Slice:
return deepEqualSlice(v1, v2, visited, depth)
case reflect.Map:
return deepEqualMap(v1, v2, visited, depth)
case reflect.Struct:
return deepEqualStruct(v1, v2, visited, depth)
case reflect.Ptr:
if v1.IsNil() || v2.IsNil() {
return v1.IsNil() && v2.IsNil()
}
return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1)
case reflect.Interface:
if v1.IsNil() || v2.IsNil() {
return v1.IsNil() && v2.IsNil()
}
return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1)
default:
// For unsupported types (func, chan, etc.), fall back to basic equality
return v1.Interface() == v2.Interface()
}
}
// floatEqual handles NaN cases properly
func floatEqual(f1, f2 float64) bool {
return f1 == f2 || (f1 != f1 && f2 != f2) // NaN == NaN
}
// deepEqualArray compares arrays using DFS (since arrays have fixed size)
func deepEqualArray(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
for i := range v1.Len() {
if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
return false
}
}
return true
}
// deepEqualSlice compares slices, choosing strategy based on size and depth
func deepEqualSlice(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if v1.IsNil() != v2.IsNil() {
return false
}
if v1.Len() != v2.Len() {
return false
}
if v1.IsNil() {
return true
}
// Use BFS for large slices at shallow depth to improve cache locality
// Use DFS for small slices or deep nesting to reduce memory overhead
if shouldUseBFS(v1.Len(), depth) {
return deepEqualSliceBFS(v1, v2, visited, depth)
}
return deepEqualSliceDFS(v1, v2, visited, depth)
}
// deepEqualSliceDFS uses depth-first traversal
func deepEqualSliceDFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
for i := range v1.Len() {
if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
return false
}
}
return true
}
// deepEqualSliceBFS uses breadth-first traversal for better cache locality
func deepEqualSliceBFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
length := v1.Len()
// First, check all direct elements
for i := range length {
elem1, elem2 := v1.Index(i), v2.Index(i)
// For simple types, compare directly
if isSimpleType(elem1.Kind()) {
if !deepEqual(elem1, elem2, visited, depth+1) {
return false
}
}
}
// Then, recursively check complex elements
for i := range length {
elem1, elem2 := v1.Index(i), v2.Index(i)
if !isSimpleType(elem1.Kind()) {
if !deepEqual(elem1, elem2, visited, depth+1) {
return false
}
}
}
return true
}
// deepEqualMap compares maps
func deepEqualMap(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
if v1.IsNil() != v2.IsNil() {
return false
}
if v1.Len() != v2.Len() {
return false
}
if v1.IsNil() {
return true
}
// Check all keys and values
for _, key := range v1.MapKeys() {
val1 := v1.MapIndex(key)
val2 := v2.MapIndex(key)
if !val2.IsValid() {
return false // key doesn't exist in v2
}
if !deepEqual(val1, val2, visited, depth+1) {
return false
}
}
return true
}
// deepEqualStruct compares structs (exported fields only)
func deepEqualStruct(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
typ := v1.Type()
for i := range typ.NumField() {
field := typ.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
if !deepEqual(v1.Field(i), v2.Field(i), visited, depth+1) {
return false
}
}
return true
}
// shouldUseBFS determines whether to use BFS or DFS based on slice size and depth
func shouldUseBFS(length, depth int) bool {
// Use BFS for large slices at shallow depth (better cache locality)
// Use DFS for small slices or deep nesting (lower memory overhead)
return length > 100 && depth < 3
}
// isSimpleType checks if a type can be compared without deep recursion
func isSimpleType(kind reflect.Kind) bool {
if kind >= reflect.Bool && kind <= reflect.Complex128 {
return true
}
return kind == reflect.String
}

View File

@@ -0,0 +1,103 @@
package functional
import (
"sync"
"github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4"
)
type Map[KT comparable, VT any] struct {
*xsync.Map[KT, VT]
}
const minParallelSize = 4
func NewMapOf[KT comparable, VT any](options ...func(*xsync.MapConfig)) Map[KT, VT] {
return Map[KT, VT]{xsync.NewMap[KT, VT](options...)}
}
func NewMapFrom[KT comparable, VT any](m map[KT]VT) (res Map[KT, VT]) {
res = NewMapOf[KT, VT](xsync.WithPresize(len(m)))
for k, v := range m {
res.Store(k, v)
}
return
}
func NewMap[MapType Map[KT, VT], KT comparable, VT any]() Map[KT, VT] {
return NewMapOf[KT, VT]()
}
// RangeAll calls the given function for each key-value pair in the map.
//
// Parameters:
//
// do: function to call for each key-value pair
//
// Returns:
//
// nothing
func (m Map[KT, VT]) RangeAll(do func(k KT, v VT)) {
m.Range(func(k KT, v VT) bool {
do(k, v)
return true
})
}
// RangeAllParallel calls the given function for each key-value pair in the map,
// in parallel. The map is not safe for modification from within the function.
//
// Parameters:
//
// do: function to call for each key-value pair
//
// Returns:
//
// nothing
func (m Map[KT, VT]) RangeAllParallel(do func(k KT, v VT)) {
if m.Size() < minParallelSize {
m.RangeAll(do)
return
}
var wg sync.WaitGroup
for k, v := range m.Range {
wg.Add(1)
go func(k KT, v VT) {
defer wg.Done()
do(k, v)
}(k, v)
}
wg.Wait()
}
// CollectErrors calls the given function for each key-value pair in the map,
// then returns a slice of errors collected.
func (m Map[KT, VT]) CollectErrors(do func(k KT, v VT) error) []error {
errs := make([]error, 0)
m.Range(func(k KT, v VT) bool {
if err := do(k, v); err != nil {
errs = append(errs, err)
}
return true
})
return errs
}
func (m Map[KT, VT]) Has(k KT) bool {
_, ok := m.Load(k)
return ok
}
func (m Map[KT, VT]) String() string {
tmp := make(map[KT]VT, m.Size())
m.RangeAll(func(k KT, v VT) {
tmp[k] = v
})
data, err := yaml.Marshal(&tmp)
if err != nil {
return err.Error()
}
return string(data)
}

View File

@@ -0,0 +1,20 @@
package functional_test
import (
"testing"
. "github.com/yusing/go-proxy/internal/utils/functional"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestNewMapFrom(t *testing.T) {
m := NewMapFrom(map[string]int{
"a": 1,
"b": 2,
"c": 3,
})
ExpectEqual(t, m.Size(), 3)
ExpectTrue(t, m.Has("a"))
ExpectTrue(t, m.Has("b"))
ExpectTrue(t, m.Has("c"))
}

View File

@@ -1,8 +1,9 @@
module github.com/yusing/go-proxy/internal/utils
go 1.25.0
go 1.25.1
require (
github.com/goccy/go-yaml v1.18.0
github.com/puzpuzpuz/xsync/v4 v4.1.0
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1

View File

@@ -1,6 +1,8 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=

View File

@@ -24,14 +24,6 @@ func Title(s string) string {
return cases.Title(language.AmericanEnglish).String(s)
}
func ContainsFold(s, substr string) bool {
return IndexFold(s, substr) >= 0
}
func IndexFold(s, substr string) int {
return strings.Index(strings.ToLower(s), strings.ToLower(substr))
}
func ToLowerNoSnake(s string) string {
var buf strings.Builder
for _, r := range s {

View File

@@ -41,28 +41,6 @@ type BytesPoolWithMemory struct {
pool chan weakBuf
}
type sliceInternal struct {
ptr unsafe.Pointer
len int
cap int
}
func sliceStruct(b *[]byte) *sliceInternal {
return (*sliceInternal)(unsafe.Pointer(b))
}
func underlyingPtr(b []byte) unsafe.Pointer {
return sliceStruct(&b).ptr
}
func setCap(b *[]byte, cap int) {
sliceStruct(b).cap = cap
}
func setLen(b *[]byte, len int) {
sliceStruct(b).len = len
}
const (
kb = 1024
mb = 1024 * kb
@@ -110,7 +88,7 @@ func (p *BytesPool) Get() []byte {
addReused(cap(bPtr))
return bPtr
default:
return make([]byte, 0, p.initSize)
return make([]byte, 0)
}
}
}
@@ -135,6 +113,10 @@ func (p *BytesPoolWithMemory) Get() []byte {
}
func (p *BytesPool) GetSized(size int) []byte {
if size <= SizedPoolThreshold {
addNonPooled(size)
return make([]byte, size)
}
for {
select {
case bWeak := <-p.sizedPool:
@@ -143,26 +125,10 @@ func (p *BytesPool) GetSized(size int) []byte {
continue
}
capB := cap(bPtr)
remainingSize := capB - size
if remainingSize == 0 {
if capB >= size {
addReused(capB)
return bPtr[:size]
return (bPtr)[:size]
}
if remainingSize > 0 { // size > capB
addReused(size)
p.Put(bPtr[size:capB])
// return the first part and limit the capacity to the requested size
ret := bPtr[:size]
setLen(&ret, size)
setCap(&ret, size)
return ret
}
// size is not enough
select {
case p.sizedPool <- bWeak:
default:
@@ -181,10 +147,11 @@ func (p *BytesPool) Put(b []byte) {
return
}
b = b[:0]
if size >= SizedPoolThreshold {
p.put(makeWeak(&b), p.sizedPool)
w := makeWeak(&b)
if size <= SizedPoolThreshold {
p.put(w, p.unsizedPool)
} else {
p.put(makeWeak(&b), p.unsizedPool)
p.put(w, p.sizedPool)
}
}

View File

@@ -21,25 +21,13 @@ func BenchmarkBytesPool_MakeSmall(b *testing.B) {
func BenchmarkBytesPool_GetLarge(b *testing.B) {
for b.Loop() {
buf := bytesPool.GetSized(DropThreshold / 2)
buf[0] = 1
bytesPool.Put(buf)
}
}
func BenchmarkBytesPool_GetLargeUnsized(b *testing.B) {
for b.Loop() {
buf := slices.Grow(bytesPool.Get(), DropThreshold/2)
buf = append(buf, 1)
bytesPool.Put(buf)
bytesPool.Put(bytesPool.GetSized(1024 * 1024))
}
}
func BenchmarkBytesPool_MakeLarge(b *testing.B) {
for b.Loop() {
buf := make([]byte, DropThreshold/2)
buf[0] = 1
_ = buf
_ = make([]byte, 1024*1024)
}
}
@@ -49,9 +37,10 @@ func BenchmarkBytesPool_GetAll(b *testing.B) {
}
}
func BenchmarkBytesPool_GetAllUnsized(b *testing.B) {
func BenchmarkBytesPoolWithMemory(b *testing.B) {
pool := GetBytesPoolWithUniqueMemory()
for i := range b.N {
bytesPool.Put(slices.Grow(bytesPool.Get(), sizes[i%len(sizes)]))
pool.Put(slices.Grow(pool.Get(), sizes[i%len(sizes)]))
}
}
@@ -60,10 +49,3 @@ func BenchmarkBytesPool_MakeAll(b *testing.B) {
_ = make([]byte, sizes[i%len(sizes)])
}
}
func BenchmarkBytesPoolWithMemory(b *testing.B) {
pool := GetBytesPoolWithUniqueMemory()
for i := range b.N {
pool.Put(slices.Grow(pool.Get(), sizes[i%len(sizes)]))
}
}

View File

@@ -1,263 +0,0 @@
package synk
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSized(t *testing.T) {
b := bytesPool.GetSized(2 * SizedPoolThreshold)
assert.Equal(t, cap(b), 2*SizedPoolThreshold)
bytesPool.Put(b)
assert.Equal(t, underlyingPtr(b), underlyingPtr(bytesPool.GetSized(SizedPoolThreshold)))
}
func TestUnsized(t *testing.T) {
b := bytesPool.Get()
assert.Equal(t, cap(b), UnsizedAvg)
bytesPool.Put(b)
assert.Equal(t, underlyingPtr(b), underlyingPtr(bytesPool.Get()))
}
func TestGetSizedExactMatch(t *testing.T) {
// Test exact size match reuse
size := SizedPoolThreshold
b1 := bytesPool.GetSized(size)
assert.Equal(t, size, len(b1))
assert.Equal(t, size, cap(b1))
// Put back into pool
bytesPool.Put(b1)
// Get same size - should reuse the same buffer
b2 := bytesPool.GetSized(size)
assert.Equal(t, size, len(b2))
assert.Equal(t, size, cap(b2))
assert.Equal(t, underlyingPtr(b1), underlyingPtr(b2))
}
func TestGetSizedBufferSplit(t *testing.T) {
// Test buffer splitting when capacity > requested size
largeSize := 2 * SizedPoolThreshold
requestedSize := SizedPoolThreshold
// Create a large buffer and put it in pool
b1 := bytesPool.GetSized(largeSize)
assert.Equal(t, largeSize, len(b1))
assert.Equal(t, largeSize, cap(b1))
bytesPool.Put(b1)
// Request smaller size - should split the buffer
b2 := bytesPool.GetSized(requestedSize)
assert.Equal(t, requestedSize, len(b2))
assert.Equal(t, requestedSize, cap(b2)) // capacity should remain the original
assert.Equal(t, underlyingPtr(b1), underlyingPtr(b2))
// The remaining part should be put back in pool
// Request the remaining size to verify
remainingSize := largeSize - requestedSize
b3 := bytesPool.GetSized(remainingSize)
assert.Equal(t, remainingSize, len(b3))
assert.Equal(t, remainingSize, cap(b3))
// Verify the remaining buffer points to the correct memory location
originalPtr := underlyingPtr(b1)
remainingPtr := underlyingPtr(b3)
// The remaining buffer should start at original + requestedSize
expectedOffset := uintptr(originalPtr) + uintptr(requestedSize)
actualOffset := uintptr(remainingPtr)
assert.Equal(t, expectedOffset, actualOffset, "Remaining buffer should point to correct offset")
}
func TestGetSizedSmallRemainder(t *testing.T) {
// Test when remaining size is smaller than SizedPoolThreshold
poolSize := SizedPoolThreshold + 100 // Just slightly larger than threshold
requestedSize := SizedPoolThreshold
// Create buffer and put in pool
b1 := bytesPool.GetSized(poolSize)
bytesPool.Put(b1)
// Request size that leaves small remainder
b2 := bytesPool.GetSized(requestedSize)
assert.Equal(t, requestedSize, len(b2))
assert.Equal(t, requestedSize, cap(b2))
// The small remainder (100 bytes) should NOT be put back in sized pool
// Try to get the remainder size - should create new buffer
b3 := bytesPool.GetSized(100)
assert.Equal(t, 100, len(b3))
assert.Equal(t, 100, cap(b3))
assert.NotEqual(t, underlyingPtr(b2), underlyingPtr(b3))
}
func TestGetSizedSmallBufferBypass(t *testing.T) {
// Test that small buffers (< SizedPoolThreshold) don't use sized pool
smallSize := SizedPoolThreshold - 1
b1 := bytesPool.GetSized(smallSize)
assert.Equal(t, smallSize, len(b1))
assert.Equal(t, smallSize, cap(b1))
b2 := bytesPool.GetSized(smallSize)
assert.Equal(t, smallSize, len(b2))
assert.Equal(t, smallSize, cap(b2))
// Should be different buffers (not pooled)
assert.NotEqual(t, underlyingPtr(b1), underlyingPtr(b2))
}
func TestGetSizedBufferTooSmall(t *testing.T) {
// Test when pool buffer is smaller than requested size
smallSize := SizedPoolThreshold
largeSize := 2 * SizedPoolThreshold
// Put small buffer in pool
b1 := bytesPool.GetSized(smallSize)
bytesPool.Put(b1)
// Request larger size - should create new buffer, not reuse small one
b2 := bytesPool.GetSized(largeSize)
assert.Equal(t, largeSize, len(b2))
assert.Equal(t, largeSize, cap(b2))
assert.NotEqual(t, underlyingPtr(b1), underlyingPtr(b2))
// The small buffer should still be in pool
b3 := bytesPool.GetSized(smallSize)
assert.Equal(t, underlyingPtr(b1), underlyingPtr(b3))
}
func TestGetSizedMultipleSplits(t *testing.T) {
// Test multiple sequential splits of the same buffer
hugeSize := 4 * SizedPoolThreshold
splitSize := SizedPoolThreshold
// Create huge buffer
b1 := bytesPool.GetSized(hugeSize)
originalPtr := underlyingPtr(b1)
bytesPool.Put(b1)
// Split it into smaller pieces
pieces := make([][]byte, 0, 4)
for i := range 4 {
piece := bytesPool.GetSized(splitSize)
pieces = append(pieces, piece)
// Each piece should point to the correct offset
expectedOffset := uintptr(originalPtr) + uintptr(i*splitSize)
actualOffset := uintptr(underlyingPtr(piece))
assert.Equal(t, expectedOffset, actualOffset, "Piece %d should point to correct offset", i)
assert.Equal(t, splitSize, len(piece))
assert.Equal(t, splitSize, cap(piece))
}
// All pieces should have the same underlying capacity
for i, piece := range pieces {
assert.Equal(t, splitSize, cap(piece), "Piece %d should have correct capacity", i)
}
}
func TestGetSizedMemorySafety(t *testing.T) {
// Test that split buffers don't interfere with each other
totalSize := 3 * SizedPoolThreshold
firstSize := SizedPoolThreshold
// Create buffer and split it
b1 := bytesPool.GetSized(totalSize)
// Fill with test data
for i := range len(b1) {
b1[i] = byte(i % 256)
}
bytesPool.Put(b1)
// Get first part
first := bytesPool.GetSized(firstSize)
assert.Equal(t, firstSize, len(first))
// Verify data integrity
for i := range len(first) {
assert.Equal(t, byte(i%256), first[i], "Data should be preserved after split")
}
// Get remaining part
remainingSize := totalSize - firstSize
remaining := bytesPool.GetSized(remainingSize)
assert.Equal(t, remainingSize, len(remaining))
// Verify remaining data
for i := range len(remaining) {
expected := byte((i + firstSize) % 256)
assert.Equal(t, expected, remaining[i], "Remaining data should be preserved")
}
}
func TestGetSizedCapacityLimiting(t *testing.T) {
// Test that returned buffers have limited capacity to prevent overwrites
largeSize := 2 * SizedPoolThreshold
requestedSize := SizedPoolThreshold
// Create large buffer and put in pool
b1 := bytesPool.GetSized(largeSize)
bytesPool.Put(b1)
// Get smaller buffer from the split
b2 := bytesPool.GetSized(requestedSize)
assert.Equal(t, requestedSize, len(b2))
assert.Equal(t, requestedSize, cap(b2), "Returned buffer should have limited capacity")
// Try to append data - should not be able to overwrite beyond capacity
original := make([]byte, len(b2))
copy(original, b2)
// This append should force a new allocation since capacity is limited
b2 = append(b2, 1, 2, 3, 4, 5)
assert.Greater(t, len(b2), requestedSize, "Buffer should have grown")
// Get the remaining buffer to verify it wasn't affected
remainingSize := largeSize - requestedSize
b3 := bytesPool.GetSized(remainingSize)
assert.Equal(t, remainingSize, len(b3))
assert.Equal(t, remainingSize, cap(b3), "Remaining buffer should have limited capacity")
}
func TestGetSizedAppendSafety(t *testing.T) {
// Test that appending to returned buffer doesn't affect remaining buffer
totalSize := 4 * SizedPoolThreshold
firstSize := SizedPoolThreshold
// Create buffer with specific pattern
b1 := bytesPool.GetSized(totalSize)
for i := range len(b1) {
b1[i] = byte(100 + i%100)
}
bytesPool.Put(b1)
// Get first part
first := bytesPool.GetSized(firstSize)
assert.Equal(t, firstSize, cap(first), "First part should have limited capacity")
// Store original first part content
originalFirst := make([]byte, len(first))
copy(originalFirst, first)
// Get remaining part to establish its state
remaining := bytesPool.GetSized(SizedPoolThreshold)
// Store original remaining content
originalRemaining := make([]byte, len(remaining))
copy(originalRemaining, remaining)
// Now try to append to first - this should not affect remaining buffers
// since capacity is limited
first = append(first, make([]byte, 1000)...)
// Verify remaining buffer content is unchanged
for i := range len(originalRemaining) {
assert.Equal(t, originalRemaining[i], remaining[i],
"Remaining buffer should be unaffected by append to first buffer")
}
}

View File

@@ -3,10 +3,10 @@ package monitor
import (
"time"
"github.com/puzpuzpuz/xsync/v4"
F "github.com/yusing/go-proxy/internal/utils/functional"
)
var lastSeenMap = xsync.NewMap[string, time.Time](xsync.WithPresize(50), xsync.WithGrowOnly())
var lastSeenMap = F.NewMapOf[string, time.Time]()
func SetLastSeen(service string, lastSeen time.Time) {
lastSeenMap.Store(service, lastSeen)

View File

@@ -72,6 +72,9 @@ func NewMonitor(r types.Route) types.HealthMonCheck {
func newMonitor(u *url.URL, config *types.HealthCheckConfig, healthCheckFunc HealthCheckFunc) *monitor {
if config.Retries == 0 {
if config.Interval == 0 {
config.Interval = common.HealthCheckIntervalDefault
}
config.Retries = int64(common.HealthCheckDownNotifyDelayDefault / config.Interval)
}
mon := &monitor{
@@ -171,7 +174,9 @@ func (mon *monitor) Task() *task.Task {
// Finish implements task.TaskFinisher.
func (mon *monitor) Finish(reason any) {
mon.task.Finish(reason)
if mon.task != nil {
mon.task.Finish(reason)
}
}
// UpdateURL implements HealthChecker.

View File

@@ -0,0 +1,75 @@
---
services:
socket-proxy:
container_name: socket-proxy
image: ghcr.io/yusing/socket-proxy:latest
environment:
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
- CONTAINERS=1
- EVENTS=1
- INFO=1
- PING=1
- POST=1
- VERSION=1
volumes:
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
restart: unless-stopped
tmpfs:
- /run
networks:
- godoxy
frontend:
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
container_name: godoxy-frontend
restart: unless-stopped
env_file: .env
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 0.0.0.0
PORT: 3000
labels:
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.#1.port: 3000
networks:
- godoxy
app:
image: yusing/godoxy:test
container_name: godoxy-proxy
restart: always
env_file: .env
depends_on:
socket-proxy:
condition: service_started
security_opt:
- no-new-privileges:true
cap_drop:
- all
cap_add:
- NET_BIND_SERVICE
environment:
- DOCKER_HOST=tcp://${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}
ports:
- 80:80
- 443:443/tcp
- 443:443/udp # http3
volumes:
- ./config:/app/config
- ./logs:/app/logs
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
- ./certs:/app/certs
networks:
- proxy
- godoxy
networks:
proxy: # bridge network for all services that needs proxying
external: true
godoxy:

72
rootless.env.example Normal file
View File

@@ -0,0 +1,72 @@
DOCKER_SOCKET=/var/run/user/1000/docker.sock
SOCKET_PROXY_LISTEN_ADDR=socket-proxy:2375
# docker image tag (latest, nightly)
TAG=latest
# set timezone to get correct log timestamp
TZ=ETC/UTC
# Set GODOXY_API_JWT_SECURE=false to allow http
GODOXY_API_JWT_SECURE=true
# API JWT Configuration (common)
# generate secret with `openssl rand -base64 32`
GODOXY_API_JWT_SECRET=
# the JWT token time-to-live
# leave empty to use default (24 hours)
# format: https://pkg.go.dev/time#Duration
GODOXY_API_JWT_TOKEN_TTL=
# API/WebUI user password login credentials (optional)
# These fields are not required for OIDC authentication
GODOXY_API_USER=admin
GODOXY_API_PASSWORD=password
# OIDC Configuration (optional)
# Uncomment and configure these values to enable OIDC authentication.
#
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
# GODOXY_OIDC_CLIENT_ID=your-client-id
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
# GODOXY_OIDC_SCOPES=openid, profile, email, groups # you may also include `offline_access` if your Idp supports it (e.g. Authentik, Pocket ID)
#
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
# These two fields act as a logical AND operator. For example, given the following membership:
# user1, group1
# user2, group1
# user3, group2
# user1, group2
# You can allow access to user3 AND all users of group1 by providing:
# # GODOXY_OIDC_ALLOWED_USERS=user3
# # GODOXY_OIDC_ALLOWED_GROUPS=group1
#
# Comma-separated list of allowed users.
# GODOXY_OIDC_ALLOWED_USERS=user1,user2
# Optional: Comma-separated list of allowed groups.
# GODOXY_OIDC_ALLOWED_GROUPS=group1,group2
# Proxy listening address
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# Enable HTTP3
GODOXY_HTTP3_ENABLED=true
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Metrics
GODOXY_METRICS_DISABLE_CPU=false
GODOXY_METRICS_DISABLE_MEMORY=false
GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
GODOXY_FRONTEND_ALIASES=godoxy
# Debug mode
GODOXY_DEBUG=false

View File

@@ -180,7 +180,24 @@ for dir in "${REQUIRED_DIRECTORIES[@]}"; do
mkdir_if_not_exists "$dir"
done
# 2. .env file
# 2. check if rootless docker is used, verify again with user input
if docker info -f "{{println .SecurityOptions}}" | grep rootless >/dev/null 2>&1; then
ask_while_empty "Rootless docker detected, is this correct? (y/n): " USE_ROOTLESS_DOCKER
if [ "$USE_ROOTLESS_DOCKER" == "n" ]; then
USE_ROOTLESS_DOCKER="false"
else
USE_ROOTLESS_DOCKER="true"
fi
fi
# 3. if rootless docker is used, switch to rootless docker compose and .env
if [ "$USE_ROOTLESS_DOCKER" == "true" ]; then
COMPOSE_EXAMPLE_FILE_NAME="rootless-compose.example.yml"
DOT_ENV_EXAMPLE_PATH="rootless.env.example"
fi
# 4. .env file
fetch_file "$DOT_ENV_EXAMPLE_PATH" "$DOT_ENV_PATH"
# set random JWT secret
@@ -192,13 +209,13 @@ if [ -n "$TIMEZONE" ]; then
setenv "TZ" "$TIMEZONE"
fi
# 3. docker-compose.yml
# 5. docker-compose.yml
fetch_file "$COMPOSE_EXAMPLE_FILE_NAME" "$COMPOSE_FILE_NAME"
# 4. config.yml
# 6. config.yml
fetch_file "$CONFIG_EXAMPLE_FILE_NAME" "$CONFIG_FILE_PATH"
# 5. setup authentication
# 7. setup authentication
# ask for user and password
echo "Setting up login user"
@@ -208,7 +225,7 @@ echo "Setting up login user \"$LOGIN_USERNAME\" with password \"$LOGIN_PASSWORD\
setenv "GODOXY_API_USER" "$LOGIN_USERNAME"
setenv "GODOXY_API_PASSWORD" "$LOGIN_PASSWORD"
# 6. setup autocert
# 8. setup autocert
ask_while_empty "Configure autocert? (y/n): " ENABLE_AUTOCERT
# quit if not using autocert
@@ -269,8 +286,34 @@ autocert:
fi
fi
# 7. set uid and gid
setenv "GODOXY_UID" "$(id -u)"
setenv "GODOXY_GID" "$(id -g)"
# 9. set uid and gid
if [ "$USE_ROOTLESS_DOCKER" == "false" ]; then
setenv "GODOXY_UID" "$(id -u)"
setenv "GODOXY_GID" "$(id -g)"
else
setenv "DOCKER_SOCKET" "/var/run/user/$(id -u)/docker.sock"
fi
# 10. proxy network (rootless docker only)
if [ "$USE_ROOTLESS_DOCKER" == "true" ]; then
echo "Setting up proxy network"
echo "Available networks:"
docker network ls
echo
ask_while_empty "Which network to use for proxy? (default: proxy): " PROXY_NETWORK
# check if network exists
if ! docker network ls | grep -q "$PROXY_NETWORK"; then
ask_while_empty "Network \"$PROXY_NETWORK\" does not exist, do you want to create it? (y/n): " CREATE_NETWORK
if [ "$CREATE_NETWORK" == "y" ]; then
docker network create "$PROXY_NETWORK"
echo "Network \"$PROXY_NETWORK\" created"
else
echo "Error: network \"$PROXY_NETWORK\" does not exist, please create it first"
exit 1
fi
fi
sed -i "s|proxy: #|\"$PROXY_NETWORK\": #|" "$COMPOSE_FILE_NAME"
sed -i "s|- proxy|- \"$PROXY_NETWORK\"|" "$COMPOSE_FILE_NAME"
fi
echo "Setup finished"

View File

@@ -1,12 +1,12 @@
module github.com/yusing/go-proxy/socketproxy
go 1.25.0
go 1.25.1
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
require (
github.com/gorilla/mux v1.8.1
github.com/yusing/go-proxy/internal/utils v0.0.0-20250903143810-e1133a2daf72
github.com/yusing/go-proxy/internal/utils v0.0.0-20250819142638-5e15fd4bbef0
golang.org/x/net v0.43.0
)