mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
373372ac59 | ||
|
|
6cf31541d9 | ||
|
|
d1448c886d | ||
|
|
4a6e821732 | ||
|
|
1c84a9b812 | ||
|
|
35c784375d | ||
|
|
656e470c8e | ||
|
|
f950630a19 | ||
|
|
6647ff448e | ||
|
|
033e9e4f68 | ||
|
|
c9ed3ed631 | ||
|
|
78c4fb0990 | ||
|
|
1a20a2cda7 | ||
|
|
19d6f3757b | ||
|
|
7e7e885c57 | ||
|
|
1ca5fc5ac6 | ||
|
|
fc88d588a0 | ||
|
|
86b655be3c | ||
|
|
2f2828ec48 | ||
|
|
a5c74d6773 | ||
|
|
a6af5779f9 | ||
|
|
dbd210b665 | ||
|
|
02e6e6f86c | ||
|
|
6d9a193fd5 | ||
|
|
5aa58e003d | ||
|
|
19f38a6cfc | ||
|
|
f3331515ea | ||
|
|
95202fd21d | ||
|
|
c44636f95a | ||
|
|
17bfc96e3d | ||
|
|
f5dcc85b12 | ||
|
|
1d1b01efd7 | ||
|
|
90948f7443 | ||
|
|
be1f7c7ec4 | ||
|
|
91317ff319 | ||
|
|
6f14a2907b | ||
|
|
73deb682bd | ||
|
|
08ce58f031 | ||
|
|
bf6d7b55f1 | ||
|
|
da8e03258d | ||
|
|
72e53773b0 | ||
|
|
7e9e0c4511 | ||
|
|
f1d2b170e2 | ||
|
|
c026a0df7c | ||
|
|
b51d280b29 | ||
|
|
ea030ebd19 | ||
|
|
64ba519f03 | ||
|
|
02d0a910f6 | ||
|
|
5a2e327cce | ||
|
|
4001e94d5c | ||
|
|
f8196216ad | ||
|
|
9bac45ce15 | ||
|
|
5c9ccd9963 | ||
|
|
7702fa6696 | ||
|
|
30eae68a91 | ||
|
|
7d404ba32f | ||
|
|
72e4f8dd34 | ||
|
|
915c5958fd | ||
|
|
ad2bfac275 | ||
|
|
65383c7061 | ||
|
|
23ceeda402 | ||
|
|
53dc70d15b | ||
|
|
b8788f68f3 | ||
|
|
0a5e8597dd | ||
|
|
739bb351bf | ||
|
|
32dba5f5f6 | ||
|
|
0884be240c | ||
|
|
021c560ff7 | ||
|
|
b6ed9abbb3 | ||
|
|
ac7bf61eb3 | ||
|
|
c12fca0cbd | ||
|
|
c3f33e7c7e | ||
|
|
79b18828d4 | ||
|
|
3346c91f96 | ||
|
|
979f712fbb | ||
|
|
59648c77d8 | ||
|
|
92848305d9 | ||
|
|
0e7223ef35 | ||
|
|
769a7ffc7c | ||
|
|
bea75d49c1 | ||
|
|
65b38c06dc | ||
|
|
526190d444 | ||
|
|
f89573e718 | ||
|
|
2c22fd6d4e | ||
|
|
1c245e61e4 | ||
|
|
1687f1d6b9 | ||
|
|
8340d93ab7 | ||
|
|
9acb9fa50f | ||
|
|
2b0cd260ce | ||
|
|
6cd1fc844d | ||
|
|
a503441539 | ||
|
|
5d225c820f | ||
|
|
1636e19937 | ||
|
|
af2975bcc4 | ||
|
|
601864a3e9 | ||
|
|
b248303487 | ||
|
|
6ca64ea3eb | ||
|
|
e6308c4caa | ||
|
|
498b0acbf9 | ||
|
|
5012c9afab | ||
|
|
8045477abf | ||
|
|
498cf7f21b | ||
|
|
679b02f790 | ||
|
|
9bdf847f56 | ||
|
|
678cf4b3c6 | ||
|
|
4299c78067 | ||
|
|
b49d565213 | ||
|
|
85b93be277 | ||
|
|
a754a2ed9f | ||
|
|
7547de6077 | ||
|
|
f7340015b5 | ||
|
|
ba8d23fada | ||
|
|
0a3332bd10 | ||
|
|
106f5b0ce2 | ||
|
|
da1e666a72 | ||
|
|
3873e2860b | ||
|
|
57d229f94b | ||
|
|
7705402360 | ||
|
|
6614783450 | ||
|
|
c50fe0ee4a | ||
|
|
8ba8726bc2 | ||
|
|
e9eba5a892 | ||
|
|
2d73dde9ff | ||
|
|
da13b3b66d | ||
|
|
80c122b3b7 | ||
|
|
6fec81deb9 | ||
|
|
30f1617d13 | ||
|
|
e9612ce6ae | ||
|
|
48db8930cc | ||
|
|
a54f6942ba | ||
|
|
37e72cda57 | ||
|
|
b783ded2e7 | ||
|
|
7d91c3548e | ||
|
|
fde5a304e8 | ||
|
|
21e26e2a14 | ||
|
|
06233759d9 | ||
|
|
9e0c189abe | ||
|
|
33180878d9 | ||
|
|
074b420848 | ||
|
|
5dfdd0c058 | ||
|
|
f2352975f5 | ||
|
|
f0e7c22216 | ||
|
|
e7c87bae77 | ||
|
|
f8b1c05a35 | ||
|
|
f561362530 | ||
|
|
0420315db6 | ||
|
|
ecd151d1ea | ||
|
|
716f0adc95 | ||
|
|
4a17245e6e | ||
|
|
682264db13 | ||
|
|
6e40f53a16 | ||
|
|
714826213f | ||
|
|
429b0d9ce8 | ||
|
|
a9adf79551 | ||
|
|
9bb71ac76e | ||
|
|
65ee6d40bd | ||
|
|
100eac1f3c | ||
|
|
f2e73af32f | ||
|
|
894cd4d982 | ||
|
|
de15dbf405 | ||
|
|
ae5063b705 | ||
|
|
b8052ef5ba | ||
|
|
d2c28b12c6 | ||
|
|
98eab1fb4f | ||
|
|
71177b0b05 |
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
20
.github/workflows/docker-image-compat.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Docker Image CI (compat)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- compat
|
||||
|
||||
jobs:
|
||||
build-compat:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
tag: latest-compat
|
||||
target: main
|
||||
build-compat-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: latest-compat
|
||||
target: agent
|
||||
1
.github/workflows/docker-image-nightly.yml
vendored
1
.github/workflows/docker-image-nightly.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
- "**" # matches every branch
|
||||
- "!dependabot/*"
|
||||
- "!main" # excludes main
|
||||
- "!compat" # excludes compat branch
|
||||
|
||||
jobs:
|
||||
build-nightly:
|
||||
|
||||
@@ -6,13 +6,12 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "socket-proxy/**"
|
||||
- "socket-proxy.Dockerfile"
|
||||
- ".github/workflows/docker-image-socket-proxy.yml"
|
||||
tags-ignore:
|
||||
- '**'
|
||||
- "**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
|
||||
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
39
.github/workflows/merge-main-into-compat.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Cherry-pick into Compat
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
paths:
|
||||
- ".github/workflows/merge-main-into-compat.yml"
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure git user
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
- name: Cherry-pick commits from last tag
|
||||
run: |
|
||||
git fetch origin compat
|
||||
git checkout compat
|
||||
CURRENT_TAG=${{ github.ref_name }}
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
|
||||
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
|
||||
else
|
||||
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
|
||||
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
|
||||
fi
|
||||
- name: Push compat
|
||||
run: |
|
||||
git push origin compat
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ tsconfig.tsbuildinfo
|
||||
|
||||
!agent.compose.yml
|
||||
!agent/pkg/**
|
||||
dev-data/
|
||||
92
.kilocode/rules/godoxy.md
Normal file
92
.kilocode/rules/godoxy.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# godoxy.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
GoDoxy is a lightweight reverse proxy with WebUI written in Go. It automatically configures routes for Docker containers using labels and provides SSL certificate management, access control, and monitoring capabilities.
|
||||
|
||||
## Development Commands
|
||||
|
||||
You should not run any commands to build or run the project.
|
||||
|
||||
## Documentation
|
||||
|
||||
If `README.md` exists in the package:
|
||||
|
||||
- Read it to understand what the package does and how it works.
|
||||
- Update it with the changes you made (if any).
|
||||
|
||||
Update the roort `README.md` if relevant.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Main Entry Point** (`cmd/main.go`)
|
||||
|
||||
- Initializes configuration and logging
|
||||
- Starts the proxy server and WebUI
|
||||
|
||||
2. **Internal Packages** (`internal/`)
|
||||
|
||||
- `config/` - Configuration management with YAML files
|
||||
- `route/` - HTTP routing and middleware management
|
||||
- `docker/` - Docker container integration and auto-discovery
|
||||
- `autocert/` - SSL certificate management with Let's Encrypt
|
||||
- `acl/` - Access control lists (IP/CIDR, country, timezone)
|
||||
- `api/` - REST API server with Swagger documentation
|
||||
- `homepage/` - WebUI dashboard and configuration interface
|
||||
- `metrics/` - System metrics and uptime monitoring
|
||||
- `idlewatcher/` - Container idle detection and power management
|
||||
- `watcher/` - File and container state watchers
|
||||
|
||||
3. **Sub-projects**
|
||||
- `agent/` - System agent for monitoring containers
|
||||
- `socket-proxy/` - Docker socket proxy
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Task Management**: Use `internal/task/task.go` for managing object lifetimes and background operations
|
||||
- **Concurrent Maps**: Use `github.com/puzpuzpuz/xsync/v4` instead of maps with mutexes
|
||||
- **Error Handling**: Use `pkg/gperr` for nested errors with subjects
|
||||
- **Configuration**: YAML-based configuration in `config/` directory
|
||||
|
||||
## Go Guidelines (from .cursor/rules/go.mdc)
|
||||
|
||||
1. Use builtin `min` and `max` functions instead of custom helpers
|
||||
2. Prefer range-over-integer syntax (`for i := range 10`) over traditional loops
|
||||
3. Use `xsync/v4` for concurrent maps instead of map+mutex
|
||||
4. Beware of variable shadowing when making edits
|
||||
5. Use `internal/task/task.go` for lifetime management:
|
||||
- `task.RootTask()` for background operations
|
||||
- `parent.Subtask()` for nested tasks
|
||||
- `OnFinished()` and `OnCancel()` callbacks for cleanup
|
||||
6. Use `pkg/gperr` for complex error scenarios:
|
||||
- `gperr.Multiline()` for multiple operation attempts
|
||||
- `gperr.NewBuilder()` for collecting multiple errors
|
||||
- `gperr.NewGroup() + group.Go()` for collecting errors of multiple concurrent operations
|
||||
- `gperr.New().Subject()` for errors with subjects
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
- `config/config.yml` - Main configuration
|
||||
- `config/middlewares/` - HTTP middleware configurations
|
||||
- `config/*.yml` - Provider and service configurations
|
||||
- `.env` - Environment variables for ports and settings
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are located in `internal/` packages and can be run with:
|
||||
|
||||
- `make test` - Run all internal package tests
|
||||
- Individual package tests: `go test ./internal/config/...`
|
||||
- You MUST use `-ldflags="-checklinkname=0"` otherwise compiler will complain
|
||||
- prefer `testify/require` over `expect`
|
||||
|
||||
## Docker Integration
|
||||
|
||||
GoDoxy integrates with Docker by:
|
||||
|
||||
1. Listing all containers and reading their labels
|
||||
2. Creating routes based on `proxy.aliases` label or container name
|
||||
3. Watching for container/config changes and updating automatically
|
||||
4. Supporting both Docker and Podman runtimes
|
||||
97
CLAUDE.md
Normal file
97
CLAUDE.md
Normal file
@@ -0,0 +1,97 @@
|
||||
______________________________________________________________________
|
||||
|
||||
## alwaysApply: true
|
||||
|
||||
# Cursor Rules
|
||||
|
||||
## Project Overview
|
||||
|
||||
GoDoxy is a lightweight reverse proxy with WebUI written in Go. It automatically configures routes for Docker containers using labels and provides SSL certificate management, access control, and monitoring capabilities.
|
||||
|
||||
## Development Commands
|
||||
|
||||
You should not run any commands to build or run the project.
|
||||
|
||||
## Documentation
|
||||
|
||||
If `README.md` exists in the package:
|
||||
|
||||
- Read it to understand what the package does and how it works.
|
||||
- Update it with the changes you made (if any).
|
||||
|
||||
Update the roort `README.md` if relevant.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Main Entry Point** (`cmd/main.go`)
|
||||
|
||||
- Initializes configuration and logging
|
||||
- Starts the proxy server and WebUI
|
||||
|
||||
1. **Internal Packages** (`internal/`)
|
||||
|
||||
- `config/` - Configuration management with YAML files
|
||||
- `route/` - HTTP routing and middleware management
|
||||
- `docker/` - Docker container integration and auto-discovery
|
||||
- `autocert/` - SSL certificate management with Let's Encrypt
|
||||
- `acl/` - Access control lists (IP/CIDR, country, timezone)
|
||||
- `api/` - REST API server with Swagger documentation
|
||||
- `homepage/` - WebUI dashboard and configuration interface
|
||||
- `metrics/` - System metrics and uptime monitoring
|
||||
- `idlewatcher/` - Container idle detection and power management
|
||||
- `watcher/` - File and container state watchers
|
||||
|
||||
1. **Sub-projects**
|
||||
|
||||
- `agent/` - System agent for monitoring containers
|
||||
- `socket-proxy/` - Docker socket proxy
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Task Management**: Use `internal/task/task.go` for managing object lifetimes and background operations
|
||||
- **Concurrent Maps**: Use `github.com/puzpuzpuz/xsync/v4` instead of maps with mutexes
|
||||
- **Error Handling**: Use `pkg/gperr` for nested errors with subjects
|
||||
- **Configuration**: YAML-based configuration in `config/` directory
|
||||
|
||||
## Go Guidelines (from .cursor/rules/go.mdc)
|
||||
|
||||
1. Use builtin `min` and `max` functions instead of custom helpers
|
||||
1. Prefer range-over-integer syntax (`for i := range 10`) over traditional loops
|
||||
1. Use `xsync/v4` for concurrent maps instead of map+mutex
|
||||
1. Beware of variable shadowing when making edits
|
||||
1. Use `internal/task/task.go` for lifetime management:
|
||||
- `task.RootTask()` for background operations
|
||||
- `parent.Subtask()` for nested tasks
|
||||
- `OnFinished()` and `OnCancel()` callbacks for cleanup
|
||||
1. Use `pkg/gperr` for complex error scenarios:
|
||||
- `gperr.Multiline()` for multiple operation attempts
|
||||
- `gperr.NewBuilder()` for collecting multiple errors
|
||||
- `gperr.NewGroup() + group.Go()` for collecting errors of multiple concurrent operations
|
||||
- `gperr.New().Subject()` for errors with subjects
|
||||
|
||||
## Configuration Structure
|
||||
|
||||
- `config/config.yml` - Main configuration
|
||||
- `config/middlewares/` - HTTP middleware configurations
|
||||
- `config/*.yml` - Provider and service configurations
|
||||
- `.env` - Environment variables for ports and settings
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are located in `internal/` packages and can be run with:
|
||||
|
||||
- `make test` - Run all internal package tests
|
||||
- Individual package tests: `go test ./internal/config/...`
|
||||
- You MUST use `-ldflags="-checklinkname=0"` otherwise compiler will complain
|
||||
- prefer `testify/require` over `expect`
|
||||
|
||||
## Docker Integration
|
||||
|
||||
GoDoxy integrates with Docker by:
|
||||
|
||||
1. Listing all containers and reading their labels
|
||||
1. Creating routes based on `proxy.aliases` label or container name
|
||||
1. Watching for container/config changes and updating automatically
|
||||
1. Supporting both Docker and Podman runtimes
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: deps
|
||||
FROM golang:1.25.4-alpine AS deps
|
||||
FROM golang:1.25.5-alpine AS deps
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
@@ -19,7 +19,9 @@ COPY go.mod go.sum ./
|
||||
# remove godoxy stuff from go.mod first
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/root/go/pkg/mod \
|
||||
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && go mod download -x
|
||||
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
|
||||
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
|
||||
go mod download -x
|
||||
|
||||
# Stage 2: builder
|
||||
FROM deps AS builder
|
||||
|
||||
25
Makefile
25
Makefile
@@ -35,7 +35,7 @@ else ifeq ($(debug), 1)
|
||||
CGO_ENABLED = 1
|
||||
GODOXY_DEBUG = 1
|
||||
GO_TAGS += debug
|
||||
BUILD_FLAGS += -asan # FIXME: -gcflags=all='-N -l'
|
||||
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
|
||||
else ifeq ($(pprof), 1)
|
||||
CGO_ENABLED = 0
|
||||
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
|
||||
@@ -75,11 +75,12 @@ endif
|
||||
.PHONY: debug
|
||||
|
||||
test:
|
||||
go test -v -race ./internal/...
|
||||
CGO_ENABLED=1 go test -v -race ${BUILD_FLAGS} ./internal/...
|
||||
|
||||
docker-build-test:
|
||||
docker build -t godoxy .
|
||||
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
|
||||
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
|
||||
|
||||
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
|
||||
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
|
||||
@@ -110,7 +111,7 @@ mod-tidy:
|
||||
|
||||
build:
|
||||
mkdir -p $(shell dirname ${BIN_PATH})
|
||||
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
|
||||
${POST_BUILD}
|
||||
|
||||
run:
|
||||
@@ -122,6 +123,15 @@ dev:
|
||||
dev-build: build
|
||||
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
|
||||
|
||||
benchmark:
|
||||
@if [ -z "$(TARGET)" ]; then \
|
||||
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
|
||||
else \
|
||||
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
|
||||
fi
|
||||
sleep 1
|
||||
@./scripts/benchmark.sh
|
||||
|
||||
dev-run: build
|
||||
cd dev-data && ${BIN_PATH}
|
||||
|
||||
@@ -141,12 +151,13 @@ ci-test:
|
||||
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
|
||||
|
||||
cloc:
|
||||
cloc --include-lang=Go --not-match-f '_test.go$$' .
|
||||
scc -w -i go --not-match '_test.go$$'
|
||||
|
||||
push-github:
|
||||
git push origin $(shell git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
gen-swagger:
|
||||
# go install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
|
||||
python3 scripts/fix-swagger-json.py
|
||||
# we don't need this
|
||||
@@ -160,4 +171,8 @@ gen-api-types: gen-swagger
|
||||
# --disable-throw-on-error
|
||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||
|
||||
.PHONY: update-wiki
|
||||
update-wiki:
|
||||
DOCS_DIR=${DOCS_DIR} bun --bun scripts/update-wiki/main.ts
|
||||
|
||||
36
RELEASE_NOTES.md
Normal file
36
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# GoDoxy v0.24.0 Release Notes
|
||||
|
||||
## New
|
||||
|
||||
- **Core/Agent**: stream tunneling (TLS/dTLS) with multiplexed TLS port supporting HTTP API and custom stream protocol via ALPN #188
|
||||
- **Core/Agent**: TCP and DTLS/UDP stream tunneling with health-check support and runtime capability detection
|
||||
- **Core/Router**: TCP/UDP route configurable bind address support
|
||||
- **WebUI/Autocert**: multiple certificates display with Carousel component
|
||||
- **WebUI/Agent**: stream support status display (TCP/UDP capabilities)
|
||||
|
||||
## Changes
|
||||
|
||||
- **Core/Agent**: refactored HTTP server to use direct setup instead of goutils/server helper
|
||||
- **Core/HealthCheck**: restructured into dedicated `internal/health/check/` package
|
||||
- **Core/Dockerfile**: updated to use bun runtime with distroless base images
|
||||
- **Core/Compose**: updated agent compose template to expose TCP and UDP ports for stream tunneling
|
||||
|
||||
## Fixes
|
||||
|
||||
- **Core/Stream**: fixed stream headers with read deadlines to prevent hangs
|
||||
- **Core/Icon**: fixed icons provider initialization on first load
|
||||
- **Core/Docker**: fixed TLS verification and dial handling for custom Docker providers
|
||||
- **Core/Stream**: fixed hostname handling for stream routes
|
||||
- **Core/HTTP**: fixed HTTPS redirect for IPv6 with `net.JoinHostPort`
|
||||
- **Core/Stream**: fixed remote stream scheme for IPv4 and IPv6 addresses
|
||||
- **Core/HealthCheck**: fixed panic on TLS errors during HTTP health checks
|
||||
- **Core/Stream**: fixed nil panic for excluded routes
|
||||
- **Core/Store**: fixed empty segment handling in nested paths
|
||||
|
||||
## Refactoring
|
||||
|
||||
- **Core/Agent**: extracted agent pool and HTTP utilities to dedicated package
|
||||
- **Core/Route**: improved References method for FQDN alias handling
|
||||
- **Core/Icon**: reorganized with health checking and retry logic
|
||||
- **Core/Error**: replaced `gperr.Builder` with `gperr.Group` for concurrent error handling
|
||||
- **Core/Utils**: removed `internal/utils` entirely, moved `RefCounter` to goutils
|
||||
52
agent/cmd/README.md
Normal file
52
agent/cmd/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# agent/cmd
|
||||
|
||||
The main entry point for the GoDoxy Agent, a secure monitoring and proxy agent that runs alongside Docker containers.
|
||||
|
||||
## Overview
|
||||
|
||||
This package contains the `main.go` entry point for the GoDoxy Agent. The agent is a TLS-enabled server that provides:
|
||||
|
||||
- Secure Docker socket proxying with client certificate authentication
|
||||
- HTTP proxy capabilities for container traffic
|
||||
- System metrics collection and monitoring
|
||||
- Health check endpoints
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[main] --> B[Logger Init]
|
||||
A --> C[Load CA Certificate]
|
||||
A --> D[Load Server Certificate]
|
||||
A --> E[Log Version Info]
|
||||
A --> F[Start Agent Server]
|
||||
A --> G[Start Socket Proxy]
|
||||
A --> H[Start System Info Poller]
|
||||
A --> I[Wait Exit]
|
||||
|
||||
F --> F1[TLS with mTLS]
|
||||
F --> F2[Agent Handler]
|
||||
G --> G1[Docker Socket Proxy]
|
||||
```
|
||||
|
||||
## Main Function Flow
|
||||
|
||||
1. **Logger Setup**: Configures zerolog with console output
|
||||
1. **Certificate Loading**: Loads CA and server certificates for TLS/mTLS
|
||||
1. **Version Logging**: Logs agent version and configuration
|
||||
1. **Agent Server**: Starts the main HTTPS server with agent handlers
|
||||
1. **Socket Proxy**: Starts Docker socket proxy if configured
|
||||
1. **System Monitoring**: Starts system info polling
|
||||
1. **Graceful Shutdown**: Waits for exit signal (3 second timeout)
|
||||
|
||||
## Configuration
|
||||
|
||||
See `agent/pkg/env/README.md` for configuration options.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `agent/pkg/agent` - Core agent types and constants
|
||||
- `agent/pkg/env` - Environment configuration
|
||||
- `agent/pkg/server` - Server implementation
|
||||
- `socketproxy/pkg` - Docker socket proxy
|
||||
- `internal/metrics/systeminfo` - System metrics
|
||||
@@ -1,21 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
stdlog "log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/server"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
|
||||
httpServer "github.com/yusing/goutils/server"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
// TODO: support IPv6
|
||||
|
||||
func main() {
|
||||
writer := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
@@ -52,27 +63,102 @@ func main() {
|
||||
Tips:
|
||||
1. To change the agent name, you can set the AGENT_NAME environment variable.
|
||||
2. To change the agent port, you can set the AGENT_PORT environment variable.
|
||||
`)
|
||||
`)
|
||||
|
||||
t := task.RootTask("agent", false)
|
||||
opts := server.Options{
|
||||
CACert: caCert,
|
||||
ServerCert: srvCert,
|
||||
Port: env.AgentPort,
|
||||
|
||||
// One TCP listener on AGENT_PORT, then multiplex by TLS ALPN:
|
||||
// - Stream ALPN: route to TCP stream tunnel handler (via http.Server.TLSNextProto)
|
||||
// - Otherwise: route to HTTPS API handler
|
||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
|
||||
if err != nil {
|
||||
gperr.LogFatal("failed to listen on port", err)
|
||||
}
|
||||
|
||||
server.StartAgentServer(t, opts)
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert.Leaf)
|
||||
|
||||
muxTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*srvCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// Keep HTTP limited to HTTP/1.1 (matching current agent server behavior)
|
||||
// and add the stream tunnel ALPN for multiplexing.
|
||||
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||
}
|
||||
if env.AgentSkipClientCertCheck {
|
||||
muxTLSConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
// TLS listener feeds the HTTP server. ALPN stream connections are intercepted
|
||||
// using http.Server.TLSNextProto.
|
||||
tlsLn := tls.NewListener(tcpListener, muxTLSConfig)
|
||||
|
||||
streamSrv := stream.NewTCPServerHandler(t.Context())
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Handler: handler.NewAgentHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
// When a client negotiates StreamALPN, net/http will call this hook instead
|
||||
// of treating the connection as HTTP.
|
||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||
// ServeConn blocks until the tunnel finishes.
|
||||
streamSrv.ServeConn(conn)
|
||||
},
|
||||
},
|
||||
}
|
||||
{
|
||||
subtask := t.Subtask("agent-http", true)
|
||||
t.OnCancel("stop_http", func() {
|
||||
_ = streamSrv.Close()
|
||||
_ = httpSrv.Close()
|
||||
_ = tlsLn.Close()
|
||||
})
|
||||
go func() {
|
||||
err := httpSrv.Serve(tlsLn)
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error().Err(err).Msg("agent HTTP server stopped with error")
|
||||
}
|
||||
subtask.Finish(err)
|
||||
}()
|
||||
log.Info().Int("port", env.AgentPort).Msg("HTTPS API server started (ALPN mux enabled)")
|
||||
}
|
||||
log.Info().Int("port", env.AgentPort).Msg("TCP stream handler started (via TLSNextProto)")
|
||||
|
||||
{
|
||||
udpServer := stream.NewUDPServer(t.Context(), "udp", &net.UDPAddr{Port: env.AgentPort}, caCert.Leaf, srvCert)
|
||||
subtask := t.Subtask("agent-stream-udp", true)
|
||||
t.OnCancel("stop_stream_udp", func() {
|
||||
_ = udpServer.Close()
|
||||
})
|
||||
go func() {
|
||||
err := udpServer.Start()
|
||||
subtask.Finish(err)
|
||||
}()
|
||||
log.Info().Int("port", env.AgentPort).Msg("UDP stream server started")
|
||||
}
|
||||
|
||||
if socketproxy.ListenAddr != "" {
|
||||
runtime := strutils.Title(string(env.Runtime))
|
||||
|
||||
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
|
||||
opts := httpServer.Options{
|
||||
Name: runtime,
|
||||
HTTPAddr: socketproxy.ListenAddr,
|
||||
Handler: socketproxy.NewHandler(),
|
||||
l, err := net.Listen("tcp", socketproxy.ListenAddr)
|
||||
if err != nil {
|
||||
gperr.LogFatal("failed to listen on port", err)
|
||||
}
|
||||
httpServer.StartServer(t, opts)
|
||||
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
|
||||
srv := http.Server{
|
||||
Handler: socketproxy.NewHandler(),
|
||||
BaseContext: func(net.Listener) context.Context {
|
||||
return t.Context()
|
||||
},
|
||||
ErrorLog: stdlog.New(&errLog, "", 0),
|
||||
}
|
||||
srv.Serve(l)
|
||||
}
|
||||
|
||||
systeminfo.Poller.Start()
|
||||
|
||||
85
agent/go.mod
85
agent/go.mod
@@ -1,106 +1,111 @@
|
||||
module github.com/yusing/godoxy/agent
|
||||
|
||||
go 1.25.4
|
||||
go 1.25.5
|
||||
|
||||
replace github.com/yusing/godoxy => ..
|
||||
|
||||
replace github.com/yusing/godoxy/socketproxy => ../socket-proxy
|
||||
|
||||
replace github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
|
||||
replace github.com/yusing/goutils => ../goutils
|
||||
replace (
|
||||
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
|
||||
github.com/yusing/godoxy => ../
|
||||
github.com/yusing/godoxy/socketproxy => ../socket-proxy
|
||||
github.com/yusing/goutils => ../goutils
|
||||
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
|
||||
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
|
||||
github.com/yusing/goutils/server => ../goutils/server
|
||||
)
|
||||
|
||||
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
|
||||
|
||||
exclude github.com/yusing/godoxy/internal/utils v0.0.0-20250927032450-e2aeef3a863f
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.2
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||
github.com/pion/dtls/v3 v3.0.10
|
||||
github.com/pion/transport/v3 v3.1.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/yusing/godoxy v0.20.2
|
||||
github.com/yusing/godoxy v0.23.1
|
||||
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/yusing/goutils v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
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 v29.0.1+incompatible // indirect
|
||||
github.com/docker/cli v29.1.4+incompatible // indirect
|
||||
github.com/docker/docker v28.5.2+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.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gotify/server/v2 v2.7.3 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
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/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.1.0 // indirect
|
||||
github.com/moby/moby/client v0.2.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.56.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
github.com/yusing/ds v0.3.1 // indirect
|
||||
github.com/yusing/gointernals v0.1.16 // indirect
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109025656-326c1f1eb362 // indirect
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260109025656-326c1f1eb362 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
223
agent/go.sum
223
agent/go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
@@ -16,14 +18,18 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -35,8 +41,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
|
||||
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
||||
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+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=
|
||||
@@ -47,14 +55,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -71,18 +79,17 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
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=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -92,14 +99,16 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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=
|
||||
@@ -112,8 +121,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -123,19 +132,27 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
|
||||
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -144,8 +161,17 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -154,10 +180,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -194,13 +220,12 @@ github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
|
||||
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
|
||||
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
|
||||
@@ -209,113 +234,63 @@ 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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
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=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
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=
|
||||
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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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.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=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
108
agent/pkg/agent/README.md
Normal file
108
agent/pkg/agent/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Agent Package
|
||||
|
||||
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph GoDoxy Server
|
||||
AP[Agent Pool] --> AC[AgentConfig]
|
||||
end
|
||||
|
||||
subgraph Agent Communication
|
||||
AC -->|HTTPS| AI[Agent Info API]
|
||||
AC -->|TLS| ST[Stream Tunneling]
|
||||
end
|
||||
|
||||
subgraph Deployment
|
||||
G[Generator] --> DC[Docker Compose]
|
||||
G --> IS[Install Script]
|
||||
end
|
||||
|
||||
subgraph Security
|
||||
NA[NewAgent] --> Certs[Certificates]
|
||||
end
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [`config.go`](agent/pkg/agent/config.go) | Core configuration, initialization, and API client logic. |
|
||||
| [`new_agent.go`](agent/pkg/agent/new_agent.go) | Agent creation and certificate generation logic. |
|
||||
| [`docker_compose.go`](agent/pkg/agent/docker_compose.go) | Generator for agent Docker Compose configurations. |
|
||||
| [`bare_metal.go`](agent/pkg/agent/bare_metal.go) | Generator for bare metal installation scripts. |
|
||||
| [`env.go`](agent/pkg/agent/env.go) | Environment configuration types and constants. |
|
||||
| [`common/`](agent/pkg/agent/common) | Shared constants and utilities for agents. |
|
||||
|
||||
## Core Types
|
||||
|
||||
### [`AgentConfig`](agent/pkg/agent/config.go:29)
|
||||
|
||||
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
|
||||
|
||||
### [`AgentInfo`](agent/pkg/agent/config.go:45)
|
||||
|
||||
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
|
||||
|
||||
### [`PEMPair`](agent/pkg/agent/new_agent.go:53)
|
||||
|
||||
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
|
||||
|
||||
## Agent Creation and Certificate Management
|
||||
|
||||
### Certificate Generation
|
||||
|
||||
The [`NewAgent`](agent/pkg/agent/new_agent.go:147) function creates a complete certificate infrastructure for an agent:
|
||||
|
||||
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
|
||||
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
|
||||
- **Client Certificate**: For the GoDoxy server to authenticate with the agent.
|
||||
|
||||
All certificates use ECDSA with P-256 curve and SHA-256 signatures.
|
||||
|
||||
### Certificate Security
|
||||
|
||||
- Certificates are encrypted using AES-GCM with a provided encryption key.
|
||||
- The [`PEMPair`](agent/pkg/agent/new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
|
||||
- Base64 encoding is used for certificate storage and transmission.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Secure Communication
|
||||
|
||||
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](agent/pkg/agent/config.go:29) handles the loading of CA and client certificates to establish secure connections.
|
||||
|
||||
### 2. Agent Discovery and Initialization
|
||||
|
||||
The [`Init`](agent/pkg/agent/config.go:231) and [`InitWithCerts`](agent/pkg/agent/config.go:110) methods allow the server to:
|
||||
|
||||
- Fetch agent metadata (version, name, runtime).
|
||||
- Verify compatibility between server and agent versions.
|
||||
- Test support for TCP and UDP stream tunneling.
|
||||
|
||||
### 3. Deployment Generators
|
||||
|
||||
The package provides interfaces and implementations for generating deployment artifacts:
|
||||
|
||||
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](agent/pkg/agent/docker_compose.go:21).
|
||||
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](agent/pkg/agent/bare_metal.go:27).
|
||||
|
||||
### 4. Fake Docker Host
|
||||
|
||||
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](agent/pkg/agent/config.go:90) and [`GetAgentAddrFromDockerHost`](agent/pkg/agent/config.go:94).
|
||||
|
||||
## Usage Example
|
||||
|
||||
```go
|
||||
cfg := &agent.AgentConfig{}
|
||||
cfg.Parse("192.168.1.100:8081")
|
||||
|
||||
ctx := context.Background()
|
||||
if err := cfg.Init(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Connected to agent: %s (Version: %s)\n", cfg.Name, cfg.Version)
|
||||
```
|
||||
@@ -1,68 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
)
|
||||
|
||||
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
|
||||
|
||||
func init() {
|
||||
if strings.HasSuffix(os.Args[0], ".test") {
|
||||
agentPool.Store("test-agent", &AgentConfig{
|
||||
Addr: "test-agent",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
||||
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||
return getAgentByAddr(agentAddrOrDockerHost)
|
||||
}
|
||||
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||
}
|
||||
|
||||
func GetAgentByName(name string) (*AgentConfig, bool) {
|
||||
for _, agent := range agentPool.Range {
|
||||
if agent.Name == name {
|
||||
return agent, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func AddAgent(agent *AgentConfig) {
|
||||
agentPool.Store(agent.Addr, agent)
|
||||
}
|
||||
|
||||
func RemoveAgent(agent *AgentConfig) {
|
||||
agentPool.Delete(agent.Addr)
|
||||
}
|
||||
|
||||
func RemoveAllAgents() {
|
||||
agentPool.Clear()
|
||||
}
|
||||
|
||||
func ListAgents() []*AgentConfig {
|
||||
agents := make([]*AgentConfig, 0, agentPool.Size())
|
||||
for _, agent := range agentPool.Range {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
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 agent, ok
|
||||
}
|
||||
3
agent/pkg/agent/common/common.go
Normal file
3
agent/pkg/agent/common/common.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package common
|
||||
|
||||
const CertsDNSName = "godoxy.agent"
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -15,33 +18,51 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
agentstream "github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/version"
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
Version version.Version `json:"version" swaggertype:"string"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
AgentInfo
|
||||
|
||||
httpClient *http.Client
|
||||
fasthttpClientHealthCheck *fasthttp.Client
|
||||
tlsConfig tls.Config
|
||||
l zerolog.Logger
|
||||
Addr string `json:"addr"`
|
||||
IsTCPStreamSupported bool `json:"supports_tcp_stream"`
|
||||
IsUDPStreamSupported bool `json:"supports_udp_stream"`
|
||||
|
||||
// for stream
|
||||
caCert *x509.Certificate
|
||||
clientCert *tls.Certificate
|
||||
|
||||
tlsConfig tls.Config
|
||||
|
||||
l zerolog.Logger
|
||||
} // @name Agent
|
||||
|
||||
type AgentInfo struct {
|
||||
Version version.Version `json:"version" swaggertype:"string"`
|
||||
Name string `json:"name"`
|
||||
Runtime ContainerRuntime `json:"runtime"`
|
||||
}
|
||||
|
||||
// Deprecated. Replaced by EndpointInfo
|
||||
const (
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
EndpointVersion = "/version"
|
||||
EndpointName = "/name"
|
||||
EndpointRuntime = "/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
EndpointInfo = "/info"
|
||||
EndpointProxyHTTP = "/proxy/http"
|
||||
EndpointHealth = "/health"
|
||||
EndpointLogs = "/logs"
|
||||
EndpointSystemInfo = "/system_info"
|
||||
|
||||
AgentHost = CertsDNSName
|
||||
AgentHost = common.CertsDNSName
|
||||
|
||||
APIEndpointBase = "/godoxy/agent"
|
||||
APIBaseURL = "https://" + AgentHost + APIEndpointBase
|
||||
@@ -85,11 +106,13 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
||||
|
||||
var serverVersion = version.Get()
|
||||
|
||||
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||
// InitWithCerts initializes the agent config with the given CA, certificate, and key.
|
||||
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||
clientCert, err := tls.X509KeyPair(crt, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.clientCert = &clientCert
|
||||
|
||||
// create tls config
|
||||
caCertPool := x509.NewCertPool()
|
||||
@@ -97,64 +120,105 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
if !ok {
|
||||
return errors.New("invalid ca certificate")
|
||||
}
|
||||
// Keep the CA leaf for stream client dialing.
|
||||
if block, _ := pem.Decode(ca); block == nil || block.Type != "CERTIFICATE" {
|
||||
return errors.New("invalid ca certificate")
|
||||
} else if cert, err := x509.ParseCertificate(block.Bytes); err != nil {
|
||||
return err
|
||||
} else {
|
||||
cfg.caCert = cert
|
||||
}
|
||||
|
||||
cfg.tlsConfig = tls.Config{
|
||||
Certificates: []tls.Certificate{clientCert},
|
||||
RootCAs: caCertPool,
|
||||
ServerName: CertsDNSName,
|
||||
ServerName: common.CertsDNSName,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// create transport and http client
|
||||
cfg.httpClient = cfg.NewHTTPClient()
|
||||
applyNormalTransportConfig(cfg.httpClient)
|
||||
|
||||
cfg.fasthttpClientHealthCheck = cfg.NewFastHTTPHealthCheckClient()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// get agent name
|
||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||
status, err := cfg.fetchJSON(ctx, EndpointInfo, &cfg.AgentInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
var streamUnsupportedErrs gperr.Builder
|
||||
|
||||
if status == http.StatusOK {
|
||||
// test stream server connection
|
||||
const fakeAddress = "localhost:8080" // it won't be used, just for testing
|
||||
// test TCP stream support
|
||||
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
|
||||
} else {
|
||||
cfg.IsTCPStreamSupported = true
|
||||
}
|
||||
|
||||
// test UDP stream support
|
||||
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
|
||||
if err != nil {
|
||||
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
|
||||
} else {
|
||||
cfg.IsUDPStreamSupported = true
|
||||
}
|
||||
} else {
|
||||
// old agent does not support EndpointInfo
|
||||
// fallback with old logic
|
||||
cfg.IsTCPStreamSupported = false
|
||||
cfg.IsUDPStreamSupported = false
|
||||
streamUnsupportedErrs.Adds("agent version is too old, does not support stream tunneling")
|
||||
|
||||
// get agent name
|
||||
name, _, err := cfg.fetchString(ctx, EndpointName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Name = name
|
||||
|
||||
// check agent version
|
||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Version = version.Parse(agentVersion)
|
||||
|
||||
// check agent runtime
|
||||
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch runtime {
|
||||
case "docker":
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
// case "nerdctl":
|
||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||
case "podman":
|
||||
cfg.Runtime = ContainerRuntimePodman
|
||||
default:
|
||||
return fmt.Errorf("invalid agent runtime: %s", runtime)
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
// backward compatibility, old agent does not have runtime endpoint
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
default:
|
||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.l = log.With().Str("agent", cfg.Name).Logger()
|
||||
|
||||
// check agent version
|
||||
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := streamUnsupportedErrs.Error(); err != nil {
|
||||
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
|
||||
}
|
||||
|
||||
// check agent runtime
|
||||
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch runtime {
|
||||
case "docker":
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
// case "nerdctl":
|
||||
// cfg.Runtime = ContainerRuntimeNerdctl
|
||||
case "podman":
|
||||
cfg.Runtime = ContainerRuntimePodman
|
||||
default:
|
||||
return fmt.Errorf("invalid agent runtime: %s", runtime)
|
||||
}
|
||||
case http.StatusNotFound:
|
||||
// backward compatibility, old agent does not have runtime endpoint
|
||||
cfg.Runtime = ContainerRuntimeDocker
|
||||
default:
|
||||
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
|
||||
}
|
||||
|
||||
cfg.Version = version.Parse(agentVersion)
|
||||
|
||||
if serverVersion.IsNewerThanMajor(cfg.Version) {
|
||||
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
|
||||
}
|
||||
@@ -163,7 +227,8 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Start(ctx context.Context) error {
|
||||
// Init initializes the agent config with the given context.
|
||||
func (cfg *AgentConfig) Init(ctx context.Context) error {
|
||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
|
||||
@@ -179,32 +244,39 @@ func (cfg *AgentConfig) Start(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to extract agent certs: %w", err)
|
||||
}
|
||||
|
||||
return cfg.StartWithCerts(ctx, ca, crt, key)
|
||||
return cfg.InitWithCerts(ctx, ca, crt, key)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: cfg.Transport(),
|
||||
// NewTCPClient creates a new TCP client for the agent.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the agent is not initialized
|
||||
// - the agent does not support TCP stream tunneling
|
||||
// - the agent stream server address is not initialized
|
||||
func (cfg *AgentConfig) NewTCPClient(targetAddress string) (net.Conn, error) {
|
||||
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||
return nil, errors.New("agent is not initialized")
|
||||
}
|
||||
if !cfg.IsTCPStreamSupported {
|
||||
return nil, errors.New("agent does not support TCP stream tunneling")
|
||||
}
|
||||
return agentstream.NewTCPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) NewFastHTTPHealthCheckClient() *fasthttp.Client {
|
||||
return &fasthttp.Client{
|
||||
Dial: func(addr string) (net.Conn, error) {
|
||||
if addr != AgentHost+":443" {
|
||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||
}
|
||||
return net.Dial("tcp", cfg.Addr)
|
||||
},
|
||||
TLSConfig: &cfg.tlsConfig,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
DisableHeaderNamesNormalizing: true,
|
||||
DisablePathNormalizing: true,
|
||||
NoDefaultUserAgentHeader: true,
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// NewUDPClient creates a new UDP client for the agent.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the agent is not initialized
|
||||
// - the agent does not support UDP stream tunneling
|
||||
// - the agent stream server address is not initialized
|
||||
func (cfg *AgentConfig) NewUDPClient(targetAddress string) (net.Conn, error) {
|
||||
if cfg.caCert == nil || cfg.clientCert == nil {
|
||||
return nil, errors.New("agent is not initialized")
|
||||
}
|
||||
if !cfg.IsUDPStreamSupported {
|
||||
return nil, errors.New("agent does not support UDP stream tunneling")
|
||||
}
|
||||
return agentstream.NewUDPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Transport() *http.Transport {
|
||||
@@ -222,6 +294,10 @@ func (cfg *AgentConfig) Transport() *http.Transport {
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) TLSConfig() *tls.Config {
|
||||
return &cfg.tlsConfig
|
||||
}
|
||||
|
||||
var dialer = &net.Dialer{Timeout: 5 * time.Second}
|
||||
|
||||
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
@@ -232,10 +308,57 @@ func (cfg *AgentConfig) String() string {
|
||||
return cfg.Name + "@" + cfg.Addr
|
||||
}
|
||||
|
||||
func applyNormalTransportConfig(client *http.Client) {
|
||||
transport := client.Transport.(*http.Transport)
|
||||
transport.MaxIdleConns = 100
|
||||
transport.MaxIdleConnsPerHost = 100
|
||||
transport.ReadBufferSize = 16384
|
||||
transport.WriteBufferSize = 16384
|
||||
func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := http.Client{
|
||||
Transport: cfg.Transport(),
|
||||
}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
|
||||
resp, err := cfg.do(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, release, err := httputils.ReadAllBody(resp)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
ret := string(data)
|
||||
release(data)
|
||||
return ret, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// fetchJSON fetches a JSON response from the agent and unmarshals it into the provided struct
|
||||
//
|
||||
// It will return the status code of the response, and error if any.
|
||||
// If the status code is not http.StatusOK, out will be unchanged but error will still be nil.
|
||||
func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any) (int, error) {
|
||||
resp, err := cfg.do(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, release, err := httputils.ReadAllBody(resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer release(data)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, out)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resp.StatusCode, nil
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CertsDNSName = "godoxy.agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
|
||||
@@ -156,7 +154,7 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
SerialNumber: caSerialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"GoDoxy"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
|
||||
@@ -196,9 +194,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Server"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
DNSNames: []string{common.CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
@@ -228,9 +226,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
|
||||
Subject: pkix.Name{
|
||||
Organization: caTemplate.Subject.Organization,
|
||||
OrganizationalUnit: []string{"Client"},
|
||||
CommonName: CertsDNSName,
|
||||
CommonName: common.CertsDNSName,
|
||||
},
|
||||
DNSNames: []string{CertsDNSName},
|
||||
DNSNames: []string{common.CertsDNSName},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1000, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
func TestNewAgent(t *testing.T) {
|
||||
@@ -72,7 +73,7 @@ func TestServerClient(t *testing.T) {
|
||||
clientTLSConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*clientTLS},
|
||||
RootCAs: caPool,
|
||||
ServerName: CertsDNSName,
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
197
agent/pkg/agent/stream/README.md
Normal file
197
agent/pkg/agent/stream/README.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Stream proxy protocol
|
||||
|
||||
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
|
||||
|
||||
## Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Client
|
||||
TC[TCPClient] -->|TLS| TSS[TCPServer]
|
||||
UC[UDPClient] -->|DTLS| USS[UDPServer]
|
||||
end
|
||||
|
||||
subgraph Stream Protocol
|
||||
H[StreamRequestHeader]
|
||||
end
|
||||
|
||||
TSS -->|Redirect| DST1[Destination TCP]
|
||||
USS -->|Forward UDP| DST2[Destination UDP]
|
||||
```
|
||||
|
||||
## Header
|
||||
|
||||
The on-wire header is a fixed-size binary blob:
|
||||
|
||||
- `Version` (8 bytes)
|
||||
- `HostLength` (1 byte)
|
||||
- `Host` (255 bytes, NUL padded)
|
||||
- `PortLength` (1 byte)
|
||||
- `Port` (5 bytes, NUL padded)
|
||||
- `Flag` (1 byte, protocol flags)
|
||||
- `Checksum` (4 bytes, big-endian CRC32)
|
||||
|
||||
Total: `headerSize = 8 + 1 + 255 + 1 + 5 + 1 + 4 = 275` bytes.
|
||||
|
||||
Checksum is `crc32.ChecksumIEEE(header[0:headerSize-4])`.
|
||||
|
||||
### Flags
|
||||
|
||||
The `Flag` field is a bitmask of protocol flags defined by `FlagType`:
|
||||
|
||||
| Flag | Value | Purpose |
|
||||
| ---------------------- | ----- | ---------------------------------------------------------------------- |
|
||||
| `FlagCloseImmediately` | `1` | Health check probe - server closes immediately after validating header |
|
||||
|
||||
See [`FlagType`](header.go:26) and [`FlagCloseImmediately`](header.go:28).
|
||||
|
||||
See [`StreamRequestHeader`](header.go:30).
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------- | ------------------------------------------------------------ |
|
||||
| [`header.go`](header.go) | Stream request header structure and validation. |
|
||||
| [`tcp_client.go`](tcp_client.go:12) | TCP client implementation with TLS transport. |
|
||||
| [`tcp_server.go`](tcp_server.go:13) | TCP server implementation for handling stream requests. |
|
||||
| [`udp_client.go`](udp_client.go:13) | UDP client implementation with DTLS transport. |
|
||||
| [`udp_server.go`](udp_server.go:17) | UDP server implementation for handling DTLS stream requests. |
|
||||
| [`common.go`](common.go:11) | Connection manager and shared constants. |
|
||||
|
||||
## Constants
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
| ---------------------- | ------------------------- | ------------------------------------------------------- |
|
||||
| `StreamALPN` | `"godoxy-agent-stream/1"` | TLS ALPN protocol for stream multiplexing. |
|
||||
| `headerSize` | `275` bytes | Total size of the stream request header. |
|
||||
| `dialTimeout` | `10s` | Timeout for establishing destination connections. |
|
||||
| `readDeadline` | `10s` | Read timeout for UDP destination sockets. |
|
||||
| `FlagCloseImmediately` | `1` | Flag for health check probe - server closes immediately |
|
||||
|
||||
See [`common.go`](common.go:11).
|
||||
|
||||
## Public API
|
||||
|
||||
### Types
|
||||
|
||||
#### `StreamRequestHeader`
|
||||
|
||||
Represents the on-wire protocol header used to negotiate a stream tunnel.
|
||||
|
||||
```go
|
||||
type StreamRequestHeader struct {
|
||||
Version [8]byte // Fixed to "0.1.0" with NUL padding
|
||||
HostLength byte // Actual host name length (0-255)
|
||||
Host [255]byte // NUL-padded host name
|
||||
PortLength byte // Actual port string length (0-5)
|
||||
Port [5]byte // NUL-padded port string
|
||||
Flag FlagType // Protocol flags (e.g., FlagCloseImmediately)
|
||||
Checksum [4]byte // CRC32 checksum of header without checksum
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error)` - Creates a header for the given host and port. Returns error if host exceeds 255 bytes or port exceeds 5 bytes.
|
||||
- `NewStreamHealthCheckHeader() *StreamRequestHeader` - Creates a header with `FlagCloseImmediately` set for health check probes.
|
||||
- `Validate() bool` - Validates the version and checksum.
|
||||
- `GetHostPort() (string, string)` - Extracts the host and port from the header.
|
||||
- `ShouldCloseImmediately() bool` - Returns true if `FlagCloseImmediately` is set.
|
||||
|
||||
### TCP Functions
|
||||
|
||||
- [`NewTCPClient()`](tcp_client.go:26) - Creates a TLS client connection and sends the stream header.
|
||||
- [`NewTCPServerHandler()`](tcp_server.go:24) - Creates a handler for ALPN-multiplexed connections (no listener).
|
||||
- [`NewTCPServerFromListener()`](tcp_server.go:36) - Wraps an existing TLS listener.
|
||||
- [`NewTCPServer()`](tcp_server.go:45) - Creates a fully-configured TCP server with TLS listener.
|
||||
|
||||
### UDP Functions
|
||||
|
||||
- [`NewUDPClient()`](udp_client.go:27) - Creates a DTLS client connection and sends the stream header.
|
||||
- [`NewUDPServer()`](udp_server.go:26) - Creates a DTLS server listening on the given UDP address.
|
||||
|
||||
## Health Check Probes
|
||||
|
||||
The protocol supports health check probes using the `FlagCloseImmediately` flag. When a client sends a header with this flag set, the server validates the header and immediately closes the connection without establishing a destination tunnel.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Connectivity testing between agent and server
|
||||
- Verifying TLS/DTLS handshake and mTLS authentication
|
||||
- Monitoring stream protocol availability
|
||||
|
||||
**Usage:**
|
||||
|
||||
```go
|
||||
header := stream.NewStreamHealthCheckHeader()
|
||||
// Send header over TLS/DTLS connection
|
||||
// Server will validate and close immediately
|
||||
```
|
||||
|
||||
Both TCP and UDP servers silently handle health check probes without logging errors.
|
||||
|
||||
See [`NewStreamHealthCheckHeader()`](header.go:66) and [`FlagCloseImmediately`](header.go:28).
|
||||
|
||||
## TCP behavior
|
||||
|
||||
1. Client establishes a TLS connection to the stream server.
|
||||
2. Client sends exactly one header as a handshake.
|
||||
3. After the handshake, both sides proxy raw TCP bytes between client and destination.
|
||||
|
||||
Server reads the header using `io.ReadFull` to avoid dropping bytes.
|
||||
|
||||
See [`NewTCPClient()`](tcp_client.go:26) and [`(*TCPServer).redirect()`](tcp_server.go:116).
|
||||
|
||||
## UDP-over-DTLS behavior
|
||||
|
||||
1. Client establishes a DTLS connection to the stream server.
|
||||
2. Client sends exactly one header as a handshake.
|
||||
3. After the handshake, both sides proxy raw UDP datagrams:
|
||||
- client -> destination: DTLS payload is written to destination `UDPConn`
|
||||
- destination -> client: destination payload is written back to the DTLS connection
|
||||
|
||||
Responses do **not** include a header.
|
||||
|
||||
The UDP server uses a bidirectional forwarding model:
|
||||
|
||||
- One goroutine forwards from client to destination
|
||||
- Another goroutine forwards from destination to client
|
||||
|
||||
The destination reader uses `readDeadline` to periodically wake up and check for context cancellation. Timeouts do not terminate the session.
|
||||
|
||||
See [`NewUDPClient()`](udp_client.go:27) and [`(*UDPServer).handleDTLSConnection()`](udp_server.go:89).
|
||||
|
||||
## Connection Management
|
||||
|
||||
Both `TCPServer` and `UDPServer` create a dedicated destination connection per incoming stream session and close it when the session ends (no destination connection reuse).
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Description |
|
||||
| --------------------- | ----------------------------------------------- |
|
||||
| `ErrInvalidHeader` | Header validation failed (version or checksum). |
|
||||
| `ErrCloseImmediately` | Health check probe - server closed immediately. |
|
||||
|
||||
Errors from connection creation are propagated to the caller.
|
||||
|
||||
See [`header.go`](header.go:23).
|
||||
|
||||
## Integration
|
||||
|
||||
This package is used by the agent to provide stream tunneling capabilities. See the parent [`agent`](../README.md) package for integration details with the GoDoxy server.
|
||||
|
||||
### Certificate Requirements
|
||||
|
||||
Both TCP and UDP servers require:
|
||||
|
||||
- CA certificate for client verification
|
||||
- Server certificate for TLS/DTLS termination
|
||||
|
||||
Both clients require:
|
||||
|
||||
- CA certificate for server verification
|
||||
- Client certificate for mTLS authentication
|
||||
|
||||
### ALPN Protocol
|
||||
|
||||
The `StreamALPN` constant (`"godoxy-agent-stream/1"`) is used to multiplex stream tunnel traffic and HTTPS API traffic on the same port. Connections negotiating this ALPN are routed to the stream handler.
|
||||
24
agent/pkg/agent/stream/common.go
Normal file
24
agent/pkg/agent/stream/common.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/yusing/goutils/synk"
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 10 * time.Second
|
||||
readDeadline = 10 * time.Second
|
||||
)
|
||||
|
||||
// StreamALPN is the TLS ALPN protocol id used to multiplex the TCP stream tunnel
|
||||
// and the HTTPS API on the same TCP port.
|
||||
//
|
||||
// When a client negotiates this ALPN, the agent will route the connection to the
|
||||
// stream tunnel handler instead of the HTTP handler.
|
||||
const StreamALPN = "godoxy-agent-stream/1"
|
||||
|
||||
var dTLSCipherSuites = []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
|
||||
|
||||
var sizedPool = synk.GetSizedBytesPool()
|
||||
117
agent/pkg/agent/stream/header.go
Normal file
117
agent/pkg/agent/stream/header.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
versionSize = 8
|
||||
hostSize = 255
|
||||
portSize = 5
|
||||
flagSize = 1
|
||||
checksumSize = 4 // crc32 checksum
|
||||
|
||||
headerSize = versionSize + 1 + hostSize + 1 + portSize + flagSize + checksumSize
|
||||
)
|
||||
|
||||
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
|
||||
|
||||
var ErrInvalidHeader = errors.New("invalid header")
|
||||
var ErrCloseImmediately = errors.New("close immediately")
|
||||
|
||||
type FlagType uint8
|
||||
|
||||
const FlagCloseImmediately FlagType = 1 << iota
|
||||
|
||||
type StreamRequestHeader struct {
|
||||
Version [versionSize]byte
|
||||
|
||||
HostLength byte
|
||||
Host [hostSize]byte
|
||||
|
||||
PortLength byte
|
||||
Port [portSize]byte
|
||||
|
||||
Flag FlagType
|
||||
Checksum [checksumSize]byte
|
||||
}
|
||||
|
||||
func init() {
|
||||
if headerSize != reflect.TypeFor[StreamRequestHeader]().Size() {
|
||||
panic("headerSize does not match the size of StreamRequestHeader")
|
||||
}
|
||||
}
|
||||
|
||||
func NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error) {
|
||||
if len(host) > hostSize {
|
||||
return nil, fmt.Errorf("host is too long: max %d characters, got %d", hostSize, len(host))
|
||||
}
|
||||
if len(port) > portSize {
|
||||
return nil, fmt.Errorf("port is too long: max %d characters, got %d", portSize, len(port))
|
||||
}
|
||||
header := &StreamRequestHeader{}
|
||||
copy(header.Version[:], version[:])
|
||||
header.HostLength = byte(len(host))
|
||||
copy(header.Host[:], host)
|
||||
header.PortLength = byte(len(port))
|
||||
copy(header.Port[:], port)
|
||||
header.updateChecksum()
|
||||
return header, nil
|
||||
}
|
||||
|
||||
func NewStreamHealthCheckHeader() *StreamRequestHeader {
|
||||
header := &StreamRequestHeader{}
|
||||
copy(header.Version[:], version[:])
|
||||
header.Flag |= FlagCloseImmediately
|
||||
header.updateChecksum()
|
||||
return header
|
||||
}
|
||||
|
||||
// ToHeader converts header byte array to a copy of itself as a StreamRequestHeader.
|
||||
func ToHeader(buf *[headerSize]byte) StreamRequestHeader {
|
||||
return *(*StreamRequestHeader)(unsafe.Pointer(buf))
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) GetHostPort() (string, string) {
|
||||
return string(h.Host[:h.HostLength]), string(h.Port[:h.PortLength])
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) Validate() bool {
|
||||
if h.Version != version {
|
||||
return false
|
||||
}
|
||||
if h.HostLength > hostSize {
|
||||
return false
|
||||
}
|
||||
if h.PortLength > portSize {
|
||||
return false
|
||||
}
|
||||
return h.validateChecksum()
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) ShouldCloseImmediately() bool {
|
||||
return h.Flag&FlagCloseImmediately != 0
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) updateChecksum() {
|
||||
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||
binary.BigEndian.PutUint32(h.Checksum[:], checksum)
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) validateChecksum() bool {
|
||||
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
|
||||
return checksum == binary.BigEndian.Uint32(h.Checksum[:])
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) BytesWithoutChecksum() []byte {
|
||||
return (*[headerSize - checksumSize]byte)(unsafe.Pointer(h))[:]
|
||||
}
|
||||
|
||||
func (h *StreamRequestHeader) Bytes() []byte {
|
||||
return (*[headerSize]byte)(unsafe.Pointer(h))[:]
|
||||
}
|
||||
26
agent/pkg/agent/stream/payload_test.go
Normal file
26
agent/pkg/agent/stream/payload_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStreamRequestHeader_RoundTripAndChecksum(t *testing.T) {
|
||||
h, err := NewStreamRequestHeader("example.com", "443")
|
||||
if err != nil {
|
||||
t.Fatalf("NewStreamRequestHeader: %v", err)
|
||||
}
|
||||
if !h.Validate() {
|
||||
t.Fatalf("expected header to validate")
|
||||
}
|
||||
|
||||
var buf [headerSize]byte
|
||||
copy(buf[:], h.Bytes())
|
||||
h2 := ToHeader(&buf)
|
||||
if !h2.Validate() {
|
||||
t.Fatalf("expected round-tripped header to validate")
|
||||
}
|
||||
host, port := h2.GetHostPort()
|
||||
if host != "example.com" || port != "443" {
|
||||
t.Fatalf("unexpected host/port: %q:%q", host, port)
|
||||
}
|
||||
}
|
||||
122
agent/pkg/agent/stream/tcp_client.go
Normal file
122
agent/pkg/agent/stream/tcp_client.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
type TCPClient struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// NewTCPClient creates a new TCP client for the agent.
|
||||
//
|
||||
// It will establish a TLS connection and send a stream request header to the server.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the target address is invalid
|
||||
// - the stream request header is invalid
|
||||
// - the TLS configuration is invalid
|
||||
// - the TLS connection fails
|
||||
// - the stream request header is not sent
|
||||
func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := NewStreamRequestHeader(host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup TLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{StreamALPN},
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
|
||||
// Establish TLS connection
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TCPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *TCPClient) Read(p []byte) (n int, err error) {
|
||||
return c.conn.Read(p)
|
||||
}
|
||||
|
||||
func (c *TCPClient) Write(p []byte) (n int, err error) {
|
||||
return c.conn.Write(p)
|
||||
}
|
||||
|
||||
func (c *TCPClient) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *TCPClient) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *TCPClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// ConnectionState exposes the underlying TLS connection state when the client is
|
||||
// backed by *tls.Conn.
|
||||
//
|
||||
// This is primarily used by tests and diagnostics.
|
||||
func (c *TCPClient) ConnectionState() tls.ConnectionState {
|
||||
if tc, ok := c.conn.(*tls.Conn); ok {
|
||||
return tc.ConnectionState()
|
||||
}
|
||||
return tls.ConnectionState{}
|
||||
}
|
||||
179
agent/pkg/agent/stream/tcp_server.go
Normal file
179
agent/pkg/agent/stream/tcp_server.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
ioutils "github.com/yusing/goutils/io"
|
||||
)
|
||||
|
||||
type TCPServer struct {
|
||||
ctx context.Context
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTCPServerHandler creates a TCP stream server that can serve already-accepted
|
||||
// connections (e.g. handed off by an ALPN multiplexer).
|
||||
//
|
||||
// This variant does not require a listener. Use TCPServer.ServeConn to handle
|
||||
// each incoming stream connection.
|
||||
func NewTCPServerHandler(ctx context.Context) *TCPServer {
|
||||
s := &TCPServer{ctx: ctx}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewTCPServerFromListener creates a TCP stream server from an already-prepared
|
||||
// listener.
|
||||
//
|
||||
// The listener is expected to yield connections that are already secured (e.g.
|
||||
// a TLS/mTLS listener, or pre-handshaked *tls.Conn). This is used when the agent
|
||||
// multiplexes HTTPS and stream-tunnel traffic on the same port.
|
||||
func NewTCPServerFromListener(ctx context.Context, listener net.Listener) *TCPServer {
|
||||
s := &TCPServer{
|
||||
ctx: ctx,
|
||||
listener: listener,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func NewTCPServer(ctx context.Context, listener *net.TCPListener, caCert *x509.Certificate, serverCert *tls.Certificate) *TCPServer {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*serverCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{StreamALPN},
|
||||
}
|
||||
|
||||
tcpListener := tls.NewListener(listener, tlsConfig)
|
||||
return NewTCPServerFromListener(ctx, tcpListener)
|
||||
}
|
||||
|
||||
func (s *TCPServer) Start() error {
|
||||
if s.listener == nil {
|
||||
return net.ErrClosed
|
||||
}
|
||||
context.AfterFunc(s.ctx, func() {
|
||||
_ = s.listener.Close()
|
||||
})
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||
return s.ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// ServeConn serves a single stream connection.
|
||||
//
|
||||
// The provided connection is expected to be already secured (TLS/mTLS) and to
|
||||
// speak the stream protocol (i.e. the client will send the stream header first).
|
||||
//
|
||||
// This method blocks until the stream finishes.
|
||||
func (s *TCPServer) ServeConn(conn net.Conn) {
|
||||
s.handle(conn)
|
||||
}
|
||||
|
||||
func (s *TCPServer) Addr() net.Addr {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
return s.listener.Addr()
|
||||
}
|
||||
|
||||
func (s *TCPServer) Close() error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *TCPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||
ev := log.With().Str("protocol", "tcp").
|
||||
Str("remote", clientConn.RemoteAddr().String())
|
||||
if s.listener != nil {
|
||||
ev = ev.Str("addr", s.listener.Addr().String())
|
||||
}
|
||||
l := ev.Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *TCPServer) loggerWithDst(dstConn net.Conn, clientConn net.Conn) *zerolog.Logger {
|
||||
ev := log.With().Str("protocol", "tcp").
|
||||
Str("remote", clientConn.RemoteAddr().String()).
|
||||
Str("dst", dstConn.RemoteAddr().String())
|
||||
if s.listener != nil {
|
||||
ev = ev.Str("addr", s.listener.Addr().String())
|
||||
}
|
||||
l := ev.Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *TCPServer) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
dst, err := s.redirect(conn)
|
||||
if err != nil {
|
||||
// Health check probe: close connection
|
||||
if errors.Is(err, ErrCloseImmediately) {
|
||||
s.logger(conn).Info().Msg("Health check received")
|
||||
return
|
||||
}
|
||||
s.logger(conn).Err(err).Msg("failed to redirect connection")
|
||||
return
|
||||
}
|
||||
|
||||
defer dst.Close()
|
||||
pipe := ioutils.NewBidirectionalPipe(s.ctx, conn, dst)
|
||||
err = pipe.Start()
|
||||
if err != nil {
|
||||
s.loggerWithDst(dst, conn).Err(err).Msg("failed to start bidirectional pipe")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
return nil, ErrInvalidHeader
|
||||
}
|
||||
|
||||
// Health check: close immediately if FlagCloseImmediately is set
|
||||
if header.ShouldCloseImmediately() {
|
||||
return nil, ErrCloseImmediately
|
||||
}
|
||||
|
||||
// get destination connection
|
||||
host, port := header.GetHostPort()
|
||||
return s.createDestConnection(host, port)
|
||||
}
|
||||
|
||||
func (s *TCPServer) createDestConnection(host, port string) (net.Conn, error) {
|
||||
addr := net.JoinHostPort(host, port)
|
||||
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
26
agent/pkg/agent/stream/tests/healthcheck_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTCPHealthCheck(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
|
||||
func TestUDPHealthCheck(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "health check")
|
||||
}
|
||||
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
94
agent/pkg/agent/stream/tests/mux_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
baseLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
require.NoError(t, err, "listen tcp")
|
||||
defer baseLn.Close()
|
||||
baseAddr := baseLn.Addr().String()
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(certs.CaCert)
|
||||
|
||||
serverTLS := &tls.Config{
|
||||
Certificates: []tls.Certificate{*certs.SrvCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", stream.StreamALPN},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
streamSrv := stream.NewTCPServerHandler(ctx)
|
||||
defer func() { _ = streamSrv.Close() }()
|
||||
|
||||
tlsLn := tls.NewListener(baseLn, serverTLS)
|
||||
defer func() { _ = tlsLn.Close() }()
|
||||
|
||||
// HTTP server
|
||||
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}),
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
||||
streamSrv.ServeConn(conn)
|
||||
},
|
||||
},
|
||||
}
|
||||
go func() { _ = httpSrv.Serve(tlsLn) }()
|
||||
defer func() { _ = httpSrv.Close() }()
|
||||
|
||||
// Stream destination
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
// HTTP client over the same port
|
||||
clientTLS := &tls.Config{
|
||||
Certificates: []tls.Certificate{*certs.ClientCert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
ServerName: common.CertsDNSName,
|
||||
}
|
||||
hc, err := tls.Dial("tcp", baseAddr, clientTLS)
|
||||
require.NoError(t, err, "dial https")
|
||||
defer hc.Close()
|
||||
_ = hc.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
_, err = hc.Write([]byte("GET / HTTP/1.1\r\nHost: godoxy-agent\r\n\r\n"))
|
||||
require.NoError(t, err, "write http request")
|
||||
r := bufio.NewReader(hc)
|
||||
statusLine, err := r.ReadString('\n')
|
||||
require.NoError(t, err, "read status line")
|
||||
require.Contains(t, statusLine, "200", "expected 200")
|
||||
|
||||
// Stream client over the same port
|
||||
client := NewTCPClient(t, baseAddr, dstAddr, certs)
|
||||
defer client.Close()
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over mux")
|
||||
_, err = client.Write(msg)
|
||||
require.NoError(t, err, "write stream payload")
|
||||
buf := make([]byte, len(msg))
|
||||
_, err = io.ReadFull(client, buf)
|
||||
require.NoError(t, err, "read stream payload")
|
||||
require.Equal(t, msg, buf)
|
||||
}
|
||||
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
201
agent/pkg/agent/stream/tests/server_flow_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
func TestTCPServer_FullFlow(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
// Ensure ALPN is negotiated as expected (required for multiplexing).
|
||||
withState, ok := client.(interface{ ConnectionState() tls.ConnectionState })
|
||||
require.True(t, ok, "tcp client should expose TLS connection state")
|
||||
require.Equal(t, stream.StreamALPN, withState.ConnectionState().NegotiatedProtocol)
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over tcp")
|
||||
_, err := client.Write(msg)
|
||||
require.NoError(t, err, "write to client")
|
||||
|
||||
buf := make([]byte, len(msg))
|
||||
_, err = io.ReadFull(client, buf)
|
||||
require.NoError(t, err, "read from client")
|
||||
require.Equal(t, string(msg), string(buf), "unexpected echo")
|
||||
}
|
||||
|
||||
func TestTCPServer_ConcurrentConnections(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startTCPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startTCPServer(t, certs)
|
||||
|
||||
const nClients = 25
|
||||
|
||||
errs := make(chan error, nClients)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(nClients)
|
||||
|
||||
for i := range nClients {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := fmt.Appendf(nil, "ping over tcp %d", i)
|
||||
if _, err := client.Write(msg); err != nil {
|
||||
errs <- fmt.Errorf("write to client: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, len(msg))
|
||||
if _, err := io.ReadFull(client, buf); err != nil {
|
||||
errs <- fmt.Errorf("read from client: %w", err)
|
||||
return
|
||||
}
|
||||
if string(msg) != string(buf) {
|
||||
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf), string(msg))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPServer_RejectInvalidClient(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
// Generate a self-signed client cert that is NOT signed by the CA
|
||||
_, _, invalidClientPEM, err := agent.NewAgent()
|
||||
require.NoError(t, err, "generate invalid client certs")
|
||||
invalidClientCert, err := invalidClientPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse invalid client cert")
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
|
||||
// Try to connect with a client cert from a different CA
|
||||
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
|
||||
require.Error(t, err, "expected error when connecting with client cert from different CA")
|
||||
|
||||
var handshakeErr *dtls.HandshakeError
|
||||
require.ErrorAs(t, err, &handshakeErr, "expected handshake error")
|
||||
}
|
||||
|
||||
func TestUDPServer_RejectClientWithoutCert(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Try to connect without any client certificate
|
||||
// Create a TLS cert without a private key to simulate no client cert
|
||||
emptyCert := &tls.Certificate{}
|
||||
_, err := stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, emptyCert)
|
||||
require.Error(t, err, "expected error when connecting without client cert")
|
||||
|
||||
require.ErrorContains(t, err, "no certificate provided", "expected no cert error")
|
||||
}
|
||||
|
||||
func TestUDPServer_FullFlow(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
msg := []byte("ping over udp")
|
||||
_, err := client.Write(msg)
|
||||
require.NoError(t, err, "write to client")
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
n, err := client.Read(buf)
|
||||
require.NoError(t, err, "read from client")
|
||||
require.Equal(t, string(msg), string(buf[:n]), "unexpected echo")
|
||||
}
|
||||
|
||||
func TestUDPServer_ConcurrentConnections(t *testing.T) {
|
||||
certs := genTestCerts(t)
|
||||
|
||||
dstAddr, closeDst := startUDPEcho(t)
|
||||
defer closeDst()
|
||||
|
||||
srv := startUDPServer(t, certs)
|
||||
|
||||
const nClients = 25
|
||||
|
||||
errs := make(chan error, nClients)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(nClients)
|
||||
|
||||
for i := range nClients {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
|
||||
defer client.Close()
|
||||
|
||||
_ = client.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
msg := fmt.Appendf(nil, "ping over udp %d", i)
|
||||
if _, err := client.Write(msg); err != nil {
|
||||
errs <- fmt.Errorf("write to client: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
n, err := client.Read(buf)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("read from client: %w", err)
|
||||
return
|
||||
}
|
||||
if string(msg) != string(buf[:n]) {
|
||||
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf[:n]), string(msg))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
177
agent/pkg/agent/stream/tests/testutils_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package stream_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/transport/v3/udp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/stream"
|
||||
)
|
||||
|
||||
// CertBundle holds all certificates needed for testing.
|
||||
type CertBundle struct {
|
||||
CaCert *x509.Certificate
|
||||
SrvCert *tls.Certificate
|
||||
ClientCert *tls.Certificate
|
||||
}
|
||||
|
||||
// genTestCerts generates certificates for testing and returns them as a CertBundle.
|
||||
func genTestCerts(t *testing.T) CertBundle {
|
||||
t.Helper()
|
||||
|
||||
caPEM, srvPEM, clientPEM, err := agent.NewAgent()
|
||||
require.NoError(t, err, "generate agent certs")
|
||||
|
||||
caCert, err := caPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse CA cert")
|
||||
srvCert, err := srvPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse server cert")
|
||||
clientCert, err := clientPEM.ToTLSCert()
|
||||
require.NoError(t, err, "parse client cert")
|
||||
|
||||
return CertBundle{
|
||||
CaCert: caCert.Leaf,
|
||||
SrvCert: srvCert,
|
||||
ClientCert: clientCert,
|
||||
}
|
||||
}
|
||||
|
||||
// startTCPEcho starts a TCP echo server and returns its address and close function.
|
||||
func startTCPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "listen tcp")
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
_, _ = io.Copy(conn, conn)
|
||||
}(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return ln.Addr().String(), func() {
|
||||
_ = ln.Close()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// startUDPEcho starts a UDP echo server and returns its address and close function.
|
||||
func startUDPEcho(t *testing.T) (addr string, closeFn func()) {
|
||||
t.Helper()
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err, "listen udp")
|
||||
uc := pc.(*net.UDPConn)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 65535)
|
||||
for {
|
||||
n, raddr, err := uc.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = uc.WriteToUDP(buf[:n], raddr)
|
||||
}
|
||||
}()
|
||||
|
||||
return uc.LocalAddr().String(), func() {
|
||||
_ = uc.Close()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer wraps a server with its startup goroutine for cleanup.
|
||||
type TestServer struct {
|
||||
Server interface{ Close() error }
|
||||
Addr net.Addr
|
||||
}
|
||||
|
||||
// startTCPServer starts a TCP server and returns a TestServer for cleanup.
|
||||
func startTCPServer(t *testing.T, certs CertBundle) TestServer {
|
||||
t.Helper()
|
||||
|
||||
tcpLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
require.NoError(t, err, "listen tcp")
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
srv := stream.NewTCPServer(ctx, tcpLn, certs.CaCert, certs.SrvCert)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.Start() }()
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = srv.Close()
|
||||
err := <-errCh
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
|
||||
t.Logf("tcp server exit: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return TestServer{
|
||||
Server: srv,
|
||||
Addr: srv.Addr(),
|
||||
}
|
||||
}
|
||||
|
||||
// startUDPServer starts a UDP server and returns a TestServer for cleanup.
|
||||
func startUDPServer(t *testing.T, certs CertBundle) TestServer {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
srv := stream.NewUDPServer(ctx, "udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}, certs.CaCert, certs.SrvCert)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.Start() }()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = srv.Close()
|
||||
err := <-errCh
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) && !errors.Is(err, udp.ErrClosedListener) {
|
||||
t.Logf("udp server exit: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return TestServer{
|
||||
Server: srv,
|
||||
Addr: srv.Addr(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTCPClient creates a TCP client connected to the server with test certificates.
|
||||
func NewTCPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||
t.Helper()
|
||||
client, err := stream.NewTCPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "create tcp client")
|
||||
return client
|
||||
}
|
||||
|
||||
// NewUDPClient creates a UDP client connected to the server with test certificates.
|
||||
func NewUDPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
|
||||
t.Helper()
|
||||
client, err := stream.NewUDPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
|
||||
require.NoError(t, err, "create udp client")
|
||||
return client
|
||||
}
|
||||
118
agent/pkg/agent/stream/udp_client.go
Normal file
118
agent/pkg/agent/stream/udp_client.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent/common"
|
||||
)
|
||||
|
||||
type UDPClient struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// NewUDPClient creates a new UDP client for the agent.
|
||||
//
|
||||
// It will establish a DTLS connection and send a stream request header to the server.
|
||||
//
|
||||
// It returns an error if
|
||||
// - the target address is invalid
|
||||
// - the stream request header is invalid
|
||||
// - the DTLS configuration is invalid
|
||||
// - the DTLS connection fails
|
||||
// - the stream request header is not sent
|
||||
func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header, err := NewStreamRequestHeader(host, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
}
|
||||
|
||||
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
|
||||
// Setup DTLS configuration
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
dtlsConfig := &dtls.Config{
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
RootCAs: caCertPool,
|
||||
InsecureSkipVerify: false,
|
||||
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||
ServerName: common.CertsDNSName,
|
||||
CipherSuites: dTLSCipherSuites,
|
||||
}
|
||||
|
||||
raddr, err := net.ResolveUDPAddr("udp", serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Establish DTLS connection
|
||||
conn, err := dtls.Dial("udp", raddr, dtlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Send the stream header once as a handshake.
|
||||
if _, err := conn.Write(header.Bytes()); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UDPClient{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
|
||||
header := NewStreamHealthCheckHeader()
|
||||
|
||||
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UDPClient) Read(p []byte) (n int, err error) {
|
||||
return c.conn.Read(p)
|
||||
}
|
||||
|
||||
func (c *UDPClient) Write(p []byte) (n int, err error) {
|
||||
return c.conn.Write(p)
|
||||
}
|
||||
|
||||
func (c *UDPClient) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *UDPClient) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetDeadline(t time.Time) error {
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) SetWriteDeadline(t time.Time) error {
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *UDPClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
208
agent/pkg/agent/stream/udp_server.go
Normal file
208
agent/pkg/agent/stream/udp_server.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type UDPServer struct {
|
||||
ctx context.Context
|
||||
network string
|
||||
laddr *net.UDPAddr
|
||||
listener net.Listener
|
||||
|
||||
dtlsConfig *dtls.Config
|
||||
}
|
||||
|
||||
func NewUDPServer(ctx context.Context, network string, laddr *net.UDPAddr, caCert *x509.Certificate, serverCert *tls.Certificate) *UDPServer {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(caCert)
|
||||
|
||||
dtlsConfig := &dtls.Config{
|
||||
Certificates: []tls.Certificate{*serverCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: dtls.RequireAndVerifyClientCert,
|
||||
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
|
||||
CipherSuites: dTLSCipherSuites,
|
||||
}
|
||||
|
||||
s := &UDPServer{
|
||||
ctx: ctx,
|
||||
network: network,
|
||||
laddr: laddr,
|
||||
dtlsConfig: dtlsConfig,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *UDPServer) Start() error {
|
||||
listener, err := dtls.Listen(s.network, s.laddr, s.dtlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
context.AfterFunc(s.ctx, func() {
|
||||
_ = s.listener.Close()
|
||||
})
|
||||
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
// Expected error when context cancelled
|
||||
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
|
||||
return s.ctx.Err()
|
||||
}
|
||||
return err
|
||||
}
|
||||
go s.handleDTLSConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPServer) Addr() net.Addr {
|
||||
if s.listener != nil {
|
||||
return s.listener.Addr()
|
||||
}
|
||||
return s.laddr
|
||||
}
|
||||
|
||||
func (s *UDPServer) Close() error {
|
||||
if s.listener != nil {
|
||||
return s.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UDPServer) logger(clientConn net.Conn) *zerolog.Logger {
|
||||
l := log.With().Str("protocol", "udp").
|
||||
Str("addr", s.Addr().String()).
|
||||
Str("remote", clientConn.RemoteAddr().String()).Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *UDPServer) loggerWithDst(clientConn net.Conn, dstConn *net.UDPConn) *zerolog.Logger {
|
||||
l := log.With().Str("protocol", "udp").
|
||||
Str("addr", s.Addr().String()).
|
||||
Str("remote", clientConn.RemoteAddr().String()).
|
||||
Str("dst", dstConn.RemoteAddr().String()).Logger()
|
||||
return &l
|
||||
}
|
||||
|
||||
func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
|
||||
// Read the stream header once as a handshake.
|
||||
var headerBuf [headerSize]byte
|
||||
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
|
||||
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to read stream header")
|
||||
return
|
||||
}
|
||||
_ = clientConn.SetReadDeadline(time.Time{})
|
||||
|
||||
header := ToHeader(&headerBuf)
|
||||
if !header.Validate() {
|
||||
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
|
||||
return
|
||||
}
|
||||
|
||||
// Health check probe: close connection
|
||||
if header.ShouldCloseImmediately() {
|
||||
s.logger(clientConn).Info().Msg("Health check received")
|
||||
return
|
||||
}
|
||||
|
||||
host, port := header.GetHostPort()
|
||||
dstConn, err := s.createDestConnection(host, port)
|
||||
if err != nil {
|
||||
s.logger(clientConn).Err(err).Msg("failed to get or create destination connection")
|
||||
return
|
||||
}
|
||||
defer dstConn.Close()
|
||||
|
||||
go s.forwardFromDestination(dstConn, clientConn)
|
||||
|
||||
buf := sizedPool.GetSized(65535)
|
||||
defer sizedPool.Put(buf)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
n, err := clientConn.Read(buf)
|
||||
// Per net.Conn contract, Read may return (n > 0, err == io.EOF).
|
||||
// Always forward any bytes we got before acting on the error.
|
||||
if n > 0 {
|
||||
if _, werr := dstConn.Write(buf[:n]); werr != nil {
|
||||
s.logger(clientConn).Err(werr).Msgf("failed to write %d bytes to destination", n)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Expected shutdown paths.
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
s.logger(clientConn).Err(err).Msg("failed to read from client")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPServer) createDestConnection(host, port string) (*net.UDPConn, error) {
|
||||
addr := net.JoinHostPort(host, port)
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dstConn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dstConn, nil
|
||||
}
|
||||
|
||||
func (s *UDPServer) forwardFromDestination(dstConn *net.UDPConn, clientConn net.Conn) {
|
||||
buffer := sizedPool.GetSized(65535)
|
||||
defer sizedPool.Put(buffer)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
_ = dstConn.SetReadDeadline(time.Now().Add(readDeadline))
|
||||
n, err := dstConn.Read(buffer)
|
||||
if err != nil {
|
||||
// The destination socket can be closed when the client disconnects (e.g. during
|
||||
// the stream support probe in AgentConfig.StartWithCerts). Treat that as a
|
||||
// normal exit and avoid noisy logs.
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
s.loggerWithDst(clientConn, dstConn).Err(err).Msg("failed to read from destination")
|
||||
return
|
||||
}
|
||||
if _, err := clientConn.Write(buffer[:n]); err != nil {
|
||||
s.loggerWithDst(clientConn, dstConn).Err(err).Msgf("failed to write %d bytes to client", n)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
services:
|
||||
agent:
|
||||
image: "{{.Image}}"
|
||||
container_name: godoxy-agent
|
||||
restart: always
|
||||
network_mode: host # do not change this
|
||||
environment:
|
||||
AGENT_NAME: "{{.Name}}"
|
||||
AGENT_PORT: "{{.Port}}"
|
||||
AGENT_CA_CERT: "{{.CACert}}"
|
||||
AGENT_SSL_CERT: "{{.SSLCert}}"
|
||||
# use agent as a docker socket proxy: [host]:port
|
||||
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
|
||||
LISTEN_ADDR:
|
||||
POST: false
|
||||
ALLOW_RESTARTS: false
|
||||
ALLOW_START: false
|
||||
ALLOW_STOP: false
|
||||
AUTH: false
|
||||
BUILD: false
|
||||
COMMIT: false
|
||||
CONFIGS: false
|
||||
CONTAINERS: false
|
||||
DISTRIBUTION: false
|
||||
EVENTS: true
|
||||
EXEC: false
|
||||
GRPC: false
|
||||
IMAGES: false
|
||||
INFO: false
|
||||
NETWORKS: false
|
||||
NODES: false
|
||||
PING: true
|
||||
PLUGINS: false
|
||||
SECRETS: false
|
||||
SERVICES: false
|
||||
SESSION: false
|
||||
SWARM: false
|
||||
SYSTEM: false
|
||||
TASKS: false
|
||||
VERSION: true
|
||||
VOLUMES: false
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./data:/app/data
|
||||
@@ -5,7 +5,8 @@ services:
|
||||
restart: always
|
||||
{{ if eq .ContainerRuntime "podman" -}}
|
||||
ports:
|
||||
- "{{.Port}}:{{.Port}}"
|
||||
- "{{.Port}}:{{.Port}}/tcp"
|
||||
- "{{.Port}}:{{.Port}}/udp"
|
||||
{{ else -}}
|
||||
network_mode: host # do not change this
|
||||
{{ end -}}
|
||||
|
||||
122
agent/pkg/agentproxy/README.md
Normal file
122
agent/pkg/agentproxy/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# agent/pkg/agentproxy
|
||||
|
||||
Package for configuring HTTP proxy connections through the GoDoxy Agent using HTTP headers.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides types and functions for parsing and setting agent proxy configuration via HTTP headers. It supports both a modern base64-encoded JSON format and a legacy header-based format for backward compatibility.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[HTTP Request] --> B[ConfigFromHeaders]
|
||||
B --> C{Modern Format?}
|
||||
C -->|Yes| D[Parse X-Proxy-Config Base64 JSON]
|
||||
C -->|No| E[Parse Legacy Headers]
|
||||
D --> F[Config]
|
||||
E --> F
|
||||
|
||||
F --> G[SetAgentProxyConfigHeaders]
|
||||
G --> H[Modern Headers]
|
||||
G --> I[Legacy Headers]
|
||||
```
|
||||
|
||||
## Public Types
|
||||
|
||||
### Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Scheme string // Proxy scheme (http or https)
|
||||
Host string // Proxy host (hostname or hostname:port)
|
||||
HTTPConfig // Extended HTTP configuration
|
||||
}
|
||||
```
|
||||
|
||||
The `HTTPConfig` embedded type (from `internal/route/types`) includes:
|
||||
|
||||
- `NoTLSVerify` - Skip TLS certificate verification
|
||||
- `ResponseHeaderTimeout` - Timeout for response headers
|
||||
- `DisableCompression` - Disable gzip compression
|
||||
|
||||
## Public Functions
|
||||
|
||||
### ConfigFromHeaders
|
||||
|
||||
```go
|
||||
func ConfigFromHeaders(h http.Header) (Config, error)
|
||||
```
|
||||
|
||||
Parses proxy configuration from HTTP request headers. Tries modern format first, falls back to legacy format if not present.
|
||||
|
||||
### proxyConfigFromHeaders
|
||||
|
||||
```go
|
||||
func proxyConfigFromHeaders(h http.Header) (Config, error)
|
||||
```
|
||||
|
||||
Parses the modern base64-encoded JSON format from `X-Proxy-Config` header.
|
||||
|
||||
### proxyConfigFromHeadersLegacy
|
||||
|
||||
```go
|
||||
func proxyConfigFromHeadersLegacy(h http.Header) Config
|
||||
```
|
||||
|
||||
Parses the legacy header format:
|
||||
|
||||
- `X-Proxy-Host` - Proxy host
|
||||
- `X-Proxy-Https` - Whether to use HTTPS
|
||||
- `X-Proxy-Skip-Tls-Verify` - Skip TLS verification
|
||||
- `X-Proxy-Response-Header-Timeout` - Response timeout in seconds
|
||||
|
||||
### SetAgentProxyConfigHeaders
|
||||
|
||||
```go
|
||||
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header)
|
||||
```
|
||||
|
||||
Sets headers for modern format with base64-encoded JSON config.
|
||||
|
||||
### SetAgentProxyConfigHeadersLegacy
|
||||
|
||||
```go
|
||||
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header)
|
||||
```
|
||||
|
||||
Sets headers for legacy format with individual header fields.
|
||||
|
||||
## Header Constants
|
||||
|
||||
Modern headers:
|
||||
|
||||
- `HeaderXProxyScheme` - Proxy scheme
|
||||
- `HeaderXProxyHost` - Proxy host
|
||||
- `HeaderXProxyConfig` - Base64-encoded JSON config
|
||||
|
||||
Legacy headers (deprecated):
|
||||
|
||||
- `HeaderXProxyHTTPS`
|
||||
- `HeaderXProxySkipTLSVerify`
|
||||
- `HeaderXProxyResponseHeaderTimeout`
|
||||
|
||||
## Usage Example
|
||||
|
||||
```go
|
||||
// Reading configuration from incoming request headers
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid proxy config", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Use cfg.Scheme and cfg.Host to proxy the request
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
This package is used by `agent/pkg/handler/proxy_http.go` to configure reverse proxy connections based on request headers.
|
||||
102
agent/pkg/certs/README.md
Normal file
102
agent/pkg/certs/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# agent/pkg/certs
|
||||
|
||||
Certificate management package for creating and extracting certificate archives.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides utilities for packaging SSL certificates into ZIP archives and extracting them. It is used by the GoDoxy Agent to distribute certificates to clients in a convenient format.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Raw Certs] --> B[ZipCert]
|
||||
B --> C[ZIP Archive]
|
||||
C --> D[ca.pem]
|
||||
C --> E[cert.pem]
|
||||
C --> F[key.pem]
|
||||
|
||||
G[ZIP Archive] --> H[ExtractCert]
|
||||
H --> I[ca, crt, key]
|
||||
```
|
||||
|
||||
## Public Functions
|
||||
|
||||
### ZipCert
|
||||
|
||||
```go
|
||||
func ZipCert(ca, crt, key []byte) ([]byte, error)
|
||||
```
|
||||
|
||||
Creates a ZIP archive containing three PEM files:
|
||||
|
||||
- `ca.pem` - CA certificate
|
||||
- `cert.pem` - Server/client certificate
|
||||
- `key.pem` - Private key
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `ca` - CA certificate in PEM format
|
||||
- `crt` - Certificate in PEM format
|
||||
- `key` - Private key in PEM format
|
||||
|
||||
**Returns:**
|
||||
|
||||
- ZIP archive bytes
|
||||
- Error if packing fails
|
||||
|
||||
### ExtractCert
|
||||
|
||||
```go
|
||||
func ExtractCert(data []byte) (ca, crt, key []byte, err error)
|
||||
```
|
||||
|
||||
Extracts certificates from a ZIP archive created by `ZipCert`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `data` - ZIP archive bytes
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `ca` - CA certificate bytes
|
||||
- `crt` - Certificate bytes
|
||||
- `key` - Private key bytes
|
||||
- Error if extraction fails
|
||||
|
||||
### AgentCertsFilepath
|
||||
|
||||
```go
|
||||
func AgentCertsFilepath(host string) (filepathOut string, ok bool)
|
||||
```
|
||||
|
||||
Generates the file path for storing agent certificates.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `host` - Agent hostname
|
||||
|
||||
**Returns:**
|
||||
|
||||
- Full file path within `certs/` directory
|
||||
- `false` if host is invalid (contains path separators or special characters)
|
||||
|
||||
### isValidAgentHost
|
||||
|
||||
```go
|
||||
func isValidAgentHost(host string) bool
|
||||
```
|
||||
|
||||
Validates that a host string is safe for use in file paths.
|
||||
|
||||
## Constants
|
||||
|
||||
```go
|
||||
const AgentCertsBasePath = "certs"
|
||||
```
|
||||
|
||||
Base directory for storing certificate archives.
|
||||
|
||||
## File Format
|
||||
|
||||
The ZIP archive uses `zip.Store` compression (no compression) for fast creation and extraction. Each file is stored with its standard name (`ca.pem`, `cert.pem`, `key.pem`).
|
||||
52
agent/pkg/env/README.md
vendored
Normal file
52
agent/pkg/env/README.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# agent/pkg/env
|
||||
|
||||
Environment configuration package for the GoDoxy Agent.
|
||||
|
||||
## Overview
|
||||
|
||||
This package manages environment variable parsing and provides a centralized location for all agent configuration options. It is automatically initialized on import.
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
| -------------------------- | ---------------- | ---------------------- | --------------------------------------- |
|
||||
| `DockerSocket` | string | `/var/run/docker.sock` | Path to Docker socket |
|
||||
| `AgentName` | string | System hostname | Agent identifier |
|
||||
| `AgentPort` | int | `8890` | Agent server port |
|
||||
| `AgentSkipClientCertCheck` | bool | `false` | Skip mTLS certificate verification |
|
||||
| `AgentCACert` | string | (empty) | Base64 Encoded CA certificate + key |
|
||||
| `AgentSSLCert` | string | (empty) | Base64 Encoded server certificate + key |
|
||||
| `Runtime` | ContainerRuntime | `docker` | Container runtime (docker or podman) |
|
||||
|
||||
## ContainerRuntime Type
|
||||
|
||||
```go
|
||||
type ContainerRuntime string
|
||||
|
||||
const (
|
||||
ContainerRuntimeDocker ContainerRuntime = "docker"
|
||||
ContainerRuntimePodman ContainerRuntime = "podman"
|
||||
)
|
||||
```
|
||||
|
||||
## Public Functions
|
||||
|
||||
### DefaultAgentName
|
||||
|
||||
```go
|
||||
func DefaultAgentName() string
|
||||
```
|
||||
|
||||
Returns the system hostname as the default agent name. Falls back to `"agent"` if hostname cannot be determined.
|
||||
|
||||
### Load
|
||||
|
||||
```go
|
||||
func Load()
|
||||
```
|
||||
|
||||
Reloads all environment variables from the environment. Called automatically on package init, but can be called again to refresh configuration.
|
||||
|
||||
## Validation
|
||||
|
||||
The `Load()` function validates that `Runtime` is either `docker` or `podman`. An invalid runtime causes a fatal error.
|
||||
127
agent/pkg/handler/README.md
Normal file
127
agent/pkg/handler/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# agent/pkg/handler
|
||||
|
||||
HTTP request handler package for the GoDoxy Agent.
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides the HTTP handler for the GoDoxy Agent server, including endpoints for:
|
||||
|
||||
- Version information
|
||||
- Agent name and runtime
|
||||
- Health checks
|
||||
- System metrics (via SSE)
|
||||
- HTTP proxy routing
|
||||
- Docker socket proxying
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Request] --> B[NewAgentHandler]
|
||||
B --> C{ServeMux Router}
|
||||
|
||||
C --> D[GET /version]
|
||||
C --> E[GET /name]
|
||||
C --> F[GET /runtime]
|
||||
C --> G[GET /health]
|
||||
C --> H[GET /system-info]
|
||||
C --> I[GET /proxy/http/#123;path...#125;]
|
||||
C --> J[ /#42; Docker Socket]
|
||||
|
||||
H --> K[Gin Router]
|
||||
K --> L[WebSocket Upgrade]
|
||||
L --> M[SystemInfo Poller]
|
||||
```
|
||||
|
||||
## Public Types
|
||||
|
||||
### ServeMux
|
||||
|
||||
```go
|
||||
type ServeMux struct{ *http.ServeMux }
|
||||
```
|
||||
|
||||
Wrapper around `http.ServeMux` with agent-specific endpoint helpers.
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `HandleEndpoint(method, endpoint string, handler http.HandlerFunc)` - Registers handler with API base path
|
||||
- `HandleFunc(endpoint string, handler http.HandlerFunc)` - Registers GET handler with API base path
|
||||
|
||||
## Public Functions
|
||||
|
||||
### NewAgentHandler
|
||||
|
||||
```go
|
||||
func NewAgentHandler() http.Handler
|
||||
```
|
||||
|
||||
Creates and configures the HTTP handler for the agent server. Sets up:
|
||||
|
||||
- Gin-based metrics handler with WebSocket support for SSE
|
||||
- All standard agent endpoints
|
||||
- HTTP proxy endpoint
|
||||
- Docker socket proxy fallback
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| ----------------------- | -------- | ------------------------------------ |
|
||||
| `/version` | GET | Returns agent version |
|
||||
| `/name` | GET | Returns agent name |
|
||||
| `/runtime` | GET | Returns container runtime |
|
||||
| `/health` | GET | Health check with scheme query param |
|
||||
| `/system-info` | GET | System metrics via SSE or WebSocket |
|
||||
| `/proxy/http/{path...}` | GET/POST | HTTP proxy with config from headers |
|
||||
| `/*` | \* | Docker socket proxy |
|
||||
|
||||
## Sub-packages
|
||||
|
||||
### proxy_http.go
|
||||
|
||||
Handles HTTP proxy requests by reading configuration from request headers and proxying to the configured upstream.
|
||||
|
||||
**Key Function:**
|
||||
|
||||
- `ProxyHTTP(w, r)` - Proxies HTTP requests based on `X-Proxy-*` headers
|
||||
|
||||
### check_health.go
|
||||
|
||||
Handles health check requests for various schemes.
|
||||
|
||||
**Key Function:**
|
||||
|
||||
- `CheckHealth(w, r)` - Performs health checks with configurable scheme
|
||||
|
||||
**Supported Schemes:**
|
||||
|
||||
- `http`, `https` - HTTP health check
|
||||
- `h2c` - HTTP/2 cleartext health check
|
||||
- `tcp`, `udp`, `tcp4`, `udp4`, `tcp6`, `udp6` - TCP/UDP health check
|
||||
- `fileserver` - File existence check
|
||||
|
||||
## Usage Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", handler.NewAgentHandler())
|
||||
|
||||
http.ListenAndServe(":8890", mux)
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Support
|
||||
|
||||
The handler includes a permissive WebSocket upgrader for internal use (no origin check). This enables real-time system metrics streaming via Server-Sent Events (SSE).
|
||||
|
||||
## Docker Socket Integration
|
||||
|
||||
All unmatched requests fall through to the Docker socket handler, allowing the agent to proxy Docker API calls when configured.
|
||||
@@ -1,19 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
healthcheck "github.com/yusing/godoxy/internal/health/check"
|
||||
"github.com/yusing/godoxy/internal/types"
|
||||
"github.com/yusing/godoxy/internal/watcher/health/monitor"
|
||||
)
|
||||
|
||||
var defaultHealthConfig = types.DefaultHealthConfig()
|
||||
|
||||
func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
scheme := query.Get("scheme")
|
||||
@@ -21,6 +20,7 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "missing scheme", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
timeout := parseMsOrDefault(query.Get("timeout"))
|
||||
|
||||
var (
|
||||
result types.HealthCheckResult
|
||||
@@ -33,24 +33,21 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "missing path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := os.Stat(path)
|
||||
result = types.HealthCheckResult{Healthy: err == nil}
|
||||
if err != nil {
|
||||
result.Detail = err.Error()
|
||||
}
|
||||
case "http", "https": // path is optional
|
||||
result, err = healthcheck.FileServer(path)
|
||||
case "http", "https", "h2c": // path is optional
|
||||
host := query.Get("host")
|
||||
path := query.Get("path")
|
||||
if host == "" {
|
||||
http.Error(w, "missing host", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}, defaultHealthConfig).CheckHealth()
|
||||
case "tcp", "udp":
|
||||
url := url.URL{Scheme: scheme, Host: host}
|
||||
if scheme == "h2c" {
|
||||
result, err = healthcheck.H2C(r.Context(), &url, http.MethodHead, path, timeout)
|
||||
} else {
|
||||
result, err = healthcheck.HTTP(&url, http.MethodHead, path, timeout)
|
||||
}
|
||||
case "tcp", "udp", "tcp4", "udp4", "tcp6", "udp6":
|
||||
host := query.Get("host")
|
||||
if host == "" {
|
||||
http.Error(w, "missing host", http.StatusBadRequest)
|
||||
@@ -63,12 +60,10 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if port != "" {
|
||||
host = fmt.Sprintf("%s:%s", host, port)
|
||||
host = net.JoinHostPort(host, port)
|
||||
}
|
||||
result, err = monitor.NewRawHealthMonitor(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
}, defaultHealthConfig).CheckHealth()
|
||||
url := url.URL{Scheme: scheme, Host: host}
|
||||
result, err = healthcheck.Stream(r.Context(), &url, timeout)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -80,3 +75,16 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func parseMsOrDefault(msStr string) time.Duration {
|
||||
if msStr == "" {
|
||||
return types.HealthCheckTimeoutDefault
|
||||
}
|
||||
|
||||
timeoutMs, _ := strconv.ParseInt(msStr, 10, 64)
|
||||
if timeoutMs == 0 {
|
||||
return types.HealthCheckTimeoutDefault
|
||||
}
|
||||
|
||||
return time.Duration(timeoutMs) * time.Millisecond
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -44,14 +44,14 @@ func NewAgentHandler() http.Handler {
|
||||
}
|
||||
|
||||
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
|
||||
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, version.Get())
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.AgentName)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, env.Runtime)
|
||||
mux.HandleFunc(agent.EndpointInfo, func(w http.ResponseWriter, r *http.Request) {
|
||||
agentInfo := agent.AgentInfo{
|
||||
Version: version.Get(),
|
||||
Name: env.AgentName,
|
||||
Runtime: env.Runtime,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
|
||||
})
|
||||
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
|
||||
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
@@ -43,10 +44,22 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
r.URL.Scheme = ""
|
||||
r.URL.Host = ""
|
||||
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
|
||||
r.RequestURI = r.URL.String()
|
||||
// Strip the {API_BASE}/proxy/http prefix while preserving URL escaping.
|
||||
//
|
||||
// NOTE: `r.URL.Path` is decoded. If we rewrite it without keeping `RawPath`
|
||||
// in sync, Go may re-escape the path (e.g. turning "%5B" into "%255B"),
|
||||
// which breaks urls with percent-encoded characters, like Next.js static chunk URLs.
|
||||
prefix := agent.APIEndpointBase + agent.EndpointProxyHTTP
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
||||
if r.URL.RawPath != "" {
|
||||
if after, ok := strings.CutPrefix(r.URL.RawPath, prefix); ok {
|
||||
r.URL.RawPath = after
|
||||
} else {
|
||||
// RawPath is no longer a valid encoding for Path; force Go to re-derive it.
|
||||
r.URL.RawPath = ""
|
||||
}
|
||||
}
|
||||
r.RequestURI = ""
|
||||
|
||||
rp := &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/env"
|
||||
"github.com/yusing/godoxy/agent/pkg/handler"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
CACert, ServerCert *tls.Certificate
|
||||
Port int
|
||||
}
|
||||
|
||||
func StartAgentServer(parent task.Parent, opt Options) {
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AddCert(opt.CACert.Leaf)
|
||||
|
||||
// Configure TLS
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*opt.ServerCert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
}
|
||||
|
||||
if env.AgentSkipClientCertCheck {
|
||||
tlsConfig.ClientAuth = tls.NoClientCert
|
||||
}
|
||||
|
||||
agentServer := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", opt.Port),
|
||||
Handler: handler.NewAgentHandler(),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
server.Start(parent.Subtask("agent-server", false), agentServer, server.WithLogger(&log.Logger))
|
||||
}
|
||||
73
cmd/README.md
Normal file
73
cmd/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# cmd
|
||||
|
||||
Main entry point package for GoDoxy, a lightweight reverse proxy with WebUI for Docker containers.
|
||||
|
||||
## Overview
|
||||
|
||||
This package contains the `main.go` entry point that initializes and starts the GoDoxy server. It coordinates the initialization of all core components including configuration loading, API server, authentication, and monitoring services.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[main] --> B[Init Profiling]
|
||||
A --> C[Init Logger]
|
||||
A --> D[Parallel Init]
|
||||
D --> D1[DNS Providers]
|
||||
D --> D2[Icon Cache]
|
||||
D --> D3[System Info Poller]
|
||||
D --> D4[Middleware Compose Files]
|
||||
A --> E[JWT Secret Setup]
|
||||
A --> F[Create Directories]
|
||||
A --> G[Load Config]
|
||||
A --> H[Start Proxy Servers]
|
||||
A --> I[Init Auth]
|
||||
A --> J[Start API Server]
|
||||
A --> K[Debug Server]
|
||||
A --> L[Uptime Poller]
|
||||
A --> M[Watch Changes]
|
||||
A --> N[Wait Exit]
|
||||
```
|
||||
|
||||
## Main Function Flow
|
||||
|
||||
The `main()` function performs the following initialization steps:
|
||||
|
||||
1. **Profiling Setup**: Initializes pprof endpoints for performance monitoring
|
||||
1. **Logger Initialization**: Configures zerolog with memory logging
|
||||
1. **Parallel Initialization**: Starts DNS providers, icon cache, system info poller, and middleware
|
||||
1. **JWT Secret**: Ensures API JWT secret is set (generates random if not provided)
|
||||
1. **Directory Preparation**: Creates required directories for logs, certificates, etc.
|
||||
1. **Configuration Loading**: Loads YAML configuration and reports any errors
|
||||
1. **Proxy Servers**: Starts HTTP/HTTPS proxy servers based on configuration
|
||||
1. **Authentication**: Initializes authentication system with access control
|
||||
1. **API Server**: Starts the REST API server with all configured routes
|
||||
1. **Debug Server**: Starts the debug page server (development mode)
|
||||
1. **Monitoring**: Starts uptime and system info polling
|
||||
1. **Change Watcher**: Starts watching for Docker container and configuration changes
|
||||
1. **Graceful Shutdown**: Waits for exit signal with configured timeout
|
||||
|
||||
## Configuration
|
||||
|
||||
The main configuration is loaded from `config/config.yml`. Required directories include:
|
||||
|
||||
- `logs/` - Log files
|
||||
- `config/` - Configuration directory
|
||||
- `certs/` - SSL certificates
|
||||
- `proxy/` - Proxy-related files
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `API_JWT_SECRET` - Secret key for JWT authentication (optional, auto-generated if not set)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `internal/api` - REST API handlers
|
||||
- `internal/auth` - Authentication and ACL
|
||||
- `internal/config` - Configuration management
|
||||
- `internal/dnsproviders` - DNS provider integration
|
||||
- `internal/homepage` - WebUI dashboard
|
||||
- `internal/logging` - Logging infrastructure
|
||||
- `internal/metrics` - System metrics collection
|
||||
- `internal/route` - HTTP routing and middleware
|
||||
- `github.com/yusing/goutils/task` - Task lifecycle management
|
||||
18
cmd/bench_server/Dockerfile
Normal file
18
cmd/bench_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY main.go ./
|
||||
|
||||
RUN go build -o bench_server main.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /src/bench_server /app/run
|
||||
|
||||
USER 1001:1001
|
||||
|
||||
CMD ["/app/run"]
|
||||
3
cmd/bench_server/go.mod
Normal file
3
cmd/bench_server/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/yusing/godoxy/cmd/bench_server
|
||||
|
||||
go 1.25.5
|
||||
0
cmd/bench_server/go.sum
Normal file
0
cmd/bench_server/go.sum
Normal file
34
cmd/bench_server/main.go
Normal file
34
cmd/bench_server/main.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var random = make([]byte, 4096)
|
||||
|
||||
func init() {
|
||||
for i := range random {
|
||||
random[i] = printables[rand.IntN(len(printables))]
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(random)
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
log.Println("Bench server listening on :80")
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe: %v", err)
|
||||
}
|
||||
}
|
||||
257
cmd/debug_page.go
Normal file
257
cmd/debug_page.go
Normal file
@@ -0,0 +1,257 @@
|
||||
//go:build !production
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/api"
|
||||
apiV1 "github.com/yusing/godoxy/internal/api/v1"
|
||||
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
|
||||
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
|
||||
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
|
||||
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
|
||||
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
|
||||
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
|
||||
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
|
||||
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
|
||||
"github.com/yusing/godoxy/internal/auth"
|
||||
"github.com/yusing/godoxy/internal/idlewatcher"
|
||||
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
|
||||
)
|
||||
|
||||
type debugMux struct {
|
||||
endpoints []debugEndpoint
|
||||
mux http.ServeMux
|
||||
}
|
||||
|
||||
type debugEndpoint struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
}
|
||||
|
||||
func newDebugMux() *debugMux {
|
||||
return &debugMux{
|
||||
endpoints: make([]debugEndpoint, 0),
|
||||
mux: *http.NewServeMux(),
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *debugMux) registerEndpoint(name, method, path string) {
|
||||
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
|
||||
}
|
||||
|
||||
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
|
||||
mux.registerEndpoint(name, method, path)
|
||||
mux.mux.HandleFunc(method+" "+path, handler)
|
||||
}
|
||||
|
||||
func (mux *debugMux) Finalize() {
|
||||
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #f8f9fa;
|
||||
background-color: #121212;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
th {
|
||||
background-color: #1e1e1e;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
td {
|
||||
color: #e9ecef;
|
||||
}
|
||||
.link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.method {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
.path {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`)
|
||||
for _, endpoint := range mux.endpoints {
|
||||
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
}
|
||||
|
||||
func listenDebugServer() {
|
||||
mux := newDebugMux()
|
||||
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
|
||||
})
|
||||
|
||||
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
|
||||
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
|
||||
apiHandler := newApiHandler(mux)
|
||||
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
|
||||
|
||||
mux.Finalize()
|
||||
|
||||
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Expires", "0")
|
||||
mux.mux.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
func newApiHandler(debugMux *debugMux) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(api.ErrorHandler())
|
||||
r.Use(api.ErrorLoggingMiddleware())
|
||||
r.Use(api.NoCache())
|
||||
|
||||
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
|
||||
if group, ok := router.(*gin.RouterGroup); ok {
|
||||
debugMux.registerEndpoint(name, method, group.BasePath()+path)
|
||||
} else {
|
||||
debugMux.registerEndpoint(name, method, path)
|
||||
}
|
||||
router.Handle(method, path, handler)
|
||||
}
|
||||
|
||||
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
if auth.IsEnabled() {
|
||||
v1Auth := v1.Group("/auth")
|
||||
{
|
||||
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
|
||||
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
|
||||
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
|
||||
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
|
||||
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
|
||||
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// enable cache for favicon
|
||||
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
|
||||
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
|
||||
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
|
||||
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
|
||||
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
|
||||
|
||||
route := v1.Group("/route")
|
||||
{
|
||||
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
|
||||
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
|
||||
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
|
||||
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
|
||||
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
|
||||
}
|
||||
|
||||
file := v1.Group("/file")
|
||||
{
|
||||
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
|
||||
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
|
||||
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
|
||||
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
|
||||
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
|
||||
}
|
||||
|
||||
homepage := v1.Group("/homepage")
|
||||
{
|
||||
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
|
||||
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
|
||||
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
|
||||
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
|
||||
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
|
||||
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
|
||||
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
|
||||
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
|
||||
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
|
||||
}
|
||||
|
||||
cert := v1.Group("/cert")
|
||||
{
|
||||
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
|
||||
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
|
||||
}
|
||||
|
||||
agent := v1.Group("/agent")
|
||||
{
|
||||
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
|
||||
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
|
||||
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
|
||||
}
|
||||
|
||||
metrics := v1.Group("/metrics")
|
||||
{
|
||||
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
|
||||
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
|
||||
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
|
||||
}
|
||||
|
||||
docker := v1.Group("/docker")
|
||||
{
|
||||
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
|
||||
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
|
||||
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
|
||||
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
|
||||
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
|
||||
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
|
||||
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
|
||||
}
|
||||
7
cmd/debug_page_prod.go
Normal file
7
cmd/debug_page_prod.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build production
|
||||
|
||||
package main
|
||||
|
||||
func listenDebugServer() {
|
||||
// no-op
|
||||
}
|
||||
18
cmd/h2c_test_server/Dockerfile
Normal file
18
cmd/h2c_test_server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
|
||||
HEALTHCHECK NONE
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY main.go ./
|
||||
|
||||
RUN go build -o h2c_test_server main.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /src/h2c_test_server /app/run
|
||||
|
||||
USER 1001:1001
|
||||
|
||||
CMD ["/app/run"]
|
||||
7
cmd/h2c_test_server/go.mod
Normal file
7
cmd/h2c_test_server/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module github.com/yusing/godoxy/cmd/h2c_test_server
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require golang.org/x/net v0.48.0
|
||||
|
||||
require golang.org/x/text v0.32.0 // indirect
|
||||
4
cmd/h2c_test_server/go.sum
Normal file
4
cmd/h2c_test_server/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
26
cmd/h2c_test_server/main.go
Normal file
26
cmd/h2c_test_server/main.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func main() {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: h2c.NewHandler(handler, &http2.Server{}),
|
||||
}
|
||||
|
||||
log.Println("H2C server listening on :80")
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe: %v", err)
|
||||
}
|
||||
}
|
||||
10
cmd/main.go
10
cmd/main.go
@@ -10,12 +10,13 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
"github.com/yusing/godoxy/internal/logging"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/godoxy/internal/metrics/uptime"
|
||||
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/server"
|
||||
"github.com/yusing/goutils/task"
|
||||
@@ -38,7 +39,7 @@ func main() {
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
iconlist.InitCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
@@ -58,9 +59,12 @@ func main() {
|
||||
}
|
||||
|
||||
config.StartProxyServers()
|
||||
|
||||
if err := auth.Initialize(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||
}
|
||||
rules.InitAuthHandler(auth.AuthOrProceed)
|
||||
|
||||
// API Handler needs to start after auth is initialized.
|
||||
server.StartServer(task.RootTask("api_server", false), server.Options{
|
||||
Name: "api",
|
||||
@@ -68,6 +72,8 @@ func main() {
|
||||
Handler: api.NewHandler(),
|
||||
})
|
||||
|
||||
listenDebugServer()
|
||||
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
|
||||
@@ -88,6 +88,12 @@ entrypoint:
|
||||
# - name: default
|
||||
# do: proxy http://other-proxy:8080
|
||||
|
||||
defaults:
|
||||
healthcheck:
|
||||
interval: 5s
|
||||
timeout: 15s
|
||||
retries: 3
|
||||
|
||||
providers:
|
||||
# include files are standalone yaml files under `config/` directory
|
||||
#
|
||||
|
||||
188
dev.compose.yml
188
dev.compose.yml
@@ -1,3 +1,8 @@
|
||||
x-benchmark: &benchmark
|
||||
restart: no
|
||||
labels:
|
||||
proxy.exclude: true
|
||||
proxy.#1.healthcheck.disable: true
|
||||
services:
|
||||
app:
|
||||
image: godoxy-dev
|
||||
@@ -54,7 +59,190 @@ services:
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
proxy.tinyauth.port: "3000"
|
||||
jotty: # issue #182
|
||||
image: ghcr.io/fccview/jotty:latest
|
||||
container_name: jotty
|
||||
user: "1000:1000"
|
||||
tmpfs:
|
||||
- /app/data:rw,uid=1000,gid=1000
|
||||
- /app/config:rw,uid=1000,gid=1000
|
||||
- /app/.next/cache:rw,uid=1000,gid=1000
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
labels:
|
||||
proxy.aliases: "jotty.my.app"
|
||||
postgres-test:
|
||||
image: postgres:18-alpine
|
||||
container_name: postgres-test
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
h2c_test_server:
|
||||
build:
|
||||
context: cmd/h2c_test_server
|
||||
dockerfile: Dockerfile
|
||||
container_name: h2c_test
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
proxy.#1.scheme: h2c
|
||||
proxy.#1.port: 80
|
||||
bench: # returns 4096 bytes of random data
|
||||
<<: *benchmark
|
||||
build:
|
||||
context: cmd/bench_server
|
||||
dockerfile: Dockerfile
|
||||
container_name: bench
|
||||
godoxy:
|
||||
<<: *benchmark
|
||||
build: .
|
||||
container_name: godoxy-benchmark
|
||||
ports:
|
||||
- 8080:80
|
||||
configs:
|
||||
- source: godoxy_config
|
||||
target: /app/config/config.yml
|
||||
- source: godoxy_provider
|
||||
target: /app/config/providers.yml
|
||||
traefik:
|
||||
<<: *benchmark
|
||||
image: traefik:latest
|
||||
container_name: traefik
|
||||
command:
|
||||
- --api.insecure=true
|
||||
- --entrypoints.web.address=:8081
|
||||
- --providers.file.directory=/etc/traefik/dynamic
|
||||
- --providers.file.watch=true
|
||||
- --log.level=ERROR
|
||||
ports:
|
||||
- 8081:8081
|
||||
configs:
|
||||
- source: traefik_config
|
||||
target: /etc/traefik/dynamic/routes.yml
|
||||
caddy:
|
||||
<<: *benchmark
|
||||
image: caddy:latest
|
||||
container_name: caddy
|
||||
ports:
|
||||
- 8082:80
|
||||
configs:
|
||||
- source: caddy_config
|
||||
target: /etc/caddy/Caddyfile
|
||||
tmpfs:
|
||||
- /data
|
||||
- /config
|
||||
nginx:
|
||||
<<: *benchmark
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
command: nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
|
||||
ports:
|
||||
- 8083:80
|
||||
configs:
|
||||
- source: nginx_config
|
||||
target: /etc/nginx/nginx.conf
|
||||
|
||||
configs:
|
||||
godoxy_config:
|
||||
content: |
|
||||
providers:
|
||||
include:
|
||||
- providers.yml
|
||||
godoxy_provider:
|
||||
content: |
|
||||
bench.domain.com:
|
||||
host: bench
|
||||
traefik_config:
|
||||
content: |
|
||||
http:
|
||||
routers:
|
||||
bench:
|
||||
rule: "Host(`bench.domain.com`)"
|
||||
entryPoints:
|
||||
- web
|
||||
service: bench
|
||||
services:
|
||||
bench:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://bench:80"
|
||||
caddy_config:
|
||||
content: |
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
default_bind 0.0.0.0
|
||||
|
||||
servers {
|
||||
protocols h1 h2c
|
||||
}
|
||||
}
|
||||
|
||||
http://bench.domain.com {
|
||||
reverse_proxy bench:80
|
||||
}
|
||||
nginx_config:
|
||||
content: |
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65535;
|
||||
error_log /dev/null;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 10240;
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log off;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
keepalive_requests 10000;
|
||||
|
||||
upstream backend {
|
||||
server bench:80;
|
||||
keepalive 128;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
http2 on;
|
||||
|
||||
return 404;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name bench.domain.com;
|
||||
http2 on;
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $$host;
|
||||
proxy_set_header X-Real-IP $$remote_addr;
|
||||
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
}
|
||||
parca:
|
||||
content: |
|
||||
object_storage:
|
||||
|
||||
144
go.mod
144
go.mod
@@ -1,63 +1,68 @@
|
||||
module github.com/yusing/godoxy
|
||||
|
||||
go 1.25.4
|
||||
go 1.25.5
|
||||
|
||||
replace github.com/yusing/godoxy/agent => ./agent
|
||||
replace (
|
||||
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||
github.com/yusing/godoxy/agent => ./agent
|
||||
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||
github.com/yusing/goutils => ./goutils
|
||||
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
|
||||
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
|
||||
github.com/yusing/goutils/server => ./goutils/server
|
||||
)
|
||||
|
||||
replace github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
|
||||
|
||||
replace github.com/coreos/go-oidc/v3 => ./internal/go-oidc
|
||||
|
||||
replace github.com/shirou/gopsutil/v4 => ./internal/gopsutil
|
||||
|
||||
replace github.com/yusing/goutils => ./goutils
|
||||
exclude github.com/luthermonson/go-proxmox v0.3.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 // oidc authentication
|
||||
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
|
||||
github.com/docker/docker v28.5.2+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/gin-gonic/gin v1.11.0 // api server
|
||||
github.com/go-acme/lego/v4 v4.28.1 // acme client
|
||||
github.com/go-playground/validator/v10 v10.28.0 // validator
|
||||
github.com/go-acme/lego/v4 v4.31.0 // acme client
|
||||
github.com/go-playground/validator/v10 v10.30.1 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
|
||||
github.com/gotify/server/v2 v2.7.3 // reference the Message struct for json response
|
||||
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.44.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.47.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.33.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.48.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
|
||||
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
|
||||
golang.org/x/time v0.14.0 // time utilities
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
|
||||
github.com/bytedance/sonic v1.14.2 // fast json parsing
|
||||
github.com/docker/cli v29.0.1+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
|
||||
github.com/luthermonson/go-proxmox v0.2.3 // proxmox API client
|
||||
github.com/moby/moby/api v1.52.0 // docker API
|
||||
github.com/moby/moby/client v0.1.0 // docker client
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
|
||||
github.com/quic-go/quic-go v0.56.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.10 // system information
|
||||
github.com/docker/cli v29.1.4+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
|
||||
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/luthermonson/go-proxmox v0.3.2
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.58.0 // http3 support
|
||||
github.com/shirou/gopsutil/v4 v4.25.12 // system information
|
||||
github.com/spf13/afero v1.15.0 // afero for file system operations
|
||||
github.com/stretchr/testify v1.11.1 // testing framework
|
||||
github.com/valyala/fasthttp v1.68.0 // fast http for health check
|
||||
github.com/valyala/fasthttp v1.69.0 // fast http for health check
|
||||
github.com/yusing/ds v0.3.1 // data structures and algorithms
|
||||
github.com/yusing/godoxy/agent v0.0.0-20251114142829-a291a49a0e42
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20251114142829-a291a49a0e42
|
||||
github.com/yusing/godoxy/agent v0.0.0-20260109134835-4ec352f1f610
|
||||
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260109134835-4ec352f1f610
|
||||
github.com/yusing/gointernals v0.1.16
|
||||
github.com/yusing/goutils v0.7.0
|
||||
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109025656-326c1f1eb362
|
||||
github.com/yusing/goutils/http/websocket v0.0.0-20260109025656-326c1f1eb362
|
||||
github.com/yusing/goutils/server v0.0.0-20260109025656-326c1f1eb362
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
@@ -79,7 +84,7 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -88,8 +93,8 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
@@ -99,7 +104,7 @@ require (
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -111,68 +116,87 @@ require (
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/samber/slog-common v0.19.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||
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.64.0
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.259.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/moby/moby/api v1.52.0
|
||||
github.com/moby/moby/client v0.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/go-resty/resty/v2 v2.17.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/linode/linodego v1.61.0 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/linode/linodego v1.64.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.12.0 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.24.0 // indirect
|
||||
github.com/vultr/govultr/v3 v3.26.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
)
|
||||
|
||||
226
go.sum
226
go.sum
@@ -1,5 +1,5 @@
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
@@ -23,6 +23,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
@@ -44,6 +46,9 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
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=
|
||||
@@ -54,12 +59,16 @@ github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2N
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -71,8 +80,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/docker/cli v29.0.1+incompatible h1:EnvMEAR9Ro5xQEKbMitlabj5vCDY0vwcDyY/Lsow7FQ=
|
||||
github.com/docker/cli v29.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
|
||||
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+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=
|
||||
@@ -83,18 +94,20 @@ github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1Ugj
|
||||
github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=
|
||||
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/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=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-acme/lego/v4 v4.28.1 h1:zt301JYF51UIEkpSXsdeGq9hRePeFzQCq070OdAmP0Q=
|
||||
github.com/go-acme/lego/v4 v4.28.1/go.mod h1:bzjilr03IgbaOwlH396hq5W56Bi0/uoRwW/JM8hP7m4=
|
||||
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
|
||||
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -113,18 +126,20 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
@@ -132,25 +147,26 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/server/v2 v2.7.3 h1:nro/ZnxdlZFvxFcw9LREGA8zdk6CK744azwhuhX/A4g=
|
||||
github.com/gotify/server/v2 v2.7.3/go.mod h1:VAtE1RIc/2j886PYs9WPQbMjqbFsoyQ0G8IdFtnAxU0=
|
||||
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
|
||||
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
@@ -169,10 +185,12 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -181,14 +199,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/linode/linodego v1.61.0 h1:9g20NWl+/SbhDFj6X5EOZXtM2hBm1Mx8I9h8+F3l1LM=
|
||||
github.com/linode/linodego v1.61.0/go.mod h1:64o30geLNwR0NeYh5HM/WrVCBXcSqkKnRK3x9xoRuJI=
|
||||
github.com/linode/linodego v1.64.0 h1:If6pULIwHuQytgogtpQaBdVLX7z2TTHUF5u1tj2TPiY=
|
||||
github.com/linode/linodego v1.64.0/go.mod h1:GoiwLVuLdBQcAebxAVKVL3mMYUgJZR/puOUSla04xBE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/luthermonson/go-proxmox v0.2.3 h1:NAjUJ5Jd1ynIK6UHMGd/VLGgNZWpGXhfL+DBmAVSEaA=
|
||||
github.com/luthermonson/go-proxmox v0.2.3/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
|
||||
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -200,27 +218,37 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
|
||||
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
|
||||
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8=
|
||||
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
|
||||
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
|
||||
github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1 h1:kR8dHXC53heV4fttilXXkDkGmkdC5bvQ2XgbVBoD+ns=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.104.1/go.mod h1:SfDIKzNQ5AGNMMOA3LGqSPnn63F6Gc4E4bsKArqymvg=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1 h1:559XLHTF3F1A0J03PCIk6LlR0G9CmhHEDCm/TSk5BWQ=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.104.1/go.mod h1:1FfSn6xcdK+wrNxUhtAAhLjYrwk9z8X3p3CNHwTc3zQ=
|
||||
github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4=
|
||||
github.com/nrdcg/goinwx v0.12.0/go.mod h1:IrVKd3ZDbFiMjdPgML4CSxZAY9wOoqLvH44zv3NodJ0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 h1:l0tH15ACQADZAzC+LZ+mo2tIX4H6uZu0ulrVmG5Tqz0=
|
||||
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2/go.mod h1:Gcs8GCaZXL3FdiDWgdnMxlOLEdRprJJnPYB22TX1jw8=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 h1:gzB4c6ztb38C/jYiqEaFC+mCGcWFHDji9e6jwymY9d4=
|
||||
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2/go.mod h1:l1qIPIq2uRV5WTSvkbhbl/ndbeOu7OCb3UZ+0+2ZSb8=
|
||||
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -235,10 +263,17 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
@@ -247,12 +282,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
|
||||
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
@@ -264,8 +301,8 @@ github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89
|
||||
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
|
||||
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35 h1:8xfn1RzeI9yoCUuEwDy08F+No6PcKZGEDOQ6hrRyLts=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.35/go.mod h1:47B1d/YXmSAxlJxUJxClzHR6b3T4M1WyCvwENPQNBWc=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
@@ -295,16 +332,16 @@ 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.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
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/vultr/govultr/v3 v3.24.0 h1:fTTTj0VBve+Miy+wGhlb90M2NMDfpGFi6Frlj3HVy6M=
|
||||
github.com/vultr/govultr/v3 v3.24.0/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/vultr/govultr/v3 v3.26.1 h1:G/M0rMQKwVSmL+gb0UgETbW5mcQi0Vf/o/ZSGdBCxJw=
|
||||
github.com/vultr/govultr/v3 v3.26.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
@@ -320,22 +357,28 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
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.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
|
||||
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
|
||||
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
@@ -346,15 +389,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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=
|
||||
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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
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=
|
||||
@@ -364,10 +407,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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=
|
||||
@@ -375,8 +418,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -396,8 +439,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -416,8 +459,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -426,24 +469,23 @@ 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=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba h1:Ze6qXW0j37YCqZdCD2LkzVSxgEWez0cO4NUyd44DiDY=
|
||||
google.golang.org/genproto v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:4FLPzLA8eGAktPOTemJGDgDYRpLYwrNu4u2JtWINhnI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
2
goutils
2
goutils
Submodule goutils updated: ca5989aeda...326c1f1eb3
282
internal/acl/README.md
Normal file
282
internal/acl/README.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# ACL (Access Control List)
|
||||
|
||||
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
|
||||
|
||||
## Overview
|
||||
|
||||
The ACL package provides network-level access control by wrapping TCP listeners and validating incoming connections against configurable allow/deny rules. It integrates with MaxMind GeoIP for geographic-based filtering and supports access logging with notification batching.
|
||||
|
||||
### Primary consumers
|
||||
|
||||
- `internal/entrypoint` - Wraps the main TCP listener for connection filtering
|
||||
- Operators - Configure rules via YAML configuration
|
||||
|
||||
### Non-goals
|
||||
|
||||
- HTTP request-level filtering (handled by middleware)
|
||||
- Authentication or authorization (see `internal/auth`)
|
||||
- VPN or tunnel integration
|
||||
|
||||
### Stability
|
||||
|
||||
Stable internal package. The public API is the `Config` struct and its methods.
|
||||
|
||||
## Public API
|
||||
|
||||
### Exported types
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Default string // "allow" or "deny" (default: "allow")
|
||||
AllowLocal *bool // Allow private/loopback IPs (default: true)
|
||||
Allow Matchers // Allow rules
|
||||
Deny Matchers // Deny rules
|
||||
Log *accesslog.ACLLoggerConfig // Access logging configuration
|
||||
|
||||
Notify struct {
|
||||
To []string // Notification providers
|
||||
Interval time.Duration // Notification frequency (default: 1m)
|
||||
IncludeAllowed *bool // Include allowed in notifications (default: false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Matcher struct {
|
||||
match MatcherFunc
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Matchers []Matcher
|
||||
```
|
||||
|
||||
### Exported functions and methods
|
||||
|
||||
```go
|
||||
func (c *Config) Validate() gperr.Error
|
||||
```
|
||||
|
||||
Validates configuration and sets defaults. Must be called before `Start`.
|
||||
|
||||
```go
|
||||
func (c *Config) Start(parent task.Parent) gperr.Error
|
||||
```
|
||||
|
||||
Initializes the ACL, starts the logger and notification goroutines.
|
||||
|
||||
```go
|
||||
func (c *Config) IPAllowed(ip net.IP) bool
|
||||
```
|
||||
|
||||
Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.
|
||||
|
||||
```go
|
||||
func (c *Config) WrapTCP(lis net.Listener) net.Listener
|
||||
```
|
||||
|
||||
Wraps a `net.Listener` to filter connections by IP.
|
||||
|
||||
```go
|
||||
func (matcher *Matcher) Parse(s string) error
|
||||
```
|
||||
|
||||
Parses a matcher string in the format `{type}:{value}`. Supported types: `ip`, `cidr`, `tz`, `country`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core components
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[TCP Listener] --> B[TCPListener Wrapper]
|
||||
B --> C{IP Allowed?}
|
||||
C -->|Yes| D[Accept Connection]
|
||||
C -->|No| E[Close Connection]
|
||||
|
||||
F[Config] --> G[Validate]
|
||||
G --> H[Start]
|
||||
H --> I[Matcher Evaluation]
|
||||
I --> C
|
||||
|
||||
J[MaxMind] -.-> K[IP Lookup]
|
||||
K -.-> I
|
||||
|
||||
L[Access Logger] -.-> M[Log & Notify]
|
||||
M -.-> B
|
||||
```
|
||||
|
||||
### Connection filtering flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant TCPListener
|
||||
participant Config
|
||||
participant MaxMind
|
||||
participant Logger
|
||||
|
||||
Client->>TCPListener: Connection Request
|
||||
TCPListener->>Config: IPAllowed(clientIP)
|
||||
|
||||
alt Loopback IP
|
||||
Config-->>TCPListener: true
|
||||
else Private IP (allow_local)
|
||||
Config-->>TCPListener: true
|
||||
else Cached Result
|
||||
Config-->>TCPListener: Cached Result
|
||||
else Evaluate Allow Rules
|
||||
Config->>Config: Check Allow list
|
||||
alt Matches
|
||||
Config->>Config: Cache true
|
||||
Config-->>TCPListener: Allowed
|
||||
else Evaluate Deny Rules
|
||||
Config->>Config: Check Deny list
|
||||
alt Matches
|
||||
Config->>Config: Cache false
|
||||
Config-->>TCPListener: Denied
|
||||
else Default Action
|
||||
Config->>MaxMind: Lookup GeoIP
|
||||
MaxMind-->>Config: IPInfo
|
||||
Config->>Config: Apply default rule
|
||||
Config->>Config: Cache result
|
||||
Config-->>TCPListener: Result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alt Logging enabled
|
||||
Config->>Logger: Log access attempt
|
||||
end
|
||||
```
|
||||
|
||||
### Matcher types
|
||||
|
||||
| Type | Format | Example |
|
||||
| -------- | ----------------- | --------------------- |
|
||||
| IP | `ip:address` | `ip:192.168.1.1` |
|
||||
| CIDR | `cidr:network` | `cidr:192.168.0.0/16` |
|
||||
| TimeZone | `tz:timezone` | `tz:Asia/Shanghai` |
|
||||
| Country | `country:ISOCode` | `country:GB` |
|
||||
|
||||
## Configuration Surface
|
||||
|
||||
### Config sources
|
||||
|
||||
Configuration is loaded from `config/config.yml` under the `acl` key.
|
||||
|
||||
### Schema
|
||||
|
||||
```yaml
|
||||
acl:
|
||||
default: "allow" # "allow" or "deny"
|
||||
allow_local: true # Allow private/loopback IPs
|
||||
log:
|
||||
log_allowed: false # Log allowed connections
|
||||
notify:
|
||||
to: ["gotify"] # Notification providers
|
||||
interval: "1m" # Notification interval
|
||||
include_allowed: false # Include allowed in notifications
|
||||
```
|
||||
|
||||
### Hot-reloading
|
||||
|
||||
Configuration requires restart. The ACL does not support dynamic rule updates.
|
||||
|
||||
## Dependency and Integration Map
|
||||
|
||||
### Internal dependencies
|
||||
|
||||
- `internal/maxmind` - IP geolocation lookup
|
||||
- `internal/logging/accesslog` - Access logging
|
||||
- `internal/notif` - Notifications
|
||||
- `internal/task/task.go` - Lifetime management
|
||||
|
||||
### Integration points
|
||||
|
||||
```go
|
||||
// Entrypoint uses ACL to wrap the TCP listener
|
||||
aclListener := config.ACL.WrapTCP(listener)
|
||||
http.Server.Serve(aclListener, entrypoint)
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
### Logs
|
||||
|
||||
- `ACL started` - Configuration summary on start
|
||||
- `log_notify_loop` - Access attempts (allowed/denied)
|
||||
|
||||
Log levels: `Info` for startup, `Debug` for client closure.
|
||||
|
||||
### Metrics
|
||||
|
||||
No metrics are currently exposed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Loopback and private IPs are always allowed unless explicitly denied
|
||||
- Cache TTL is 1 minute to limit memory usage
|
||||
- Notification channel has a buffer of 100 to prevent blocking
|
||||
- Failed connections are immediately closed without response
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
| Failure | Behavior | Recovery |
|
||||
| --------------------------------- | ------------------------------------- | --------------------------------------------- |
|
||||
| Invalid matcher syntax | Validation fails on startup | Fix configuration syntax |
|
||||
| MaxMind database unavailable | GeoIP lookups return unknown location | Default action applies; cache hit still works |
|
||||
| Notification provider unavailable | Notification dropped | Error logged, continues operation |
|
||||
| Cache full | No eviction, uses Go map | No action needed |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic configuration
|
||||
|
||||
```go
|
||||
aclConfig := &acl.Config{
|
||||
Default: "allow",
|
||||
AllowLocal: ptr(true),
|
||||
Allow: acl.Matchers{
|
||||
{match: matchIP(net.ParseIP("192.168.1.0/24"))},
|
||||
},
|
||||
Deny: acl.Matchers{
|
||||
{match: matchISOCode("CN")},
|
||||
},
|
||||
}
|
||||
if err := aclConfig.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := aclConfig.Start(parent); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Wrapping a TCP listener
|
||||
|
||||
```go
|
||||
listener, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Wrap with ACL
|
||||
aclListener := aclConfig.WrapTCP(listener)
|
||||
|
||||
// Use with HTTP server
|
||||
server := &http.Server{}
|
||||
server.Serve(aclListener)
|
||||
```
|
||||
|
||||
### Creating custom matchers
|
||||
|
||||
```go
|
||||
matcher := &acl.Matcher{}
|
||||
err := matcher.Parse("country:US")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Use the matcher
|
||||
allowed := matcher.match(ipInfo)
|
||||
```
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/yusing/godoxy/internal/logging/accesslog"
|
||||
"github.com/yusing/godoxy/internal/maxmind"
|
||||
"github.com/yusing/godoxy/internal/notif"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
"github.com/yusing/goutils/task"
|
||||
@@ -82,7 +81,7 @@ var ActiveConfig atomic.Pointer[Config]
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
return c.created.Add(cacheTTL).Before(utils.TimeNow())
|
||||
return c.created.Add(cacheTTL).Before(time.Now())
|
||||
}
|
||||
|
||||
// TODO: add stats
|
||||
@@ -180,7 +179,7 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
|
||||
c.ipCache.Store(info.Str, &checkCache{
|
||||
IPInfo: info,
|
||||
allow: allow,
|
||||
created: utils.TimeNow(),
|
||||
created: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
281
internal/agentpool/README.md
Normal file
281
internal/agentpool/README.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Agent Pool
|
||||
|
||||
Thread-safe pool for managing remote Docker agent connections.
|
||||
|
||||
## Overview
|
||||
|
||||
The agentpool package provides a centralized pool for storing and retrieving remote agent configurations. It enables GoDoxy to connect to Docker hosts via agent connections instead of direct socket access, enabling secure remote container management.
|
||||
|
||||
### Primary consumers
|
||||
|
||||
- `internal/route/provider` - Creates agent-based route providers
|
||||
- `internal/docker` - Manages agent-based Docker client connections
|
||||
- Configuration loading during startup
|
||||
|
||||
### Non-goals
|
||||
|
||||
- Agent lifecycle management (handled by `agent/pkg/agent`)
|
||||
- Agent health monitoring
|
||||
- Agent authentication/authorization
|
||||
|
||||
### Stability
|
||||
|
||||
Stable internal package. The pool uses `xsync.Map` for lock-free concurrent access.
|
||||
|
||||
## Public API
|
||||
|
||||
### Exported types
|
||||
|
||||
```go
|
||||
type Agent struct {
|
||||
*agent.AgentConfig
|
||||
httpClient *http.Client
|
||||
fasthttpHcClient *fasthttp.Client
|
||||
}
|
||||
```
|
||||
|
||||
### Exported functions
|
||||
|
||||
```go
|
||||
func Add(cfg *agent.AgentConfig) (added bool)
|
||||
```
|
||||
|
||||
Adds an agent to the pool. Returns `true` if added, `false` if already exists. Uses `LoadOrCompute` to prevent duplicates.
|
||||
|
||||
```go
|
||||
func Has(cfg *agent.AgentConfig) bool
|
||||
```
|
||||
|
||||
Checks if an agent exists in the pool.
|
||||
|
||||
```go
|
||||
func Remove(cfg *agent.AgentConfig)
|
||||
```
|
||||
|
||||
Removes an agent from the pool.
|
||||
|
||||
```go
|
||||
func RemoveAll()
|
||||
```
|
||||
|
||||
Removes all agents from the pool. Called during configuration reload.
|
||||
|
||||
```go
|
||||
func Get(agentAddrOrDockerHost string) (*Agent, bool)
|
||||
```
|
||||
|
||||
Retrieves an agent by address or Docker host URL. Automatically detects if the input is an agent address or Docker host URL and resolves accordingly.
|
||||
|
||||
```go
|
||||
func GetAgent(name string) (*Agent, bool)
|
||||
```
|
||||
|
||||
Retrieves an agent by name. O(n) iteration over pool contents.
|
||||
|
||||
```go
|
||||
func List() []*Agent
|
||||
```
|
||||
|
||||
Returns all agents as a slice. Creates a new copy for thread safety.
|
||||
|
||||
```go
|
||||
func Iter() iter.Seq2[string, *Agent]
|
||||
```
|
||||
|
||||
Returns an iterator over all agents. Uses `xsync.Map.Range`.
|
||||
|
||||
```go
|
||||
func Num() int
|
||||
```
|
||||
|
||||
Returns the number of agents in the pool.
|
||||
|
||||
```go
|
||||
func (agent *Agent) HTTPClient() *http.Client
|
||||
```
|
||||
|
||||
Returns an HTTP client configured for the agent.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core components
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Agent Config] --> B[Add to Pool]
|
||||
B --> C[xsync.Map Storage]
|
||||
C --> D{Get Request}
|
||||
D -->|By Address| E[Load from map]
|
||||
D -->|By Docker Host| F[Resolve agent addr]
|
||||
D -->|By Name| G[Iterate & match]
|
||||
|
||||
H[Docker Client] --> I[Get Agent]
|
||||
I --> C
|
||||
I --> J[HTTP Client]
|
||||
J --> K[Agent Connection]
|
||||
|
||||
L[Route Provider] --> M[List Agents]
|
||||
M --> C
|
||||
```
|
||||
|
||||
### Thread safety model
|
||||
|
||||
The pool uses `xsync.Map[string, *Agent]` for concurrent-safe operations:
|
||||
|
||||
- `Add`: `LoadOrCompute` prevents race conditions and duplicates
|
||||
- `Get`: Lock-free read operations
|
||||
- `Iter`: Consistent snapshot iteration via `Range`
|
||||
- `Remove`: Thread-safe deletion
|
||||
|
||||
### Test mode
|
||||
|
||||
When running tests (binary ends with `.test`), a test agent is automatically added:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
if strings.HasSuffix(os.Args[0], ".test") {
|
||||
agentPool.Store("test-agent", &Agent{
|
||||
AgentConfig: &agent.AgentConfig{
|
||||
Addr: "test-agent",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Surface
|
||||
|
||||
No direct configuration. Agents are added via configuration loading from `config/config.yml`:
|
||||
|
||||
```yaml
|
||||
providers:
|
||||
agents:
|
||||
- addr: agent.example.com:443
|
||||
name: remote-agent
|
||||
tls:
|
||||
ca_file: /path/to/ca.pem
|
||||
cert_file: /path/to/cert.pem
|
||||
key_file: /path/to/key.pem
|
||||
```
|
||||
|
||||
## Dependency and Integration Map
|
||||
|
||||
### Internal dependencies
|
||||
|
||||
- `agent/pkg/agent` - Agent configuration and connection settings
|
||||
- `xsync/v4` - Concurrent map implementation
|
||||
|
||||
### External dependencies
|
||||
|
||||
- `valyala/fasthttp` - Fast HTTP client for agent communication
|
||||
|
||||
### Integration points
|
||||
|
||||
```go
|
||||
// Docker package uses agent pool for remote connections
|
||||
if agent.IsDockerHostAgent(host) {
|
||||
a, ok := agentpool.Get(host)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("agent %q not found", host))
|
||||
}
|
||||
opt := []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
client.WithHTTPClient(a.HTTPClient()),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
### Logs
|
||||
|
||||
No specific logging in the agentpool package. Client creation/destruction is logged in the docker package.
|
||||
|
||||
### Metrics
|
||||
|
||||
No metrics are currently exposed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- TLS configuration is loaded from agent configuration
|
||||
- Connection credentials are not stored in the pool after agent creation
|
||||
- HTTP clients are created per-request to ensure credential freshness
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
| Failure | Behavior | Recovery |
|
||||
| -------------------- | -------------------- | ---------------------------- |
|
||||
| Agent not found | Returns `nil, false` | Add agent to pool before use |
|
||||
| Duplicate add | Returns `false` | Existing agent is preserved |
|
||||
| Test mode activation | Test agent added | Only during test binaries |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
- O(1) lookup by address
|
||||
- O(n) iteration for name-based lookup
|
||||
- Pre-sized to 10 entries via `xsync.WithPresize(10)`
|
||||
- No locks required for read operations
|
||||
- HTTP clients are created per-call to ensure fresh connections
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Adding an agent
|
||||
|
||||
```go
|
||||
agentConfig := &agent.AgentConfig{
|
||||
Addr: "agent.example.com:443",
|
||||
Name: "my-agent",
|
||||
}
|
||||
|
||||
added := agentpool.Add(agentConfig)
|
||||
if !added {
|
||||
log.Println("Agent already exists")
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving an agent
|
||||
|
||||
```go
|
||||
// By address
|
||||
agent, ok := agentpool.Get("agent.example.com:443")
|
||||
if !ok {
|
||||
log.Fatal("Agent not found")
|
||||
}
|
||||
|
||||
// By Docker host URL
|
||||
agent, ok := agentpool.Get("http://docker-host:2375")
|
||||
if !ok {
|
||||
log.Fatal("Agent not found")
|
||||
}
|
||||
|
||||
// By name
|
||||
agent, ok := agentpool.GetAgent("my-agent")
|
||||
if !ok {
|
||||
log.Fatal("Agent not found")
|
||||
}
|
||||
```
|
||||
|
||||
### Iterating over all agents
|
||||
|
||||
```go
|
||||
for addr, agent := range agentpool.Iter() {
|
||||
log.Printf("Agent: %s at %s", agent.Name, addr)
|
||||
}
|
||||
```
|
||||
|
||||
### Using with Docker client
|
||||
|
||||
```go
|
||||
// When creating a Docker client with an agent host
|
||||
if agent.IsDockerHostAgent(host) {
|
||||
a, ok := agentpool.Get(host)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("agent %q not found", host))
|
||||
}
|
||||
opt := []client.Opt{
|
||||
client.WithHost(agent.DockerHost),
|
||||
client.WithHTTPClient(a.HTTPClient()),
|
||||
}
|
||||
dockerClient, err := client.New(opt...)
|
||||
}
|
||||
```
|
||||
54
internal/agentpool/agent.go
Normal file
54
internal/agentpool/agent.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package agentpool
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
*agent.AgentConfig
|
||||
|
||||
httpClient *http.Client
|
||||
fasthttpHcClient *fasthttp.Client
|
||||
}
|
||||
|
||||
func newAgent(cfg *agent.AgentConfig) *Agent {
|
||||
transport := cfg.Transport()
|
||||
transport.MaxIdleConns = 100
|
||||
transport.MaxIdleConnsPerHost = 100
|
||||
transport.ReadBufferSize = 16384
|
||||
transport.WriteBufferSize = 16384
|
||||
|
||||
return &Agent{
|
||||
AgentConfig: cfg,
|
||||
httpClient: &http.Client{
|
||||
Transport: transport,
|
||||
},
|
||||
fasthttpHcClient: &fasthttp.Client{
|
||||
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
if addr != agent.AgentHost+":443" {
|
||||
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
|
||||
}
|
||||
return net.DialTimeout("tcp", cfg.Addr, timeout)
|
||||
},
|
||||
TLSConfig: cfg.TLSConfig(),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
DisableHeaderNamesNormalizing: true,
|
||||
DisablePathNormalizing: true,
|
||||
NoDefaultUserAgentHeader: true,
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (agent *Agent) HTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: agent.Transport(),
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package agent
|
||||
package agentpool
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,22 +10,22 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/goutils/http/reverseproxy"
|
||||
)
|
||||
|
||||
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
|
||||
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg.httpClient.Do(req)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||
req.URL.Host = AgentHost
|
||||
func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
|
||||
req.URL.Host = agent.AgentHost
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Path = APIEndpointBase + endpoint
|
||||
req.URL.Path = agent.APIEndpointBase + endpoint
|
||||
req.RequestURI = ""
|
||||
resp, err := cfg.httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -40,20 +40,20 @@ type HealthCheckResponse struct {
|
||||
Latency time.Duration `json:"latency"`
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||
func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.SetRequestURI(APIBaseURL + EndpointHealth + "?" + query)
|
||||
req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
|
||||
req.Header.SetMethod(fasthttp.MethodGet)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
req.SetConnectionClose()
|
||||
|
||||
start := time.Now()
|
||||
err = cfg.fasthttpClientHealthCheck.DoTimeout(req, resp, timeout)
|
||||
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
|
||||
ret.Latency = time.Since(start)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
@@ -71,30 +71,14 @@ func (cfg *AgentConfig) DoHealthCheck(timeout time.Duration, query string) (ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
|
||||
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, release, err := httputils.ReadAllBody(resp)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
ret := string(data)
|
||||
release(data)
|
||||
return ret, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
|
||||
transport := cfg.Transport()
|
||||
dialer := websocket.Dialer{
|
||||
NetDialContext: transport.DialContext,
|
||||
NetDialTLSContext: transport.DialTLSContext,
|
||||
}
|
||||
return dialer.DialContext(ctx, APIBaseURL+endpoint, http.Header{
|
||||
"Host": {AgentHost},
|
||||
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
|
||||
"Host": {agent.AgentHost},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,9 +86,9 @@ func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websoc
|
||||
//
|
||||
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
|
||||
// If the request has a query, it will be added to the proxy request's URL
|
||||
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||
rp := reverseproxy.NewReverseProxy("agent", AgentURL, cfg.Transport())
|
||||
req.URL.Host = AgentHost
|
||||
func (cfg *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
|
||||
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
|
||||
req.URL.Host = agent.AgentHost
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Path = endpoint
|
||||
req.RequestURI = ""
|
||||
79
internal/agentpool/pool.go
Normal file
79
internal/agentpool/pool.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package agentpool
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
)
|
||||
|
||||
var agentPool = xsync.NewMap[string, *Agent](xsync.WithPresize(10))
|
||||
|
||||
func init() {
|
||||
if strings.HasSuffix(os.Args[0], ".test") {
|
||||
agentPool.Store("test-agent", &Agent{
|
||||
AgentConfig: &agent.AgentConfig{
|
||||
Addr: "test-agent",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Get(agentAddrOrDockerHost string) (*Agent, bool) {
|
||||
if !agent.IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||
return getAgentByAddr(agentAddrOrDockerHost)
|
||||
}
|
||||
return getAgentByAddr(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||
}
|
||||
|
||||
func GetAgent(name string) (*Agent, bool) {
|
||||
for _, agent := range agentPool.Range {
|
||||
if agent.Name == name {
|
||||
return agent, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func Add(cfg *agent.AgentConfig) (added bool) {
|
||||
_, loaded := agentPool.LoadOrCompute(cfg.Addr, func() (*Agent, bool) {
|
||||
return newAgent(cfg), false
|
||||
})
|
||||
return !loaded
|
||||
}
|
||||
|
||||
func Has(cfg *agent.AgentConfig) bool {
|
||||
_, ok := agentPool.Load(cfg.Addr)
|
||||
return ok
|
||||
}
|
||||
|
||||
func Remove(cfg *agent.AgentConfig) {
|
||||
agentPool.Delete(cfg.Addr)
|
||||
}
|
||||
|
||||
func RemoveAll() {
|
||||
agentPool.Clear()
|
||||
}
|
||||
|
||||
func List() []*Agent {
|
||||
agents := make([]*Agent, 0, agentPool.Size())
|
||||
for _, agent := range agentPool.Range {
|
||||
agents = append(agents, agent)
|
||||
}
|
||||
return agents
|
||||
}
|
||||
|
||||
func Iter() iter.Seq2[string, *Agent] {
|
||||
return agentPool.Range
|
||||
}
|
||||
|
||||
func Num() int {
|
||||
return agentPool.Size()
|
||||
}
|
||||
|
||||
func getAgentByAddr(addr string) (agent *Agent, ok bool) {
|
||||
agent, ok = agentPool.Load(addr)
|
||||
return agent, ok
|
||||
}
|
||||
197
internal/api/v1/README.md
Normal file
197
internal/api/v1/README.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# API v1 Package
|
||||
|
||||
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
|
||||
|
||||
## Overview
|
||||
|
||||
The `internal/api/v1` package implements the HTTP handlers that power GoDoxy's REST API. It uses the Gin web framework and provides endpoints for route management, container operations, certificate handling, system metrics, and configuration.
|
||||
|
||||
### Primary Consumers
|
||||
|
||||
- **WebUI**: The homepage dashboard and admin interface consume these endpoints
|
||||
|
||||
### Non-goals
|
||||
|
||||
- Authentication and authorization logic (delegated to `internal/auth`)
|
||||
- Route proxying and request handling (handled by `internal/route`)
|
||||
- Docker container lifecycle management (delegated to `internal/docker`)
|
||||
- Certificate issuance and storage (handled by `internal/autocert`)
|
||||
|
||||
### Stability
|
||||
|
||||
This package is stable. Public API endpoints follow semantic versioning for request/response contracts. Internal implementation may change between minor versions.
|
||||
|
||||
## Public API
|
||||
|
||||
### Exported Types
|
||||
|
||||
Types are defined in `goutils/apitypes`:
|
||||
|
||||
| Type | Purpose |
|
||||
| -------------------------- | -------------------------------- |
|
||||
| `apitypes.ErrorResponse` | Standard error response format |
|
||||
| `apitypes.SuccessResponse` | Standard success response format |
|
||||
|
||||
### Handler Subpackages
|
||||
|
||||
| Package | Purpose |
|
||||
| ---------- | ---------------------------------------------- |
|
||||
| `route` | Route listing, details, and playground testing |
|
||||
| `docker` | Docker container management and monitoring |
|
||||
| `cert` | Certificate information and renewal |
|
||||
| `metrics` | System metrics and uptime information |
|
||||
| `homepage` | Homepage items and category management |
|
||||
| `file` | Configuration file read/write operations |
|
||||
| `auth` | Authentication and session management |
|
||||
| `agent` | Remote agent creation and management |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Handler Organization
|
||||
|
||||
Package structure mirrors the API endpoint paths (e.g., `auth/login.go` handles `/auth/login`).
|
||||
|
||||
### Request Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant GinRouter
|
||||
participant Handler
|
||||
participant Service
|
||||
participant Response
|
||||
|
||||
Client->>GinRouter: HTTP Request
|
||||
GinRouter->>Handler: Route to handler
|
||||
Handler->>Service: Call service layer
|
||||
Service-->>Handler: Data or error
|
||||
Handler->>Response: Format JSON response
|
||||
Response-->>Client: JSON or redirect
|
||||
```
|
||||
|
||||
## Configuration Surface
|
||||
|
||||
API listening address is configured with `GODOXY_API_ADDR` environment variable.
|
||||
|
||||
## Dependency and Integration Map
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
| ----------------------- | --------------------------- |
|
||||
| `internal/route/routes` | Route storage and iteration |
|
||||
| `internal/docker` | Docker client management |
|
||||
| `internal/config` | Configuration access |
|
||||
| `internal/metrics` | System metrics collection |
|
||||
| `internal/homepage` | Homepage item generation |
|
||||
| `internal/agentpool` | Remote agent management |
|
||||
| `internal/auth` | Authentication services |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
| ------------------------------ | --------------------------- |
|
||||
| `github.com/gin-gonic/gin` | HTTP routing and middleware |
|
||||
| `github.com/gorilla/websocket` | WebSocket support |
|
||||
| `github.com/moby/moby/client` | Docker API client |
|
||||
|
||||
## Observability
|
||||
|
||||
### Logs
|
||||
|
||||
Handlers log at `INFO` level for requests and `ERROR` level for failures. Logs include:
|
||||
|
||||
- Request path and method
|
||||
- Response status code
|
||||
- Error details (when applicable)
|
||||
|
||||
### Metrics
|
||||
|
||||
No dedicated metrics exposed by handlers. Request metrics collected by middleware.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All endpoints (except `/api/v1/version`) require authentication
|
||||
- Input validation using Gin binding tags
|
||||
- Path traversal prevention in file operations
|
||||
- WebSocket connections use same auth middleware as HTTP
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
| Failure | Behavior |
|
||||
| ----------------------------------- | ------------------------------------------ |
|
||||
| Docker host unreachable | Returns partial results with errors logged |
|
||||
| Certificate provider not configured | Returns 404 |
|
||||
| Invalid request body | Returns 400 with error details |
|
||||
| Authentication failure | Returns 302 redirect to login |
|
||||
| Agent not found | Returns 404 |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Listing All Routes via WebSocket
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func watchRoutes(provider string) error {
|
||||
url := "ws://localhost:8888/api/v1/route/list"
|
||||
if provider != "" {
|
||||
url += "?provider=" + provider
|
||||
}
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// message contains JSON array of routes
|
||||
processRoutes(message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Getting Container Status
|
||||
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
Server string `json:"server"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
func listContainers() ([]Container, error) {
|
||||
resp, err := http.Get("http://localhost:8888/api/v1/docker/containers")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var containers []Container
|
||||
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
```
|
||||
|
||||
)
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
|
||||
if _, ok := agent.GetAgent(hostport); ok {
|
||||
if _, ok := agentpool.Get(hostport); ok {
|
||||
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/goutils/http/httpheaders"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
|
||||
@@ -19,15 +19,15 @@ import (
|
||||
// @Tags agent,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} Agent
|
||||
// @Success 200 {array} agent.AgentConfig
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Router /agent/list [get]
|
||||
func List(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
|
||||
return agent.ListAgents(), nil
|
||||
return agentpool.List(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, agent.ListAgents())
|
||||
c.JSON(http.StatusOK, agentpool.List())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/agent/pkg/certs"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
config "github.com/yusing/godoxy/internal/config/types"
|
||||
"github.com/yusing/godoxy/internal/route/provider"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -79,21 +80,28 @@ func Verify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
|
||||
}
|
||||
|
||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
cfgState := config.ActiveState.Load()
|
||||
for _, a := range cfgState.Value().Providers.Agents {
|
||||
if a.Addr == host {
|
||||
return 0, gperr.New("agent already exists")
|
||||
}
|
||||
}
|
||||
var errAgentAlreadyExists = gperr.New("agent already exists")
|
||||
|
||||
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
agentCfg.Runtime = containerRuntime
|
||||
|
||||
err := agentCfg.StartWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||
// check if agent host exists in the config
|
||||
cfgState := config.ActiveState.Load()
|
||||
for _, a := range cfgState.Value().Providers.Agents {
|
||||
if a.Addr == host {
|
||||
return 0, errAgentAlreadyExists
|
||||
}
|
||||
}
|
||||
// check if agent host exists in the agent pool
|
||||
if agentpool.Has(&agentCfg) {
|
||||
return 0, errAgentAlreadyExists
|
||||
}
|
||||
|
||||
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to start agent")
|
||||
return 0, gperr.Wrap(err, "failed to initialize agent config")
|
||||
}
|
||||
|
||||
provider := provider.NewAgentProvider(&agentCfg)
|
||||
@@ -102,11 +110,14 @@ func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, contain
|
||||
}
|
||||
|
||||
// agent must be added before loading routes
|
||||
agent.AddAgent(&agentCfg)
|
||||
added := agentpool.Add(&agentCfg)
|
||||
if !added {
|
||||
return 0, errAgentAlreadyExists
|
||||
}
|
||||
err = provider.LoadRoutes()
|
||||
if err != nil {
|
||||
cfgState.DeleteProvider(provider.String())
|
||||
agent.RemoveAgent(&agentCfg)
|
||||
agentpool.Remove(&agentCfg)
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package certapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -8,46 +9,33 @@ import (
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type CertInfo struct {
|
||||
Subject string `json:"subject"`
|
||||
Issuer string `json:"issuer"`
|
||||
NotBefore int64 `json:"not_before"`
|
||||
NotAfter int64 `json:"not_after"`
|
||||
DNSNames []string `json:"dns_names"`
|
||||
EmailAddresses []string `json:"email_addresses"`
|
||||
} // @name CertInfo
|
||||
|
||||
// @x-id "info"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get cert info
|
||||
// @Description Get cert info
|
||||
// @Tags cert
|
||||
// @Produce json
|
||||
// @Success 200 {object} CertInfo
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 404 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /cert/info [get]
|
||||
// @Success 200 {array} autocert.CertInfo
|
||||
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
|
||||
// @Failure 404 {object} apitypes.ErrorResponse "No certificates found or autocert is not enabled"
|
||||
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
|
||||
// @Router /cert/info [get]
|
||||
func Info(c *gin.Context) {
|
||||
autocert := autocert.ActiveProvider.Load()
|
||||
if autocert == nil {
|
||||
provider := autocert.ActiveProvider.Load()
|
||||
if provider == nil {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := autocert.GetCert(nil)
|
||||
certInfos, err := provider.GetCertInfos()
|
||||
if err != nil {
|
||||
if errors.Is(err, autocert.ErrNoCertificates) {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("no certificate found"))
|
||||
return
|
||||
}
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
|
||||
return
|
||||
}
|
||||
|
||||
certInfo := CertInfo{
|
||||
Subject: cert.Leaf.Subject.CommonName,
|
||||
Issuer: cert.Leaf.Issuer.CommonName,
|
||||
NotBefore: cert.Leaf.NotBefore.Unix(),
|
||||
NotAfter: cert.Leaf.NotAfter.Unix(),
|
||||
DNSNames: cert.Leaf.DNSNames,
|
||||
EmailAddresses: cert.Leaf.EmailAddresses,
|
||||
}
|
||||
c.JSON(http.StatusOK, certInfo)
|
||||
c.JSON(http.StatusOK, certInfos)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/logging/memlogger"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
"github.com/yusing/goutils/http/websocket"
|
||||
)
|
||||
|
||||
@@ -40,33 +39,33 @@ func Renew(c *gin.Context) {
|
||||
logs, cancel := memlogger.Events()
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
// Stream logs until WebSocket connection closes (renewal runs in background)
|
||||
for {
|
||||
select {
|
||||
case <-manager.Context().Done():
|
||||
return
|
||||
case l := <-logs:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = autocert.ObtainCert()
|
||||
if err != nil {
|
||||
gperr.LogError("failed to obtain cert", err)
|
||||
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
|
||||
} else {
|
||||
log.Info().Msg("cert obtained successfully")
|
||||
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case l := <-logs:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
// renewal happens in background
|
||||
ok := autocert.ForceExpiryAll()
|
||||
if !ok {
|
||||
log.Error().Msg("cert renewal already in progress")
|
||||
time.Sleep(1 * time.Second) // wait for the log above to be sent
|
||||
return
|
||||
}
|
||||
log.Info().Msg("cert force renewal requested")
|
||||
|
||||
autocert.WaitRenewalDone(manager.Context())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
@@ -29,13 +28,13 @@ func GetContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
dockerClient, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
@@ -43,22 +42,22 @@ func GetContainer(c *gin.Context) {
|
||||
|
||||
defer dockerClient.Close()
|
||||
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id, client.ContainerInspectOptions{})
|
||||
cont, err := dockerClient.ContainerInspect(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to inspect container"))
|
||||
return
|
||||
}
|
||||
|
||||
var state ContainerState
|
||||
if cont.Container.State != nil {
|
||||
state = cont.Container.State.Status
|
||||
if cont.State != nil {
|
||||
state = cont.State.Status
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, &Container{
|
||||
Server: dockerHost,
|
||||
Name: cont.Container.Name,
|
||||
ID: cont.Container.ID,
|
||||
Image: cont.Container.Image,
|
||||
Server: dockerCfg.URL,
|
||||
Name: cont.Name,
|
||||
ID: cont.ID,
|
||||
Image: cont.Image,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
@@ -40,12 +39,12 @@ func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Containe
|
||||
errs := gperr.NewBuilder("failed to get containers")
|
||||
containers := make([]Container, 0)
|
||||
for server, dockerClient := range dockerClients {
|
||||
conts, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{All: true})
|
||||
conts, err := dockerClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
for _, cont := range conts.Items {
|
||||
for _, cont := range conts {
|
||||
containers = append(containers, Container{
|
||||
Server: server,
|
||||
Name: cont.Names[0],
|
||||
|
||||
@@ -4,9 +4,8 @@ import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
dockerSystem "github.com/docker/docker/api/types/system"
|
||||
"github.com/gin-gonic/gin"
|
||||
dockerSystem "github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
|
||||
@@ -65,13 +64,13 @@ func GetDockerInfo(ctx context.Context, dockerClients DockerClients) ([]dockerIn
|
||||
|
||||
i := 0
|
||||
for name, dockerClient := range dockerClients {
|
||||
info, err := dockerClient.Info(ctx, client.InfoOptions{})
|
||||
info, err := dockerClient.Info(ctx)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
info.Info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info.Info)
|
||||
info.Name = name
|
||||
dockerInfos[i] = toDockerInfo(info)
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -57,20 +57,20 @@ func Logs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// TODO: implement levels
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(id)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error(fmt.Sprintf("container %s not found", id)))
|
||||
return
|
||||
}
|
||||
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
dockerClient, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to get docker client"))
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
opts := client.ContainerLogsOptions{
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: queryParams.Stdout,
|
||||
ShowStderr: queryParams.Stderr,
|
||||
Since: queryParams.Since,
|
||||
@@ -105,7 +105,7 @@ func Logs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
log.Err(err).
|
||||
Str("server", dockerHost).
|
||||
Str("server", dockerCfg.URL).
|
||||
Str("container", id).
|
||||
Msg("failed to de-multiplex logs")
|
||||
}
|
||||
|
||||
@@ -4,23 +4,17 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type RestartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerRestartOptions
|
||||
}
|
||||
|
||||
// @x-id "restart"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Restart container
|
||||
// @Description Restart container by container id
|
||||
// @Tags docker
|
||||
// @Produce json
|
||||
// @Param request body RestartRequest true "Request"
|
||||
// @Param request body StopRequest true "Request"
|
||||
// @Success 200 {object} apitypes.SuccessResponse
|
||||
// @Failure 400 {object} apitypes.ErrorResponse "Invalid request"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
@@ -28,19 +22,19 @@ type RestartRequest struct {
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /docker/restart [post]
|
||||
func Restart(c *gin.Context) {
|
||||
var req RestartRequest
|
||||
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)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
@@ -48,7 +42,7 @@ func Restart(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerRestart(c.Request.Context(), req.ID, req.ContainerRestartOptions)
|
||||
err = client.ContainerRestart(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to restart container"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StartRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStartOptions
|
||||
container.StartOptions
|
||||
}
|
||||
|
||||
// @x-id "start"
|
||||
@@ -34,13 +34,13 @@ func Start(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
@@ -48,7 +48,7 @@ func Start(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStart(c.Request.Context(), req.ID, req.ContainerStartOptions)
|
||||
err = client.ContainerStart(c.Request.Context(), req.ID, req.StartOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to start container"))
|
||||
return
|
||||
|
||||
@@ -3,15 +3,15 @@ package dockerapi
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type StopRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
client.ContainerStopOptions
|
||||
container.StopOptions
|
||||
}
|
||||
|
||||
// @x-id "stop"
|
||||
@@ -34,13 +34,13 @@ func Stop(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dockerHost, ok := docker.GetDockerHostByContainerID(req.ID)
|
||||
dockerCfg, ok := docker.GetDockerCfgByContainerID(req.ID)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("container not found"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := docker.NewClient(dockerHost)
|
||||
client, err := docker.NewClient(dockerCfg)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to create docker client"))
|
||||
return
|
||||
@@ -48,7 +48,7 @@ func Stop(c *gin.Context) {
|
||||
|
||||
defer client.Close()
|
||||
|
||||
_, err = client.ContainerStop(c.Request.Context(), req.ID, req.ContainerStopOptions)
|
||||
err = client.ContainerStop(c.Request.Context(), req.ID, req.StopOptions)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to stop container"))
|
||||
return
|
||||
|
||||
@@ -328,23 +328,26 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CertInfo"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/CertInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"description": "No certificates found or autocert is not enabled",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
@@ -2353,6 +2356,16 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"supports_tcp_stream": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"supports_udp_stream": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
@@ -2436,7 +2449,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent": {
|
||||
"$ref": "#/definitions/Agent",
|
||||
"$ref": "#/definitions/agentpool.Agent",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -2458,8 +2471,8 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_host": {
|
||||
"type": "string",
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -2715,7 +2728,7 @@
|
||||
"required": [
|
||||
"container_id",
|
||||
"container_name",
|
||||
"docker_host"
|
||||
"docker_cfg"
|
||||
],
|
||||
"properties": {
|
||||
"container_id": {
|
||||
@@ -2728,7 +2741,24 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"docker_host": {
|
||||
"docker_cfg": {
|
||||
"$ref": "#/definitions/DockerProviderConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tls": {
|
||||
"$ref": "#/definitions/DockerTLSConfig",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -2737,6 +2767,27 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"DockerTLSConfig": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ca_file"
|
||||
],
|
||||
"properties": {
|
||||
"ca_file": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"cert_file": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_file": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2881,7 +2932,7 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"retries": {
|
||||
"description": "<0: immediate, >=0: threshold",
|
||||
"description": "<0: immediate, 0: default, >0: threshold",
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
@@ -2918,43 +2969,6 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"HealthInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"latency": {
|
||||
"description": "latency in microseconds",
|
||||
"type": "number",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"healthy",
|
||||
"unhealthy",
|
||||
"napping",
|
||||
"starting",
|
||||
"error",
|
||||
"unknown"
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"uptime": {
|
||||
"description": "uptime in milliseconds",
|
||||
"type": "number",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"HealthInfoWithoutDetail": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3009,22 +3023,14 @@
|
||||
"x-nullable": true
|
||||
},
|
||||
"lastSeen": {
|
||||
"description": "unix timestamp in seconds",
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"lastSeenStr": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"latency": {
|
||||
"type": "number",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"latencyStr": {
|
||||
"type": "string",
|
||||
"description": "latency in milliseconds",
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
@@ -3034,30 +3040,22 @@
|
||||
"x-omitempty": false
|
||||
},
|
||||
"started": {
|
||||
"description": "unix timestamp in seconds",
|
||||
"type": "integer",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"startedStr": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/HealthStatusString",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"uptime": {
|
||||
"description": "uptime in seconds",
|
||||
"type": "number",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"uptimeStr": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
@@ -3070,11 +3068,32 @@
|
||||
"HealthMap": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/HealthInfo"
|
||||
"$ref": "#/definitions/HealthStatusString"
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"HealthStatusString": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"unknown",
|
||||
"healthy",
|
||||
"napping",
|
||||
"starting",
|
||||
"unhealthy",
|
||||
"error"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"StatusUnknownStr",
|
||||
"StatusHealthyStr",
|
||||
"StatusNappingStr",
|
||||
"StatusStartingStr",
|
||||
"StatusUnhealthyStr",
|
||||
"StatusErrorStr"
|
||||
],
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"HomepageCategory": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3430,6 +3449,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"no_loading_page": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"proxmox": {
|
||||
"$ref": "#/definitions/ProxmoxConfig",
|
||||
"x-nullable": false,
|
||||
@@ -4175,6 +4199,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"bind": {
|
||||
"description": "for TCP and UDP routes, bind address to listen on",
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"container": {
|
||||
"description": "Docker only",
|
||||
"allOf": [
|
||||
@@ -4234,6 +4263,12 @@
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"index": {
|
||||
"description": "Index file to serve for single-page app mode",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"load_balance": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4308,6 +4343,7 @@
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"h2c",
|
||||
"tcp",
|
||||
"udp",
|
||||
"fileserver"
|
||||
@@ -4315,6 +4351,12 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"spa": {
|
||||
"description": "Single-page app mode: serves index for non-existent paths",
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"description": "Path to client certificate",
|
||||
"type": "string",
|
||||
@@ -4500,6 +4542,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"is_excluded": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"statuses": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -4872,6 +4919,43 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"agentpool.Agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addr": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"runtime": {
|
||||
"$ref": "#/definitions/agent.ContainerRuntime",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"supports_tcp_stream": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"supports_udp_stream": {
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
}
|
||||
},
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"auth.UserPassAuthCallbackRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5295,6 +5379,11 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"bind": {
|
||||
"description": "for TCP and UDP routes, bind address to listen on",
|
||||
"type": "string",
|
||||
"x-nullable": true
|
||||
},
|
||||
"container": {
|
||||
"description": "Docker only",
|
||||
"allOf": [
|
||||
@@ -5354,6 +5443,12 @@
|
||||
],
|
||||
"x-nullable": true
|
||||
},
|
||||
"index": {
|
||||
"description": "Index file to serve for single-page app mode",
|
||||
"type": "string",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"load_balance": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -5428,6 +5523,7 @@
|
||||
"enum": [
|
||||
"http",
|
||||
"https",
|
||||
"h2c",
|
||||
"tcp",
|
||||
"udp",
|
||||
"fileserver"
|
||||
@@ -5435,6 +5531,12 @@
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"spa": {
|
||||
"description": "Single-page app mode: serves index for non-existent paths",
|
||||
"type": "boolean",
|
||||
"x-nullable": false,
|
||||
"x-omitempty": false
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"description": "Path to client certificate",
|
||||
"type": "string",
|
||||
|
||||
@@ -8,6 +8,10 @@ definitions:
|
||||
type: string
|
||||
runtime:
|
||||
$ref: '#/definitions/agent.ContainerRuntime'
|
||||
supports_tcp_stream:
|
||||
type: boolean
|
||||
supports_udp_stream:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
@@ -48,7 +52,7 @@ definitions:
|
||||
Container:
|
||||
properties:
|
||||
agent:
|
||||
$ref: '#/definitions/Agent'
|
||||
$ref: '#/definitions/agentpool.Agent'
|
||||
aliases:
|
||||
items:
|
||||
type: string
|
||||
@@ -57,8 +61,8 @@ definitions:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_host:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
errors:
|
||||
type: string
|
||||
idlewatcher_config:
|
||||
@@ -192,12 +196,30 @@ definitions:
|
||||
type: string
|
||||
container_name:
|
||||
type: string
|
||||
docker_host:
|
||||
type: string
|
||||
docker_cfg:
|
||||
$ref: '#/definitions/DockerProviderConfig'
|
||||
required:
|
||||
- container_id
|
||||
- container_name
|
||||
- docker_host
|
||||
- docker_cfg
|
||||
type: object
|
||||
DockerProviderConfig:
|
||||
properties:
|
||||
tls:
|
||||
$ref: '#/definitions/DockerTLSConfig'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
DockerTLSConfig:
|
||||
properties:
|
||||
ca_file:
|
||||
type: string
|
||||
cert_file:
|
||||
type: string
|
||||
key_file:
|
||||
type: string
|
||||
required:
|
||||
- ca_file
|
||||
type: object
|
||||
ErrorResponse:
|
||||
properties:
|
||||
@@ -269,7 +291,7 @@ definitions:
|
||||
path:
|
||||
type: string
|
||||
retries:
|
||||
description: '<0: immediate, >=0: threshold'
|
||||
description: '<0: immediate, 0: default, >0: threshold'
|
||||
type: integer
|
||||
timeout:
|
||||
type: integer
|
||||
@@ -284,26 +306,6 @@ definitions:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: object
|
||||
HealthInfo:
|
||||
properties:
|
||||
detail:
|
||||
type: string
|
||||
latency:
|
||||
description: latency in microseconds
|
||||
type: number
|
||||
status:
|
||||
enum:
|
||||
- healthy
|
||||
- unhealthy
|
||||
- napping
|
||||
- starting
|
||||
- error
|
||||
- unknown
|
||||
type: string
|
||||
uptime:
|
||||
description: uptime in milliseconds
|
||||
type: number
|
||||
type: object
|
||||
HealthInfoWithoutDetail:
|
||||
properties:
|
||||
latency:
|
||||
@@ -333,32 +335,44 @@ definitions:
|
||||
- $ref: '#/definitions/HealthExtra'
|
||||
x-nullable: true
|
||||
lastSeen:
|
||||
description: unix timestamp in seconds
|
||||
type: integer
|
||||
lastSeenStr:
|
||||
type: string
|
||||
latency:
|
||||
type: number
|
||||
latencyStr:
|
||||
type: string
|
||||
description: latency in milliseconds
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
started:
|
||||
description: unix timestamp in seconds
|
||||
type: integer
|
||||
startedStr:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
$ref: '#/definitions/HealthStatusString'
|
||||
uptime:
|
||||
description: uptime in seconds
|
||||
type: number
|
||||
uptimeStr:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
HealthMap:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/HealthInfo'
|
||||
$ref: '#/definitions/HealthStatusString'
|
||||
type: object
|
||||
HealthStatusString:
|
||||
enum:
|
||||
- unknown
|
||||
- healthy
|
||||
- napping
|
||||
- starting
|
||||
- unhealthy
|
||||
- error
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- StatusUnknownStr
|
||||
- StatusHealthyStr
|
||||
- StatusNappingStr
|
||||
- StatusStartingStr
|
||||
- StatusUnhealthyStr
|
||||
- StatusErrorStr
|
||||
HomepageCategory:
|
||||
properties:
|
||||
items:
|
||||
@@ -517,6 +531,8 @@ definitions:
|
||||
description: "0: no idle watcher.\nPositive: idle watcher with idle timeout.\nNegative:
|
||||
idle watcher as a dependency.\tIdleTimeout time.Duration `json:\"idle_timeout\"
|
||||
json_ext:\"duration\"`"
|
||||
no_loading_page:
|
||||
type: boolean
|
||||
proxmox:
|
||||
$ref: '#/definitions/ProxmoxConfig'
|
||||
start_endpoint:
|
||||
@@ -867,6 +883,10 @@ definitions:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
bind:
|
||||
description: for TCP and UDP routes, bind address to listen on
|
||||
type: string
|
||||
x-nullable: true
|
||||
container:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Container'
|
||||
@@ -897,6 +917,9 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/IdlewatcherConfig'
|
||||
x-nullable: true
|
||||
index:
|
||||
description: Index file to serve for single-page app mode
|
||||
type: string
|
||||
load_balance:
|
||||
allOf:
|
||||
- $ref: '#/definitions/LoadBalancerConfig'
|
||||
@@ -940,10 +963,14 @@ definitions:
|
||||
enum:
|
||||
- http
|
||||
- https
|
||||
- h2c
|
||||
- tcp
|
||||
- udp
|
||||
- fileserver
|
||||
type: string
|
||||
spa:
|
||||
description: 'Single-page app mode: serves index for non-existent paths'
|
||||
type: boolean
|
||||
ssl_certificate:
|
||||
description: Path to client certificate
|
||||
type: string
|
||||
@@ -1030,6 +1057,8 @@ definitions:
|
||||
type: number
|
||||
is_docker:
|
||||
type: boolean
|
||||
is_excluded:
|
||||
type: boolean
|
||||
statuses:
|
||||
items:
|
||||
$ref: '#/definitions/RouteStatus'
|
||||
@@ -1211,6 +1240,21 @@ definitions:
|
||||
x-enum-varnames:
|
||||
- ContainerRuntimeDocker
|
||||
- ContainerRuntimePodman
|
||||
agentpool.Agent:
|
||||
properties:
|
||||
addr:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
runtime:
|
||||
$ref: '#/definitions/agent.ContainerRuntime'
|
||||
supports_tcp_stream:
|
||||
type: boolean
|
||||
supports_udp_stream:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
auth.UserPassAuthCallbackRequest:
|
||||
properties:
|
||||
password:
|
||||
@@ -1474,6 +1518,10 @@ definitions:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
bind:
|
||||
description: for TCP and UDP routes, bind address to listen on
|
||||
type: string
|
||||
x-nullable: true
|
||||
container:
|
||||
allOf:
|
||||
- $ref: '#/definitions/Container'
|
||||
@@ -1504,6 +1552,9 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/IdlewatcherConfig'
|
||||
x-nullable: true
|
||||
index:
|
||||
description: Index file to serve for single-page app mode
|
||||
type: string
|
||||
load_balance:
|
||||
allOf:
|
||||
- $ref: '#/definitions/LoadBalancerConfig'
|
||||
@@ -1547,10 +1598,14 @@ definitions:
|
||||
enum:
|
||||
- http
|
||||
- https
|
||||
- h2c
|
||||
- tcp
|
||||
- udp
|
||||
- fileserver
|
||||
type: string
|
||||
spa:
|
||||
description: 'Single-page app mode: serves index for non-existent paths'
|
||||
type: boolean
|
||||
ssl_certificate:
|
||||
description: Path to client certificate
|
||||
type: string
|
||||
@@ -1858,17 +1913,19 @@ paths:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/CertInfo'
|
||||
items:
|
||||
$ref: '#/definitions/CertInfo'
|
||||
type: array
|
||||
"403":
|
||||
description: Forbidden
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"404":
|
||||
description: Not Found
|
||||
description: No certificates found or autocert is not enabled
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
summary: Get cert info
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
|
||||
"github.com/yusing/godoxy/internal/route/routes"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
@@ -13,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
Alias string `form:"alias" binding:"required_without=URL"`
|
||||
Variant icons.Variant `form:"variant" binding:"omitempty,oneof=light dark"`
|
||||
} // @name GetFavIconRequest
|
||||
|
||||
// @x-id "favicon"
|
||||
@@ -41,14 +43,18 @@ func FavIcon(c *gin.Context) {
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
var iconURL icons.URL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), &iconURL)
|
||||
icon := &iconURL
|
||||
if request.Variant != icons.VariantNone {
|
||||
icon = icon.WithVariant(request.Variant)
|
||||
}
|
||||
fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
iconfetch.GinError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
@@ -56,36 +62,42 @@ func FavIcon(c *gin.Context) {
|
||||
}
|
||||
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias)
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
iconfetch.GinError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string) (homepage.FetchResult, error) {
|
||||
func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
|
||||
// try with route.Icon
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
result iconfetch.Result
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL)
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != icons.VariantNone {
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if err != nil {
|
||||
// fallback to no variant
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(icons.VariantNone))
|
||||
}
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/")
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", variant)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/fs"
|
||||
)
|
||||
|
||||
type ListFilesResponse struct {
|
||||
@@ -35,7 +35,7 @@ func List(c *gin.Context) {
|
||||
}
|
||||
|
||||
// config/
|
||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||
files, err := fs.ListFiles(common.ConfigBasePath, 0, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
||||
return
|
||||
@@ -48,7 +48,7 @@ func List(c *gin.Context) {
|
||||
}
|
||||
|
||||
// config/middlewares/
|
||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||
mids, err := fs.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||
if err != nil {
|
||||
c.Error(apitypes.InternalServerError(err, "failed to list files"))
|
||||
return
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
_ "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
||||
|
||||
// @x-id "health"
|
||||
// @BasePath /api/v1
|
||||
// @Summary Get routes health info
|
||||
@@ -21,16 +19,16 @@ type HealthMap = map[string]routes.HealthInfo // @name HealthMap
|
||||
// @Tags v1,websocket
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} HealthMap "Health info by route name"
|
||||
// @Success 200 {object} routes.HealthMap "Health info by route name"
|
||||
// @Failure 403 {object} apitypes.ErrorResponse
|
||||
// @Failure 500 {object} apitypes.ErrorResponse
|
||||
// @Router /health [get]
|
||||
func Health(c *gin.Context) {
|
||||
if httpheaders.IsWebsocket(c.Request.Header) {
|
||||
websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) {
|
||||
return routes.GetHealthInfo(), nil
|
||||
return routes.GetHealthInfoSimple(), nil
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, routes.GetHealthInfo())
|
||||
c.JSON(http.StatusOK, routes.GetHealthInfoSimple())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
)
|
||||
|
||||
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
icons := iconlist.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/godoxy/internal/metrics/period"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -80,7 +80,7 @@ func AllSystemInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
// leave 5 extra slots for buffering in case new agents are added.
|
||||
dataCh := make(chan SystemInfoData, 1+agent.NumAgents()+5)
|
||||
dataCh := make(chan SystemInfoData, 1+agentpool.Num()+5)
|
||||
defer close(dataCh)
|
||||
|
||||
ticker := time.NewTicker(req.Interval)
|
||||
@@ -103,54 +103,52 @@ func AllSystemInfo(c *gin.Context) {
|
||||
|
||||
// processing function for one round.
|
||||
doRound := func() (bool, error) {
|
||||
var roundWg sync.WaitGroup
|
||||
var numErrs atomic.Int32
|
||||
|
||||
totalAgents := int32(1) // myself
|
||||
|
||||
errs := gperr.NewBuilderWithConcurrency()
|
||||
var errs gperr.Group
|
||||
// get system info for me and all agents in parallel.
|
||||
roundWg.Go(func() {
|
||||
errs.Go(func() error {
|
||||
data, err := systeminfo.Poller.GetRespData(req.Period, query)
|
||||
if err != nil {
|
||||
errs.Add(gperr.Wrap(err, "Main server"))
|
||||
numErrs.Add(1)
|
||||
return
|
||||
return gperr.PrependSubject("Main server", err)
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
return nil
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: "GoDoxy",
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, a := range agent.IterAgents() {
|
||||
for _, a := range agentpool.Iter() {
|
||||
totalAgents++
|
||||
agentShallowCopy := *a
|
||||
|
||||
roundWg.Go(func() {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), &agentShallowCopy, queryEncoded)
|
||||
errs.Go(func() error {
|
||||
data, err := getAgentSystemInfoWithRetry(manager.Context(), a, queryEncoded)
|
||||
if err != nil {
|
||||
errs.Add(gperr.Wrap(err, "Agent "+agentShallowCopy.Name))
|
||||
numErrs.Add(1)
|
||||
return
|
||||
return gperr.PrependSubject("Agent "+a.Name, err)
|
||||
}
|
||||
select {
|
||||
case <-manager.Done():
|
||||
return
|
||||
return nil
|
||||
case dataCh <- SystemInfoData{
|
||||
AgentName: agentShallowCopy.Name,
|
||||
AgentName: a.Name,
|
||||
SystemInfo: data,
|
||||
}:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
roundWg.Wait()
|
||||
return numErrs.Load() == totalAgents, errs.Error()
|
||||
err := errs.Wait().Error()
|
||||
return numErrs.Load() == totalAgents, err
|
||||
}
|
||||
|
||||
// write system info immediately once.
|
||||
@@ -178,7 +176,7 @@ func AllSystemInfo(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string) (bytesFromPool, error) {
|
||||
func getAgentSystemInfo(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -197,7 +195,7 @@ func getAgentSystemInfo(ctx context.Context, a *agent.AgentConfig, query string)
|
||||
return bytesFromPool{json.RawMessage(bytesBuf), release}, nil
|
||||
}
|
||||
|
||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agent.AgentConfig, query string) (bytesFromPool, error) {
|
||||
func getAgentSystemInfoWithRetry(ctx context.Context, a *agentpool.Agent, query string) (bytesFromPool, error) {
|
||||
const maxRetries = 3
|
||||
var lastErr error
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
agentPkg "github.com/yusing/godoxy/agent/pkg/agent"
|
||||
"github.com/yusing/godoxy/internal/agentpool"
|
||||
"github.com/yusing/godoxy/internal/metrics/period"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
@@ -49,9 +50,9 @@ func SystemInfo(c *gin.Context) {
|
||||
}
|
||||
c.Request.URL.RawQuery = query.Encode()
|
||||
|
||||
agent, ok := agentPkg.GetAgent(agentAddr)
|
||||
agent, ok := agentpool.Get(agentAddr)
|
||||
if !ok {
|
||||
agent, ok = agentPkg.GetAgentByName(agentName)
|
||||
agent, ok = agentpool.GetAgent(agentName)
|
||||
}
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, apitypes.Error("agent_addr or agent_name not found"))
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/route/rules"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
)
|
||||
|
||||
type RawRule struct {
|
||||
@@ -348,7 +349,7 @@ func checkMatchedRules(rulesList rules.Rules, w http.ResponseWriter, r *http.Req
|
||||
var matched []string
|
||||
|
||||
// Create a ResponseModifier to properly check rules
|
||||
rm := rules.NewResponseModifier(w)
|
||||
rm := httputils.NewResponseModifier(w)
|
||||
|
||||
for _, rule := range rulesList {
|
||||
// Check if rule matches
|
||||
|
||||
@@ -34,12 +34,12 @@ func Routes(c *gin.Context) {
|
||||
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.Iter))
|
||||
c.JSON(http.StatusOK, slices.Collect(routes.IterAll))
|
||||
return
|
||||
}
|
||||
|
||||
rts := make([]types.Route, 0, routes.NumRoutes())
|
||||
for r := range routes.Iter {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
@@ -51,14 +51,14 @@ func RoutesWS(c *gin.Context) {
|
||||
provider := c.Query("provider")
|
||||
if provider == "" {
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
return slices.Collect(routes.Iter), nil
|
||||
return slices.Collect(routes.IterAll), nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
websocket.PeriodicWrite(c, 3*time.Second, func() (any, error) {
|
||||
rts := make([]types.Route, 0, routes.NumRoutes())
|
||||
for r := range routes.Iter {
|
||||
rts := make([]types.Route, 0, routes.NumAllRoutes())
|
||||
for r := range routes.IterAll {
|
||||
if r.ProviderName() == provider {
|
||||
rts = append(rts, r)
|
||||
}
|
||||
|
||||
349
internal/auth/README.md
Normal file
349
internal/auth/README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Authentication
|
||||
|
||||
Authentication providers supporting OIDC and username/password authentication with JWT-based sessions.
|
||||
|
||||
## Overview
|
||||
|
||||
The auth package implements authentication middleware and login handlers that integrate with GoDoxy's HTTP routing system. It provides flexible authentication that can be enabled/disabled based on configuration and supports multiple authentication providers.
|
||||
|
||||
### Primary consumers
|
||||
|
||||
- `internal/route/rules` - Authentication middleware for routes
|
||||
- `internal/api/v1/auth` - Login and session management endpoints
|
||||
- `internal/homepage` - WebUI login page
|
||||
|
||||
### Non-goals
|
||||
|
||||
- ACL or authorization (see `internal/acl`)
|
||||
- User management database
|
||||
- Multi-factor authentication
|
||||
- Rate limiting (basic OIDC rate limiting only)
|
||||
|
||||
### Stability
|
||||
|
||||
Stable internal package. Public API consists of the `Provider` interface and initialization functions.
|
||||
|
||||
## Public API
|
||||
|
||||
### Exported types
|
||||
|
||||
```go
|
||||
type Provider interface {
|
||||
CheckToken(r *http.Request) error
|
||||
LoginHandler(w http.ResponseWriter, r *http.Request)
|
||||
PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||
LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
```
|
||||
|
||||
### OIDC Provider
|
||||
|
||||
```go
|
||||
type OIDCProvider struct {
|
||||
oauthConfig *oauth2.Config
|
||||
oidcProvider *oidc.Provider
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
endSessionURL *url.URL
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
rateLimit *rate.Limiter
|
||||
}
|
||||
```
|
||||
|
||||
### Username/Password Provider
|
||||
|
||||
```go
|
||||
type UserPassAuth struct {
|
||||
username string
|
||||
pwdHash []byte
|
||||
secret []byte
|
||||
tokenTTL time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
### Exported functions
|
||||
|
||||
```go
|
||||
func Initialize() error
|
||||
```
|
||||
|
||||
Sets up authentication providers based on environment configuration. Returns error if OIDC issuer is configured but cannot be reached.
|
||||
|
||||
```go
|
||||
func IsEnabled() bool
|
||||
```
|
||||
|
||||
Returns whether authentication is enabled. Checks `DEBUG_DISABLE_AUTH`, `API_JWT_SECRET`, and `OIDC_ISSUER_URL`.
|
||||
|
||||
```go
|
||||
func IsOIDCEnabled() bool
|
||||
```
|
||||
|
||||
Returns whether OIDC authentication is configured.
|
||||
|
||||
```go
|
||||
func GetDefaultAuth() Provider
|
||||
```
|
||||
|
||||
Returns the configured authentication provider.
|
||||
|
||||
```go
|
||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request)
|
||||
```
|
||||
|
||||
HTTP handler that checks if the request has a valid token. Returns 200 if valid, invokes login handler otherwise.
|
||||
|
||||
```go
|
||||
func AuthOrProceed(w http.ResponseWriter, r *http.Request) bool
|
||||
```
|
||||
|
||||
Authenticates request or proceeds if valid. Returns `false` if login handler was invoked, `true` if authenticated.
|
||||
|
||||
```go
|
||||
func ProceedNext(w http.ResponseWriter, r *http.Request)
|
||||
```
|
||||
|
||||
Continues to the next handler after successful authentication.
|
||||
|
||||
```go
|
||||
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error)
|
||||
```
|
||||
|
||||
Creates a new username/password auth provider with bcrypt password hashing.
|
||||
|
||||
```go
|
||||
func NewUserPassAuthFromEnv() (*UserPassAuth, error)
|
||||
```
|
||||
|
||||
Creates username/password auth from environment variables `API_USER`, `API_PASSWORD`, `API_JWT_SECRET`.
|
||||
|
||||
```go
|
||||
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error)
|
||||
```
|
||||
|
||||
Creates a new OIDC provider. Returns error if issuer cannot be reached or no allowed users/groups are configured.
|
||||
|
||||
```go
|
||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error)
|
||||
```
|
||||
|
||||
Creates OIDC provider from environment variables `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, etc.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core components
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Request] --> B{Auth Enabled?}
|
||||
B -->|No| C[Proceed Direct]
|
||||
B -->|Yes| D[Check Token]
|
||||
D -->|Valid| E[Proceed]
|
||||
D -->|Invalid| F[Login Handler]
|
||||
|
||||
G[OIDC Provider] --> H[Token Validation]
|
||||
I[UserPass Provider] --> J[Credential Check]
|
||||
|
||||
F --> K{OIDC Configured?}
|
||||
K -->|Yes| G
|
||||
K -->|No| I
|
||||
|
||||
subgraph Cookie Management
|
||||
L[Token Cookie]
|
||||
M[State Cookie]
|
||||
N[Session Cookie]
|
||||
end
|
||||
```
|
||||
|
||||
### OIDC authentication flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant App
|
||||
participant IdP
|
||||
|
||||
User->>App: Access Protected Resource
|
||||
App->>App: Check Token
|
||||
alt No valid token
|
||||
App-->>User: Redirect to /auth/
|
||||
User->>IdP: Login & Authorize
|
||||
IdP-->>User: Redirect with Code
|
||||
User->>App: /auth/callback?code=...
|
||||
App->>IdP: Exchange Code for Token
|
||||
IdP-->>App: Access Token + ID Token
|
||||
App->>App: Validate Token
|
||||
App->>App: Check allowed users/groups
|
||||
App-->>User: Protected Resource
|
||||
else Valid token exists
|
||||
App-->>User: Protected Resource
|
||||
end
|
||||
```
|
||||
|
||||
### Username/password flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant App
|
||||
|
||||
User->>App: POST /auth/callback
|
||||
App->>App: Validate credentials
|
||||
alt Valid
|
||||
App->>App: Generate JWT
|
||||
App-->>User: Set token cookie, redirect to /
|
||||
else Invalid
|
||||
App-->>User: 401 Unauthorized
|
||||
end
|
||||
```
|
||||
|
||||
## Configuration Surface
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------ | ----------------------------------------------------------- |
|
||||
| `DEBUG_DISABLE_AUTH` | Set to "true" to disable auth for debugging |
|
||||
| `API_JWT_SECRET` | Secret key for JWT token validation (enables userpass auth) |
|
||||
| `API_USER` | Username for userpass authentication |
|
||||
| `API_PASSWORD` | Password for userpass authentication |
|
||||
| `API_JWT_TOKEN_TTL` | Token TTL duration (default: 24h) |
|
||||
| `OIDC_ISSUER_URL` | OIDC provider URL (enables OIDC) |
|
||||
| `OIDC_CLIENT_ID` | OIDC client ID |
|
||||
| `OIDC_CLIENT_SECRET` | OIDC client secret |
|
||||
| `OIDC_REDIRECT_URL` | OIDC redirect URL |
|
||||
| `OIDC_ALLOWED_USERS` | Comma-separated list of allowed users |
|
||||
| `OIDC_ALLOWED_GROUPS` | Comma-separated list of allowed groups |
|
||||
| `OIDC_SCOPES` | Comma-separated OIDC scopes (default: openid,profile,email) |
|
||||
| `OIDC_RATE_LIMIT` | Rate limit requests (default: 10) |
|
||||
| `OIDC_RATE_LIMIT_PERIOD` | Rate limit period (default: 1m) |
|
||||
|
||||
### Hot-reloading
|
||||
|
||||
Authentication configuration requires restart. No dynamic reconfiguration is supported.
|
||||
|
||||
## Dependency and Integration Map
|
||||
|
||||
### Internal dependencies
|
||||
|
||||
- `internal/common` - Environment variable access
|
||||
|
||||
### External dependencies
|
||||
|
||||
- `golang.org/x/crypto/bcrypt` - Password hashing
|
||||
- `github.com/coreos/go-oidc/v3/oidc` - OIDC protocol
|
||||
- `golang.org/x/oauth2` - OAuth2/OIDC implementation
|
||||
- `github.com/golang-jwt/jwt/v5` - JWT token handling
|
||||
- `golang.org/x/time/rate` - OIDC rate limiting
|
||||
|
||||
### Integration points
|
||||
|
||||
```go
|
||||
// Route middleware uses AuthOrProceed
|
||||
routeHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
if !auth.AuthOrProceed(w, r) {
|
||||
return // Auth failed, login handler was invoked
|
||||
}
|
||||
// Continue with authenticated request
|
||||
}
|
||||
```
|
||||
|
||||
## Observability
|
||||
|
||||
### Logs
|
||||
|
||||
- OIDC provider initialization errors
|
||||
- Token validation failures
|
||||
- Rate limit exceeded events
|
||||
|
||||
### Metrics
|
||||
|
||||
No metrics are currently exposed.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- JWT tokens use HS512 signing for userpass auth
|
||||
- OIDC tokens are validated against the issuer
|
||||
- Session tokens are scoped by client ID to prevent conflicts
|
||||
- Passwords are hashed with bcrypt (cost 10)
|
||||
- OIDC rate limiting prevents brute-force attacks
|
||||
- State parameter prevents CSRF attacks
|
||||
- Refresh tokens are stored and invalidated on logout
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
| Failure | Behavior | Recovery |
|
||||
| ------------------------ | ------------------------------ | ----------------------------- |
|
||||
| OIDC issuer unreachable | Initialize returns error | Fix network/URL configuration |
|
||||
| Invalid JWT secret | Initialize uses API_JWT_SECRET | Provide correct secret |
|
||||
| Token expired | CheckToken returns error | User must re-authenticate |
|
||||
| User not in allowed list | Returns ErrUserNotAllowed | Add user to allowed list |
|
||||
| Rate limit exceeded | Returns 429 Too Many Requests | Wait for rate limit reset |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic setup
|
||||
|
||||
```go
|
||||
// Initialize authentication during startup
|
||||
err := auth.Initialize()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Check if auth is enabled
|
||||
if auth.IsEnabled() {
|
||||
log.Println("Authentication is enabled")
|
||||
}
|
||||
|
||||
// Check OIDC status
|
||||
if auth.IsOIDCEnabled() {
|
||||
log.Println("OIDC authentication configured")
|
||||
}
|
||||
```
|
||||
|
||||
### Using AuthOrProceed middleware
|
||||
|
||||
```go
|
||||
func protectedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !auth.AuthOrProceed(w, r) {
|
||||
return // Auth failed, login handler was invoked
|
||||
}
|
||||
// Continue with authenticated request
|
||||
}
|
||||
```
|
||||
|
||||
### Using AuthCheckHandler
|
||||
|
||||
```go
|
||||
http.HandleFunc("/api/", auth.AuthCheckHandler(apiHandler))
|
||||
```
|
||||
|
||||
### Custom OIDC provider
|
||||
|
||||
```go
|
||||
provider, err := auth.NewOIDCProvider(
|
||||
"https://your-idp.com",
|
||||
"your-client-id",
|
||||
"your-client-secret",
|
||||
[]string{"user1", "user2"},
|
||||
[]string{"group1"},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom userpass provider
|
||||
|
||||
```go
|
||||
provider, err := auth.NewUserPassAuth(
|
||||
"admin",
|
||||
"password123",
|
||||
[]byte("jwt-secret-key"),
|
||||
24*time.Hour,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
@@ -65,14 +65,12 @@ func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func AuthOrProceed(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if defaultAuth == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
err := defaultAuth.CheckToken(r)
|
||||
if err != nil {
|
||||
defaultAuth.LoginHandler(w, r)
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -12,11 +12,12 @@ var blockPageHTML string
|
||||
|
||||
var blockPageTemplate = template.Must(template.New("block_page").Parse(blockPageHTML))
|
||||
|
||||
func WriteBlockPage(w http.ResponseWriter, status int, error string, logoutURL string) {
|
||||
func WriteBlockPage(w http.ResponseWriter, status int, errorMessage, actionText, actionURL string) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
blockPageTemplate.Execute(w, map[string]string{
|
||||
"StatusText": http.StatusText(status),
|
||||
"Error": error,
|
||||
"LogoutURL": logoutURL,
|
||||
"Error": errorMessage,
|
||||
"ActionURL": actionURL,
|
||||
"ActionText": actionText,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Access Denied</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{.StatusText}}</h1>
|
||||
<p>{{.Error}}</p>
|
||||
<a href="{{.LogoutURL}}">Logout</a>
|
||||
</body>
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg0: #070a12;
|
||||
--bg1: #0b1020;
|
||||
--card: rgba(255, 255, 255, 0.055);
|
||||
--card2: rgba(255, 255, 255, 0.05);
|
||||
--text: rgba(255, 255, 255, 0.92);
|
||||
--muted: rgba(255, 255, 255, 0.68);
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--borderSoft: rgba(255, 255, 255, 0.08);
|
||||
--borderStrong: rgba(255, 255, 255, 0.14);
|
||||
--borderHover: rgba(255, 255, 255, 0.22);
|
||||
--shadow: 0 22px 60px rgba(0, 0, 0, 0.55);
|
||||
--shadowCard: 0 22px 60px rgba(0, 0, 0, 0.58);
|
||||
--shadowButton: 0 12px 28px rgba(0, 0, 0, 0.35);
|
||||
--insetHighlight: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
--ring: rgba(120, 160, 210, 0.42);
|
||||
--accent0: #7aa3c8;
|
||||
--accent1: #9a8bc7;
|
||||
--btn: rgba(255, 255, 255, 0.06);
|
||||
--btnHover: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
|
||||
Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
|
||||
color: var(--text);
|
||||
background-color: var(--bg1);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 28px 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(720px, 100%);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadowCard), var(--insetHighlight);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 18px 18px 12px;
|
||||
border-bottom: 1px solid var(--borderSoft);
|
||||
background: var(--card2);
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--card2);
|
||||
}
|
||||
|
||||
.badge svg {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.badge .bang {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
padding: 14px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
a.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--btn);
|
||||
transition: transform 120ms ease, border-color 120ms ease,
|
||||
background 120ms ease, box-shadow 120ms ease;
|
||||
box-shadow: var(--shadowButton);
|
||||
}
|
||||
|
||||
a.button:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--borderHover);
|
||||
background: var(--btnHover);
|
||||
}
|
||||
|
||||
a.button:focus-visible {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 3px var(--ring), var(--shadowButton);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint kbd {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--borderStrong);
|
||||
background: var(--btn);
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kbd-container {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<main class="card" role="main" aria-labelledby="title">
|
||||
<header class="topbar">
|
||||
<div class="badge" aria-hidden="true">
|
||||
<span class="bang">!</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 id="title">{{.StatusText}}</h1>
|
||||
<p class="sub">
|
||||
You don’t have permission to access this resource.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="content">
|
||||
<pre class="error">{{.Error}}</pre>
|
||||
<div class="actions">
|
||||
<a class="button" href="{{.ActionURL}}">
|
||||
<span>{{.ActionText}}</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
<div class="hint">
|
||||
If you just signed in, try refreshing the page.
|
||||
<span aria-hidden="true"> </span>
|
||||
<div class="kbd-container">
|
||||
<kbd>Ctrl</kbd>
|
||||
<span>+</span>
|
||||
<kbd>R</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,6 +32,8 @@ type (
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
|
||||
rateLimit *rate.Limiter
|
||||
|
||||
onUnknownPathHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
@@ -66,9 +68,9 @@ func (auth *OIDCProvider) getAppScopedCookieName(baseName string) string {
|
||||
|
||||
const (
|
||||
OIDCAuthInitPath = "/"
|
||||
OIDCAuthBasePath = "/auth"
|
||||
OIDCPostAuthPath = OIDCAuthBasePath + "/callback"
|
||||
OIDCLogoutPath = OIDCAuthBasePath + "/logout"
|
||||
OIDCAuthBasePath = "/auth/"
|
||||
OIDCPostAuthPath = OIDCAuthBasePath + "callback"
|
||||
OIDCLogoutPath = OIDCAuthBasePath + "logout"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -123,6 +125,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, all
|
||||
endSessionURL: endSessionURL,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedGroups: allowedGroups,
|
||||
rateLimit: rate.NewLimiter(rate.Every(common.OIDCRateLimitPeriod), common.OIDCRateLimit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ func NewOIDCProviderWithCustomClient(baseProvider *OIDCProvider, clientID, clien
|
||||
endSessionURL: baseProvider.endSessionURL,
|
||||
allowedUsers: baseProvider.allowedUsers,
|
||||
allowedGroups: baseProvider.allowedGroups,
|
||||
rateLimit: baseProvider.rateLimit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -228,9 +232,12 @@ func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
var rateLimit = rate.NewLimiter(rate.Every(time.Second), 1)
|
||||
|
||||
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !httputils.GetAccept(r.Header).AcceptHTML() {
|
||||
http.Error(w, "authentication is required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// check for session token
|
||||
sessionToken, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthSessionToken))
|
||||
if err == nil { // session token exists
|
||||
@@ -250,8 +257,8 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !rateLimit.Allow() {
|
||||
http.Error(w, "auth rate limit exceeded", http.StatusTooManyRequests)
|
||||
if !auth.rateLimit.Allow() {
|
||||
WriteBlockPage(w, http.StatusTooManyRequests, "auth rate limit exceeded", "Try again", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -318,34 +325,39 @@ func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http
|
||||
// verify state
|
||||
state, err := r.Cookie(auth.getAppScopedCookieName(CookieOauthState))
|
||||
if err != nil {
|
||||
http.Error(w, "missing state cookie", http.StatusBadRequest)
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusBadRequest, "missing state cookie", "Back to Login", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
http.Error(w, "invalid oauth state", http.StatusBadRequest)
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusBadRequest, "invalid oauth state", "Back to Login", OIDCAuthInitPath)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to exchange token: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to exchange token", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to exchange token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
idTokenJWT, idToken, err := auth.getIDToken(r.Context(), oauth2Token)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to get ID token: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to get ID token", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to get ID token: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if oauth2Token.RefreshToken != "" {
|
||||
claims, err := parseClaims(idToken)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
httputils.LogError(r).Msg(fmt.Sprintf("failed to parse claims: %v", err))
|
||||
auth.clearCookie(w, r)
|
||||
WriteBlockPage(w, http.StatusInternalServerError, "failed to parse claims", "Try again", OIDCAuthInitPath)
|
||||
httputils.LogError(r).Msgf("failed to parse claims: %v", err)
|
||||
return
|
||||
}
|
||||
session := newSession(claims.Username, claims.Groups)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
@@ -42,6 +43,7 @@ func setupMockOIDC(t *testing.T) {
|
||||
}),
|
||||
allowedUsers: []string{"test-user"},
|
||||
allowedGroups: []string{"test-group1", "test-group2"},
|
||||
rateLimit: rate.NewLimiter(rate.Every(common.OIDCRateLimitPeriod), common.OIDCRateLimit),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
349
internal/autocert/README.md
Normal file
349
internal/autocert/README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Autocert Package
|
||||
|
||||
Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).
|
||||
|
||||
## Overview
|
||||
|
||||
### Purpose
|
||||
|
||||
This package provides complete SSL certificate lifecycle management:
|
||||
|
||||
- ACME account registration and management
|
||||
- Certificate issuance via DNS-01 challenge
|
||||
- Automatic renewal scheduling (1 month before expiry)
|
||||
- SNI-based certificate selection for multi-domain setups
|
||||
|
||||
### Primary Consumers
|
||||
|
||||
- `goutils/server` - TLS handshake certificate provider
|
||||
- `internal/api/v1/cert/` - REST API for certificate management
|
||||
- Configuration loading via `internal/config/`
|
||||
|
||||
### Non-goals
|
||||
|
||||
- HTTP-01 challenge support
|
||||
- Certificate transparency log monitoring
|
||||
- OCSP stapling
|
||||
- Private CA support (except via custom CADirURL)
|
||||
|
||||
### Stability
|
||||
|
||||
Internal package with stable public APIs. ACME protocol compliance depends on lego library.
|
||||
|
||||
## Public API
|
||||
|
||||
### Config (`config.go`)
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Email string // ACME account email
|
||||
Domains []string // Domains to certify
|
||||
CertPath string // Output cert path
|
||||
KeyPath string // Output key path
|
||||
Extra []ConfigExtra // Additional cert configs
|
||||
ACMEKeyPath string // ACME account private key
|
||||
Provider string // DNS provider name
|
||||
Options map[string]strutils.Redacted // Provider options
|
||||
Resolvers []string // DNS resolvers
|
||||
CADirURL string // Custom ACME CA directory
|
||||
CACerts []string // Custom CA certificates
|
||||
EABKid string // External Account Binding Key ID
|
||||
EABHmac string // External Account Binding HMAC
|
||||
}
|
||||
|
||||
// Merge extra config with main provider
|
||||
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra
|
||||
```
|
||||
|
||||
### Provider (`provider.go`)
|
||||
|
||||
```go
|
||||
type Provider struct {
|
||||
logger zerolog.Logger
|
||||
cfg *Config
|
||||
user *User
|
||||
legoCfg *lego.Config
|
||||
client *lego.Client
|
||||
lastFailure time.Time
|
||||
legoCert *certificate.Resource
|
||||
tlsCert *tls.Certificate
|
||||
certExpiries CertExpiries
|
||||
extraProviders []*Provider
|
||||
sniMatcher sniMatcher
|
||||
}
|
||||
|
||||
// Create new provider (initializes extras atomically)
|
||||
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error)
|
||||
|
||||
// TLS certificate getter for SNI
|
||||
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
|
||||
// Certificate info for API
|
||||
func (p *Provider) GetCertInfos() ([]CertInfo, error)
|
||||
|
||||
// Provider name ("main" or "extra[N]")
|
||||
func (p *Provider) GetName() string
|
||||
|
||||
// Obtain certificate if not exists
|
||||
func (p *Provider) ObtainCertIfNotExistsAll() error
|
||||
|
||||
// Force immediate renewal
|
||||
func (p *Provider) ForceExpiryAll() bool
|
||||
|
||||
// Schedule automatic renewal
|
||||
func (p *Provider) ScheduleRenewalAll(parent task.Parent)
|
||||
|
||||
// Print expiry dates
|
||||
func (p *Provider) PrintCertExpiriesAll()
|
||||
```
|
||||
|
||||
### User (`user.go`)
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
Email string // Account email
|
||||
Registration *registration.Resource // ACME registration
|
||||
Key crypto.PrivateKey // Account key
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Certificate Lifecycle
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> B[Load Existing Cert]
|
||||
B --> C{Cert Exists?}
|
||||
C -->|Yes| D[Load Cert from Disk]
|
||||
C -->|No| E[Obtain New Cert]
|
||||
|
||||
D --> F{Valid & Not Expired?}
|
||||
F -->|Yes| G[Schedule Renewal]
|
||||
F -->|No| H{Renewal Time?}
|
||||
H -->|Yes| I[Renew Certificate]
|
||||
H -->|No| G
|
||||
|
||||
E --> J[Init ACME Client]
|
||||
J --> K[Register Account]
|
||||
K --> L[DNS-01 Challenge]
|
||||
L --> M[Complete Challenge]
|
||||
M --> N[Download Certificate]
|
||||
N --> O[Save to Disk]
|
||||
O --> G
|
||||
|
||||
G --> P[Wait Until Renewal Time]
|
||||
P --> Q[Trigger Renewal]
|
||||
Q --> I
|
||||
|
||||
I --> R[Renew via ACME]
|
||||
R --> S{Same Domains?}
|
||||
S -->|Yes| T[Bundle & Save]
|
||||
S -->|No| U[Re-obtain Certificate]
|
||||
U --> T
|
||||
|
||||
T --> V[Update SNI Matcher]
|
||||
V --> G
|
||||
|
||||
style E fill:#22553F,color:#fff
|
||||
style I fill:#8B8000,color:#fff
|
||||
style N fill:#22553F,color:#fff
|
||||
style U fill:#84261A,color:#fff
|
||||
```
|
||||
|
||||
### SNI Matching Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Client["TLS Client"] -->|ClientHello SNI| Proxy["GoDoxy Proxy"]
|
||||
Proxy -->|Certificate| Client
|
||||
|
||||
subgraph "SNI Matching Process"
|
||||
direction TB
|
||||
A[Extract SNI from ClientHello] --> B{Normalize SNI}
|
||||
B --> C{Exact Match?}
|
||||
C -->|Yes| D[Return cert]
|
||||
C -->|No| E[Wildcard Suffix Tree]
|
||||
E --> F{Match Found?}
|
||||
F -->|Yes| D
|
||||
F -->|No| G[Return default cert]
|
||||
end
|
||||
|
||||
style C fill:#27632A,color:#fff
|
||||
style E fill:#18597A,color:#fff
|
||||
style F fill:#836C03,color:#fff
|
||||
```
|
||||
|
||||
### Suffix Tree Structure
|
||||
|
||||
```
|
||||
Certificate: *.example.com, example.com, *.api.example.com
|
||||
|
||||
exact:
|
||||
"example.com" -> Provider_A
|
||||
|
||||
root:
|
||||
└── "com"
|
||||
└── "example"
|
||||
├── "*" -> Provider_A [wildcard at *.example.com]
|
||||
└── "api"
|
||||
└── "*" -> Provider_B [wildcard at *.api.example.com]
|
||||
```
|
||||
|
||||
## Configuration Surface
|
||||
|
||||
### Provider Types
|
||||
|
||||
| Type | Description | Use Case |
|
||||
| -------------- | ---------------------------- | ------------------------- |
|
||||
| `local` | No ACME, use existing cert | Pre-existing certificates |
|
||||
| `pseudo` | Mock provider for testing | Development |
|
||||
| ACME providers | Let's Encrypt, ZeroSSL, etc. | Production |
|
||||
|
||||
### Supported DNS Providers
|
||||
|
||||
| Provider | Name | Required Options |
|
||||
| ------------ | -------------- | ----------------------------------- |
|
||||
| Cloudflare | `cloudflare` | `CF_API_TOKEN` |
|
||||
| Route 53 | `route53` | AWS credentials |
|
||||
| DigitalOcean | `digitalocean` | `DO_API_TOKEN` |
|
||||
| GoDaddy | `godaddy` | `GD_API_KEY`, `GD_API_SECRET` |
|
||||
| OVH | `ovh` | `OVH_ENDPOINT`, `OVH_APP_KEY`, etc. |
|
||||
| CloudDNS | `clouddns` | GCP credentials |
|
||||
| AzureDNS | `azuredns` | Azure credentials |
|
||||
| DuckDNS | `duckdns` | `DUCKDNS_TOKEN` |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
email: admin@example.com
|
||||
domains:
|
||||
- example.com
|
||||
- "*.example.com"
|
||||
options:
|
||||
auth_token: ${CF_API_TOKEN}
|
||||
resolvers:
|
||||
- 1.1.1.1:53
|
||||
```
|
||||
|
||||
### Extra Providers
|
||||
|
||||
```yaml
|
||||
autocert:
|
||||
provider: cloudflare
|
||||
email: admin@example.com
|
||||
domains:
|
||||
- example.com
|
||||
- "*.example.com"
|
||||
cert_path: certs/example.com.crt
|
||||
key_path: certs/example.com.key
|
||||
options:
|
||||
auth_token: ${CF_API_TOKEN}
|
||||
extra:
|
||||
- domains:
|
||||
- api.example.com
|
||||
- "*.api.example.com"
|
||||
cert_path: certs/api.example.com.crt
|
||||
key_path: certs/api.example.com.key
|
||||
```
|
||||
|
||||
## Dependency and Integration Map
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- `github.com/go-acme/lego/v4` - ACME protocol implementation
|
||||
- `github.com/rs/zerolog` - Structured logging
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
- `internal/task/task.go` - Lifetime management
|
||||
- `internal/notif/` - Renewal notifications
|
||||
- `internal/config/` - Configuration loading
|
||||
- `internal/dnsproviders/` - DNS provider implementations
|
||||
|
||||
## Observability
|
||||
|
||||
### Logs
|
||||
|
||||
| Level | When |
|
||||
| ------- | ----------------------------- |
|
||||
| `Info` | Certificate obtained/renewed |
|
||||
| `Info` | Registration reused |
|
||||
| `Warn` | Renewal failure |
|
||||
| `Error` | Certificate retrieval failure |
|
||||
|
||||
### Notifications
|
||||
|
||||
- Certificate renewal success/failure
|
||||
- Service startup with expiry dates
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Account private key stored at `certs/acme.key` (mode 0600)
|
||||
- Certificate private keys stored at configured paths (mode 0600)
|
||||
- Certificate files world-readable (mode 0644)
|
||||
- ACME account email used for Let's Encrypt ToS
|
||||
- EAB credentials for zero-touch enrollment
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
| Failure Mode | Impact | Recovery |
|
||||
| ------------------------------ | -------------------------- | ----------------------------- |
|
||||
| DNS-01 challenge timeout | Certificate issuance fails | Check DNS provider API |
|
||||
| Rate limiting (too many certs) | 1-hour cooldown | Wait or use different account |
|
||||
| DNS provider API error | Renewal fails | 1-hour cooldown, retry |
|
||||
| Certificate domains mismatch | Must re-obtain | Force renewal via API |
|
||||
| Account key corrupted | Must register new account | New key, may lose certs |
|
||||
|
||||
### Failure Tracking
|
||||
|
||||
Last failure persisted per-certificate to prevent rate limiting:
|
||||
|
||||
```
|
||||
File: <cert_dir>/.last_failure-<hash>
|
||||
Where hash = SHA256(certPath|keyPath)[:6]
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```go
|
||||
autocertCfg := state.AutoCert
|
||||
user, legoCfg, err := autocertCfg.GetLegoConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provider, err := autocert.NewProvider(autocertCfg, user, legoCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("autocert error: %w", err)
|
||||
}
|
||||
|
||||
if err := provider.ObtainCertIfNotExistsAll(); err != nil {
|
||||
return fmt.Errorf("failed to obtain certificates: %w", err)
|
||||
}
|
||||
|
||||
provider.ScheduleRenewalAll(state.Task())
|
||||
provider.PrintCertExpiriesAll()
|
||||
```
|
||||
|
||||
### Force Renewal via API
|
||||
|
||||
```go
|
||||
// WebSocket endpoint: GET /api/v1/cert/renew
|
||||
if provider.ForceExpiryAll() {
|
||||
// Wait for renewal to complete
|
||||
provider.WaitRenewalDone(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Notes
|
||||
|
||||
- `config_test.go` - Configuration validation
|
||||
- `provider_test/` - Provider functionality tests
|
||||
- `sni_test.go` - SNI matching tests
|
||||
- `multi_cert_test.go` - Extra provider tests
|
||||
- Integration tests require mock DNS provider
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -15,18 +16,20 @@ import (
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/utils"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type ConfigExtra Config
|
||||
type Config struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
CertPath string `json:"cert_path,omitempty"`
|
||||
KeyPath string `json:"key_path,omitempty"`
|
||||
Extra []ConfigExtra `json:"extra,omitempty"`
|
||||
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||
|
||||
Resolvers []string `json:"resolvers,omitempty"`
|
||||
|
||||
@@ -41,13 +44,13 @@ type Config struct {
|
||||
HTTPClient *http.Client `json:"-"` // for tests only
|
||||
|
||||
challengeProvider challenge.Provider
|
||||
|
||||
idx int // 0: main, 1+: extra[i]
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMissingDomain = gperr.New("missing field 'domains'")
|
||||
ErrMissingEmail = gperr.New("missing field 'email'")
|
||||
ErrMissingProvider = gperr.New("missing field 'provider'")
|
||||
ErrMissingCADirURL = gperr.New("missing field 'ca_dir_url'")
|
||||
ErrMissingField = gperr.New("missing field")
|
||||
ErrDuplicatedPath = gperr.New("duplicated path")
|
||||
ErrInvalidDomain = gperr.New("invalid domain")
|
||||
ErrUnknownProvider = gperr.New("unknown provider")
|
||||
)
|
||||
@@ -62,69 +65,22 @@ var domainOrWildcardRE = regexp.MustCompile(`^\*?([^.]+\.)+[^.]+$`)
|
||||
|
||||
// Validate implements the utils.CustomValidator interface.
|
||||
func (cfg *Config) Validate() gperr.Error {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
seenPaths := make(map[string]int) // path -> provider idx (0 for main, 1+ for extras)
|
||||
return cfg.validate(seenPaths)
|
||||
}
|
||||
|
||||
func (cfg *ConfigExtra) Validate() gperr.Error {
|
||||
return nil // done by main config's validate
|
||||
}
|
||||
|
||||
func (cfg *ConfigExtra) AsConfig() *Config {
|
||||
return (*Config)(cfg)
|
||||
}
|
||||
|
||||
func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
|
||||
if cfg.Provider == "" {
|
||||
cfg.Provider = ProviderLocal
|
||||
return nil
|
||||
}
|
||||
|
||||
b := gperr.NewBuilder("autocert errors")
|
||||
if cfg.Provider == ProviderCustom && cfg.CADirURL == "" {
|
||||
b.Add(ErrMissingCADirURL)
|
||||
}
|
||||
|
||||
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
||||
if len(cfg.Domains) == 0 {
|
||||
b.Add(ErrMissingDomain)
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
b.Add(ErrMissingEmail)
|
||||
}
|
||||
if cfg.Provider != ProviderCustom {
|
||||
for i, d := range cfg.Domains {
|
||||
if !domainOrWildcardRE.MatchString(d) {
|
||||
b.Add(ErrInvalidDomain.Subjectf("domains[%d]", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
// check if provider is implemented
|
||||
providerConstructor, ok := Providers[cfg.Provider]
|
||||
if !ok {
|
||||
if cfg.Provider != ProviderCustom {
|
||||
b.Add(ErrUnknownProvider.
|
||||
Subject(cfg.Provider).
|
||||
With(gperr.DoYouMean(utils.NearestField(cfg.Provider, Providers))))
|
||||
}
|
||||
} else {
|
||||
provider, err := providerConstructor(cfg.Options)
|
||||
if err != nil {
|
||||
b.Add(err)
|
||||
} else {
|
||||
cfg.challengeProvider = provider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.challengeProvider == nil {
|
||||
cfg.challengeProvider, _ = Providers[ProviderLocal](nil)
|
||||
}
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
func (cfg *Config) dns01Options() []dns01.ChallengeOption {
|
||||
return []dns01.ChallengeOption{
|
||||
dns01.CondOption(len(cfg.Resolvers) > 0, dns01.AddRecursiveNameservers(cfg.Resolvers)),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if cfg.CertPath == "" {
|
||||
cfg.CertPath = CertFileDefault
|
||||
}
|
||||
@@ -135,6 +91,83 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||
cfg.ACMEKeyPath = ACMEKeyFileDefault
|
||||
}
|
||||
|
||||
b := gperr.NewBuilder("certificate error")
|
||||
|
||||
// check if cert_path is unique
|
||||
if first, ok := seenPaths[cfg.CertPath]; ok {
|
||||
b.Add(ErrDuplicatedPath.Subjectf("cert_path %s", cfg.CertPath).Withf("first seen in %s", fmt.Sprintf("extra[%d]", first)))
|
||||
} else {
|
||||
seenPaths[cfg.CertPath] = cfg.idx
|
||||
}
|
||||
|
||||
// check if key_path is unique
|
||||
if first, ok := seenPaths[cfg.KeyPath]; ok {
|
||||
b.Add(ErrDuplicatedPath.Subjectf("key_path %s", cfg.KeyPath).Withf("first seen in %s", fmt.Sprintf("extra[%d]", first)))
|
||||
} else {
|
||||
seenPaths[cfg.KeyPath] = cfg.idx
|
||||
}
|
||||
|
||||
if cfg.Provider == ProviderCustom && cfg.CADirURL == "" {
|
||||
b.Add(ErrMissingField.Subject("ca_dir_url"))
|
||||
}
|
||||
|
||||
if cfg.Provider != ProviderLocal && cfg.Provider != ProviderPseudo {
|
||||
if len(cfg.Domains) == 0 {
|
||||
b.Add(ErrMissingField.Subject("domains"))
|
||||
}
|
||||
if cfg.Email == "" {
|
||||
b.Add(ErrMissingField.Subject("email"))
|
||||
}
|
||||
if cfg.Provider != ProviderCustom {
|
||||
for i, d := range cfg.Domains {
|
||||
if !domainOrWildcardRE.MatchString(d) {
|
||||
b.Add(ErrInvalidDomain.Subjectf("domains[%d]", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if provider is implemented
|
||||
providerConstructor, ok := Providers[cfg.Provider]
|
||||
if !ok {
|
||||
if cfg.Provider != ProviderCustom {
|
||||
b.Add(ErrUnknownProvider.
|
||||
Subject(cfg.Provider).
|
||||
With(gperr.DoYouMeanField(cfg.Provider, Providers)))
|
||||
}
|
||||
} else {
|
||||
provider, err := providerConstructor(cfg.Options)
|
||||
if err != nil {
|
||||
b.Add(err)
|
||||
} else {
|
||||
cfg.challengeProvider = provider
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.challengeProvider == nil {
|
||||
cfg.challengeProvider, _ = Providers[ProviderLocal](nil)
|
||||
}
|
||||
|
||||
if len(cfg.Extra) > 0 {
|
||||
for i := range cfg.Extra {
|
||||
cfg.Extra[i] = MergeExtraConfig(cfg, &cfg.Extra[i])
|
||||
cfg.Extra[i].AsConfig().idx = i + 1
|
||||
err := cfg.Extra[i].AsConfig().validate(seenPaths)
|
||||
if err != nil {
|
||||
b.Add(err.Subjectf("extra[%d]", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
func (cfg *Config) dns01Options() []dns01.ChallengeOption {
|
||||
return []dns01.ChallengeOption{
|
||||
dns01.CondOption(len(cfg.Resolvers) > 0, dns01.AddRecursiveNameservers(cfg.Resolvers)),
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, error) {
|
||||
var privKey *ecdsa.PrivateKey
|
||||
var err error
|
||||
|
||||
@@ -178,6 +211,46 @@ func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
|
||||
return user, legoCfg, nil
|
||||
}
|
||||
|
||||
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra {
|
||||
merged := ConfigExtra(*mainCfg)
|
||||
merged.Extra = nil
|
||||
merged.CertPath = extraCfg.CertPath
|
||||
merged.KeyPath = extraCfg.KeyPath
|
||||
// NOTE: Using same ACME key as main provider
|
||||
|
||||
if extraCfg.Provider != "" {
|
||||
merged.Provider = extraCfg.Provider
|
||||
}
|
||||
if extraCfg.Email != "" {
|
||||
merged.Email = extraCfg.Email
|
||||
}
|
||||
if len(extraCfg.Domains) > 0 {
|
||||
merged.Domains = extraCfg.Domains
|
||||
}
|
||||
if len(extraCfg.Options) > 0 {
|
||||
merged.Options = extraCfg.Options
|
||||
}
|
||||
if len(extraCfg.Resolvers) > 0 {
|
||||
merged.Resolvers = extraCfg.Resolvers
|
||||
}
|
||||
if extraCfg.CADirURL != "" {
|
||||
merged.CADirURL = extraCfg.CADirURL
|
||||
}
|
||||
if len(extraCfg.CACerts) > 0 {
|
||||
merged.CACerts = extraCfg.CACerts
|
||||
}
|
||||
if extraCfg.EABKid != "" {
|
||||
merged.EABKid = extraCfg.EABKid
|
||||
}
|
||||
if extraCfg.EABHmac != "" {
|
||||
merged.EABHmac = extraCfg.EABHmac
|
||||
}
|
||||
if extraCfg.HTTPClient != nil {
|
||||
merged.HTTPClient = extraCfg.HTTPClient
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func (cfg *Config) LoadACMEKey() (*ecdsa.PrivateKey, error) {
|
||||
if common.IsTest {
|
||||
return nil, os.ErrNotExist
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
package autocert
|
||||
package autocert_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/godoxy/internal/autocert"
|
||||
"github.com/yusing/godoxy/internal/dnsproviders"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
)
|
||||
|
||||
func TestEABConfigRequired(t *testing.T) {
|
||||
dnsproviders.InitProviders()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
cfg *autocert.Config
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "Missing EABKid", cfg: &Config{EABHmac: "1234567890"}, wantErr: true},
|
||||
{name: "Missing EABHmac", cfg: &Config{EABKid: "1234567890"}, wantErr: true},
|
||||
{name: "Valid EAB", cfg: &Config{EABKid: "1234567890", EABHmac: "1234567890"}, wantErr: false},
|
||||
{name: "Missing EABKid", cfg: &autocert.Config{EABHmac: "1234567890"}, wantErr: true},
|
||||
{name: "Missing EABHmac", cfg: &autocert.Config{EABKid: "1234567890"}, wantErr: true},
|
||||
{name: "Valid EAB", cfg: &autocert.Config{EABKid: "1234567890", EABHmac: "1234567890"}, wantErr: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac)
|
||||
cfg := Config{}
|
||||
cfg := autocert.Config{}
|
||||
err := serialization.UnmarshalValidateYAML(yaml, &cfg)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr)
|
||||
@@ -29,3 +34,27 @@ func TestEABConfigRequired(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtraCertKeyPathsUnique(t *testing.T) {
|
||||
t.Run("duplicate cert_path rejected", func(t *testing.T) {
|
||||
cfg := &autocert.Config{
|
||||
Provider: autocert.ProviderLocal,
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||
{CertPath: "a.crt", KeyPath: "b.key"},
|
||||
},
|
||||
}
|
||||
require.Error(t, cfg.Validate())
|
||||
})
|
||||
|
||||
t.Run("duplicate key_path rejected", func(t *testing.T) {
|
||||
cfg := &autocert.Config{
|
||||
Provider: autocert.ProviderLocal,
|
||||
Extra: []autocert.ConfigExtra{
|
||||
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||
{CertPath: "b.crt", KeyPath: "a.key"},
|
||||
},
|
||||
}
|
||||
require.Error(t, cfg.Validate())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,5 +5,4 @@ const (
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||
LastFailureFile = certBasePath + ".last_failure"
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user