mirror of
https://github.com/yusing/godoxy.git
synced 2026-02-16 07:27:42 +01:00
Compare commits
50 Commits
feat/custo
...
v0.11.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604e2481a6 | ||
|
|
4f557043a5 | ||
|
|
03d609e4e1 | ||
|
|
db6fc65876 | ||
|
|
c6a05f7b35 | ||
|
|
9e4aa32120 | ||
|
|
759995972d | ||
|
|
03401488f6 | ||
|
|
1e790be70c | ||
|
|
4410637f8b | ||
|
|
3947152336 | ||
|
|
af8d2c74f6 | ||
|
|
e107f8d476 | ||
|
|
b427ff1f88 | ||
|
|
e513db62b0 | ||
|
|
2f33ee02d9 | ||
|
|
59490dcac0 | ||
|
|
5afa93a8f1 | ||
|
|
c8e9ed8440 | ||
|
|
8363dfe257 | ||
|
|
080bbc18eb | ||
|
|
1a0edc8bfe | ||
|
|
e8d1d524b9 | ||
|
|
edada22ac0 | ||
|
|
76fb0cfdbb | ||
|
|
5df2553774 | ||
|
|
31812430f1 | ||
|
|
d668b03175 | ||
|
|
663a107c06 | ||
|
|
806184e98b | ||
|
|
08ee82d7b0 | ||
|
|
bcc19167d4 | ||
|
|
858f65ee5a | ||
|
|
43566bbcfd | ||
|
|
ec8cca1245 | ||
|
|
4a65de99a8 | ||
|
|
7461344004 | ||
|
|
b815c6fd69 | ||
|
|
28c9a2e9d0 | ||
|
|
9e0bdd964c | ||
|
|
077641beaa | ||
|
|
ef483403da | ||
|
|
0a8aa2b215 | ||
|
|
5a984f5c0c | ||
|
|
d60688c66f | ||
|
|
23482da259 | ||
|
|
62776229cb | ||
|
|
36fab0cd50 | ||
|
|
8f03662982 | ||
|
|
aad44031c4 |
20
.env.example
20
.env.example
@@ -1,26 +1,26 @@
|
||||
# set timezone to get correct log timestamp
|
||||
TZ=ETC/UTC
|
||||
|
||||
# API JWT Configuration (common)
|
||||
# generate secret with `openssl rand -base64 32`
|
||||
GODOXY_API_JWT_SECRET=
|
||||
# the JWT token time-to-live
|
||||
# leave empty to use default (24 hours)
|
||||
# format: https://pkg.go.dev/time#Duration
|
||||
GODOXY_API_JWT_TOKEN_TTL=
|
||||
|
||||
# API/WebUI user password login credentials (optional)
|
||||
# These fields are not required for OIDC authentication
|
||||
GODOXY_API_USER=admin
|
||||
GODOXY_API_PASSWORD=password
|
||||
|
||||
# Enable `secure` cookie flag
|
||||
GODOXY_API_JWT_SECURE=true
|
||||
# generate secret with `openssl rand -base64 32`
|
||||
GODOXY_API_JWT_SECRET=
|
||||
# the JWT token time-to-live
|
||||
GODOXY_API_JWT_TOKEN_TTL=1h
|
||||
|
||||
# OIDC Configuration (optional)
|
||||
# Uncomment and configure these values to enable OIDC authentication.
|
||||
# For `GODOXY_OIDC_SCOPES` you may also include `offline_access` if your Idp supports it (e.g. Authentik)
|
||||
#
|
||||
# GODOXY_OIDC_ISSUER_URL=https://accounts.google.com
|
||||
# GODOXY_OIDC_CLIENT_ID=your-client-id
|
||||
# GODOXY_OIDC_CLIENT_SECRET=your-client-secret
|
||||
# Keep /api/auth/callback as the redirect URL, change the domain to match your setup.
|
||||
# GODOXY_OIDC_REDIRECT_URL=https://your-domain/api/auth/callback
|
||||
# Comma-separated list of scopes
|
||||
# GODOXY_OIDC_SCOPES=openid, profile, email
|
||||
#
|
||||
# User definitions: Uncomment and configure these values to restrict access to specific users or groups.
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,6 +29,8 @@ todo.md
|
||||
.aider*
|
||||
mtrace.json
|
||||
.env
|
||||
.cursorrules
|
||||
.windsurfrules
|
||||
test.Dockerfile
|
||||
|
||||
node_modules/
|
||||
|
||||
@@ -24,7 +24,6 @@ COPY cmd ./cmd
|
||||
COPY internal ./internal
|
||||
COPY pkg ./pkg
|
||||
COPY agent ./agent
|
||||
COPY migrations ./migrations
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=${VERSION}
|
||||
@@ -60,4 +59,4 @@ ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/app/run"]
|
||||
CMD ["/app/run"]
|
||||
13
Makefile
13
Makefile
@@ -50,6 +50,12 @@ export GODEBUG
|
||||
export GORACE
|
||||
export BUILD_FLAGS
|
||||
|
||||
ifeq ($(shell id -u), 0)
|
||||
SETCAP_CMD = setcap
|
||||
else
|
||||
SETCAP_CMD = sudo setcap
|
||||
endif
|
||||
|
||||
.PHONY: debug
|
||||
|
||||
test:
|
||||
@@ -61,10 +67,9 @@ get:
|
||||
build:
|
||||
mkdir -p bin
|
||||
go build ${BUILD_FLAGS} -o bin/${NAME} ${CMD_PATH}
|
||||
if [ $(shell id -u) -eq 0 ]; \
|
||||
then setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
||||
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/${NAME}; \
|
||||
fi
|
||||
|
||||
# CAP_NET_BIND_SERVICE: permission for binding to :80 and :443
|
||||
$(SETCAP_CMD) CAP_NET_BIND_SERVICE=+ep bin/${NAME}
|
||||
|
||||
run:
|
||||
[ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ${CMD_PATH}
|
||||
|
||||
84
README.md
84
README.md
@@ -5,7 +5,7 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||

|
||||

|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||
@@ -27,8 +27,8 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||
- [Running demo](#running-demo)
|
||||
- [Key Features](#key-features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||
- [Setup](#setup)
|
||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Metrics and Logs](#metrics-and-logs)
|
||||
@@ -38,46 +38,52 @@ For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki
|
||||
|
||||
## Running demo
|
||||
|
||||
<https://godoxy.demo.6uo.me>
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
## Key Features
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Simple multi-node setup with GoDoxy agents or Docker Socket Proxies
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- **Auto SSL** with Let's Encrypt (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- **Auto hot-reload** on container state / config file changes
|
||||
- **Container aware**: create routes dynamically from running docker containers
|
||||
- **idlesleeper**: stop and wake containers based on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP reserve proxy and TCP/UDP port forwarding
|
||||
- **OpenID Connect integration**: SSO and secure your apps easily
|
||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares) and [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- **Web UI with App dashboard, config editor, _uptime and system metrics_, _docker logs viewer_**
|
||||
- Supports **linux/amd64** and **linux/arm64**
|
||||
- Written in **[Go](https://go.dev)**
|
||||
- **Simple**
|
||||
- Effortless configuration with [simple labels](https://github.com/yusing/godoxy/wiki/Docker-labels-and-Route-Files) or WebUI
|
||||
- [Simple multi-node setup](https://github.com/yusing/godoxy/wiki/Configurations#multi-docker-nodes-setup)
|
||||
- Detailed error messages for easy troubleshooting.
|
||||
- **ACL**: connection / request level access control
|
||||
- IP/CIDR
|
||||
- Country **(Maxmind account required)**
|
||||
- Timezone **(Maxmind account required)**
|
||||
- **Access logging**
|
||||
- **Advanced Automation**
|
||||
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- Auto-configuration for Docker containers
|
||||
- Hot-reloading of configurations and container state changes
|
||||
- **Idle-sleep**: stop and wake containers based on traffic _(see [screenshots](#idlesleeper))_
|
||||
- Docker containers
|
||||
- Proxmox LXCs
|
||||
- **Traffic Management**
|
||||
- HTTP reserve proxy
|
||||
- TCP/UDP port forwarding
|
||||
- **OpenID Connect support**: SSO and secure your apps easily
|
||||
- **Customization**
|
||||
- [HTTP middlewares](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- **Web UI**
|
||||
- App Dashboard
|
||||
- Config Editor
|
||||
- Uptime and System Metrics
|
||||
- Docker Logs Viewer
|
||||
- **Cross-Platform support**
|
||||
- Supports **linux/amd64** and **linux/arm64**
|
||||
- **Efficient and Performant**
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
||||
Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
|
||||
|
||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||
|
||||
## How does GoDoxy work
|
||||
|
||||
1. List all the containers
|
||||
2. Read container name, labels and port configurations for each of them
|
||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||
4. Watch for container / config changes and update automatically
|
||||
|
||||
> [!NOTE]
|
||||
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||
>
|
||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||
|
||||
## Setup
|
||||
|
||||
> [!NOTE]
|
||||
@@ -89,12 +95,24 @@ Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
||||
|
||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
3. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||
|
||||
## How does GoDoxy work
|
||||
|
||||
1. List all the containers
|
||||
2. Read container name, labels and port configurations for each of them
|
||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||
4. Watch for container / config changes and update automatically
|
||||
|
||||
> [!NOTE]
|
||||
> GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to the `container_name` field in docker compose.
|
||||
>
|
||||
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||

|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
輕量、易用、 [高效能](https://github.com/yusing/godoxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
## 運行示例
|
||||
|
||||
<https://godoxy.demo.6uo.me>
|
||||
<https://demo.godoxy.dev>
|
||||
|
||||
[](https://zeabur.com/referral?referralCode=yusing&utm_source=yusing&utm_campaign=oss)
|
||||
|
||||
@@ -52,13 +52,13 @@
|
||||
- 容器狀態/配置文件變更時自動熱重載
|
||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||
- OpenID Connect:輕鬆實現單點登入
|
||||
- HTTP(s) 反向代理和TCP 和 UDP 埠轉發
|
||||
- HTTP(s) 反向代理和 TCP 和 UDP 埠轉發
|
||||
- [HTTP 中介軟體](https://github.com/yusing/godoxy/wiki/Middlewares) 和 [自定義錯誤頁面](https://github.com/yusing/godoxy/wiki/Middlewares#custom-error-pages)
|
||||
- **網頁介面,具有應用儀表板和配置編輯器**
|
||||
- 支援 linux/amd64、linux/arm64
|
||||
- 使用 **[Go](https://go.dev)** 編寫
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
## 前置需求
|
||||
|
||||
@@ -78,13 +78,13 @@
|
||||
|
||||
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
### 手動安裝
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||

|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
### 監控
|
||||
|
||||
@@ -166,4 +166,4 @@
|
||||
|
||||
5. 使用 `make build` 編譯二進制檔案
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
[🔼 回到頂部](#目錄)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"github.com/yusing/go-proxy/internal/utils/pool"
|
||||
)
|
||||
|
||||
type agents struct{ pool.Pool[*AgentConfig] }
|
||||
|
||||
var Agents = agents{pool.New[*AgentConfig]("agents")}
|
||||
|
||||
func (agents agents) Get(agentAddrOrDockerHost string) (*AgentConfig, bool) {
|
||||
if !IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||
return agents.Base().Load(agentAddrOrDockerHost)
|
||||
}
|
||||
return agents.Base().Load(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||
}
|
||||
@@ -4,16 +4,20 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
)
|
||||
|
||||
@@ -23,6 +27,7 @@ type AgentConfig struct {
|
||||
httpClient *http.Client
|
||||
tlsConfig *tls.Config
|
||||
name string
|
||||
l zerolog.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -45,20 +50,11 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
AgentURL, _ = url.Parse(APIBaseURL)
|
||||
HTTPProxyURL, _ = url.Parse(APIBaseURL + EndpointProxyHTTP)
|
||||
AgentURL = types.MustParseURL(APIBaseURL)
|
||||
HTTPProxyURL = types.MustParseURL(APIBaseURL + EndpointProxyHTTP)
|
||||
HTTPProxyURLPrefixLen = len(APIEndpointBase + EndpointProxyHTTP)
|
||||
)
|
||||
|
||||
// TestAgentConfig is a helper function to create an AgentConfig for testing purposes.
|
||||
// Not used in production.
|
||||
func TestAgentConfig(name string, addr string) *AgentConfig {
|
||||
return &AgentConfig{
|
||||
name: name,
|
||||
Addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
func IsDockerHostAgent(dockerHost string) bool {
|
||||
return strings.HasPrefix(dockerHost, FakeDockerHostPrefix)
|
||||
}
|
||||
@@ -67,11 +63,6 @@ func GetAgentAddrFromDockerHost(dockerHost string) string {
|
||||
return dockerHost[FakeDockerHostPrefixLen:]
|
||||
}
|
||||
|
||||
// Key implements pool.Object
|
||||
func (cfg *AgentConfig) Key() string {
|
||||
return cfg.Addr
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) FakeDockerHost() string {
|
||||
return FakeDockerHostPrefix + cfg.Addr
|
||||
}
|
||||
@@ -81,7 +72,15 @@ func (cfg *AgentConfig) Parse(addr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
|
||||
func withoutBuildTime(version string) string {
|
||||
return strings.Split(version, "-")[0]
|
||||
}
|
||||
|
||||
func checkVersion(a, b string) bool {
|
||||
return withoutBuildTime(a) == withoutBuildTime(b)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) StartWithCerts(parent task.Parent, ca, crt, key []byte) error {
|
||||
clientCert, err := tls.X509KeyPair(crt, key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -103,7 +102,7 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
// create transport and http client
|
||||
cfg.httpClient = cfg.NewHTTPClient()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(parent.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// check agent version
|
||||
@@ -112,10 +111,10 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
return err
|
||||
}
|
||||
|
||||
agentVer := pkg.ParseVersion(string(version))
|
||||
serverVer := pkg.GetVersion()
|
||||
if !agentVer.IsEqual(serverVer) {
|
||||
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", serverVer, agentVer)
|
||||
versionStr := string(version)
|
||||
// skip version check for dev versions
|
||||
if strings.HasPrefix(versionStr, "v") && !checkVersion(versionStr, pkg.GetVersion()) {
|
||||
return gperr.Errorf("agent version mismatch: server: %s, agent: %s", pkg.GetVersion(), versionStr)
|
||||
}
|
||||
|
||||
// get agent name
|
||||
@@ -125,10 +124,13 @@ func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte)
|
||||
}
|
||||
|
||||
cfg.name = string(name)
|
||||
cfg.l = logging.With().Str("agent", cfg.name).Logger()
|
||||
|
||||
logging.Info().Msgf("agent %q initialized", cfg.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Init(ctx context.Context) gperr.Error {
|
||||
func (cfg *AgentConfig) Start(parent task.Parent) gperr.Error {
|
||||
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
|
||||
if !ok {
|
||||
return gperr.New("invalid agent host").Subject(cfg.Addr)
|
||||
@@ -144,7 +146,7 @@ func (cfg *AgentConfig) Init(ctx context.Context) gperr.Error {
|
||||
return gperr.Wrap(err, "failed to extract agent certs")
|
||||
}
|
||||
|
||||
return gperr.Wrap(cfg.InitWithCerts(ctx, ca, crt, key))
|
||||
return gperr.Wrap(cfg.StartWithCerts(parent, ca, crt, key))
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
|
||||
@@ -172,10 +174,6 @@ func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
return gphttp.DefaultDialer.DialContext(ctx, "tcp", cfg.Addr)
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) IsInitialized() bool {
|
||||
return cfg.name != ""
|
||||
}
|
||||
|
||||
func (cfg *AgentConfig) Name() string {
|
||||
return cfg.name
|
||||
}
|
||||
@@ -184,10 +182,9 @@ func (cfg *AgentConfig) String() string {
|
||||
return cfg.name + "@" + cfg.Addr
|
||||
}
|
||||
|
||||
// MarshalMap implements pool.Object
|
||||
func (cfg *AgentConfig) MarshalMap() map[string]any {
|
||||
return map[string]any{
|
||||
func (cfg *AgentConfig) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"name": cfg.Name(),
|
||||
"addr": cfg.Addr,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func AgentCertsFilepath(host string) (filepathOut string, ok bool) {
|
||||
if !isValidAgentHost(host) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(common.CertsDir, host+".zip"), true
|
||||
return filepath.Join(common.AgentCertsBasePath, host+".zip"), true
|
||||
}
|
||||
|
||||
func ExtractCert(data []byte) (ca, crt, key []byte, err error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -8,8 +9,6 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/agent/pkg/handler"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
func serviceUnavailable(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -21,10 +22,10 @@ func DockerSocketHandler() http.HandlerFunc {
|
||||
logging.Warn().Err(err).Msg("failed to connect to docker client")
|
||||
return serviceUnavailable
|
||||
}
|
||||
rp := reverseproxy.NewReverseProxy("docker", &url.URL{
|
||||
rp := reverseproxy.NewReverseProxy("docker", types.NewURL(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: client.DummyHost,
|
||||
}, dockerClient.HTTPClient().Transport)
|
||||
}), dockerClient.HTTPClient().Transport)
|
||||
|
||||
return rp.ServeHTTP
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get(agentproxy.HeaderXProxyHost)
|
||||
isHTTPS := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||
skipTLSVerify := strutils.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
||||
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
|
||||
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
|
||||
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
|
||||
if err != nil {
|
||||
responseHeaderTimeout = 0
|
||||
@@ -54,9 +54,9 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
logging.Debug().Msgf("proxy http request: %s %s", r.Method, r.URL.String())
|
||||
|
||||
rp := reverseproxy.NewReverseProxy("agent", &url.URL{
|
||||
rp := reverseproxy.NewReverseProxy("agent", types.NewURL(&url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
}, transport)
|
||||
}), transport)
|
||||
rp.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
server.Start(parent, agentServer, logger)
|
||||
server.Start(parent, agentServer, nil, logger)
|
||||
}
|
||||
|
||||
21
cmd/main.go
21
cmd/main.go
@@ -6,21 +6,19 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
debugapi "github.com/yusing/go-proxy/internal/api/v1/debug"
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/query"
|
||||
"github.com/yusing/go-proxy/internal/auth"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/config"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/migrations"
|
||||
"github.com/yusing/go-proxy/pkg"
|
||||
)
|
||||
|
||||
@@ -40,9 +38,6 @@ func parallel(fns ...func()) {
|
||||
|
||||
func main() {
|
||||
initProfiling()
|
||||
if err := migrations.RunMigrations(); err != nil {
|
||||
gperr.LogFatal("migration error", err)
|
||||
}
|
||||
args := pkg.GetArgs(common.MainServerCommandValidator{})
|
||||
|
||||
switch args.Command {
|
||||
@@ -53,7 +48,7 @@ func main() {
|
||||
rawLogger.Println("ok")
|
||||
return
|
||||
case common.CommandListIcons:
|
||||
icons, err := homepage.ListAvailableIcons()
|
||||
icons, err := internal.ListAvailableIcons()
|
||||
if err != nil {
|
||||
rawLogger.Fatal(err)
|
||||
}
|
||||
@@ -82,9 +77,7 @@ func main() {
|
||||
logging.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
|
||||
logging.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
homepage.InitIconListCache,
|
||||
homepage.InitIconCache,
|
||||
homepage.InitOverridesConfig,
|
||||
internal.InitIconListCache,
|
||||
systeminfo.Poller.Start,
|
||||
)
|
||||
|
||||
@@ -124,7 +117,7 @@ func main() {
|
||||
switch args.Command {
|
||||
case common.CommandListRoutes:
|
||||
cfg.StartProxyProviders()
|
||||
printJSON(routequery.RoutesByAlias())
|
||||
printJSON(routes.ByAlias())
|
||||
return
|
||||
case common.CommandListConfigs:
|
||||
printJSON(cfg.Value())
|
||||
@@ -151,8 +144,6 @@ func main() {
|
||||
uptime.Poller.Start()
|
||||
config.WatchChanges()
|
||||
|
||||
debugapi.StartServer(cfg)
|
||||
|
||||
task.WaitExit(cfg.Value().TimeoutShutdown)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,25 @@
|
||||
|
||||
# 3. other providers, see https://github.com/yusing/godoxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||
|
||||
# acl:
|
||||
# default: allow # or deny (default: allow)
|
||||
# allow_local: true # or false (default: true)
|
||||
# allow:
|
||||
# - ip:1.2.3.4
|
||||
# - cidr:1.2.3.4/32
|
||||
# - country:US
|
||||
# - timezone:Asia/Shanghai
|
||||
# deny:
|
||||
# - ip:1.2.3.4
|
||||
# - cidr:1.2.3.4/32
|
||||
# - country:US
|
||||
# - timezone:Asia/Shanghai
|
||||
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
|
||||
# buffer_size: 65536 # (default: 64KB)
|
||||
# path: /app/logs/acl.log # (default: none)
|
||||
# stdout: false # (default: false)
|
||||
# keep: last 10 # (default: none)
|
||||
|
||||
entrypoint:
|
||||
# Below define an example of middleware config
|
||||
# 1. block non local IP connections
|
||||
@@ -73,6 +92,14 @@ providers:
|
||||
# url: https://discord.com/api/webhooks/...
|
||||
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||
|
||||
# Proxmox providers (for idlesleep support for proxmox LXCs)
|
||||
#
|
||||
# proxmox:
|
||||
# - url: https://pve.domain.com:8006/api2/json
|
||||
# token_id: root@pam!abcdef
|
||||
# secret: aaaa-bbbb-cccc-dddd
|
||||
# no_tls_verify: true
|
||||
|
||||
# Check https://github.com/yusing/godoxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||
# for explaination of `match_domains`
|
||||
#
|
||||
|
||||
232
go.mod
232
go.mod
@@ -2,148 +2,242 @@ module github.com/yusing/go-proxy
|
||||
|
||||
go 1.24.2
|
||||
|
||||
// misc
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.2 // faster json unmarshal (for marshal it's using custom implementation)
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||
github.com/docker/docker v28.1.1+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.9.0 // file watcher
|
||||
github.com/go-acme/lego/v4 v4.22.2 // acme client
|
||||
github.com/go-acme/lego/v4 v4.23.1 // acme client
|
||||
github.com/go-playground/validator/v10 v10.26.0 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/goccy/go-yaml v1.17.1 // yaml parsing for different config files
|
||||
github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.39.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
||||
golang.org/x/text v0.24.0 // string utilities
|
||||
golang.org/x/time v0.11.0 // time utilities
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect; yaml parsing for different config files
|
||||
)
|
||||
|
||||
// http
|
||||
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.14.2
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.13 // websocket for API and agent
|
||||
github.com/quic-go/quic-go v0.50.1 // http3 server
|
||||
golang.org/x/net v0.39.0 // HTTP header utilities
|
||||
github.com/docker/cli v28.1.1+incompatible
|
||||
github.com/goccy/go-yaml v1.17.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/luthermonson/go-proxmox v0.2.2
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/quic-go/quic-go v0.51.0
|
||||
github.com/samber/slog-zerolog/v2 v2.7.3
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
)
|
||||
|
||||
// authentication
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // oidc authentication
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // jwt for default auth
|
||||
golang.org/x/crypto v0.37.0 // encrypting password with bcrypt
|
||||
golang.org/x/oauth2 v0.29.0 // oauth2 authentication
|
||||
)
|
||||
|
||||
// favicon extraction
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
)
|
||||
|
||||
// docker
|
||||
|
||||
require (
|
||||
github.com/docker/cli v28.0.4+incompatible // docker cli
|
||||
github.com/docker/docker v28.0.4+incompatible // docker daemon
|
||||
github.com/docker/go-connections v0.5.0 // docker connection utilities
|
||||
)
|
||||
|
||||
// logging
|
||||
|
||||
require (
|
||||
github.com/rs/zerolog v1.34.0 // logging
|
||||
github.com/samber/slog-zerolog/v2 v2.7.3 // zerlog to slog adapter for quic-go
|
||||
)
|
||||
|
||||
// metrics
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.22.0 // metrics
|
||||
github.com/shirou/gopsutil/v4 v4.25.3 // system info metrics
|
||||
github.com/stretchr/testify v1.10.0 // testing utilities
|
||||
)
|
||||
|
||||
require github.com/luthermonson/go-proxmox v0.2.2
|
||||
replace github.com/docker/docker => github.com/godoxy-app/docker v0.0.0-20250425105916-b2ad800de7a1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.16.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.51.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.224 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.98 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.115.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/diskfs/go-diskfs v1.6.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/exoscale/egoscale/v3 v3.1.15 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // 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-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect; indirectindirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // 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.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.1 // indirect
|
||||
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.146 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client/v2 v2.9.0 // indirect
|
||||
github.com/jinzhu/copier v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/linode/linodego v1.49.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
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.65 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
|
||||
github.com/nrdcg/desec v0.11.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.11.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/oracle/oci-go-sdk/v65 v65.89.2 // indirect
|
||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.9 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.14.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.11 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/samber/slog-common v0.18.1 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
|
||||
github.com/selectel/domains-go v1.1.0 // indirect
|
||||
github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1151 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1136 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.206 // indirect
|
||||
github.com/vultr/govultr/v3 v3.19.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/mock v0.5.1 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.1 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
google.golang.org/api v0.230.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect
|
||||
google.golang.org/grpc v1.72.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.14.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/api v0.33.0 // indirect
|
||||
k8s.io/apimachinery v0.33.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
39
internal/acl/city_cache.go
Normal file
39
internal/acl/city_cache.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var cityCache = xsync.NewMapOf[string, *acl.City]()
|
||||
var numCachedLookup atomic.Uint64
|
||||
|
||||
func (cfg *MaxMindConfig) lookupCity(ip *acl.IPInfo) (*acl.City, bool) {
|
||||
if ip.City != nil {
|
||||
return ip.City, true
|
||||
}
|
||||
|
||||
if cfg.db.Reader == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
city, ok := cityCache.Load(ip.Str)
|
||||
if ok {
|
||||
numCachedLookup.Inc()
|
||||
return city, true
|
||||
}
|
||||
|
||||
cfg.db.RLock()
|
||||
defer cfg.db.RUnlock()
|
||||
|
||||
city = new(acl.City)
|
||||
err := cfg.db.Lookup(ip.IP, city)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
cityCache.Store(ip.Str, city)
|
||||
ip.City = city
|
||||
return city, true
|
||||
}
|
||||
215
internal/acl/config.go
Normal file
215
internal/acl/config.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/rs/zerolog"
|
||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Default string `json:"default" validate:"omitempty,oneof=allow deny"` // default: allow
|
||||
AllowLocal *bool `json:"allow_local"` // default: true
|
||||
Allow []string `json:"allow"`
|
||||
Deny []string `json:"deny"`
|
||||
Log *accesslog.ACLLoggerConfig `json:"log"`
|
||||
|
||||
MaxMind *MaxMindConfig `json:"maxmind" validate:"omitempty"`
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
type (
|
||||
MaxMindDatabaseType string
|
||||
MaxMindConfig struct {
|
||||
AccountID string `json:"account_id" validate:"required"`
|
||||
LicenseKey string `json:"license_key" validate:"required"`
|
||||
Database MaxMindDatabaseType `json:"database" validate:"required,oneof=geolite geoip2"`
|
||||
|
||||
logger zerolog.Logger
|
||||
lastUpdate time.Time
|
||||
db struct {
|
||||
*maxminddb.Reader
|
||||
sync.RWMutex
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
type config struct {
|
||||
defaultAllow bool
|
||||
allowLocal bool
|
||||
allow []matcher
|
||||
deny []matcher
|
||||
ipCache *xsync.MapOf[string, *checkCache]
|
||||
logAllowed bool
|
||||
logger *accesslog.AccessLogger
|
||||
}
|
||||
|
||||
type checkCache struct {
|
||||
*acl.IPInfo
|
||||
allow bool
|
||||
created time.Time
|
||||
}
|
||||
|
||||
const cacheTTL = 1 * time.Minute
|
||||
|
||||
func (c *checkCache) Expired() bool {
|
||||
return c.created.Add(cacheTTL).After(utils.TimeNow())
|
||||
}
|
||||
|
||||
//TODO: add stats
|
||||
|
||||
const (
|
||||
ACLAllow = "allow"
|
||||
ACLDeny = "deny"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxMindGeoLite MaxMindDatabaseType = "geolite"
|
||||
MaxMindGeoIP2 MaxMindDatabaseType = "geoip2"
|
||||
)
|
||||
|
||||
func (c *Config) Validate() gperr.Error {
|
||||
switch c.Default {
|
||||
case "", ACLAllow:
|
||||
c.defaultAllow = true
|
||||
case ACLDeny:
|
||||
c.defaultAllow = false
|
||||
default:
|
||||
return gperr.New("invalid default value").Subject(c.Default)
|
||||
}
|
||||
|
||||
if c.AllowLocal != nil {
|
||||
c.allowLocal = *c.AllowLocal
|
||||
} else {
|
||||
c.allowLocal = true
|
||||
}
|
||||
|
||||
if c.MaxMind != nil {
|
||||
c.MaxMind.logger = logging.With().Str("type", string(c.MaxMind.Database)).Logger()
|
||||
}
|
||||
|
||||
if c.Log != nil {
|
||||
c.logAllowed = c.Log.LogAllowed
|
||||
}
|
||||
|
||||
errs := gperr.NewBuilder("syntax error")
|
||||
c.allow = make([]matcher, 0, len(c.Allow))
|
||||
c.deny = make([]matcher, 0, len(c.Deny))
|
||||
|
||||
for _, s := range c.Allow {
|
||||
m, err := c.parseMatcher(s)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(s))
|
||||
continue
|
||||
}
|
||||
c.allow = append(c.allow, m)
|
||||
}
|
||||
for _, s := range c.Deny {
|
||||
m, err := c.parseMatcher(s)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(s))
|
||||
continue
|
||||
}
|
||||
c.deny = append(c.deny, m)
|
||||
}
|
||||
|
||||
if errs.HasError() {
|
||||
c.allow = nil
|
||||
c.deny = nil
|
||||
return errMatcherFormat.With(errs.Error())
|
||||
}
|
||||
|
||||
c.ipCache = xsync.NewMapOf[string, *checkCache]()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Valid() bool {
|
||||
return c != nil && (len(c.allow) > 0 || len(c.deny) > 0 || c.allowLocal)
|
||||
}
|
||||
|
||||
func (c *Config) Start(parent *task.Task) gperr.Error {
|
||||
if c.MaxMind != nil {
|
||||
if err := c.MaxMind.LoadMaxMindDB(parent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if c.Log != nil {
|
||||
logger, err := accesslog.NewAccessLogger(parent, c.Log)
|
||||
if err != nil {
|
||||
return gperr.New("failed to start access logger").With(err)
|
||||
}
|
||||
c.logger = logger
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) cacheRecord(info *acl.IPInfo, allow bool) {
|
||||
c.ipCache.Store(info.Str, &checkCache{
|
||||
IPInfo: info,
|
||||
allow: allow,
|
||||
created: utils.TimeNow(),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *config) log(info *acl.IPInfo, allowed bool) {
|
||||
if c.logger == nil {
|
||||
return
|
||||
}
|
||||
if !allowed || c.logAllowed {
|
||||
c.logger.LogACL(info, !allowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) IPAllowed(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// always allow private and loopback
|
||||
// loopback is not logged
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
|
||||
if c.allowLocal && ip.IsPrivate() {
|
||||
c.log(&acl.IPInfo{IP: ip, Str: ip.String()}, true)
|
||||
return true
|
||||
}
|
||||
|
||||
ipStr := ip.String()
|
||||
record, ok := c.ipCache.Load(ipStr)
|
||||
if ok && !record.Expired() {
|
||||
c.log(record.IPInfo, record.allow)
|
||||
return record.allow
|
||||
}
|
||||
|
||||
ipAndStr := &acl.IPInfo{IP: ip, Str: ipStr}
|
||||
for _, m := range c.allow {
|
||||
if m(ipAndStr) {
|
||||
c.log(ipAndStr, true)
|
||||
c.cacheRecord(ipAndStr, true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, m := range c.deny {
|
||||
if m(ipAndStr) {
|
||||
c.log(ipAndStr, false)
|
||||
c.cacheRecord(ipAndStr, false)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
c.log(ipAndStr, c.defaultAllow)
|
||||
c.cacheRecord(ipAndStr, c.defaultAllow)
|
||||
return c.defaultAllow
|
||||
}
|
||||
99
internal/acl/matcher.go
Normal file
99
internal/acl/matcher.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
acl "github.com/yusing/go-proxy/internal/acl/types"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
)
|
||||
|
||||
type matcher func(*acl.IPInfo) bool
|
||||
|
||||
const (
|
||||
MatcherTypeIP = "ip"
|
||||
MatcherTypeCIDR = "cidr"
|
||||
MatcherTypeTimeZone = "tz"
|
||||
MatcherTypeCountry = "country"
|
||||
)
|
||||
|
||||
var errMatcherFormat = gperr.Multiline().AddLines(
|
||||
"invalid matcher format, expect {type}:{value}",
|
||||
"Available types: ip|cidr|tz|country",
|
||||
"ip:127.0.0.1",
|
||||
"cidr:127.0.0.0/8",
|
||||
"tz:Asia/Shanghai",
|
||||
"country:GB",
|
||||
)
|
||||
var (
|
||||
errSyntax = gperr.New("syntax error")
|
||||
errInvalidIP = gperr.New("invalid IP")
|
||||
errInvalidCIDR = gperr.New("invalid CIDR")
|
||||
errMaxMindNotConfigured = gperr.New("MaxMind not configured")
|
||||
)
|
||||
|
||||
func (cfg *Config) parseMatcher(s string) (matcher, gperr.Error) {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, errSyntax
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case MatcherTypeIP:
|
||||
ip := net.ParseIP(parts[1])
|
||||
if ip == nil {
|
||||
return nil, errInvalidIP
|
||||
}
|
||||
return matchIP(ip), nil
|
||||
case MatcherTypeCIDR:
|
||||
_, net, err := net.ParseCIDR(parts[1])
|
||||
if err != nil {
|
||||
return nil, errInvalidCIDR
|
||||
}
|
||||
return matchCIDR(net), nil
|
||||
case MatcherTypeTimeZone:
|
||||
if cfg.MaxMind == nil {
|
||||
return nil, errMaxMindNotConfigured
|
||||
}
|
||||
return cfg.MaxMind.matchTimeZone(parts[1]), nil
|
||||
case MatcherTypeCountry:
|
||||
if cfg.MaxMind == nil {
|
||||
return nil, errMaxMindNotConfigured
|
||||
}
|
||||
return cfg.MaxMind.matchISOCode(parts[1]), nil
|
||||
default:
|
||||
return nil, errSyntax
|
||||
}
|
||||
}
|
||||
|
||||
func matchIP(ip net.IP) matcher {
|
||||
return func(ip2 *acl.IPInfo) bool {
|
||||
return ip.Equal(ip2.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func matchCIDR(n *net.IPNet) matcher {
|
||||
return func(ip *acl.IPInfo) bool {
|
||||
return n.Contains(ip.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) matchTimeZone(tz string) matcher {
|
||||
return func(ip *acl.IPInfo) bool {
|
||||
city, ok := cfg.lookupCity(ip)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return city.Location.TimeZone == tz
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) matchISOCode(iso string) matcher {
|
||||
return func(ip *acl.IPInfo) bool {
|
||||
city, ok := cfg.lookupCity(ip)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return city.Country.IsoCode == iso
|
||||
}
|
||||
}
|
||||
281
internal/acl/maxmind.go
Normal file
281
internal/acl/maxmind.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
var (
|
||||
updateInterval = 24 * time.Hour
|
||||
httpClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
ErrResponseNotOK = gperr.New("response not OK")
|
||||
ErrDownloadFailure = gperr.New("download failure")
|
||||
)
|
||||
|
||||
func dbPathImpl(dbType MaxMindDatabaseType) string {
|
||||
if dbType == MaxMindGeoLite {
|
||||
return filepath.Join(dataDir, "GeoLite2-City.mmdb")
|
||||
}
|
||||
return filepath.Join(dataDir, "GeoIP2-City.mmdb")
|
||||
}
|
||||
|
||||
func dbURLimpl(dbType MaxMindDatabaseType) string {
|
||||
if dbType == MaxMindGeoLite {
|
||||
return "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"
|
||||
}
|
||||
return "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"
|
||||
}
|
||||
|
||||
func dbFilename(dbType MaxMindDatabaseType) string {
|
||||
if dbType == MaxMindGeoLite {
|
||||
return "GeoLite2-City.mmdb"
|
||||
}
|
||||
return "GeoIP2-City.mmdb"
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) LoadMaxMindDB(parent task.Parent) gperr.Error {
|
||||
if cfg.Database == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := dbPath(cfg.Database)
|
||||
reader, err := maxmindDBOpen(path)
|
||||
exists := true
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
default:
|
||||
// ignore invalid error, just download it again
|
||||
var invalidErr maxminddb.InvalidDatabaseError
|
||||
if !errors.As(err, &invalidErr) {
|
||||
return gperr.Wrap(err)
|
||||
}
|
||||
}
|
||||
exists = false
|
||||
}
|
||||
|
||||
if !exists {
|
||||
cfg.logger.Info().Msg("MaxMind DB not found/invalid, downloading...")
|
||||
reader, err = cfg.download()
|
||||
if err != nil {
|
||||
return ErrDownloadFailure.With(err)
|
||||
}
|
||||
}
|
||||
cfg.logger.Info().Msg("MaxMind DB loaded")
|
||||
|
||||
cfg.db.Reader = reader
|
||||
go cfg.scheduleUpdate(parent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) loadLastUpdate() {
|
||||
f, err := os.Stat(dbPath(cfg.Database))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cfg.lastUpdate = f.ModTime()
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) setLastUpdate(t time.Time) {
|
||||
cfg.lastUpdate = t
|
||||
_ = os.Chtimes(dbPath(cfg.Database), t, t)
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) scheduleUpdate(parent task.Parent) {
|
||||
task := parent.Subtask("schedule_update", true)
|
||||
ticker := time.NewTicker(updateInterval)
|
||||
|
||||
cfg.loadLastUpdate()
|
||||
cfg.update()
|
||||
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
if cfg.db.Reader != nil {
|
||||
cfg.db.Reader.Close()
|
||||
}
|
||||
task.Finish(nil)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-task.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
cfg.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) update() {
|
||||
// check for update
|
||||
cfg.logger.Info().Msg("checking for MaxMind DB update...")
|
||||
remoteLastModified, err := cfg.checkLastest()
|
||||
if err != nil {
|
||||
cfg.logger.Err(err).Msg("failed to check MaxMind DB update")
|
||||
return
|
||||
}
|
||||
if remoteLastModified.Equal(cfg.lastUpdate) {
|
||||
cfg.logger.Info().Msg("MaxMind DB is up to date")
|
||||
return
|
||||
}
|
||||
|
||||
cfg.logger.Info().
|
||||
Time("latest", remoteLastModified.Local()).
|
||||
Time("current", cfg.lastUpdate).
|
||||
Msg("MaxMind DB update available")
|
||||
reader, err := cfg.download()
|
||||
if err != nil {
|
||||
cfg.logger.Err(err).Msg("failed to update MaxMind DB")
|
||||
return
|
||||
}
|
||||
cfg.db.Lock()
|
||||
cfg.db.Close()
|
||||
cfg.db.Reader = reader
|
||||
cfg.setLastUpdate(*remoteLastModified)
|
||||
cfg.db.Unlock()
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB updated")
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) newReq(method string) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, dbURL(cfg.Database), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(cfg.AccountID, cfg.LicenseKey)
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) checkLastest() (lastModifiedT *time.Time, err error) {
|
||||
resp, err := newReq(cfg, http.MethodHead)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
lastModified := resp.Header.Get("Last-Modified")
|
||||
if lastModified == "" {
|
||||
cfg.logger.Warn().Msg("MaxMind responded no last modified time, update skipped")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
|
||||
if err != nil {
|
||||
cfg.logger.Warn().Err(err).Msg("MaxMind responded invalid last modified time, update skipped")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lastModifiedTime, nil
|
||||
}
|
||||
|
||||
func (cfg *MaxMindConfig) download() (*maxminddb.Reader, error) {
|
||||
resp, err := newReq(cfg, http.MethodGet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d", ErrResponseNotOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
path := dbPath(cfg.Database)
|
||||
tmpPath := path + "-tmp.tar.gz"
|
||||
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.logger.Info().Msg("MaxMind DB downloading...")
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
// extract .tar.gz and move only the dbFilename to path
|
||||
err = extractFileFromTarGz(tmpPath, dbFilename(cfg.Database), path)
|
||||
if err != nil {
|
||||
return nil, gperr.New("failed to extract database from archive").With(err)
|
||||
}
|
||||
// cleanup the tar.gz file
|
||||
_ = os.Remove(tmpPath)
|
||||
|
||||
db, err := maxmindDBOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func extractFileFromTarGz(tarGzPath, targetFilename, destPath string) error {
|
||||
f, err := os.Open(tarGzPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Only extract the file that matches targetFilename (basename match)
|
||||
if filepath.Base(hdr.Name) == targetFilename {
|
||||
outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, hdr.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
_, err = io.Copy(outFile, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil // Done
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("file %s not found in archive", targetFilename)
|
||||
}
|
||||
|
||||
var (
|
||||
dataDir = common.DataDir
|
||||
dbURL = dbURLimpl
|
||||
dbPath = dbPathImpl
|
||||
maxmindDBOpen = maxminddb.Open
|
||||
newReq = (*MaxMindConfig).newReq
|
||||
)
|
||||
213
internal/acl/maxmind_test.go
Normal file
213
internal/acl/maxmind_test.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
func Test_dbPath(t *testing.T) {
|
||||
tmpDataDir := "/tmp/testdata"
|
||||
oldDataDir := dataDir
|
||||
dataDir = tmpDataDir
|
||||
defer func() { dataDir = oldDataDir }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dbType MaxMindDatabaseType
|
||||
want string
|
||||
}{
|
||||
{"GeoLite", MaxMindGeoLite, filepath.Join(tmpDataDir, "GeoLite2-City.mmdb")},
|
||||
{"GeoIP2", MaxMindGeoIP2, filepath.Join(tmpDataDir, "GeoIP2-City.mmdb")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := dbPath(tt.dbType); got != tt.want {
|
||||
t.Errorf("dbPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_dbURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dbType MaxMindDatabaseType
|
||||
want string
|
||||
}{
|
||||
{"GeoLite", MaxMindGeoLite, "https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz"},
|
||||
{"GeoIP2", MaxMindGeoIP2, "https://download.maxmind.com/geoip/databases/GeoIP2-City/download?suffix=tar.gz"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := dbURL(tt.dbType); got != tt.want {
|
||||
t.Errorf("dbURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper for MaxMindConfig ---
|
||||
type testLogger struct{ zerolog.Logger }
|
||||
|
||||
func (testLogger) Info() *zerolog.Event { return &zerolog.Event{} }
|
||||
func (testLogger) Warn() *zerolog.Event { return &zerolog.Event{} }
|
||||
func (testLogger) Err(_ error) *zerolog.Event { return &zerolog.Event{} }
|
||||
|
||||
func Test_MaxMindConfig_newReq(t *testing.T) {
|
||||
cfg := &MaxMindConfig{
|
||||
AccountID: "testid",
|
||||
LicenseKey: "testkey",
|
||||
Database: MaxMindGeoLite,
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
|
||||
// Patch httpClient to use httptest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if u, p, ok := r.BasicAuth(); !ok || u != "testid" || p != "testkey" {
|
||||
t.Errorf("basic auth not set correctly")
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
oldURL := dbURL
|
||||
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||
defer func() { dbURL = oldURL }()
|
||||
|
||||
resp, err := cfg.newReq(http.MethodGet)
|
||||
if err != nil {
|
||||
t.Fatalf("newReq() error = %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("unexpected status: %v", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MaxMindConfig_checkUpdate(t *testing.T) {
|
||||
cfg := &MaxMindConfig{
|
||||
AccountID: "id",
|
||||
LicenseKey: "key",
|
||||
Database: MaxMindGeoLite,
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
lastMod := time.Now().UTC().Format(http.TimeFormat)
|
||||
buildTime := time.Now().Add(-time.Hour)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Last-Modified", lastMod)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
oldURL := dbURL
|
||||
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||
defer func() { dbURL = oldURL }()
|
||||
|
||||
latest, err := cfg.checkLastest()
|
||||
if err != nil {
|
||||
t.Fatalf("checkUpdate() error = %v", err)
|
||||
}
|
||||
if latest.Equal(buildTime) {
|
||||
t.Errorf("expected update needed")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeReadCloser struct {
|
||||
firstRead bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (c *fakeReadCloser) Read(p []byte) (int, error) {
|
||||
if !c.firstRead {
|
||||
c.firstRead = true
|
||||
return strings.NewReader("FAKEMMDB").Read(p)
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (c *fakeReadCloser) Close() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_MaxMindConfig_download(t *testing.T) {
|
||||
cfg := &MaxMindConfig{
|
||||
AccountID: "id",
|
||||
LicenseKey: "key",
|
||||
Database: MaxMindGeoLite,
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, strings.NewReader("FAKEMMDB"))
|
||||
}))
|
||||
defer server.Close()
|
||||
oldURL := dbURL
|
||||
dbURL = func(MaxMindDatabaseType) string { return server.URL }
|
||||
defer func() { dbURL = oldURL }()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
oldDataDir := dataDir
|
||||
dataDir = tmpDir
|
||||
defer func() { dataDir = oldDataDir }()
|
||||
|
||||
// Patch maxminddb.Open to always succeed
|
||||
origOpen := maxmindDBOpen
|
||||
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
|
||||
return &maxminddb.Reader{}, nil
|
||||
}
|
||||
defer func() { maxmindDBOpen = origOpen }()
|
||||
|
||||
rw := &fakeReadCloser{}
|
||||
oldNewReq := newReq
|
||||
newReq = func(cfg *MaxMindConfig, method string) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: rw,
|
||||
}, nil
|
||||
}
|
||||
defer func() { newReq = oldNewReq }()
|
||||
|
||||
db, err := cfg.download()
|
||||
if err != nil {
|
||||
t.Fatalf("download() error = %v", err)
|
||||
}
|
||||
if db == nil {
|
||||
t.Error("expected db instance")
|
||||
}
|
||||
if !rw.closed {
|
||||
t.Error("expected rw to be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_MaxMindConfig_loadMaxMindDB(t *testing.T) {
|
||||
// This test should cover both the path where DB exists and where it does not
|
||||
// For brevity, only the non-existing path is tested here
|
||||
cfg := &MaxMindConfig{
|
||||
AccountID: "id",
|
||||
LicenseKey: "key",
|
||||
Database: MaxMindGeoLite,
|
||||
logger: zerolog.Nop(),
|
||||
}
|
||||
oldOpen := maxmindDBOpen
|
||||
maxmindDBOpen = func(path string) (*maxminddb.Reader, error) {
|
||||
return &maxminddb.Reader{}, nil
|
||||
}
|
||||
defer func() { maxmindDBOpen = oldOpen }()
|
||||
|
||||
oldDBPath := dbPath
|
||||
dbPath = func(MaxMindDatabaseType) string { return filepath.Join(t.TempDir(), "maxmind.mmdb") }
|
||||
defer func() { dbPath = oldDBPath }()
|
||||
|
||||
task := task.RootTask("test")
|
||||
defer task.Finish(nil)
|
||||
err := cfg.LoadMaxMindDB(task)
|
||||
if err != nil {
|
||||
t.Errorf("loadMaxMindDB() error = %v", err)
|
||||
}
|
||||
}
|
||||
46
internal/acl/tcp_listener.go
Normal file
46
internal/acl/tcp_listener.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type TCPListener struct {
|
||||
acl *Config
|
||||
lis net.Listener
|
||||
}
|
||||
|
||||
func (cfg *Config) WrapTCP(lis net.Listener) net.Listener {
|
||||
if cfg == nil {
|
||||
return lis
|
||||
}
|
||||
return &TCPListener{
|
||||
acl: cfg,
|
||||
lis: lis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPListener) Addr() net.Addr {
|
||||
return s.lis.Addr()
|
||||
}
|
||||
|
||||
func (s *TCPListener) Accept() (net.Conn, error) {
|
||||
c, err := s.lis.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
// Not a TCPAddr, drop
|
||||
c.Close()
|
||||
return nil, nil
|
||||
}
|
||||
if !s.acl.IPAllowed(addr.IP) {
|
||||
c.Close()
|
||||
return nil, nil
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *TCPListener) Close() error {
|
||||
return s.lis.Close()
|
||||
}
|
||||
10
internal/acl/types/city_info.go
Normal file
10
internal/acl/types/city_info.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package acl
|
||||
|
||||
type City struct {
|
||||
Location struct {
|
||||
TimeZone string `maxminddb:"time_zone"`
|
||||
} `maxminddb:"location"`
|
||||
Country struct {
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
} `maxminddb:"country"`
|
||||
}
|
||||
9
internal/acl/types/ip_info.go
Normal file
9
internal/acl/types/ip_info.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package acl
|
||||
|
||||
import "net"
|
||||
|
||||
type IPInfo struct {
|
||||
IP net.IP
|
||||
Str string
|
||||
City *City
|
||||
}
|
||||
79
internal/acl/udp_listener.go
Normal file
79
internal/acl/udp_listener.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UDPListener struct {
|
||||
acl *Config
|
||||
lis net.PacketConn
|
||||
}
|
||||
|
||||
func (cfg *Config) WrapUDP(lis net.PacketConn) net.PacketConn {
|
||||
if cfg == nil {
|
||||
return lis
|
||||
}
|
||||
return &UDPListener{
|
||||
acl: cfg,
|
||||
lis: lis,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) LocalAddr() net.Addr {
|
||||
return s.lis.LocalAddr()
|
||||
}
|
||||
|
||||
func (s *UDPListener) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
for {
|
||||
n, addr, err := s.lis.ReadFrom(p)
|
||||
if err != nil {
|
||||
return n, addr, err
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||
// Drop packet from disallowed IP
|
||||
continue
|
||||
}
|
||||
return n, addr, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
for {
|
||||
n, err := s.lis.WriteTo(p, addr)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
// Not a UDPAddr, drop
|
||||
continue
|
||||
}
|
||||
if !s.acl.IPAllowed(udpAddr.IP) {
|
||||
// Drop packet to disallowed IP
|
||||
continue
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetDeadline(t time.Time) error {
|
||||
return s.lis.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetReadDeadline(t time.Time) error {
|
||||
return s.lis.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (s *UDPListener) SetWriteDeadline(t time.Time) error {
|
||||
return s.lis.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (s *UDPListener) Close() error {
|
||||
return s.lis.Close()
|
||||
}
|
||||
@@ -1,24 +1,69 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/auth"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/certapi"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/dockerapi"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/auth"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/logging/memlogger"
|
||||
"github.com/yusing/go-proxy/internal/metrics/uptime"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/servemux"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type (
|
||||
ServeMux struct {
|
||||
*http.ServeMux
|
||||
cfg config.ConfigInstance
|
||||
}
|
||||
WithCfgHandler = func(config.ConfigInstance, http.ResponseWriter, *http.Request)
|
||||
)
|
||||
|
||||
func (mux ServeMux) HandleFunc(methods, endpoint string, h any, requireAuth ...bool) {
|
||||
var handler http.HandlerFunc
|
||||
switch h := h.(type) {
|
||||
case func(http.ResponseWriter, *http.Request):
|
||||
handler = h
|
||||
case http.Handler:
|
||||
handler = h.ServeHTTP
|
||||
case WithCfgHandler:
|
||||
handler = func(w http.ResponseWriter, r *http.Request) {
|
||||
h(mux.cfg, w, r)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported handler type: %T", h))
|
||||
}
|
||||
|
||||
matchDomains := mux.cfg.Value().MatchDomains
|
||||
if len(matchDomains) > 0 {
|
||||
origHandler := handler
|
||||
handler = func(w http.ResponseWriter, r *http.Request) {
|
||||
if httpheaders.IsWebsocket(r.Header) {
|
||||
httpheaders.SetWebsocketAllowedDomains(r.Header, matchDomains)
|
||||
}
|
||||
origHandler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
if len(requireAuth) > 0 && requireAuth[0] {
|
||||
handler = auth.RequireAuth(handler)
|
||||
}
|
||||
if methods == "" {
|
||||
mux.ServeMux.HandleFunc(endpoint, handler)
|
||||
} else {
|
||||
for _, m := range strutils.CommaSeperatedList(methods) {
|
||||
mux.ServeMux.HandleFunc(m+" "+endpoint, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux := servemux.NewServeMux(cfg)
|
||||
mux := ServeMux{http.NewServeMux(), cfg}
|
||||
mux.HandleFunc("GET", "/v1", v1.Index)
|
||||
mux.HandleFunc("GET", "/v1/version", v1.GetVersion)
|
||||
|
||||
@@ -45,26 +90,14 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux.HandleFunc("GET", "/v1/docker/logs/{server}/{container}", dockerapi.Logs, true)
|
||||
mux.HandleFunc("GET", "/v1/docker/containers", dockerapi.Containers, true)
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||
logging.Info().Msg("prometheus metrics enabled")
|
||||
defaultAuth := auth.GetDefaultAuth()
|
||||
if defaultAuth == nil {
|
||||
return mux
|
||||
}
|
||||
|
||||
defaultAuth := auth.GetDefaultAuth()
|
||||
if defaultAuth != nil {
|
||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.LoginCallbackHandler)
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutCallbackHandler)
|
||||
} else {
|
||||
mux.HandleFunc("GET", "/v1/auth/check", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
mux.HandleFunc("GET", "/v1/auth/check", auth.AuthCheckHandler)
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/redirect", defaultAuth.LoginHandler)
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/callback", defaultAuth.PostAuthCallbackHandler)
|
||||
mux.HandleFunc("GET,POST", "/v1/auth/logout", defaultAuth.LogoutHandler)
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -4,11 +4,21 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
)
|
||||
|
||||
func ListAgents(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
gpwebsocket.DynamicJSONHandler(w, r, agent.Agents.Slice, 10*time.Second)
|
||||
if httpheaders.IsWebsocket(r.Header) {
|
||||
gpwebsocket.Periodic(w, r, 10*time.Second, func(conn *websocket.Conn) error {
|
||||
wsjson.Write(r.Context(), conn, cfg.ListAgents())
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
gphttp.RespondJSON(w, r, cfg.ListAgents())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type (
|
||||
OIDCProvider struct {
|
||||
oauthConfig *oauth2.Config
|
||||
oidcProvider *oidc.Provider
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
oidcEndSessionURL *url.URL
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
isMiddleware bool
|
||||
}
|
||||
|
||||
providerJSON struct {
|
||||
oidc.ProviderConfig
|
||||
EndSessionURL string `json:"end_session_endpoint"`
|
||||
}
|
||||
)
|
||||
|
||||
const CookieOauthState = "godoxy_oidc_state"
|
||||
|
||||
const (
|
||||
OIDCMiddlewareCallbackPath = "/auth/callback"
|
||||
OIDCLogoutPath = "/auth/logout"
|
||||
)
|
||||
|
||||
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
||||
}
|
||||
|
||||
wellKnown := strings.TrimSuffix(issuerURL, "/") + "/.well-known/openid-configuration"
|
||||
resp, err := gphttp.Get(wellKnown)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("oidc: %s: %s", resp.Status, body)
|
||||
}
|
||||
|
||||
var p providerJSON
|
||||
err = json.Unmarshal(body, &p)
|
||||
if err != nil {
|
||||
mimeType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err == nil && mimeType != "application/json" {
|
||||
return nil, fmt.Errorf("oidc: unexpected content type: %q from OIDC provider discovery, have you configured the correct issuer URL?", mimeType)
|
||||
}
|
||||
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
|
||||
}
|
||||
|
||||
if p.IssuerURL != issuerURL {
|
||||
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuerURL, p.IssuerURL)
|
||||
}
|
||||
|
||||
var endSessionURL *url.URL
|
||||
if p.EndSessionURL != "" {
|
||||
endSessionURL, err = url.Parse(p.EndSessionURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("oidc: failed to parse end session URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
provider := p.NewProvider(context.Background())
|
||||
return &OIDCProvider{
|
||||
oauthConfig: &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
||||
},
|
||||
oidcProvider: provider,
|
||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||
ClientID: clientID,
|
||||
}),
|
||||
oidcEndSessionURL: endSessionURL,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedGroups: allowedGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||
return NewOIDCProvider(
|
||||
common.OIDCIssuerURL,
|
||||
common.OIDCClientID,
|
||||
common.OIDCClientSecret,
|
||||
common.OIDCRedirectURL,
|
||||
common.OIDCAllowedUsers,
|
||||
common.OIDCAllowedGroups,
|
||||
)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) TokenCookieName() string {
|
||||
return "godoxy_oidc_token"
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetIsMiddleware(enabled bool) {
|
||||
auth.isMiddleware = enabled
|
||||
auth.oauthConfig.RedirectURL = ""
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||
auth.allowedUsers = users
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||
auth.allowedGroups = groups
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||
token, err := r.Cookie(auth.TokenCookieName())
|
||||
if err != nil {
|
||||
return ErrMissingToken
|
||||
}
|
||||
|
||||
// checks for Expiry, Audience == ClientID, Issuer, etc.
|
||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), token.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify ID token: %w: %w", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
if len(idToken.Audience) == 0 {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return fmt.Errorf("failed to parse claims: %w", err)
|
||||
}
|
||||
|
||||
// Logical AND between allowed users and groups.
|
||||
allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
|
||||
allowedGroup := len(utils.Intersect(claims.Groups, auth.allowedGroups)) > 0
|
||||
if !allowedUser && !allowedGroup {
|
||||
return ErrUserNotAllowed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateState generates a random string for OIDC state.
|
||||
const oidcStateLength = 32
|
||||
|
||||
func generateState() (string, error) {
|
||||
b := make([]byte, oidcStateLength)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength], nil
|
||||
}
|
||||
|
||||
// RedirectOIDC initiates the OIDC login flow.
|
||||
func (auth *OIDCProvider) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := generateState()
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: CookieOauthState,
|
||||
Value: state,
|
||||
MaxAge: 300,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: common.APIJWTSecure,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
redirURL := auth.oauthConfig.AuthCodeURL(state)
|
||||
if auth.isMiddleware {
|
||||
u, err := r.URL.Parse(redirURL)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("redirect_uri", "https://"+r.Host+OIDCMiddlewareCallbackPath+q.Get("redirect_uri"))
|
||||
u.RawQuery = q.Encode()
|
||||
redirURL = u.String()
|
||||
}
|
||||
http.Redirect(w, r, redirURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) exchange(r *http.Request) (*oauth2.Token, error) {
|
||||
if auth.isMiddleware {
|
||||
cfg := *auth.oauthConfig
|
||||
cfg.RedirectURL = "https://" + r.Host + OIDCMiddlewareCallbackPath
|
||||
return cfg.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||
}
|
||||
return auth.oauthConfig.Exchange(r.Context(), r.URL.Query().Get("code"))
|
||||
}
|
||||
|
||||
// OIDCCallbackHandler handles the OIDC callback.
|
||||
func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// For testing purposes, skip provider verification
|
||||
if common.IsTest {
|
||||
auth.handleTestCallback(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, "missing state cookie")
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if query.Get("state") != state.Value {
|
||||
gphttp.BadRequest(w, "invalid oauth state")
|
||||
return
|
||||
}
|
||||
|
||||
oauth2Token, err := auth.exchange(r)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
gphttp.BadRequest(w, "missing id_token")
|
||||
return
|
||||
}
|
||||
|
||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), rawIDToken)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, fmt.Errorf("failed to verify ID token: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
setTokenCookie(w, r, auth.TokenCookieName(), rawIDToken, time.Until(idToken.Expiry))
|
||||
|
||||
// Redirect to home page
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if auth.oidcEndSessionURL == nil {
|
||||
DefaultLogoutCallbackHandler(auth, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := r.Cookie(auth.TokenCookieName())
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, "missing token cookie")
|
||||
return
|
||||
}
|
||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||
|
||||
logoutURL := *auth.oidcEndSessionURL
|
||||
logoutURL.Query().Add("id_token_hint", token.Value)
|
||||
|
||||
http.Redirect(w, r, logoutURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// handleTestCallback handles OIDC callback in test environment.
|
||||
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, "missing state cookie")
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
gphttp.BadRequest(w, "invalid oauth state")
|
||||
return
|
||||
}
|
||||
|
||||
// Create test JWT token
|
||||
setTokenCookie(w, r, auth.TokenCookieName(), "test", time.Hour)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
TokenCookieName() string
|
||||
CheckToken(r *http.Request) error
|
||||
RedirectLoginPage(w http.ResponseWriter, r *http.Request)
|
||||
LoginCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||
LogoutCallbackHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package certapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func fileType(file string) FileType {
|
||||
switch {
|
||||
case strings.HasPrefix(path.Base(file), "config."):
|
||||
return FileTypeConfig
|
||||
case strings.HasPrefix(file, common.MiddlewareComposeDir):
|
||||
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
|
||||
return FileTypeMiddleware
|
||||
}
|
||||
return FileTypeProvider
|
||||
@@ -43,9 +43,9 @@ func (t FileType) IsValid() bool {
|
||||
|
||||
func (t FileType) GetPath(filename string) string {
|
||||
if t == FileTypeMiddleware {
|
||||
return path.Join(common.MiddlewareComposeDir, filename)
|
||||
return path.Join(common.MiddlewareComposeBasePath, filename)
|
||||
}
|
||||
return path.Join(common.ConfigDir, filename)
|
||||
return path.Join(common.ConfigBasePath, filename)
|
||||
}
|
||||
|
||||
func getArgs(r *http.Request) (fileType FileType, filename string, err error) {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
//go:build debug
|
||||
|
||||
package debugapi
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/idlewatcher"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/servemux"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||
"github.com/yusing/go-proxy/internal/proxmox"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
)
|
||||
|
||||
func StartServer(cfg config.ConfigInstance) {
|
||||
srv := server.NewServer(server.Options{
|
||||
Name: "debug",
|
||||
HTTPAddr: "127.0.0.1:7777",
|
||||
Handler: newHandler(cfg),
|
||||
})
|
||||
srv.Start(task.RootTask("debug_server", false))
|
||||
}
|
||||
|
||||
type debuggable interface {
|
||||
MarshalMap() map[string]any
|
||||
Key() string
|
||||
}
|
||||
|
||||
func toSortedSlice[T debuggable](data iter.Seq2[string, T]) []map[string]any {
|
||||
s := make([]map[string]any, 0)
|
||||
for _, v := range data {
|
||||
m := v.MarshalMap()
|
||||
m["key"] = v.Key()
|
||||
s = append(s, m)
|
||||
}
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
return s[i]["key"].(string) < s[j]["key"].(string)
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
func jsonHandler[T debuggable](getData iter.Seq2[string, T]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gpwebsocket.DynamicJSONHandler(w, r, func() []map[string]any {
|
||||
return toSortedSlice(getData)
|
||||
}, 200*time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func iterMap[K comparable, V debuggable](m func() map[K]V) iter.Seq2[K, V] {
|
||||
return func(yield func(K, V) bool) {
|
||||
for k, v := range m() {
|
||||
if !yield(k, v) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux := servemux.NewServeMux(cfg)
|
||||
mux.HandleFunc("GET", "/tasks", jsonHandler(task.AllTasks()))
|
||||
mux.HandleFunc("GET", "/idlewatcher", jsonHandler(idlewatcher.Watchers()))
|
||||
mux.HandleFunc("GET", "/agents", jsonHandler(agent.Agents.Iter))
|
||||
mux.HandleFunc("GET", "/proxmox", jsonHandler(proxmox.Clients.Iter))
|
||||
mux.HandleFunc("GET", "/docker", jsonHandler(iterMap(docker.Clients)))
|
||||
return mux
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !debug
|
||||
|
||||
package debugapi
|
||||
|
||||
import (
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
)
|
||||
|
||||
func StartServer(cfg config.ConfigInstance) {
|
||||
// do nothing
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type Container struct {
|
||||
}
|
||||
|
||||
func Containers(w http.ResponseWriter, r *http.Request) {
|
||||
serveHTTP[Container](w, r, GetContainers)
|
||||
serveHTTP[Container, []Container](w, r, GetContainers)
|
||||
}
|
||||
|
||||
func GetContainers(ctx context.Context, dockerClients DockerClients) ([]Container, gperr.Error) {
|
||||
|
||||
@@ -2,11 +2,10 @@ package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
dockerSystem "github.com/docker/docker/api/types/system"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
@@ -14,8 +13,8 @@ import (
|
||||
|
||||
type dockerInfo dockerSystem.Info
|
||||
|
||||
func (d *dockerInfo) MarshalJSONTo(buf []byte) []byte {
|
||||
return json.MarshalTo(map[string]any{
|
||||
func (d *dockerInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"name": d.Name,
|
||||
"version": d.ServerVersion,
|
||||
"containers": map[string]int{
|
||||
@@ -27,7 +26,7 @@ func (d *dockerInfo) MarshalJSONTo(buf []byte) []byte {
|
||||
"images": d.Images,
|
||||
"n_cpu": d.NCPU,
|
||||
"memory": strutils.FormatByteSize(d.MemTotal),
|
||||
}, buf)
|
||||
})
|
||||
}
|
||||
|
||||
func DockerInfo(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package dockerapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -9,20 +10,19 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
func Logs(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
server := r.PathValue("server")
|
||||
containerID := r.PathValue("container")
|
||||
stdout := strutils.ParseBool(query.Get("stdout"))
|
||||
stderr := strutils.ParseBool(query.Get("stderr"))
|
||||
stdout, _ := strconv.ParseBool(query.Get("stdout"))
|
||||
stderr, _ := strconv.ParseBool(query.Get("stderr"))
|
||||
since := query.Get("from")
|
||||
until := query.Get("to")
|
||||
levels := query.Get("levels") // TODO: implement levels
|
||||
|
||||
dockerClient, found, err := getDockerClient(server)
|
||||
dockerClient, found, err := getDockerClient(w, server)
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, err.Error())
|
||||
return
|
||||
|
||||
@@ -2,14 +2,12 @@ package dockerapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
@@ -46,7 +44,7 @@ func getDockerClients() (DockerClients, gperr.Error) {
|
||||
dockerClients[name] = dockerClient
|
||||
}
|
||||
|
||||
for _, agent := range agent.Agents.Iter {
|
||||
for _, agent := range cfg.ListAgents() {
|
||||
dockerClient, err := docker.NewClient(agent.FakeDockerHost())
|
||||
if err != nil {
|
||||
connErrs.Add(err)
|
||||
@@ -58,7 +56,7 @@ func getDockerClients() (DockerClients, gperr.Error) {
|
||||
return dockerClients, connErrs.Error()
|
||||
}
|
||||
|
||||
func getDockerClient(server string) (*docker.SharedClient, bool, error) {
|
||||
func getDockerClient(w http.ResponseWriter, server string) (*docker.SharedClient, bool, error) {
|
||||
cfg := config.GetInstance()
|
||||
var host string
|
||||
for name, h := range cfg.Value().Providers.Docker {
|
||||
@@ -67,7 +65,7 @@ func getDockerClient(server string) (*docker.SharedClient, bool, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, agent := range agent.Agents.Iter {
|
||||
for _, agent := range cfg.ListAgents() {
|
||||
if agent.Name() == server {
|
||||
host = agent.FakeDockerHost()
|
||||
break
|
||||
@@ -121,6 +119,6 @@ func serveHTTP[V any, T ResultType[V]](w http.ResponseWriter, r *http.Request, g
|
||||
})
|
||||
} else {
|
||||
result, err := getResult(r.Context(), dockerClients)
|
||||
handleResult[V](w, err, result)
|
||||
handleResult[V, T](w, err, result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
gphttp.ClientError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
fetchResult := homepage.FetchFavIconFromURL(&iconURL)
|
||||
fetchResult := homepage.FetchFavIconFromURL(req.Context(), &iconURL)
|
||||
if !fetchResult.OK() {
|
||||
http.Error(w, fetchResult.ErrMsg, fetchResult.StatusCode)
|
||||
return
|
||||
@@ -47,7 +47,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// try with route.Icon
|
||||
r, ok := routes.GetHTTPRoute(alias)
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
gphttp.ClientError(w, errors.New("no such route"), http.StatusNotFound)
|
||||
return
|
||||
@@ -59,7 +59,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result = homepage.FindIcon(req.Context(), r, hp.Icon.Value)
|
||||
} else {
|
||||
result = homepage.FetchFavIconFromURL(hp.Icon)
|
||||
result = homepage.FetchFavIconFromURL(req.Context(), hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
|
||||
@@ -4,10 +4,20 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
)
|
||||
|
||||
func Health(w http.ResponseWriter, r *http.Request) {
|
||||
gpwebsocket.DynamicJSONHandler(w, r, routequery.HealthMap, 1*time.Second)
|
||||
if httpheaders.IsWebsocket(r.Header) {
|
||||
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||
return wsjson.Write(r.Context(), conn, routes.HealthMap())
|
||||
})
|
||||
} else {
|
||||
gphttp.RespondJSON(w, r, routes.HealthMap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/go-proxy/internal/route/routes/routequery"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ const (
|
||||
ListRouteProviders = "route_providers"
|
||||
ListHomepageCategories = "homepage_categories"
|
||||
ListIcons = "icons"
|
||||
ListTasks = "tasks"
|
||||
)
|
||||
|
||||
func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -45,7 +47,7 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
gphttp.RespondJSON(w, r, route)
|
||||
}
|
||||
case ListRoutes:
|
||||
gphttp.RespondJSON(w, r, routequery.RoutesByAlias(route.RouteType(r.FormValue("type"))))
|
||||
gphttp.RespondJSON(w, r, routes.ByAlias(route.RouteType(r.FormValue("type"))))
|
||||
case ListFiles:
|
||||
listFiles(w, r)
|
||||
case ListMiddlewares:
|
||||
@@ -55,17 +57,17 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
case ListMatchDomains:
|
||||
gphttp.RespondJSON(w, r, cfg.Value().MatchDomains)
|
||||
case ListHomepageConfig:
|
||||
gphttp.RespondJSON(w, r, routequery.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
||||
gphttp.RespondJSON(w, r, routes.HomepageConfig(r.FormValue("category"), r.FormValue("provider")))
|
||||
case ListRouteProviders:
|
||||
gphttp.RespondJSON(w, r, cfg.RouteProviderList())
|
||||
case ListHomepageCategories:
|
||||
gphttp.RespondJSON(w, r, routequery.HomepageCategories())
|
||||
gphttp.RespondJSON(w, r, routes.HomepageCategories())
|
||||
case ListIcons:
|
||||
limit, err := strconv.Atoi(r.FormValue("limit"))
|
||||
if err != nil {
|
||||
limit = 0
|
||||
}
|
||||
icons, err := homepage.SearchIcons(r.FormValue("keyword"), limit)
|
||||
icons, err := internal.SearchIcons(r.FormValue("keyword"), limit)
|
||||
if err != nil {
|
||||
gphttp.ClientError(w, err)
|
||||
return
|
||||
@@ -74,6 +76,8 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
icons = []string{}
|
||||
}
|
||||
gphttp.RespondJSON(w, r, icons)
|
||||
case ListTasks:
|
||||
gphttp.RespondJSON(w, r, task.DebugTaskList())
|
||||
default:
|
||||
gphttp.BadRequest(w, fmt.Sprintf("invalid what: %s", what))
|
||||
}
|
||||
@@ -83,9 +87,9 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
// otherwise, return a single Route with alias which or nil if not found.
|
||||
func listRoute(which string) any {
|
||||
if which == "" || which == "all" {
|
||||
return routequery.RoutesByAlias()
|
||||
return routes.ByAlias()
|
||||
}
|
||||
routes := routequery.RoutesByAlias()
|
||||
routes := routes.ByAlias()
|
||||
route, ok := routes[which]
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -94,7 +98,7 @@ func listRoute(which string) any {
|
||||
}
|
||||
|
||||
func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := utils.ListFiles(common.ConfigDir, 0, true)
|
||||
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
@@ -107,17 +111,17 @@ func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
for _, file := range files {
|
||||
t := fileType(file)
|
||||
file = strings.TrimPrefix(file, common.ConfigDir+"/")
|
||||
file = strings.TrimPrefix(file, common.ConfigBasePath+"/")
|
||||
resp[t] = append(resp[t], file)
|
||||
}
|
||||
|
||||
mids, err := utils.ListFiles(common.MiddlewareComposeDir, 0, true)
|
||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
for _, mid := range mids {
|
||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeDir+"/")
|
||||
mid = strings.TrimPrefix(mid, common.MiddlewareComposeBasePath+"/")
|
||||
resp[FileTypeMiddleware] = append(resp[FileTypeMiddleware], mid)
|
||||
}
|
||||
gphttp.RespondJSON(w, r, resp)
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/agent/pkg/certs"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -41,7 +39,7 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
hostport := fmt.Sprintf("%s:%d", host, port)
|
||||
if _, ok := agent.Agents.Get(hostport); ok {
|
||||
if _, ok := config.GetInstance().GetAgent(hostport); ok {
|
||||
gphttp.ClientError(w, gphttp.ErrAlreadyExists("agent", hostport), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
@@ -57,7 +55,7 @@ func NewAgent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nightly := strutils.ParseBool(q.Get("nightly"))
|
||||
nightly, _ := strconv.ParseBool(q.Get("nightly"))
|
||||
var image string
|
||||
if nightly {
|
||||
image = agent.DockerImageNightly
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
v1 "github.com/yusing/go-proxy/internal/api/v1"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
@@ -59,3 +58,7 @@ func ListRoutes() (map[string]map[string]any, gperr.Error) {
|
||||
func ListMiddlewareTraces() (middleware.Traces, gperr.Error) {
|
||||
return List[middleware.Traces](v1.ListMiddlewareTraces)
|
||||
}
|
||||
|
||||
func DebugListTasks() (map[string]any, gperr.Error) {
|
||||
return List[map[string]any](v1.ListTasks)
|
||||
}
|
||||
|
||||
@@ -4,18 +4,30 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/gpwebsocket"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
func Stats(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
gpwebsocket.DynamicJSONHandler(w, r, func() map[string]any {
|
||||
return map[string]any{
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": strutils.FormatDuration(time.Since(startTime)),
|
||||
}
|
||||
}, 1*time.Second)
|
||||
if httpheaders.IsWebsocket(r.Header) {
|
||||
gpwebsocket.Periodic(w, r, 1*time.Second, func(conn *websocket.Conn) error {
|
||||
return wsjson.Write(r.Context(), conn, getStats(cfg))
|
||||
})
|
||||
} else {
|
||||
gphttp.RespondJSON(w, r, getStats(cfg))
|
||||
}
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func getStats(cfg config.ConfigInstance) map[string]any {
|
||||
return map[string]any{
|
||||
"proxies": cfg.Statistics(),
|
||||
"uptime": strutils.FormatDuration(time.Since(startTime)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package v1
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
agentPkg "github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
|
||||
)
|
||||
|
||||
func SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
func SystemInfo(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
agentAddr := query.Get("agent_addr")
|
||||
query.Del("agent_addr")
|
||||
@@ -21,7 +21,7 @@ func SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
agent, ok := agent.Agents.Get(agentAddr)
|
||||
agent, ok := cfg.GetAgent(agentAddr)
|
||||
if !ok {
|
||||
gphttp.NotFound(w, "agent_addr")
|
||||
return
|
||||
|
||||
@@ -8,5 +8,5 @@ import (
|
||||
)
|
||||
|
||||
func GetVersion(w http.ResponseWriter, r *http.Request) {
|
||||
gphttp.WriteBody(w, []byte(pkg.GetVersion().String()))
|
||||
gphttp.WriteBody(w, []byte(pkg.GetVersion()))
|
||||
}
|
||||
|
||||
@@ -50,3 +50,11 @@ func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func AuthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err := defaultAuth.CheckToken(r); err != nil {
|
||||
http.Redirect(w, r, "/v1/auth/login", http.StatusFound)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
185
internal/auth/oauth_refresh.go
Normal file
185
internal/auth/oauth_refresh.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type oauthRefreshToken struct {
|
||||
Username string `json:"username"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Expiry time.Time `json:"expiry"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
SessionID sessionID `json:"session_id"`
|
||||
Username string `json:"username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
type sessionClaims struct {
|
||||
Session
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type sessionID string
|
||||
|
||||
var oauthRefreshTokens jsonstore.MapStore[oauthRefreshToken]
|
||||
|
||||
var (
|
||||
defaultRefreshTokenExpiry = 30 * 24 * time.Hour // 1 month
|
||||
refreshBefore = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errNoRefreshToken = errors.New("no refresh token")
|
||||
ErrRefreshTokenFailure = errors.New("failed to refresh token")
|
||||
)
|
||||
|
||||
const sessionTokenIssuer = "GoDoxy"
|
||||
|
||||
func init() {
|
||||
if IsOIDCEnabled() {
|
||||
oauthRefreshTokens = jsonstore.Store[oauthRefreshToken]("oauth_refresh_tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func (token *oauthRefreshToken) expired() bool {
|
||||
return time.Now().After(token.Expiry)
|
||||
}
|
||||
|
||||
func newSessionID() sessionID {
|
||||
b := make([]byte, 32)
|
||||
_, _ = rand.Read(b)
|
||||
return sessionID(base64.StdEncoding.EncodeToString(b))
|
||||
}
|
||||
|
||||
func newSession(username string, groups []string) Session {
|
||||
return Session{
|
||||
SessionID: newSessionID(),
|
||||
Username: username,
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
// getOnceOAuthRefreshToken returns the refresh token for the given session.
|
||||
//
|
||||
// The token is removed from the store after retrieval.
|
||||
func getOnceOAuthRefreshToken(claims *Session) (*oauthRefreshToken, bool) {
|
||||
token, ok := oauthRefreshTokens.Load(string(claims.SessionID))
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
invalidateOAuthRefreshToken(claims.SessionID)
|
||||
if token.expired() {
|
||||
return nil, false
|
||||
}
|
||||
if claims.Username != token.Username {
|
||||
return nil, false
|
||||
}
|
||||
return &token, true
|
||||
}
|
||||
|
||||
func storeOAuthRefreshToken(sessionID sessionID, username, token string) {
|
||||
oauthRefreshTokens.Store(string(sessionID), oauthRefreshToken{
|
||||
Username: username,
|
||||
RefreshToken: token,
|
||||
Expiry: time.Now().Add(defaultRefreshTokenExpiry),
|
||||
})
|
||||
logging.Debug().Str("username", username).Msg("stored oauth refresh token")
|
||||
}
|
||||
|
||||
func invalidateOAuthRefreshToken(sessionID sessionID) {
|
||||
logging.Debug().Str("session_id", string(sessionID)).Msg("invalidating oauth refresh token")
|
||||
oauthRefreshTokens.Delete(string(sessionID))
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) setSessionTokenCookie(w http.ResponseWriter, r *http.Request, session Session) {
|
||||
claims := &sessionClaims{
|
||||
Session: session,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: sessionTokenIssuer,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(common.APIJWTTokenTTL)),
|
||||
},
|
||||
}
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||
signed, err := jwtToken.SignedString(common.APIJWTSecret)
|
||||
if err != nil {
|
||||
logging.Err(err).Msg("failed to sign session token")
|
||||
return
|
||||
}
|
||||
setTokenCookie(w, r, CookieOauthSessionToken, signed, common.APIJWTTokenTTL)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) parseSessionJWT(sessionJWT string) (claims *sessionClaims, valid bool, err error) {
|
||||
claims = &sessionClaims{}
|
||||
sessionToken, err := jwt.ParseWithClaims(sessionJWT, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return common.APIJWTSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return claims, sessionToken.Valid && claims.Issuer == sessionTokenIssuer, nil
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) TryRefreshToken(w http.ResponseWriter, r *http.Request, sessionJWT string) error {
|
||||
// verify the session cookie
|
||||
claims, valid, err := auth.parseSessionJWT(sessionJWT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidSessionToken, err)
|
||||
}
|
||||
if !valid {
|
||||
return ErrInvalidSessionToken
|
||||
}
|
||||
|
||||
// check if refresh is possible
|
||||
refreshToken, ok := getOnceOAuthRefreshToken(&claims.Session)
|
||||
if !ok {
|
||||
return errNoRefreshToken
|
||||
}
|
||||
|
||||
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||
return ErrUserNotAllowed
|
||||
}
|
||||
|
||||
// this step refreshes the token
|
||||
// see https://cs.opensource.google/go/x/oauth2/+/refs/tags/v0.29.0:oauth2.go;l=313
|
||||
newToken, err := auth.oauthConfig.TokenSource(r.Context(), &oauth2.Token{
|
||||
RefreshToken: refreshToken.RefreshToken,
|
||||
}).Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrRefreshTokenFailure, err)
|
||||
}
|
||||
|
||||
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), newToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionID := newSessionID()
|
||||
|
||||
logging.Debug().Str("username", claims.Username).Time("expiry", newToken.Expiry).Msg("refreshed token")
|
||||
storeOAuthRefreshToken(sessionID, claims.Username, newToken.RefreshToken)
|
||||
|
||||
// set new idToken and new sessionToken
|
||||
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
|
||||
auth.setSessionTokenCookie(w, r, Session{
|
||||
SessionID: sessionID,
|
||||
Username: claims.Username,
|
||||
Groups: claims.Groups,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
314
internal/auth/oidc.go
Normal file
314
internal/auth/oidc.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type (
|
||||
OIDCProvider struct {
|
||||
oauthConfig *oauth2.Config
|
||||
oidcProvider *oidc.Provider
|
||||
oidcVerifier *oidc.IDTokenVerifier
|
||||
endSessionURL *url.URL
|
||||
allowedUsers []string
|
||||
allowedGroups []string
|
||||
}
|
||||
|
||||
IDTokenClaims struct {
|
||||
Username string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
CookieOauthState = "godoxy_oidc_state"
|
||||
CookieOauthSessionID = "godoxy_session_id"
|
||||
CookieOauthToken = "godoxy_oauth_token"
|
||||
CookieOauthSessionToken = "godoxy_session_token"
|
||||
)
|
||||
|
||||
const (
|
||||
OIDCAuthInitPath = "/"
|
||||
OIDCPostAuthPath = "/auth/callback"
|
||||
OIDCLogoutPath = "/auth/logout"
|
||||
)
|
||||
|
||||
var errMissingIDToken = errors.New("missing id_token field from oauth token")
|
||||
|
||||
// generateState generates a random string for OIDC state.
|
||||
const oidcStateLength = 32
|
||||
|
||||
func generateState() string {
|
||||
b := make([]byte, oidcStateLength)
|
||||
_, _ = rand.Read(b)
|
||||
return base64.URLEncoding.EncodeToString(b)[:oidcStateLength]
|
||||
}
|
||||
|
||||
func NewOIDCProvider(issuerURL, clientID, clientSecret string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
|
||||
if len(allowedUsers)+len(allowedGroups) == 0 {
|
||||
return nil, errors.New("OIDC users, groups, or both must not be empty")
|
||||
}
|
||||
provider, err := oidc.NewProvider(context.Background(), issuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
|
||||
}
|
||||
|
||||
endSessionURL, err := url.Parse(provider.EndSessionEndpoint())
|
||||
if err != nil && provider.EndSessionEndpoint() != "" {
|
||||
// non critical, just warn
|
||||
logging.Warn().
|
||||
Str("issuer", issuerURL).
|
||||
Err(err).
|
||||
Msg("failed to parse end session URL")
|
||||
}
|
||||
|
||||
return &OIDCProvider{
|
||||
oauthConfig: &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: "",
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: strutils.CommaSeperatedList(common.OIDCScopes),
|
||||
},
|
||||
oidcProvider: provider,
|
||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||
ClientID: clientID,
|
||||
}),
|
||||
endSessionURL: endSessionURL,
|
||||
allowedUsers: allowedUsers,
|
||||
allowedGroups: allowedGroups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewOIDCProviderFromEnv creates a new OIDCProvider from environment variables.
|
||||
func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
|
||||
return NewOIDCProvider(
|
||||
common.OIDCIssuerURL,
|
||||
common.OIDCClientID,
|
||||
common.OIDCClientSecret,
|
||||
common.OIDCAllowedUsers,
|
||||
common.OIDCAllowedGroups,
|
||||
)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedUsers(users []string) {
|
||||
auth.allowedUsers = users
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) SetAllowedGroups(groups []string) {
|
||||
auth.allowedGroups = groups
|
||||
}
|
||||
|
||||
// optRedirectPostAuth returns an oauth2 option that sets the "redirect_uri"
|
||||
// parameter of the authorization URL to the post auth path of the current
|
||||
// request host.
|
||||
func optRedirectPostAuth(r *http.Request) oauth2.AuthCodeOption {
|
||||
return oauth2.SetAuthURLParam("redirect_uri", "https://"+requestHost(r)+OIDCPostAuthPath)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) getIdToken(ctx context.Context, oauthToken *oauth2.Token) (string, *oidc.IDToken, error) {
|
||||
idTokenJWT, ok := oauthToken.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return "", nil, errMissingIDToken
|
||||
}
|
||||
idToken, err := auth.oidcVerifier.Verify(ctx, idTokenJWT)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||
}
|
||||
return idTokenJWT, idToken, nil
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) HandleAuth(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case OIDCAuthInitPath:
|
||||
auth.LoginHandler(w, r)
|
||||
case OIDCPostAuthPath:
|
||||
auth.PostAuthCallbackHandler(w, r)
|
||||
case OIDCLogoutPath:
|
||||
auth.LogoutHandler(w, r)
|
||||
default:
|
||||
http.Redirect(w, r, OIDCAuthInitPath, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// check for session token
|
||||
sessionToken, err := r.Cookie(CookieOauthSessionToken)
|
||||
if err == nil {
|
||||
err = auth.TryRefreshToken(w, r, sessionToken.Value)
|
||||
if err != nil {
|
||||
logging.Debug().Err(err).Msg("failed to refresh token")
|
||||
auth.clearCookie(w, r)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
state := generateState()
|
||||
setTokenCookie(w, r, CookieOauthState, state, 300*time.Second)
|
||||
// redirect user to Idp
|
||||
http.Redirect(w, r, auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r)), http.StatusFound)
|
||||
}
|
||||
|
||||
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {
|
||||
var claim IDTokenClaims
|
||||
if err := idToken.Claims(&claim); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse claims: %w", err)
|
||||
}
|
||||
if claim.Username == "" {
|
||||
return nil, fmt.Errorf("missing username in ID token")
|
||||
}
|
||||
return &claim, nil
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) checkAllowed(user string, groups []string) bool {
|
||||
userAllowed := slices.Contains(auth.allowedUsers, user)
|
||||
if !userAllowed {
|
||||
return false
|
||||
}
|
||||
if len(auth.allowedGroups) == 0 {
|
||||
return true
|
||||
}
|
||||
return len(utils.Intersect(groups, auth.allowedGroups)) > 0
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) CheckToken(r *http.Request) error {
|
||||
tokenCookie, err := r.Cookie(CookieOauthToken)
|
||||
if err != nil {
|
||||
return ErrMissingOAuthToken
|
||||
}
|
||||
|
||||
idToken, err := auth.oidcVerifier.Verify(r.Context(), tokenCookie.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||
}
|
||||
|
||||
claims, err := parseClaims(idToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidOAuthToken, err)
|
||||
}
|
||||
|
||||
if !auth.checkAllowed(claims.Username, claims.Groups) {
|
||||
return ErrUserNotAllowed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// For testing purposes, skip provider verification
|
||||
if common.IsTest {
|
||||
auth.handleTestCallback(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// verify state
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, "missing state cookie")
|
||||
return
|
||||
}
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
gphttp.BadRequest(w, "invalid oauth state")
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
oauth2Token, err := auth.oauthConfig.Exchange(r.Context(), code, optRedirectPostAuth(r))
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, fmt.Errorf("failed to exchange token: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
idTokenJWT, idToken, err := auth.getIdToken(r.Context(), oauth2Token)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if oauth2Token.RefreshToken != "" {
|
||||
claims, err := parseClaims(idToken)
|
||||
if err != nil {
|
||||
gphttp.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
session := newSession(claims.Username, claims.Groups)
|
||||
storeOAuthRefreshToken(session.SessionID, claims.Username, oauth2Token.RefreshToken)
|
||||
auth.setSessionTokenCookie(w, r, session)
|
||||
}
|
||||
auth.setIDTokenCookie(w, r, idTokenJWT, time.Until(idToken.Expiry))
|
||||
|
||||
// Redirect to home page
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
oauthToken, _ := r.Cookie(CookieOauthToken)
|
||||
sessionToken, _ := r.Cookie(CookieOauthSessionToken)
|
||||
auth.clearCookie(w, r)
|
||||
|
||||
if sessionToken != nil {
|
||||
claims, _, err := auth.parseSessionJWT(sessionToken.Value)
|
||||
if err == nil {
|
||||
invalidateOAuthRefreshToken(claims.SessionID)
|
||||
}
|
||||
}
|
||||
|
||||
url := "/"
|
||||
if auth.endSessionURL != nil && oauthToken != nil {
|
||||
query := auth.endSessionURL.Query()
|
||||
query.Set("id_token_hint", oauthToken.Value)
|
||||
query.Set("post_logout_redirect_uri", "https://"+requestHost(r))
|
||||
|
||||
clone := *auth.endSessionURL
|
||||
clone.RawQuery = query.Encode()
|
||||
url = clone.String()
|
||||
} else if auth.endSessionURL != nil {
|
||||
url = auth.endSessionURL.String()
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) setIDTokenCookie(w http.ResponseWriter, r *http.Request, jwt string, ttl time.Duration) {
|
||||
setTokenCookie(w, r, CookieOauthToken, jwt, ttl)
|
||||
}
|
||||
|
||||
func (auth *OIDCProvider) clearCookie(w http.ResponseWriter, r *http.Request) {
|
||||
clearTokenCookie(w, r, CookieOauthToken)
|
||||
clearTokenCookie(w, r, CookieOauthSessionToken)
|
||||
}
|
||||
|
||||
// handleTestCallback handles OIDC callback in test environment.
|
||||
func (auth *OIDCProvider) handleTestCallback(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := r.Cookie(CookieOauthState)
|
||||
if err != nil {
|
||||
gphttp.BadRequest(w, "missing state cookie")
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != state.Value {
|
||||
gphttp.BadRequest(w, "invalid oauth state")
|
||||
return
|
||||
}
|
||||
|
||||
// Create test JWT token
|
||||
setTokenCookie(w, r, CookieOauthToken, "test", time.Hour)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -36,7 +36,8 @@ func setupMockOIDC(t *testing.T) {
|
||||
},
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
},
|
||||
oidcProvider: provider,
|
||||
endSessionURL: Must(url.Parse("http://mock-provider/logout")),
|
||||
oidcProvider: provider,
|
||||
oidcVerifier: provider.Verifier(&oidc.Config{
|
||||
ClientID: "test-client",
|
||||
}),
|
||||
@@ -149,17 +150,17 @@ func TestOIDCLoginHandler(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Success - Redirects to provider",
|
||||
wantStatus: http.StatusTemporaryRedirect,
|
||||
wantStatus: http.StatusFound,
|
||||
wantRedirect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/redirect", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, OIDCAuthInitPath, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
defaultAuth.RedirectLoginPage(w, req)
|
||||
defaultAuth.(*OIDCProvider).HandleAuth(w, req)
|
||||
|
||||
if got := w.Code; got != tt.wantStatus {
|
||||
t.Errorf("OIDCLoginHandler() status = %v, want %v", got, tt.wantStatus)
|
||||
@@ -195,7 +196,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||
state: "valid-state",
|
||||
code: "valid-code",
|
||||
setupMocks: true,
|
||||
wantStatus: http.StatusTemporaryRedirect,
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
name: "Failure - Missing state",
|
||||
@@ -220,7 +221,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
defaultAuth.LoginCallbackHandler(w, req)
|
||||
defaultAuth.(*OIDCProvider).PostAuthCallbackHandler(w, req)
|
||||
|
||||
if got := w.Code; got != tt.wantStatus {
|
||||
t.Errorf("OIDCCallbackHandler() status = %v, want %v", got, tt.wantStatus)
|
||||
@@ -228,7 +229,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||
|
||||
if tt.wantStatus == http.StatusTemporaryRedirect {
|
||||
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
||||
ExpectEqual(t, setCookie.Name, CookieOauthToken)
|
||||
ExpectTrue(t, setCookie.Value != "")
|
||||
ExpectEqual(t, setCookie.Path, "/")
|
||||
ExpectEqual(t, setCookie.SameSite, http.SameSiteLaxMode)
|
||||
@@ -271,7 +272,6 @@ func TestInitOIDC(t *testing.T) {
|
||||
issuerURL: server.URL,
|
||||
clientID: "client_id",
|
||||
clientSecret: "client_secret",
|
||||
redirectURL: "https://example.com/callback",
|
||||
allowedUsers: []string{"user1", "user2"},
|
||||
wantErr: false,
|
||||
},
|
||||
@@ -280,7 +280,6 @@ func TestInitOIDC(t *testing.T) {
|
||||
issuerURL: server.URL,
|
||||
clientID: "client_id",
|
||||
clientSecret: "client_secret",
|
||||
redirectURL: "https://example.com/callback",
|
||||
allowedGroups: []string{"group1", "group2"},
|
||||
wantErr: false,
|
||||
},
|
||||
@@ -289,7 +288,6 @@ func TestInitOIDC(t *testing.T) {
|
||||
issuerURL: server.URL,
|
||||
clientID: "client_id",
|
||||
clientSecret: "client_secret",
|
||||
redirectURL: "https://example.com/callback",
|
||||
logoutURL: "https://example.com/logout",
|
||||
allowedUsers: []string{"user1", "user2"},
|
||||
allowedGroups: []string{"group1", "group2"},
|
||||
@@ -300,14 +298,13 @@ func TestInitOIDC(t *testing.T) {
|
||||
issuerURL: "https://example.com",
|
||||
clientID: "client_id",
|
||||
clientSecret: "client_secret",
|
||||
redirectURL: "https://example.com/callback",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.redirectURL, tt.allowedUsers, tt.allowedGroups)
|
||||
_, err := NewOIDCProvider(tt.issuerURL, tt.clientID, tt.clientSecret, tt.allowedUsers, tt.allowedGroups)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("InitOIDC() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
@@ -401,7 +398,7 @@ func TestCheckToken(t *testing.T) {
|
||||
"preferred_username": "user1",
|
||||
"groups": []string{"group1"},
|
||||
},
|
||||
wantErr: ErrInvalidToken,
|
||||
wantErr: ErrInvalidOAuthToken,
|
||||
},
|
||||
{
|
||||
name: "Error - Server returns incorrect audience",
|
||||
@@ -412,7 +409,7 @@ func TestCheckToken(t *testing.T) {
|
||||
"preferred_username": "user1",
|
||||
"groups": []string{"group1"},
|
||||
},
|
||||
wantErr: ErrInvalidToken,
|
||||
wantErr: ErrInvalidOAuthToken,
|
||||
},
|
||||
{
|
||||
name: "Error - Server returns expired token",
|
||||
@@ -423,7 +420,7 @@ func TestCheckToken(t *testing.T) {
|
||||
"preferred_username": "user1",
|
||||
"groups": []string{"group1"},
|
||||
},
|
||||
wantErr: ErrInvalidToken,
|
||||
wantErr: ErrInvalidOAuthToken,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
@@ -439,7 +436,7 @@ func TestCheckToken(t *testing.T) {
|
||||
// Craft a test HTTP request that includes the token as a cookie.
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: auth.TokenCookieName(),
|
||||
Name: CookieOauthToken,
|
||||
Value: signedToken,
|
||||
})
|
||||
|
||||
@@ -453,3 +450,35 @@ func TestCheckToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogoutHandler(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
setupMockOIDC(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, OIDCLogoutPath, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: CookieOauthToken,
|
||||
Value: "test-token",
|
||||
})
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: CookieOauthSessionToken,
|
||||
Value: "test-session-token",
|
||||
})
|
||||
|
||||
defaultAuth.(*OIDCProvider).LogoutHandler(w, req)
|
||||
|
||||
if got := w.Code; got != http.StatusFound {
|
||||
t.Errorf("LogoutHandler() status = %v, want %v", got, http.StatusFound)
|
||||
}
|
||||
|
||||
if got := w.Header().Get("Location"); got == "" {
|
||||
t.Error("LogoutHandler() missing redirect location")
|
||||
}
|
||||
|
||||
if len(w.Header().Values("Set-Cookie")) != 2 {
|
||||
t.Error("LogoutHandler() did not clear all cookies")
|
||||
}
|
||||
}
|
||||
10
internal/auth/provider.go
Normal file
10
internal/auth/provider.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package auth
|
||||
|
||||
import "net/http"
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
@@ -77,7 +76,7 @@ func (auth *UserPassAuth) NewToken() (token string, err error) {
|
||||
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
||||
if err != nil {
|
||||
return ErrMissingToken
|
||||
return ErrMissingSessionToken
|
||||
}
|
||||
var claims UserPassClaims
|
||||
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
||||
@@ -91,7 +90,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||
}
|
||||
switch {
|
||||
case !token.Valid:
|
||||
return ErrInvalidToken
|
||||
return ErrInvalidSessionToken
|
||||
case claims.Username != auth.username:
|
||||
return ErrUserNotAllowed.Subject(claims.Username)
|
||||
case claims.ExpiresAt.Before(time.Now()):
|
||||
@@ -101,11 +100,7 @@ func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) RedirectLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var creds struct {
|
||||
User string `json:"username"`
|
||||
Pass string `json:"password"`
|
||||
@@ -128,8 +123,13 @@ func (auth *UserPassAuth) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
DefaultLogoutCallbackHandler(auth, w, r)
|
||||
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusFound) // redirects to WebUI login page
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
||||
@@ -2,14 +2,13 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -99,7 +98,7 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
|
||||
Host: "app.example.com",
|
||||
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
||||
}
|
||||
auth.LoginCallbackHandler(w, req)
|
||||
auth.LoginHandler(w, req)
|
||||
if tt.wantErr {
|
||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||
} else {
|
||||
@@ -1,7 +1,6 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -11,25 +10,34 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingToken = gperr.New("missing token")
|
||||
ErrInvalidToken = gperr.New("invalid token")
|
||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||
ErrMissingOAuthToken = gperr.New("missing oauth token")
|
||||
ErrMissingSessionToken = gperr.New("missing session token")
|
||||
ErrInvalidOAuthToken = gperr.New("invalid oauth token")
|
||||
ErrInvalidSessionToken = gperr.New("invalid session token")
|
||||
ErrUserNotAllowed = gperr.New("user not allowed")
|
||||
)
|
||||
|
||||
// cookieFQDN returns the fully qualified domain name of the request host
|
||||
func requestHost(r *http.Request) string {
|
||||
// check if it's from backend
|
||||
switch r.Host {
|
||||
case common.APIHTTPAddr:
|
||||
// use XFH
|
||||
return r.Header.Get("X-Forwarded-Host")
|
||||
default:
|
||||
return r.Host
|
||||
}
|
||||
}
|
||||
|
||||
// cookieDomain returns the fully qualified domain name of the request host
|
||||
// with subdomain stripped.
|
||||
//
|
||||
// If the request host does not have a subdomain,
|
||||
// an empty string is returned
|
||||
//
|
||||
// "abc.example.com" -> "example.com"
|
||||
// "example.com" -> ""
|
||||
func cookieFQDN(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
parts := strutils.SplitRune(host, '.')
|
||||
// "abc.example.com" -> ".example.com" (cross subdomain)
|
||||
// "example.com" -> "" (same domain only)
|
||||
func cookieDomain(r *http.Request) string {
|
||||
parts := strutils.SplitRune(requestHost(r), '.')
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
@@ -42,7 +50,7 @@ func setTokenCookie(w http.ResponseWriter, r *http.Request, name, value string,
|
||||
Name: name,
|
||||
Value: value,
|
||||
MaxAge: int(ttl.Seconds()),
|
||||
Domain: cookieFQDN(r),
|
||||
Domain: cookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: common.APIJWTSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
@@ -55,16 +63,10 @@ func clearTokenCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||
Name: name,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: cookieFQDN(r),
|
||||
Domain: cookieDomain(r),
|
||||
HttpOnly: true,
|
||||
Secure: common.APIJWTSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Path: "/",
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultLogoutCallbackHandler clears the token cookie and redirects to the login page..
|
||||
func DefaultLogoutCallbackHandler(auth Provider, w http.ResponseWriter, r *http.Request) {
|
||||
clearTokenCookie(w, r, auth.TokenCookieName())
|
||||
auth.RedirectLoginPage(w, r)
|
||||
}
|
||||
@@ -64,11 +64,11 @@ func (cfg *AutocertConfig) Validate() gperr.Error {
|
||||
}
|
||||
}
|
||||
// check if provider is implemented
|
||||
providerConstructor, ok := providersGenMap[cfg.Provider]
|
||||
providerConstructor, ok := providers[cfg.Provider]
|
||||
if !ok {
|
||||
b.Add(ErrUnknownProvider.
|
||||
Subject(cfg.Provider).
|
||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providersGenMap))))
|
||||
Withf(strutils.DoYouMean(utils.NearestField(cfg.Provider, providers))))
|
||||
} else {
|
||||
_, err := providerConstructor(cfg.Options)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package autocert
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var (
|
||||
CertFileDefault = filepath.Join(common.CertsDir, "cert.crt")
|
||||
KeyFileDefault = filepath.Join(common.CertsDir, "priv.key")
|
||||
ACMEKeyFileDefault = filepath.Join(common.CertsDir, "acme.key")
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderLocal = "local"
|
||||
ProviderCloudflare = "cloudflare"
|
||||
ProviderClouddns = "clouddns"
|
||||
ProviderDuckdns = "duckdns"
|
||||
ProviderOVH = "ovh"
|
||||
ProviderPseudo = "pseudo" // for testing
|
||||
ProviderPorkbun = "porkbun"
|
||||
)
|
||||
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderCloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderPorkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
||||
}
|
||||
53
internal/autocert/gen.py
Normal file
53
internal/autocert/gen.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import requests
|
||||
import os
|
||||
|
||||
class Entry:
|
||||
def __init__(self, name: str, type: str, **kwargs) -> None:
|
||||
self.name = name
|
||||
self.type = type
|
||||
|
||||
url = "https://api.github.com/repos/go-acme/lego/contents/providers/dns"
|
||||
response = requests.get(url)
|
||||
data: list[Entry] = [Entry(**i) for i in response.json()]
|
||||
|
||||
header = "//go:generate /usr/bin/python3 gen.py\n\npackage autocert\n\n"
|
||||
names: list[str] = [
|
||||
"ProviderLocal = \"local\"",
|
||||
"ProviderPseudo = \"pseudo\"",
|
||||
]
|
||||
imports: list[str] = []
|
||||
genMap: list[str] = [
|
||||
"ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),",
|
||||
"ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),",
|
||||
]
|
||||
|
||||
blacklists = [
|
||||
"internal",
|
||||
# deprecated
|
||||
"azure",
|
||||
"brandit",
|
||||
"cloudxns",
|
||||
"dnspod",
|
||||
"mythicbeasts",
|
||||
"yandexcloud"
|
||||
]
|
||||
|
||||
for item in data:
|
||||
if item.type != "dir" or item.name in blacklists:
|
||||
continue
|
||||
imports.append(f"import \"github.com/go-acme/lego/v4/providers/dns/{item.name}\"")
|
||||
names.append(f"Provider{item.name} = \"{item.name}\"")
|
||||
genMap.append(f"Provider{item.name}: providerGenerator({item.name}.NewDefaultConfig, {item.name}.NewDNSProviderConfig),")
|
||||
|
||||
with open("providers.go", "w") as f:
|
||||
f.write(header)
|
||||
f.write("\n".join(imports))
|
||||
f.write("\n\n")
|
||||
f.write("const (\n")
|
||||
f.write("\n".join(names))
|
||||
f.write("\n)\n\n")
|
||||
f.write("var providers = map[string]ProviderGenerator{\n")
|
||||
f.write("\n".join(genMap))
|
||||
f.write("\n}\n\n")
|
||||
|
||||
os.execvp("go", ["go", "fmt", "providers.go"])
|
||||
8
internal/autocert/paths.go
Normal file
8
internal/autocert/paths.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package autocert
|
||||
|
||||
const (
|
||||
certBasePath = "certs/"
|
||||
CertFileDefault = certBasePath + "cert.crt"
|
||||
KeyFileDefault = certBasePath + "priv.key"
|
||||
ACMEKeyFileDefault = certBasePath + "acme.key"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -205,7 +205,7 @@ func (p *Provider) initClient() error {
|
||||
return err
|
||||
}
|
||||
|
||||
generator := providersGenMap[p.cfg.Provider]
|
||||
generator := providers[p.cfg.Provider]
|
||||
legoProvider, pErr := generator(p.cfg.Options)
|
||||
if pErr != nil {
|
||||
return pErr
|
||||
@@ -329,7 +329,7 @@ func providerGenerator[CT any, PT challenge.Provider](
|
||||
) ProviderGenerator {
|
||||
return func(opt ProviderOpt) (challenge.Provider, gperr.Error) {
|
||||
cfg := defaultCfg()
|
||||
err := utils.MapUnmarshalValidate(opt, &cfg)
|
||||
err := U.Deserialize(opt, &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
@@ -45,6 +45,6 @@ oauth2_config:
|
||||
testYaml = testYaml[1:] // remove first \n
|
||||
opt := make(map[string]any)
|
||||
ExpectNoError(t, yaml.Unmarshal([]byte(testYaml), &opt))
|
||||
ExpectNoError(t, utils.MapUnmarshalValidate(opt, cfg))
|
||||
ExpectNoError(t, U.Deserialize(opt, cfg))
|
||||
ExpectEqual(t, cfg, cfgExpected)
|
||||
}
|
||||
|
||||
452
internal/autocert/providers.go
Normal file
452
internal/autocert/providers.go
Normal file
@@ -0,0 +1,452 @@
|
||||
//go:generate /usr/bin/python3 gen.py
|
||||
|
||||
package autocert
|
||||
|
||||
import "github.com/go-acme/lego/v4/providers/dns/acmedns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/active24"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/allinkl"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/auroradns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/autodns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/axelname"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/baiducloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/bindman"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/bluecat"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/bookmyname"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/bunny"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/civo"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/corenetworks"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/derak"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/desec"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/designate"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/directadmin"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dode"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/domeneshop"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dyn"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/dynu"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/easydns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/edgedns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/efficientip"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/epik"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/exec"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/exoscale"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/f5xc"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/freemyip"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/gandi"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/gcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/gcore"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/glesys"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/googledomains"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/hetzner"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/hostingde"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/hosttech"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/httpnet"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/httpreq"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/hurricane"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/hyperone"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ibmcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/iij"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/iijdpf"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/infoblox"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/infomaniak"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/internetbs"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/inwx"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ionos"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ipv64"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/iwantmyname"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/joker"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/liara"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/lightsail"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/limacity"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/linode"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/liquidweb"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/loopia"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/luadns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/manageengine"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/metaname"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/metaregistrar"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/mijnhost"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/mittwald"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/myaddr"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/netcup"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/netlify"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/nodion"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/oraclecloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/otc"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/plesk"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/rackspace"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/rainyun"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/regfish"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/regru"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/safedns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/sakuracloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/scaleway"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/selectel"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/selectelv2"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/selfhostde"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/servercow"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/shellrent"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/simply"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/sonic"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/spaceship"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/stackpath"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/technitium"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/timewebcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/transip"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/ultradns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/variomedia"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vegadns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vercel"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/versio"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vinyldns"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vkcloud"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/volcengine"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vscale"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/vultr"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/webnames"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/websupport"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/wedos"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/westcn"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/yandex"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/yandex360"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/zoneee"
|
||||
import "github.com/go-acme/lego/v4/providers/dns/zonomi"
|
||||
|
||||
const (
|
||||
ProviderLocal = "local"
|
||||
ProviderPseudo = "pseudo"
|
||||
Provideracmedns = "acmedns"
|
||||
Provideractive24 = "active24"
|
||||
Provideralidns = "alidns"
|
||||
Providerallinkl = "allinkl"
|
||||
Providerarvancloud = "arvancloud"
|
||||
Providerauroradns = "auroradns"
|
||||
Providerautodns = "autodns"
|
||||
Provideraxelname = "axelname"
|
||||
Providerazuredns = "azuredns"
|
||||
Providerbaiducloud = "baiducloud"
|
||||
Providerbindman = "bindman"
|
||||
Providerbluecat = "bluecat"
|
||||
Providerbookmyname = "bookmyname"
|
||||
Providerbunny = "bunny"
|
||||
Providercheckdomain = "checkdomain"
|
||||
Providercivo = "civo"
|
||||
Providerclouddns = "clouddns"
|
||||
Providercloudflare = "cloudflare"
|
||||
Providercloudns = "cloudns"
|
||||
Providercloudru = "cloudru"
|
||||
Providerconoha = "conoha"
|
||||
Providerconstellix = "constellix"
|
||||
Providercorenetworks = "corenetworks"
|
||||
Providercpanel = "cpanel"
|
||||
Providerderak = "derak"
|
||||
Providerdesec = "desec"
|
||||
Providerdesignate = "designate"
|
||||
Providerdigitalocean = "digitalocean"
|
||||
Providerdirectadmin = "directadmin"
|
||||
Providerdnshomede = "dnshomede"
|
||||
Providerdnsimple = "dnsimple"
|
||||
Providerdnsmadeeasy = "dnsmadeeasy"
|
||||
Providerdode = "dode"
|
||||
Providerdomeneshop = "domeneshop"
|
||||
Providerdreamhost = "dreamhost"
|
||||
Providerduckdns = "duckdns"
|
||||
Providerdyn = "dyn"
|
||||
Providerdynu = "dynu"
|
||||
Providereasydns = "easydns"
|
||||
Provideredgedns = "edgedns"
|
||||
Providerefficientip = "efficientip"
|
||||
Providerepik = "epik"
|
||||
Providerexec = "exec"
|
||||
Providerexoscale = "exoscale"
|
||||
Providerf5xc = "f5xc"
|
||||
Providerfreemyip = "freemyip"
|
||||
Providergandi = "gandi"
|
||||
Providergandiv5 = "gandiv5"
|
||||
Providergcloud = "gcloud"
|
||||
Providergcore = "gcore"
|
||||
Providerglesys = "glesys"
|
||||
Providergodaddy = "godaddy"
|
||||
Providergoogledomains = "googledomains"
|
||||
Providerhetzner = "hetzner"
|
||||
Providerhostingde = "hostingde"
|
||||
Providerhosttech = "hosttech"
|
||||
Providerhttpnet = "httpnet"
|
||||
Providerhttpreq = "httpreq"
|
||||
Providerhuaweicloud = "huaweicloud"
|
||||
Providerhurricane = "hurricane"
|
||||
Providerhyperone = "hyperone"
|
||||
Provideribmcloud = "ibmcloud"
|
||||
Provideriij = "iij"
|
||||
Provideriijdpf = "iijdpf"
|
||||
Providerinfoblox = "infoblox"
|
||||
Providerinfomaniak = "infomaniak"
|
||||
Providerinternetbs = "internetbs"
|
||||
Providerinwx = "inwx"
|
||||
Providerionos = "ionos"
|
||||
Provideripv64 = "ipv64"
|
||||
Provideriwantmyname = "iwantmyname"
|
||||
Providerjoker = "joker"
|
||||
Providerliara = "liara"
|
||||
Providerlightsail = "lightsail"
|
||||
Providerlimacity = "limacity"
|
||||
Providerlinode = "linode"
|
||||
Providerliquidweb = "liquidweb"
|
||||
Providerloopia = "loopia"
|
||||
Providerluadns = "luadns"
|
||||
Providermailinabox = "mailinabox"
|
||||
Providermanageengine = "manageengine"
|
||||
Providermetaname = "metaname"
|
||||
Providermetaregistrar = "metaregistrar"
|
||||
Providermijnhost = "mijnhost"
|
||||
Providermittwald = "mittwald"
|
||||
Providermyaddr = "myaddr"
|
||||
Providermydnsjp = "mydnsjp"
|
||||
Providernamecheap = "namecheap"
|
||||
Providernamedotcom = "namedotcom"
|
||||
Providernamesilo = "namesilo"
|
||||
Providernearlyfreespeech = "nearlyfreespeech"
|
||||
Providernetcup = "netcup"
|
||||
Providernetlify = "netlify"
|
||||
Providernicmanager = "nicmanager"
|
||||
Providernifcloud = "nifcloud"
|
||||
Providernjalla = "njalla"
|
||||
Providernodion = "nodion"
|
||||
Providerns1 = "ns1"
|
||||
Provideroraclecloud = "oraclecloud"
|
||||
Providerotc = "otc"
|
||||
Providerovh = "ovh"
|
||||
Providerpdns = "pdns"
|
||||
Providerplesk = "plesk"
|
||||
Providerporkbun = "porkbun"
|
||||
Providerrackspace = "rackspace"
|
||||
Providerrainyun = "rainyun"
|
||||
Providerrcodezero = "rcodezero"
|
||||
Providerregfish = "regfish"
|
||||
Providerregru = "regru"
|
||||
Providerrfc2136 = "rfc2136"
|
||||
Providerrimuhosting = "rimuhosting"
|
||||
Providerroute53 = "route53"
|
||||
Providersafedns = "safedns"
|
||||
Providersakuracloud = "sakuracloud"
|
||||
Providerscaleway = "scaleway"
|
||||
Providerselectel = "selectel"
|
||||
Providerselectelv2 = "selectelv2"
|
||||
Providerselfhostde = "selfhostde"
|
||||
Providerservercow = "servercow"
|
||||
Providershellrent = "shellrent"
|
||||
Providersimply = "simply"
|
||||
Providersonic = "sonic"
|
||||
Providerspaceship = "spaceship"
|
||||
Providerstackpath = "stackpath"
|
||||
Providertechnitium = "technitium"
|
||||
Providertencentcloud = "tencentcloud"
|
||||
Providertimewebcloud = "timewebcloud"
|
||||
Providertransip = "transip"
|
||||
Providerultradns = "ultradns"
|
||||
Providervariomedia = "variomedia"
|
||||
Providervegadns = "vegadns"
|
||||
Providervercel = "vercel"
|
||||
Providerversio = "versio"
|
||||
Providervinyldns = "vinyldns"
|
||||
Providervkcloud = "vkcloud"
|
||||
Providervolcengine = "volcengine"
|
||||
Providervscale = "vscale"
|
||||
Providervultr = "vultr"
|
||||
Providerwebnames = "webnames"
|
||||
Providerwebsupport = "websupport"
|
||||
Providerwedos = "wedos"
|
||||
Providerwestcn = "westcn"
|
||||
Provideryandex = "yandex"
|
||||
Provideryandex360 = "yandex360"
|
||||
Providerzoneee = "zoneee"
|
||||
Providerzonomi = "zonomi"
|
||||
)
|
||||
|
||||
var providers = map[string]ProviderGenerator{
|
||||
ProviderLocal: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
ProviderPseudo: providerGenerator(NewDummyDefaultConfig, NewDummyDNSProviderConfig),
|
||||
Provideracmedns: providerGenerator(acmedns.NewDefaultConfig, acmedns.NewDNSProviderConfig),
|
||||
Provideractive24: providerGenerator(active24.NewDefaultConfig, active24.NewDNSProviderConfig),
|
||||
Provideralidns: providerGenerator(alidns.NewDefaultConfig, alidns.NewDNSProviderConfig),
|
||||
Providerallinkl: providerGenerator(allinkl.NewDefaultConfig, allinkl.NewDNSProviderConfig),
|
||||
Providerarvancloud: providerGenerator(arvancloud.NewDefaultConfig, arvancloud.NewDNSProviderConfig),
|
||||
Providerauroradns: providerGenerator(auroradns.NewDefaultConfig, auroradns.NewDNSProviderConfig),
|
||||
Providerautodns: providerGenerator(autodns.NewDefaultConfig, autodns.NewDNSProviderConfig),
|
||||
Provideraxelname: providerGenerator(axelname.NewDefaultConfig, axelname.NewDNSProviderConfig),
|
||||
Providerazuredns: providerGenerator(azuredns.NewDefaultConfig, azuredns.NewDNSProviderConfig),
|
||||
Providerbaiducloud: providerGenerator(baiducloud.NewDefaultConfig, baiducloud.NewDNSProviderConfig),
|
||||
Providerbindman: providerGenerator(bindman.NewDefaultConfig, bindman.NewDNSProviderConfig),
|
||||
Providerbluecat: providerGenerator(bluecat.NewDefaultConfig, bluecat.NewDNSProviderConfig),
|
||||
Providerbookmyname: providerGenerator(bookmyname.NewDefaultConfig, bookmyname.NewDNSProviderConfig),
|
||||
Providerbunny: providerGenerator(bunny.NewDefaultConfig, bunny.NewDNSProviderConfig),
|
||||
Providercheckdomain: providerGenerator(checkdomain.NewDefaultConfig, checkdomain.NewDNSProviderConfig),
|
||||
Providercivo: providerGenerator(civo.NewDefaultConfig, civo.NewDNSProviderConfig),
|
||||
Providerclouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
Providercloudflare: providerGenerator(cloudflare.NewDefaultConfig, cloudflare.NewDNSProviderConfig),
|
||||
Providercloudns: providerGenerator(cloudns.NewDefaultConfig, cloudns.NewDNSProviderConfig),
|
||||
Providercloudru: providerGenerator(cloudru.NewDefaultConfig, cloudru.NewDNSProviderConfig),
|
||||
Providerconoha: providerGenerator(conoha.NewDefaultConfig, conoha.NewDNSProviderConfig),
|
||||
Providerconstellix: providerGenerator(constellix.NewDefaultConfig, constellix.NewDNSProviderConfig),
|
||||
Providercorenetworks: providerGenerator(corenetworks.NewDefaultConfig, corenetworks.NewDNSProviderConfig),
|
||||
Providercpanel: providerGenerator(cpanel.NewDefaultConfig, cpanel.NewDNSProviderConfig),
|
||||
Providerderak: providerGenerator(derak.NewDefaultConfig, derak.NewDNSProviderConfig),
|
||||
Providerdesec: providerGenerator(desec.NewDefaultConfig, desec.NewDNSProviderConfig),
|
||||
Providerdesignate: providerGenerator(designate.NewDefaultConfig, designate.NewDNSProviderConfig),
|
||||
Providerdigitalocean: providerGenerator(digitalocean.NewDefaultConfig, digitalocean.NewDNSProviderConfig),
|
||||
Providerdirectadmin: providerGenerator(directadmin.NewDefaultConfig, directadmin.NewDNSProviderConfig),
|
||||
Providerdnshomede: providerGenerator(dnshomede.NewDefaultConfig, dnshomede.NewDNSProviderConfig),
|
||||
Providerdnsimple: providerGenerator(dnsimple.NewDefaultConfig, dnsimple.NewDNSProviderConfig),
|
||||
Providerdnsmadeeasy: providerGenerator(dnsmadeeasy.NewDefaultConfig, dnsmadeeasy.NewDNSProviderConfig),
|
||||
Providerdode: providerGenerator(dode.NewDefaultConfig, dode.NewDNSProviderConfig),
|
||||
Providerdomeneshop: providerGenerator(domeneshop.NewDefaultConfig, domeneshop.NewDNSProviderConfig),
|
||||
Providerdreamhost: providerGenerator(dreamhost.NewDefaultConfig, dreamhost.NewDNSProviderConfig),
|
||||
Providerduckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
Providerdyn: providerGenerator(dyn.NewDefaultConfig, dyn.NewDNSProviderConfig),
|
||||
Providerdynu: providerGenerator(dynu.NewDefaultConfig, dynu.NewDNSProviderConfig),
|
||||
Providereasydns: providerGenerator(easydns.NewDefaultConfig, easydns.NewDNSProviderConfig),
|
||||
Provideredgedns: providerGenerator(edgedns.NewDefaultConfig, edgedns.NewDNSProviderConfig),
|
||||
Providerefficientip: providerGenerator(efficientip.NewDefaultConfig, efficientip.NewDNSProviderConfig),
|
||||
Providerepik: providerGenerator(epik.NewDefaultConfig, epik.NewDNSProviderConfig),
|
||||
Providerexec: providerGenerator(exec.NewDefaultConfig, exec.NewDNSProviderConfig),
|
||||
Providerexoscale: providerGenerator(exoscale.NewDefaultConfig, exoscale.NewDNSProviderConfig),
|
||||
Providerf5xc: providerGenerator(f5xc.NewDefaultConfig, f5xc.NewDNSProviderConfig),
|
||||
Providerfreemyip: providerGenerator(freemyip.NewDefaultConfig, freemyip.NewDNSProviderConfig),
|
||||
Providergandi: providerGenerator(gandi.NewDefaultConfig, gandi.NewDNSProviderConfig),
|
||||
Providergandiv5: providerGenerator(gandiv5.NewDefaultConfig, gandiv5.NewDNSProviderConfig),
|
||||
Providergcloud: providerGenerator(gcloud.NewDefaultConfig, gcloud.NewDNSProviderConfig),
|
||||
Providergcore: providerGenerator(gcore.NewDefaultConfig, gcore.NewDNSProviderConfig),
|
||||
Providerglesys: providerGenerator(glesys.NewDefaultConfig, glesys.NewDNSProviderConfig),
|
||||
Providergodaddy: providerGenerator(godaddy.NewDefaultConfig, godaddy.NewDNSProviderConfig),
|
||||
Providergoogledomains: providerGenerator(googledomains.NewDefaultConfig, googledomains.NewDNSProviderConfig),
|
||||
Providerhetzner: providerGenerator(hetzner.NewDefaultConfig, hetzner.NewDNSProviderConfig),
|
||||
Providerhostingde: providerGenerator(hostingde.NewDefaultConfig, hostingde.NewDNSProviderConfig),
|
||||
Providerhosttech: providerGenerator(hosttech.NewDefaultConfig, hosttech.NewDNSProviderConfig),
|
||||
Providerhttpnet: providerGenerator(httpnet.NewDefaultConfig, httpnet.NewDNSProviderConfig),
|
||||
Providerhttpreq: providerGenerator(httpreq.NewDefaultConfig, httpreq.NewDNSProviderConfig),
|
||||
Providerhuaweicloud: providerGenerator(huaweicloud.NewDefaultConfig, huaweicloud.NewDNSProviderConfig),
|
||||
Providerhurricane: providerGenerator(hurricane.NewDefaultConfig, hurricane.NewDNSProviderConfig),
|
||||
Providerhyperone: providerGenerator(hyperone.NewDefaultConfig, hyperone.NewDNSProviderConfig),
|
||||
Provideribmcloud: providerGenerator(ibmcloud.NewDefaultConfig, ibmcloud.NewDNSProviderConfig),
|
||||
Provideriij: providerGenerator(iij.NewDefaultConfig, iij.NewDNSProviderConfig),
|
||||
Provideriijdpf: providerGenerator(iijdpf.NewDefaultConfig, iijdpf.NewDNSProviderConfig),
|
||||
Providerinfoblox: providerGenerator(infoblox.NewDefaultConfig, infoblox.NewDNSProviderConfig),
|
||||
Providerinfomaniak: providerGenerator(infomaniak.NewDefaultConfig, infomaniak.NewDNSProviderConfig),
|
||||
Providerinternetbs: providerGenerator(internetbs.NewDefaultConfig, internetbs.NewDNSProviderConfig),
|
||||
Providerinwx: providerGenerator(inwx.NewDefaultConfig, inwx.NewDNSProviderConfig),
|
||||
Providerionos: providerGenerator(ionos.NewDefaultConfig, ionos.NewDNSProviderConfig),
|
||||
Provideripv64: providerGenerator(ipv64.NewDefaultConfig, ipv64.NewDNSProviderConfig),
|
||||
Provideriwantmyname: providerGenerator(iwantmyname.NewDefaultConfig, iwantmyname.NewDNSProviderConfig),
|
||||
Providerjoker: providerGenerator(joker.NewDefaultConfig, joker.NewDNSProviderConfig),
|
||||
Providerliara: providerGenerator(liara.NewDefaultConfig, liara.NewDNSProviderConfig),
|
||||
Providerlightsail: providerGenerator(lightsail.NewDefaultConfig, lightsail.NewDNSProviderConfig),
|
||||
Providerlimacity: providerGenerator(limacity.NewDefaultConfig, limacity.NewDNSProviderConfig),
|
||||
Providerlinode: providerGenerator(linode.NewDefaultConfig, linode.NewDNSProviderConfig),
|
||||
Providerliquidweb: providerGenerator(liquidweb.NewDefaultConfig, liquidweb.NewDNSProviderConfig),
|
||||
Providerloopia: providerGenerator(loopia.NewDefaultConfig, loopia.NewDNSProviderConfig),
|
||||
Providerluadns: providerGenerator(luadns.NewDefaultConfig, luadns.NewDNSProviderConfig),
|
||||
Providermailinabox: providerGenerator(mailinabox.NewDefaultConfig, mailinabox.NewDNSProviderConfig),
|
||||
Providermanageengine: providerGenerator(manageengine.NewDefaultConfig, manageengine.NewDNSProviderConfig),
|
||||
Providermetaname: providerGenerator(metaname.NewDefaultConfig, metaname.NewDNSProviderConfig),
|
||||
Providermetaregistrar: providerGenerator(metaregistrar.NewDefaultConfig, metaregistrar.NewDNSProviderConfig),
|
||||
Providermijnhost: providerGenerator(mijnhost.NewDefaultConfig, mijnhost.NewDNSProviderConfig),
|
||||
Providermittwald: providerGenerator(mittwald.NewDefaultConfig, mittwald.NewDNSProviderConfig),
|
||||
Providermyaddr: providerGenerator(myaddr.NewDefaultConfig, myaddr.NewDNSProviderConfig),
|
||||
Providermydnsjp: providerGenerator(mydnsjp.NewDefaultConfig, mydnsjp.NewDNSProviderConfig),
|
||||
Providernamecheap: providerGenerator(namecheap.NewDefaultConfig, namecheap.NewDNSProviderConfig),
|
||||
Providernamedotcom: providerGenerator(namedotcom.NewDefaultConfig, namedotcom.NewDNSProviderConfig),
|
||||
Providernamesilo: providerGenerator(namesilo.NewDefaultConfig, namesilo.NewDNSProviderConfig),
|
||||
Providernearlyfreespeech: providerGenerator(nearlyfreespeech.NewDefaultConfig, nearlyfreespeech.NewDNSProviderConfig),
|
||||
Providernetcup: providerGenerator(netcup.NewDefaultConfig, netcup.NewDNSProviderConfig),
|
||||
Providernetlify: providerGenerator(netlify.NewDefaultConfig, netlify.NewDNSProviderConfig),
|
||||
Providernicmanager: providerGenerator(nicmanager.NewDefaultConfig, nicmanager.NewDNSProviderConfig),
|
||||
Providernifcloud: providerGenerator(nifcloud.NewDefaultConfig, nifcloud.NewDNSProviderConfig),
|
||||
Providernjalla: providerGenerator(njalla.NewDefaultConfig, njalla.NewDNSProviderConfig),
|
||||
Providernodion: providerGenerator(nodion.NewDefaultConfig, nodion.NewDNSProviderConfig),
|
||||
Providerns1: providerGenerator(ns1.NewDefaultConfig, ns1.NewDNSProviderConfig),
|
||||
Provideroraclecloud: providerGenerator(oraclecloud.NewDefaultConfig, oraclecloud.NewDNSProviderConfig),
|
||||
Providerotc: providerGenerator(otc.NewDefaultConfig, otc.NewDNSProviderConfig),
|
||||
Providerovh: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
Providerpdns: providerGenerator(pdns.NewDefaultConfig, pdns.NewDNSProviderConfig),
|
||||
Providerplesk: providerGenerator(plesk.NewDefaultConfig, plesk.NewDNSProviderConfig),
|
||||
Providerporkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
||||
Providerrackspace: providerGenerator(rackspace.NewDefaultConfig, rackspace.NewDNSProviderConfig),
|
||||
Providerrainyun: providerGenerator(rainyun.NewDefaultConfig, rainyun.NewDNSProviderConfig),
|
||||
Providerrcodezero: providerGenerator(rcodezero.NewDefaultConfig, rcodezero.NewDNSProviderConfig),
|
||||
Providerregfish: providerGenerator(regfish.NewDefaultConfig, regfish.NewDNSProviderConfig),
|
||||
Providerregru: providerGenerator(regru.NewDefaultConfig, regru.NewDNSProviderConfig),
|
||||
Providerrfc2136: providerGenerator(rfc2136.NewDefaultConfig, rfc2136.NewDNSProviderConfig),
|
||||
Providerrimuhosting: providerGenerator(rimuhosting.NewDefaultConfig, rimuhosting.NewDNSProviderConfig),
|
||||
Providerroute53: providerGenerator(route53.NewDefaultConfig, route53.NewDNSProviderConfig),
|
||||
Providersafedns: providerGenerator(safedns.NewDefaultConfig, safedns.NewDNSProviderConfig),
|
||||
Providersakuracloud: providerGenerator(sakuracloud.NewDefaultConfig, sakuracloud.NewDNSProviderConfig),
|
||||
Providerscaleway: providerGenerator(scaleway.NewDefaultConfig, scaleway.NewDNSProviderConfig),
|
||||
Providerselectel: providerGenerator(selectel.NewDefaultConfig, selectel.NewDNSProviderConfig),
|
||||
Providerselectelv2: providerGenerator(selectelv2.NewDefaultConfig, selectelv2.NewDNSProviderConfig),
|
||||
Providerselfhostde: providerGenerator(selfhostde.NewDefaultConfig, selfhostde.NewDNSProviderConfig),
|
||||
Providerservercow: providerGenerator(servercow.NewDefaultConfig, servercow.NewDNSProviderConfig),
|
||||
Providershellrent: providerGenerator(shellrent.NewDefaultConfig, shellrent.NewDNSProviderConfig),
|
||||
Providersimply: providerGenerator(simply.NewDefaultConfig, simply.NewDNSProviderConfig),
|
||||
Providersonic: providerGenerator(sonic.NewDefaultConfig, sonic.NewDNSProviderConfig),
|
||||
Providerspaceship: providerGenerator(spaceship.NewDefaultConfig, spaceship.NewDNSProviderConfig),
|
||||
Providerstackpath: providerGenerator(stackpath.NewDefaultConfig, stackpath.NewDNSProviderConfig),
|
||||
Providertechnitium: providerGenerator(technitium.NewDefaultConfig, technitium.NewDNSProviderConfig),
|
||||
Providertencentcloud: providerGenerator(tencentcloud.NewDefaultConfig, tencentcloud.NewDNSProviderConfig),
|
||||
Providertimewebcloud: providerGenerator(timewebcloud.NewDefaultConfig, timewebcloud.NewDNSProviderConfig),
|
||||
Providertransip: providerGenerator(transip.NewDefaultConfig, transip.NewDNSProviderConfig),
|
||||
Providerultradns: providerGenerator(ultradns.NewDefaultConfig, ultradns.NewDNSProviderConfig),
|
||||
Providervariomedia: providerGenerator(variomedia.NewDefaultConfig, variomedia.NewDNSProviderConfig),
|
||||
Providervegadns: providerGenerator(vegadns.NewDefaultConfig, vegadns.NewDNSProviderConfig),
|
||||
Providervercel: providerGenerator(vercel.NewDefaultConfig, vercel.NewDNSProviderConfig),
|
||||
Providerversio: providerGenerator(versio.NewDefaultConfig, versio.NewDNSProviderConfig),
|
||||
Providervinyldns: providerGenerator(vinyldns.NewDefaultConfig, vinyldns.NewDNSProviderConfig),
|
||||
Providervkcloud: providerGenerator(vkcloud.NewDefaultConfig, vkcloud.NewDNSProviderConfig),
|
||||
Providervolcengine: providerGenerator(volcengine.NewDefaultConfig, volcengine.NewDNSProviderConfig),
|
||||
Providervscale: providerGenerator(vscale.NewDefaultConfig, vscale.NewDNSProviderConfig),
|
||||
Providervultr: providerGenerator(vultr.NewDefaultConfig, vultr.NewDNSProviderConfig),
|
||||
Providerwebnames: providerGenerator(webnames.NewDefaultConfig, webnames.NewDNSProviderConfig),
|
||||
Providerwebsupport: providerGenerator(websupport.NewDefaultConfig, websupport.NewDNSProviderConfig),
|
||||
Providerwedos: providerGenerator(wedos.NewDefaultConfig, wedos.NewDNSProviderConfig),
|
||||
Providerwestcn: providerGenerator(westcn.NewDefaultConfig, westcn.NewDNSProviderConfig),
|
||||
Provideryandex: providerGenerator(yandex.NewDefaultConfig, yandex.NewDNSProviderConfig),
|
||||
Provideryandex360: providerGenerator(yandex360.NewDefaultConfig, yandex360.NewDNSProviderConfig),
|
||||
Providerzoneee: providerGenerator(zoneee.NewDefaultConfig, zoneee.NewDNSProviderConfig),
|
||||
Providerzonomi: providerGenerator(zonomi.NewDefaultConfig, zonomi.NewDNSProviderConfig),
|
||||
}
|
||||
@@ -1,10 +1,51 @@
|
||||
package common
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// file, folder structure
|
||||
|
||||
const (
|
||||
DotEnvPath = ".env"
|
||||
DotEnvExamplePath = ".env.example"
|
||||
|
||||
ConfigBasePath = "config"
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigExampleFileName = "config.example.yml"
|
||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||
|
||||
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
||||
IconCachePath = ConfigBasePath + "/.icon_cache.json"
|
||||
|
||||
NamespaceHomepageOverrides = ".homepage"
|
||||
NamespaceIconCache = ".icon_cache"
|
||||
|
||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||
|
||||
ComposeFileName = "compose.yml"
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
|
||||
DataDir = "data"
|
||||
|
||||
ErrorPagesBasePath = "error_pages"
|
||||
|
||||
AgentCertsBasePath = "certs"
|
||||
)
|
||||
|
||||
var RequiredDirectories = []string{
|
||||
ConfigBasePath,
|
||||
ErrorPagesBasePath,
|
||||
MiddlewareComposeBasePath,
|
||||
}
|
||||
|
||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||
|
||||
const (
|
||||
HealthCheckIntervalDefault = 5 * time.Second
|
||||
HealthCheckTimeoutDefault = 5 * time.Second
|
||||
|
||||
WakeTimeoutDefault = "3m"
|
||||
StopTimeoutDefault = "3m"
|
||||
StopMethodDefault = "stop"
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ func decodeJWTKey(key string) []byte {
|
||||
}
|
||||
bytes, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to decode jwt key")
|
||||
log.Fatal().Str("key", key).Err(err).Msg("failed to decode secret")
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func RandomJWTKey() []byte {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to generate random jwt key")
|
||||
log.Fatal().Err(err).Msg("failed to generate random jwt key")
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ var (
|
||||
IsDebug = GetEnvBool("DEBUG", IsTest)
|
||||
IsTrace = GetEnvBool("TRACE", false) && IsDebug
|
||||
|
||||
RootDir = GetEnvString("ROOT_DIR", "./")
|
||||
|
||||
HTTP3Enabled = GetEnvBool("HTTP3_ENABLED", true)
|
||||
|
||||
ProxyHTTPAddr,
|
||||
@@ -38,11 +36,9 @@ var (
|
||||
APIHTTPPort,
|
||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||
|
||||
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
||||
|
||||
APIJWTSecure = GetEnvBool("API_JWT_SECURE", true)
|
||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", 24*time.Hour)
|
||||
APIUser = GetEnvString("API_USER", "admin")
|
||||
APIPassword = GetEnvString("API_PASSWORD", "password")
|
||||
|
||||
@@ -52,7 +48,6 @@ var (
|
||||
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
|
||||
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
|
||||
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
|
||||
OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "")
|
||||
OIDCScopes = GetEnvString("OIDC_SCOPES", "openid, profile, email")
|
||||
OIDCAllowedUsers = GetCommaSepEnv("OIDC_ALLOWED_USERS", "")
|
||||
OIDCAllowedGroups = GetCommaSepEnv("OIDC_ALLOWED_GROUPS", "")
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// file, folder structure
|
||||
|
||||
var (
|
||||
ConfigDir = filepath.Join(RootDir, "config")
|
||||
ConfigFileName = "config.yml"
|
||||
ConfigExampleFileName = "config.example.yml"
|
||||
ConfigPath = filepath.Join(ConfigDir, ConfigFileName)
|
||||
|
||||
MiddlewareComposeDir = filepath.Join(ConfigDir, "middlewares")
|
||||
ErrorPagesDir = filepath.Join(RootDir, "error_pages")
|
||||
CertsDir = filepath.Join(RootDir, "certs")
|
||||
|
||||
DataDir = filepath.Join(RootDir, "data")
|
||||
MetricsDataDir = filepath.Join(DataDir, "metrics")
|
||||
|
||||
HomepageJSONConfigPath = filepath.Join(DataDir, "homepage.json")
|
||||
IconListCachePath = filepath.Join(DataDir, "icon_list_cache.json")
|
||||
IconCachePath = filepath.Join(DataDir, "icon_cache.json")
|
||||
)
|
||||
|
||||
var RequiredDirectories = []string{
|
||||
ConfigDir,
|
||||
ErrorPagesDir,
|
||||
MiddlewareComposeDir,
|
||||
DataDir,
|
||||
MetricsDataDir,
|
||||
}
|
||||
66
internal/config/agent_pool.go
Normal file
66
internal/config/agent_pool.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
"github.com/yusing/go-proxy/internal/utils/functional"
|
||||
)
|
||||
|
||||
var agentPool = functional.NewMapOf[string, *agent.AgentConfig]()
|
||||
|
||||
func addAgent(agent *agent.AgentConfig) {
|
||||
agentPool.Store(agent.Addr, agent)
|
||||
}
|
||||
|
||||
func removeAllAgents() {
|
||||
agentPool.Clear()
|
||||
}
|
||||
|
||||
func GetAgent(addr string) (agent *agent.AgentConfig, ok bool) {
|
||||
agent, ok = agentPool.Load(addr)
|
||||
return
|
||||
}
|
||||
|
||||
func (cfg *Config) GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool) {
|
||||
if !agent.IsDockerHostAgent(agentAddrOrDockerHost) {
|
||||
return GetAgent(agentAddrOrDockerHost)
|
||||
}
|
||||
return GetAgent(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
|
||||
}
|
||||
|
||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
|
||||
if slices.ContainsFunc(cfg.value.Providers.Agents, func(a *agent.AgentConfig) bool {
|
||||
return a.Addr == host
|
||||
}) {
|
||||
return 0, gperr.New("agent already exists")
|
||||
}
|
||||
|
||||
var agentCfg agent.AgentConfig
|
||||
agentCfg.Addr = host
|
||||
err := agentCfg.StartWithCerts(cfg.Task(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to start agent")
|
||||
}
|
||||
addAgent(&agentCfg)
|
||||
|
||||
provider := provider.NewAgentProvider(&agentCfg)
|
||||
if err := cfg.errIfExists(provider); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = provider.LoadRoutes()
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
}
|
||||
return provider.NumRoutes(), nil
|
||||
}
|
||||
|
||||
func (cfg *Config) ListAgents() []*agent.AgentConfig {
|
||||
agents := make([]*agent.AgentConfig, 0, agentPool.Size())
|
||||
agentPool.RangeAll(func(key string, value *agent.AgentConfig) {
|
||||
agents = append(agents, value)
|
||||
})
|
||||
return agents
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/api"
|
||||
"github.com/yusing/go-proxy/internal/autocert"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/server"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
"github.com/yusing/go-proxy/internal/proxmox"
|
||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
@@ -53,8 +51,6 @@ You may run "ls-config" to show or dump the current config.`
|
||||
|
||||
var Validate = config.Validate
|
||||
|
||||
var ErrProviderNameConflict = gperr.New("provider name conflict")
|
||||
|
||||
func newConfig() *Config {
|
||||
return &Config{
|
||||
value: config.DefaultConfig(),
|
||||
@@ -84,7 +80,9 @@ func WatchChanges() {
|
||||
t,
|
||||
configEventFlushInterval,
|
||||
OnConfigChange,
|
||||
onReloadError,
|
||||
func(err gperr.Error) {
|
||||
gperr.LogError("config reload error", err)
|
||||
},
|
||||
)
|
||||
eventQueue.Start(cfgWatcher.Events(t.Context()))
|
||||
}
|
||||
@@ -107,10 +105,6 @@ func OnConfigChange(ev []events.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func onReloadError(err gperr.Error) {
|
||||
logging.Error().Msgf("config reload error: %s", err)
|
||||
}
|
||||
|
||||
func Reload() gperr.Error {
|
||||
// avoid race between config change and API reload request
|
||||
reloadMu.Lock()
|
||||
@@ -203,6 +197,7 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
HTTPAddr: common.ProxyHTTPAddr,
|
||||
HTTPSAddr: common.ProxyHTTPSAddr,
|
||||
Handler: cfg.entrypoint,
|
||||
ACL: cfg.value.ACL,
|
||||
})
|
||||
}
|
||||
if opt.API {
|
||||
@@ -216,22 +211,23 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
}
|
||||
|
||||
func (cfg *Config) load() gperr.Error {
|
||||
const errMsg = "config load error"
|
||||
|
||||
data, err := os.ReadFile(common.ConfigPath)
|
||||
if err != nil {
|
||||
gperr.LogFatal("error reading config", err)
|
||||
gperr.LogFatal(errMsg, err)
|
||||
}
|
||||
|
||||
model := config.DefaultConfig()
|
||||
if err := utils.UnmarshalValidateYAML(data, model); err != nil {
|
||||
gperr.LogFatal("error unmarshalling config", err)
|
||||
if err := utils.DeserializeYAML(data, model); err != nil {
|
||||
gperr.LogFatal(errMsg, err)
|
||||
}
|
||||
|
||||
// errors are non fatal below
|
||||
errs := gperr.NewBuilder()
|
||||
errs := gperr.NewBuilder(errMsg)
|
||||
errs.Add(cfg.entrypoint.SetMiddlewares(model.Entrypoint.Middlewares))
|
||||
errs.Add(cfg.entrypoint.SetAccessLogger(cfg.task, model.Entrypoint.AccessLog))
|
||||
cfg.initNotification(model.Providers.Notification)
|
||||
errs.Add(cfg.initProxmox(model.Providers.Proxmox))
|
||||
errs.Add(cfg.initAutoCert(model.AutoCert))
|
||||
errs.Add(cfg.loadRouteProviders(&model.Providers))
|
||||
|
||||
@@ -242,6 +238,14 @@ func (cfg *Config) load() gperr.Error {
|
||||
}
|
||||
}
|
||||
cfg.entrypoint.SetFindRouteDomains(model.MatchDomains)
|
||||
if model.ACL.Valid() {
|
||||
err := model.ACL.Start(cfg.task)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
} else {
|
||||
logging.Info().Msg("ACL started")
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Error()
|
||||
}
|
||||
@@ -256,18 +260,6 @@ func (cfg *Config) initNotification(notifCfg []notif.NotificationConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) initProxmox(proxmoxCfgs []proxmox.Config) (err gperr.Error) {
|
||||
errs := gperr.NewBuilder("proxmox config errors")
|
||||
for _, proxmoxCfg := range proxmoxCfgs {
|
||||
if err := proxmoxCfg.Init(); err != nil {
|
||||
errs.Add(err.Subject(proxmoxCfg.URL))
|
||||
} else {
|
||||
proxmox.Clients.Add(proxmoxCfg.Client())
|
||||
}
|
||||
}
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err gperr.Error) {
|
||||
if cfg.autocertProvider != nil {
|
||||
return
|
||||
@@ -278,70 +270,56 @@ func (cfg *Config) initAutoCert(autocertCfg *autocert.AutocertConfig) (err gperr
|
||||
}
|
||||
|
||||
func (cfg *Config) errIfExists(p *proxy.Provider) gperr.Error {
|
||||
if conflict, ok := cfg.providers.Load(p.String()); ok {
|
||||
return ErrProviderNameConflict.
|
||||
Subject(p.String()).
|
||||
Withf("one is %q", conflict.Type()).
|
||||
Withf("the other is %q", p.Type())
|
||||
if _, ok := cfg.providers.Load(p.String()); ok {
|
||||
return gperr.Errorf("provider %s already exists", p.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cfg *Config) initAgents(agentCfgs []*agent.AgentConfig) gperr.Error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
errs := gperr.NewBuilderWithConcurrency()
|
||||
wg.Add(len(agentCfgs))
|
||||
for _, agentCfg := range agentCfgs {
|
||||
go func(agentCfg *agent.AgentConfig) {
|
||||
defer wg.Done()
|
||||
if err := agentCfg.Init(cfg.task.Context()); err != nil {
|
||||
errs.Add(err.Subject(agentCfg.String()))
|
||||
} else {
|
||||
agent.Agents.Add(agentCfg)
|
||||
}
|
||||
}(agentCfg)
|
||||
}
|
||||
wg.Wait()
|
||||
return errs.Error()
|
||||
func (cfg *Config) storeProvider(p *proxy.Provider) {
|
||||
cfg.providers.Store(p.String(), p)
|
||||
}
|
||||
|
||||
func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||
errs := gperr.NewBuilder("route provider errors")
|
||||
results := gperr.NewBuilder("loaded route providers")
|
||||
|
||||
agent.Agents.Clear()
|
||||
removeAllAgents()
|
||||
|
||||
n := len(providers.Agents) + len(providers.Docker) + len(providers.Files)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
routeProviders := make([]*proxy.Provider, 0, n)
|
||||
|
||||
errs.Add(cfg.initAgents(providers.Agents))
|
||||
|
||||
for _, a := range providers.Agents {
|
||||
if !a.IsInitialized() { // failed to initialize
|
||||
for _, agent := range providers.Agents {
|
||||
if err := agent.Start(cfg.task); err != nil {
|
||||
errs.Add(err.Subject(agent.String()))
|
||||
continue
|
||||
}
|
||||
agent.Agents.Add(a)
|
||||
routeProviders = append(routeProviders, proxy.NewAgentProvider(a))
|
||||
addAgent(agent)
|
||||
p := proxy.NewAgentProvider(agent)
|
||||
if err := cfg.errIfExists(p); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
for _, filename := range providers.Files {
|
||||
routeProviders = append(routeProviders, proxy.NewFileProvider(filename))
|
||||
}
|
||||
for name, dockerHost := range providers.Docker {
|
||||
routeProviders = append(routeProviders, proxy.NewDockerProvider(name, dockerHost))
|
||||
}
|
||||
|
||||
// check if all providers are unique (should not happen but just in case)
|
||||
for _, p := range routeProviders {
|
||||
if err := cfg.errIfExists(p); err != nil {
|
||||
errs.Add(err)
|
||||
p, err := proxy.NewFileProvider(filename)
|
||||
if err == nil {
|
||||
err = cfg.errIfExists(p)
|
||||
}
|
||||
if err != nil {
|
||||
errs.Add(gperr.PrependSubject(filename, err))
|
||||
continue
|
||||
}
|
||||
cfg.providers.Store(p.String(), p)
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
for name, dockerHost := range providers.Docker {
|
||||
p := proxy.NewDockerProvider(name, dockerHost)
|
||||
if err := cfg.errIfExists(p); err != nil {
|
||||
errs.Add(err.Subject(p.String()))
|
||||
continue
|
||||
}
|
||||
cfg.storeProvider(p)
|
||||
}
|
||||
if cfg.providers.Size() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lenLongestName := 0
|
||||
@@ -350,7 +328,6 @@ func (cfg *Config) loadRouteProviders(providers *config.Providers) gperr.Error {
|
||||
lenLongestName = len(k)
|
||||
}
|
||||
})
|
||||
errs.EnableConcurrency()
|
||||
results.EnableConcurrency()
|
||||
cfg.providers.RangeAllParallel(func(_ string, p *proxy.Provider) {
|
||||
if err := p.LoadRoutes(); err != nil {
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestFileProviderValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filenames []string
|
||||
init, cleanup func(filepath string) error
|
||||
expectedErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "file not exists",
|
||||
filenames: []string{"not_exists.yaml"},
|
||||
expectedErrorContains: "config_file_exists",
|
||||
},
|
||||
{
|
||||
name: "file is a directory",
|
||||
filenames: []string{"testdata"},
|
||||
expectedErrorContains: "config_file_exists",
|
||||
},
|
||||
{
|
||||
name: "same file exists multiple times",
|
||||
filenames: []string{"test.yml", "test.yml"},
|
||||
expectedErrorContains: "unique",
|
||||
},
|
||||
{
|
||||
name: "file ok",
|
||||
filenames: []string{"routes.yaml"},
|
||||
init: func(filepath string) error {
|
||||
os.MkdirAll(path.Dir(filepath), 0755)
|
||||
_, err := os.Create(filepath)
|
||||
return err
|
||||
},
|
||||
cleanup: func(filepath string) error {
|
||||
return os.RemoveAll(path.Dir(filepath))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
if tt.init != nil {
|
||||
for _, filename := range tt.filenames {
|
||||
filepath := path.Join(common.ConfigDir, filename)
|
||||
assert.NoError(t, tt.init(filepath))
|
||||
}
|
||||
}
|
||||
err := utils.UnmarshalValidateYAML(Must(yaml.Marshal(map[string]any{
|
||||
"providers": map[string]any{
|
||||
"include": tt.filenames,
|
||||
},
|
||||
})), cfg)
|
||||
if tt.cleanup != nil {
|
||||
for _, filename := range tt.filenames {
|
||||
filepath := path.Join(common.ConfigDir, filename)
|
||||
assert.NoError(t, tt.cleanup(filepath))
|
||||
}
|
||||
}
|
||||
if tt.expectedErrorContains != "" {
|
||||
assert.ErrorContains(t, err, tt.expectedErrorContains)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRouteProviders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providers *config.Providers
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "duplicate file provider",
|
||||
providers: &config.Providers{
|
||||
Files: []string{"routes.yaml", "routes.yaml"},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate docker provider",
|
||||
providers: &config.Providers{
|
||||
Docker: map[string]string{
|
||||
"docker1": "unix:///var/run/docker.sock",
|
||||
"docker2": "unix:///var/run/docker.sock",
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "docker provider with different hosts",
|
||||
providers: &config.Providers{
|
||||
Docker: map[string]string{
|
||||
"docker1": "unix:///var/run/docker1.sock",
|
||||
"docker2": "unix:///var/run/docker2.sock",
|
||||
},
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate agent addresses",
|
||||
providers: &config.Providers{
|
||||
Agents: []*agent.AgentConfig{
|
||||
{Addr: "192.168.1.100:8080"},
|
||||
{Addr: "192.168.1.100:8080"},
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := utils.Validate(tt.providers)
|
||||
if tt.expectedError {
|
||||
assert.ErrorContains(t, err, "unique")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderNameUniqueness(t *testing.T) {
|
||||
file := provider.NewFileProvider("routes.yaml")
|
||||
docker := provider.NewDockerProvider("routes", "unix:///var/run/docker.sock")
|
||||
agent := provider.NewAgentProvider(agent.TestAgentConfig("routes", "192.168.1.100:8080"))
|
||||
|
||||
assert.True(t, file.String() != docker.String())
|
||||
assert.True(t, file.String() != agent.String())
|
||||
assert.True(t, docker.String() != agent.String())
|
||||
}
|
||||
|
||||
func TestFileProviderNameFromFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expectedName string
|
||||
}{
|
||||
{"routes.yaml", "routes"},
|
||||
{"service.yml", "service"},
|
||||
{"complex-name.yaml", "complex-name"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filename, func(t *testing.T) {
|
||||
p := provider.NewFileProvider(tt.filename)
|
||||
assert.Equal(t, tt.expectedName, p.ShortName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerProviderString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
dockerHost string
|
||||
expected string
|
||||
}{
|
||||
{"docker1", "unix:///var/run/docker.sock", "docker@docker1"},
|
||||
{"host2", "tcp://192.168.1.100:2375", "docker@host2"},
|
||||
{"explicit!", "unix:///var/run/docker.sock", "docker@explicit!"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := provider.NewDockerProvider(tt.name, tt.dockerHost)
|
||||
assert.Equal(t, tt.expected, p.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitOnlyProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedFlag bool
|
||||
}{
|
||||
{"docker", false},
|
||||
{"explicit!", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := provider.NewDockerProvider(tt.name, "unix:///var/run/docker.sock")
|
||||
assert.Equal(t, tt.expectedFlag, p.IsExplicitOnly())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
)
|
||||
@@ -55,32 +51,3 @@ func (cfg *Config) Statistics() map[string]any {
|
||||
"providers": providerStats,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error) {
|
||||
if slices.ContainsFunc(cfg.value.Providers.Agents, func(a *agent.AgentConfig) bool {
|
||||
return a.Addr == host
|
||||
}) {
|
||||
return 0, gperr.New("agent already exists")
|
||||
}
|
||||
|
||||
agentCfg := new(agent.AgentConfig)
|
||||
agentCfg.Addr = host
|
||||
err := agentCfg.InitWithCerts(cfg.task.Context(), ca.Cert, client.Cert, client.Key)
|
||||
if err != nil {
|
||||
return 0, gperr.Wrap(err, "failed to start agent")
|
||||
}
|
||||
// must add it first to let LoadRoutes() reference from it
|
||||
agent.Agents.Add(agentCfg)
|
||||
|
||||
provider := provider.NewAgentProvider(agentCfg)
|
||||
if err := cfg.errIfExists(provider); err != nil {
|
||||
agent.Agents.Del(agentCfg)
|
||||
return 0, err
|
||||
}
|
||||
err = provider.LoadRoutes()
|
||||
if err != nil {
|
||||
agent.Agents.Del(agentCfg)
|
||||
return 0, gperr.Wrap(err, "failed to load routes")
|
||||
}
|
||||
return provider.NumRoutes(), nil
|
||||
}
|
||||
|
||||
@@ -2,24 +2,22 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/acl"
|
||||
"github.com/yusing/go-proxy/internal/autocert"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
"github.com/yusing/go-proxy/internal/proxmox"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
ACL *acl.Config `json:"acl"`
|
||||
AutoCert *autocert.AutocertConfig `json:"autocert"`
|
||||
Entrypoint Entrypoint `json:"entrypoint"`
|
||||
Providers Providers `json:"providers"`
|
||||
@@ -28,15 +26,17 @@ type (
|
||||
TimeoutShutdown int `json:"timeout_shutdown" validate:"gte=0"`
|
||||
}
|
||||
Providers struct {
|
||||
Files []string `json:"include" validate:"unique,dive,config_file_exists"`
|
||||
Docker map[string]string `json:"docker" validate:"unique,dive,unix_addr|url"`
|
||||
Proxmox []proxmox.Config `json:"proxmox"`
|
||||
Agents []*agent.AgentConfig `json:"agents" validate:"unique=Addr"`
|
||||
Notification []notif.NotificationConfig `json:"notification" validate:"unique=ProviderName"`
|
||||
Files []string `json:"include" yaml:"include,omitempty" validate:"dive,filepath"`
|
||||
Docker map[string]string `json:"docker" yaml:"docker,omitempty" validate:"non_empty_docker_keys,dive,unix_addr|url"`
|
||||
Agents []*agent.AgentConfig `json:"agents" yaml:"agents,omitempty"`
|
||||
Notification []notif.NotificationConfig `json:"notification" yaml:"notification,omitempty"`
|
||||
}
|
||||
Entrypoint struct {
|
||||
Middlewares []map[string]any `json:"middlewares"`
|
||||
AccessLog *accesslog.Config `json:"access_log" validate:"omitempty"`
|
||||
Middlewares []map[string]any `json:"middlewares"`
|
||||
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
|
||||
}
|
||||
HomepageConfig struct {
|
||||
UseDefaultCategories bool `json:"use_default_categories"`
|
||||
}
|
||||
|
||||
ConfigInstance interface {
|
||||
@@ -45,7 +45,9 @@ type (
|
||||
Statistics() map[string]any
|
||||
RouteProviderList() []string
|
||||
Context() context.Context
|
||||
GetAgent(agentAddrOrDockerHost string) (*agent.AgentConfig, bool)
|
||||
VerifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair) (int, gperr.Error)
|
||||
ListAgents() []*agent.AgentConfig
|
||||
AutoCertProvider() *autocert.Provider
|
||||
}
|
||||
)
|
||||
@@ -84,7 +86,7 @@ func HasInstance() bool {
|
||||
|
||||
func Validate(data []byte) gperr.Error {
|
||||
var model Config
|
||||
return utils.UnmarshalValidateYAML(data, &model)
|
||||
return utils.DeserializeYAML(data, &model)
|
||||
}
|
||||
|
||||
var matchDomainsRegex = regexp.MustCompile(`^[^\.]?([\w\d\-_]\.?)+[^\.]?$`)
|
||||
@@ -100,9 +102,13 @@ func init() {
|
||||
}
|
||||
return true
|
||||
})
|
||||
utils.MustRegisterValidation("config_file_exists", func(fl validator.FieldLevel) bool {
|
||||
filename := fl.Field().Interface().(string)
|
||||
info, err := os.Stat(path.Join(common.ConfigDir, filename))
|
||||
return err == nil && !info.IsDir()
|
||||
utils.MustRegisterValidation("non_empty_docker_keys", func(fl validator.FieldLevel) bool {
|
||||
m := fl.Field().Interface().(map[string]string)
|
||||
for k := range m {
|
||||
if k == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package config
|
||||
|
||||
type HomepageConfig struct {
|
||||
UseDefaultCategories bool `json:"use_default_categories"`
|
||||
}
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -46,7 +46,7 @@ const (
|
||||
)
|
||||
|
||||
func initClientCleaner() {
|
||||
cleaner := task.RootTask("docker_clients_cleaner")
|
||||
cleaner := task.RootTask("docker_clients_cleaner", false)
|
||||
go func() {
|
||||
ticker := time.NewTicker(cleanInterval)
|
||||
defer ticker.Stop()
|
||||
@@ -125,7 +125,7 @@ func NewClient(host string) (*SharedClient, error) {
|
||||
var dial func(ctx context.Context) (net.Conn, error)
|
||||
|
||||
if agent.IsDockerHostAgent(host) {
|
||||
cfg, ok := agent.Agents.Get(host)
|
||||
cfg, ok := config.GetInstance().GetAgent(host)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("agent %q not found", host))
|
||||
}
|
||||
@@ -220,12 +220,3 @@ func (c *SharedClient) Close() {
|
||||
atomic.StoreInt64(&c.closedOn, time.Now().Unix())
|
||||
atomic.AddUint32(&c.refCount, ^uint32(0))
|
||||
}
|
||||
|
||||
func (c *SharedClient) MarshalMap() map[string]any {
|
||||
return map[string]any{
|
||||
"host": c.DaemonHost(),
|
||||
"addr": c.addr,
|
||||
"ref_count": c.refCount,
|
||||
"closed_on": strutils.FormatUnixTime(c.closedOn),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,19 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/yusing/go-proxy/agent/pkg/agent"
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/gperr"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
PortMapping = map[int]*container.Port
|
||||
PortMapping = map[int]container.Port
|
||||
Container struct {
|
||||
_ utils.NoCopy
|
||||
_ U.NoCopy
|
||||
|
||||
DockerHost string `json:"docker_host"`
|
||||
Image *ContainerImage `json:"image"`
|
||||
@@ -27,7 +27,7 @@ type (
|
||||
|
||||
Agent *agent.AgentConfig `json:"agent"`
|
||||
|
||||
RouteConfig map[string]string `json:"route_config"`
|
||||
Labels map[string]string `json:"-"`
|
||||
IdlewatcherConfig *idlewatcher.Config `json:"idlewatcher_config"`
|
||||
|
||||
Mounts []string `json:"mounts"`
|
||||
@@ -51,29 +51,40 @@ type (
|
||||
|
||||
var DummyContainer = new(Container)
|
||||
|
||||
func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
||||
func FromDocker(c *container.SummaryTrimmed, dockerHost string) (res *Container) {
|
||||
isExplicit := false
|
||||
helper := containerHelper{c}
|
||||
for lbl := range c.Labels {
|
||||
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||
isExplicit = true
|
||||
} else {
|
||||
delete(c.Labels, lbl)
|
||||
}
|
||||
}
|
||||
|
||||
isExcluded, _ := strconv.ParseBool(helper.getDeleteLabel(LabelExclude))
|
||||
res = &Container{
|
||||
DockerHost: dockerHost,
|
||||
Image: helper.parseImage(),
|
||||
ContainerName: helper.getName(),
|
||||
ContainerID: c.ID,
|
||||
|
||||
Labels: c.Labels,
|
||||
|
||||
Mounts: helper.getMounts(),
|
||||
|
||||
PublicPortMapping: helper.getPublicPortMapping(),
|
||||
PrivatePortMapping: helper.getPrivatePortMapping(),
|
||||
|
||||
Aliases: helper.getAliases(),
|
||||
IsExcluded: strutils.ParseBool(helper.getDeleteLabel(LabelExclude)),
|
||||
IsExcluded: isExcluded,
|
||||
IsExplicit: isExplicit,
|
||||
Running: c.Status == "running" || c.State == "running",
|
||||
}
|
||||
|
||||
if agent.IsDockerHostAgent(dockerHost) {
|
||||
var ok bool
|
||||
res.Agent, ok = agent.Agents.Get(dockerHost)
|
||||
res.Agent, ok = config.GetInstance().GetAgent(dockerHost)
|
||||
if !ok {
|
||||
logging.Error().Msgf("agent %q not found", dockerHost)
|
||||
}
|
||||
@@ -82,53 +93,9 @@ func FromDocker(c *container.Summary, dockerHost string) (res *Container) {
|
||||
res.setPrivateHostname(helper)
|
||||
res.setPublicHostname()
|
||||
res.loadDeleteIdlewatcherLabels(helper)
|
||||
|
||||
for lbl := range c.Labels {
|
||||
if strings.HasPrefix(lbl, NSProxy+".") {
|
||||
isExplicit = true
|
||||
} else {
|
||||
delete(c.Labels, lbl)
|
||||
}
|
||||
}
|
||||
res.RouteConfig = utils.FitMap(c.Labels)
|
||||
return
|
||||
}
|
||||
|
||||
func FromInspectResponse(json container.InspectResponse, dockerHost string) *Container {
|
||||
ports := make([]container.Port, 0, len(json.NetworkSettings.Ports))
|
||||
for k, bindings := range json.NetworkSettings.Ports {
|
||||
proto, privPortStr := nat.SplitProtoPort(string(k))
|
||||
privPort, _ := strconv.ParseUint(privPortStr, 10, 16)
|
||||
ports = append(ports, container.Port{
|
||||
PrivatePort: uint16(privPort),
|
||||
Type: proto,
|
||||
})
|
||||
for _, v := range bindings {
|
||||
pubPort, _ := strconv.ParseUint(v.HostPort, 10, 16)
|
||||
ports = append(ports, container.Port{
|
||||
IP: v.HostIP,
|
||||
PublicPort: uint16(pubPort),
|
||||
PrivatePort: uint16(privPort),
|
||||
Type: proto,
|
||||
})
|
||||
}
|
||||
}
|
||||
cont := FromDocker(&container.Summary{
|
||||
ID: json.ID,
|
||||
Names: []string{strings.TrimPrefix(json.Name, "/")},
|
||||
Image: json.Image,
|
||||
Ports: ports,
|
||||
Labels: json.Config.Labels,
|
||||
State: json.State.Status,
|
||||
Status: json.State.Status,
|
||||
Mounts: json.Mounts,
|
||||
NetworkSettings: &container.NetworkSettingsSummary{
|
||||
Networks: json.NetworkSettings.Networks,
|
||||
},
|
||||
}, dockerHost)
|
||||
return cont
|
||||
}
|
||||
|
||||
func (c *Container) IsBlacklisted() bool {
|
||||
return c.Image.IsBlacklisted() || c.isDatabase()
|
||||
}
|
||||
@@ -211,7 +178,7 @@ func (c *Container) loadDeleteIdlewatcherLabels(helper containerHelper) {
|
||||
ContainerName: c.ContainerName,
|
||||
},
|
||||
}
|
||||
err := utils.MapUnmarshalValidate(cfg, idwCfg)
|
||||
err := utils.Deserialize(cfg, idwCfg)
|
||||
if err != nil {
|
||||
gperr.LogWarn("invalid idlewatcher config", gperr.PrependSubject(c.ContainerName, err))
|
||||
} else {
|
||||
|
||||
@@ -4,12 +4,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type containerHelper struct {
|
||||
*container.Summary
|
||||
*container.SummaryTrimmed
|
||||
}
|
||||
|
||||
// getDeleteLabel gets the value of a label and then deletes it from the container.
|
||||
@@ -63,15 +62,15 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[int(v.PublicPort)] = &v
|
||||
}
|
||||
return utils.FitMap(res)
|
||||
}
|
||||
|
||||
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
||||
res := make(PortMapping, len(c.Ports))
|
||||
for _, v := range c.Ports {
|
||||
res[int(v.PrivatePort)] = &v
|
||||
res[int(v.PublicPort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
res[int(v.PrivatePort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestContainerExplicit(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := FromDocker(&container.Summary{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
||||
c := FromDocker(&container.SummaryTrimmed{Names: []string{"test"}, State: "test", Labels: tt.labels}, "")
|
||||
ExpectEqual(t, c.IsExplicit, tt.isExplicit)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Inspect(dockerHost string, containerID string) (*Container, error) {
|
||||
client, err := NewClient(dockerHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
return client.Inspect(containerID)
|
||||
}
|
||||
|
||||
func (c *SharedClient) Inspect(containerID string) (*Container, error) {
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 3*time.Second, errors.New("docker container inspect timeout"))
|
||||
defer cancel()
|
||||
|
||||
json, err := c.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromInspectResponse(json, c.DaemonHost()), nil
|
||||
}
|
||||
@@ -21,7 +21,7 @@ var listOptions = container.ListOptions{
|
||||
All: true,
|
||||
}
|
||||
|
||||
func ListContainers(clientHost string) ([]container.Summary, error) {
|
||||
func ListContainers(clientHost string) ([]container.SummaryTrimmed, error) {
|
||||
dockerClient, err := NewClient(clientHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/logging/accesslog"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/gphttp"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
|
||||
"github.com/yusing/go-proxy/internal/net/gphttp/middleware/errorpage"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
@@ -20,7 +19,7 @@ import (
|
||||
type Entrypoint struct {
|
||||
middleware *middleware.Middleware
|
||||
accessLogger *accesslog.AccessLogger
|
||||
findRouteFunc func(host string) (route.HTTPRoute, error)
|
||||
findRouteFunc func(host string) (routes.HTTPRoute, error)
|
||||
}
|
||||
|
||||
var ErrNoSuchRoute = errors.New("no such route")
|
||||
@@ -55,7 +54,7 @@ func (ep *Entrypoint) SetMiddlewares(mws []map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.Config) (err error) {
|
||||
func (ep *Entrypoint) SetAccessLogger(parent task.Parent, cfg *accesslog.RequestLoggerConfig) (err error) {
|
||||
if cfg == nil {
|
||||
ep.accessLogger = nil
|
||||
return
|
||||
@@ -108,7 +107,7 @@ func (ep *Entrypoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func findRouteAnyDomain(host string) (route.HTTPRoute, error) {
|
||||
func findRouteAnyDomain(host string) (routes.HTTPRoute, error) {
|
||||
hostSplit := strutils.SplitRune(host, '.')
|
||||
target := hostSplit[0]
|
||||
|
||||
@@ -118,19 +117,19 @@ func findRouteAnyDomain(host string) (route.HTTPRoute, error) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, target)
|
||||
}
|
||||
|
||||
func findRouteByDomains(domains []string) func(host string) (route.HTTPRoute, error) {
|
||||
return func(host string) (route.HTTPRoute, error) {
|
||||
func findRouteByDomains(domains []string) func(host string) (routes.HTTPRoute, error) {
|
||||
return func(host string) (routes.HTTPRoute, error) {
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(host, domain) {
|
||||
target := strings.TrimSuffix(host, domain)
|
||||
if r, ok := routes.GetHTTPRoute(target); ok {
|
||||
if r, ok := routes.HTTP.Get(target); ok {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to exact match
|
||||
if r, ok := routes.GetHTTPRoute(host); ok {
|
||||
if r, ok := routes.HTTP.Get(host); ok {
|
||||
return r, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchRoute, host)
|
||||
|
||||
@@ -5,37 +5,43 @@ import (
|
||||
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
r route.ReveseProxyRoute
|
||||
ep = NewEntrypoint()
|
||||
)
|
||||
var ep = NewEntrypoint()
|
||||
|
||||
func addRoute(alias string) {
|
||||
routes.HTTP.Add(&route.ReveseProxyRoute{
|
||||
Route: &route.Route{
|
||||
Alias: alias,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func run(t *testing.T, match []string, noMatch []string) {
|
||||
t.Helper()
|
||||
t.Cleanup(routes.TestClear)
|
||||
t.Cleanup(routes.Clear)
|
||||
t.Cleanup(func() { ep.SetFindRouteDomains(nil) })
|
||||
|
||||
for _, test := range match {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
found, err := ep.findRouteFunc(test)
|
||||
ExpectNoError(t, err)
|
||||
ExpectTrue(t, found == &r)
|
||||
expect.NoError(t, err)
|
||||
expect.NotNil(t, found)
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range noMatch {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
_, err := ep.findRouteFunc(test)
|
||||
ExpectError(t, ErrNoSuchRoute, err)
|
||||
expect.ErrorIs(t, ErrNoSuchRoute, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindRouteAnyDomain(t *testing.T) {
|
||||
routes.SetHTTPRoute("app1", &r)
|
||||
addRoute("app1")
|
||||
|
||||
tests := []string{
|
||||
"app1.com",
|
||||
@@ -66,7 +72,7 @@ func TestFindRouteExactHostMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
routes.SetHTTPRoute(test, &r)
|
||||
addRoute(test)
|
||||
}
|
||||
|
||||
run(t, tests, testsNoMatch)
|
||||
@@ -78,7 +84,7 @@ func TestFindRouteByDomains(t *testing.T) {
|
||||
".sub.domain.com",
|
||||
})
|
||||
|
||||
routes.SetHTTPRoute("app1", &r)
|
||||
addRoute("app1")
|
||||
|
||||
tests := []string{
|
||||
"app1.domain.com",
|
||||
@@ -103,7 +109,7 @@ func TestFindRouteByDomainsExactMatch(t *testing.T) {
|
||||
".sub.domain.com",
|
||||
})
|
||||
|
||||
routes.SetHTTPRoute("app1.foo.bar", &r)
|
||||
addRoute("app1.foo.bar")
|
||||
|
||||
tests := []string{
|
||||
"app1.foo.bar", // exact match
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
)
|
||||
|
||||
// baseError is an immutable wrapper around an error.
|
||||
@@ -49,6 +48,17 @@ func (err *baseError) Error() string {
|
||||
return err.Err.Error()
|
||||
}
|
||||
|
||||
func (err *baseError) MarshalJSONTo(buf []byte) []byte {
|
||||
return json.MarshalTo(err.Err, buf)
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (err *baseError) MarshalJSON() ([]byte, error) {
|
||||
//nolint:errorlint
|
||||
switch err := err.Err.(type) {
|
||||
case Error, *withSubject:
|
||||
return json.Marshal(err)
|
||||
case json.Marshaler:
|
||||
return err.MarshalJSON()
|
||||
case interface{ MarshalText() ([]byte, error) }:
|
||||
return err.MarshalText()
|
||||
default:
|
||||
return json.Marshal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +77,15 @@ func (b *Builder) String() string {
|
||||
// Add adds an error to the Builder.
|
||||
//
|
||||
// adding nil is no-op.
|
||||
func (b *Builder) Add(err error) *Builder {
|
||||
func (b *Builder) Add(err error) {
|
||||
if err == nil {
|
||||
return b
|
||||
return
|
||||
}
|
||||
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
|
||||
b.add(err)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) add(err error) {
|
||||
@@ -106,14 +105,13 @@ func (b *Builder) add(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) Adds(err string) *Builder {
|
||||
func (b *Builder) Adds(err string) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
b.errs = append(b.errs, newError(err))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) Addf(format string, args ...any) *Builder {
|
||||
func (b *Builder) Addf(format string, args ...any) {
|
||||
if len(args) > 0 {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
@@ -121,13 +119,11 @@ func (b *Builder) Addf(format string, args ...any) *Builder {
|
||||
} else {
|
||||
b.Adds(format)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
|
||||
func (b *Builder) AddFrom(other *Builder, flatten bool) {
|
||||
if other == nil || !other.HasError() {
|
||||
return b
|
||||
return
|
||||
}
|
||||
|
||||
b.Lock()
|
||||
@@ -137,10 +133,9 @@ func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder {
|
||||
} else {
|
||||
b.errs = append(b.errs, other.Error())
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) AddRange(errs ...error) *Builder {
|
||||
func (b *Builder) AddRange(errs ...error) {
|
||||
nonNilErrs := make([]error, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
@@ -154,7 +149,6 @@ func (b *Builder) AddRange(errs ...error) *Builder {
|
||||
for _, err := range nonNilErrs {
|
||||
b.add(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Builder) ForEach(fn func(error)) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestBaseString(t *testing.T) {
|
||||
ExpectEqual(t, New("error").Error(), "error")
|
||||
expect.Equal(t, New("error").Error(), "error")
|
||||
}
|
||||
|
||||
func TestBaseWithSubject(t *testing.T) {
|
||||
@@ -18,13 +18,13 @@ func TestBaseWithSubject(t *testing.T) {
|
||||
withSubject := err.Subject("foo")
|
||||
withSubjectf := err.Subjectf("%s %s", "foo", "bar")
|
||||
|
||||
ExpectError(t, err, withSubject)
|
||||
ExpectEqual(t, ansi.StripANSI(withSubject.Error()), "foo: error")
|
||||
ExpectTrue(t, withSubject.Is(err))
|
||||
expect.ErrorIs(t, err, withSubject)
|
||||
expect.Equal(t, ansi.StripANSI(withSubject.Error()), "foo: error")
|
||||
expect.True(t, withSubject.Is(err))
|
||||
|
||||
ExpectError(t, err, withSubjectf)
|
||||
ExpectEqual(t, ansi.StripANSI(withSubjectf.Error()), "foo bar: error")
|
||||
ExpectTrue(t, withSubjectf.Is(err))
|
||||
expect.ErrorIs(t, err, withSubjectf)
|
||||
expect.Equal(t, ansi.StripANSI(withSubjectf.Error()), "foo bar: error")
|
||||
expect.True(t, withSubjectf.Is(err))
|
||||
}
|
||||
|
||||
func TestBaseWithExtra(t *testing.T) {
|
||||
@@ -32,22 +32,22 @@ func TestBaseWithExtra(t *testing.T) {
|
||||
extra := New("bar").Subject("baz")
|
||||
withExtra := err.With(extra)
|
||||
|
||||
ExpectTrue(t, withExtra.Is(extra))
|
||||
ExpectTrue(t, withExtra.Is(err))
|
||||
expect.True(t, withExtra.Is(extra))
|
||||
expect.True(t, withExtra.Is(err))
|
||||
|
||||
ExpectTrue(t, errors.Is(withExtra, extra))
|
||||
ExpectTrue(t, errors.Is(withExtra, err))
|
||||
expect.True(t, errors.Is(withExtra, extra))
|
||||
expect.True(t, errors.Is(withExtra, err))
|
||||
|
||||
ExpectTrue(t, strings.Contains(withExtra.Error(), err.Error()))
|
||||
ExpectTrue(t, strings.Contains(withExtra.Error(), extra.Error()))
|
||||
ExpectTrue(t, strings.Contains(withExtra.Error(), "baz"))
|
||||
expect.True(t, strings.Contains(withExtra.Error(), err.Error()))
|
||||
expect.True(t, strings.Contains(withExtra.Error(), extra.Error()))
|
||||
expect.True(t, strings.Contains(withExtra.Error(), "baz"))
|
||||
}
|
||||
|
||||
func TestBaseUnwrap(t *testing.T) {
|
||||
err := errors.New("err")
|
||||
wrapped := Wrap(err)
|
||||
|
||||
ExpectError(t, err, errors.Unwrap(wrapped))
|
||||
expect.ErrorIs(t, err, errors.Unwrap(wrapped))
|
||||
}
|
||||
|
||||
func TestNestedUnwrap(t *testing.T) {
|
||||
@@ -56,24 +56,24 @@ func TestNestedUnwrap(t *testing.T) {
|
||||
wrapped := Wrap(err).Subject("foo").With(err2.Subject("bar"))
|
||||
|
||||
unwrapper, ok := wrapped.(interface{ Unwrap() []error })
|
||||
ExpectTrue(t, ok)
|
||||
expect.True(t, ok)
|
||||
|
||||
ExpectError(t, err, wrapped)
|
||||
ExpectError(t, err2, wrapped)
|
||||
ExpectEqual(t, len(unwrapper.Unwrap()), 2)
|
||||
expect.ErrorIs(t, err, wrapped)
|
||||
expect.ErrorIs(t, err2, wrapped)
|
||||
expect.Equal(t, len(unwrapper.Unwrap()), 2)
|
||||
}
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
from := errors.New("error")
|
||||
err := Wrap(from)
|
||||
ExpectError(t, from, err)
|
||||
expect.ErrorIs(t, from, err)
|
||||
|
||||
ExpectTrue(t, err.Is(from))
|
||||
ExpectFalse(t, err.Is(New("error")))
|
||||
expect.True(t, err.Is(from))
|
||||
expect.False(t, err.Is(New("error")))
|
||||
|
||||
ExpectTrue(t, errors.Is(err.Subject("foo"), from))
|
||||
ExpectTrue(t, errors.Is(err.Withf("foo"), from))
|
||||
ExpectTrue(t, errors.Is(err.Subject("foo").Withf("bar"), from))
|
||||
expect.True(t, errors.Is(err.Subject("foo"), from))
|
||||
expect.True(t, errors.Is(err.Withf("foo"), from))
|
||||
expect.True(t, errors.Is(err.Subject("foo").Withf("bar"), from))
|
||||
}
|
||||
|
||||
func TestErrorImmutability(t *testing.T) {
|
||||
@@ -83,14 +83,14 @@ func TestErrorImmutability(t *testing.T) {
|
||||
for range 3 {
|
||||
// t.Logf("%d: %v %T %s", i, errors.Unwrap(err), err, err)
|
||||
_ = err.Subject("foo")
|
||||
ExpectFalse(t, strings.Contains(err.Error(), "foo"))
|
||||
expect.False(t, strings.Contains(err.Error(), "foo"))
|
||||
|
||||
_ = err.With(err2)
|
||||
ExpectFalse(t, strings.Contains(err.Error(), "extra"))
|
||||
ExpectFalse(t, err.Is(err2))
|
||||
expect.False(t, strings.Contains(err.Error(), "extra"))
|
||||
expect.False(t, err.Is(err2))
|
||||
|
||||
err = err.Subject("bar").Withf("baz")
|
||||
ExpectTrue(t, err != nil)
|
||||
expect.True(t, err != nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,24 +100,24 @@ func TestErrorWith(t *testing.T) {
|
||||
|
||||
err3 := err1.With(err2)
|
||||
|
||||
ExpectTrue(t, err3.Is(err1))
|
||||
ExpectTrue(t, err3.Is(err2))
|
||||
expect.True(t, err3.Is(err1))
|
||||
expect.True(t, err3.Is(err2))
|
||||
|
||||
_ = err2.Subject("foo")
|
||||
|
||||
ExpectTrue(t, err3.Is(err1))
|
||||
ExpectTrue(t, err3.Is(err2))
|
||||
expect.True(t, err3.Is(err1))
|
||||
expect.True(t, err3.Is(err2))
|
||||
|
||||
// check if err3 is affected by err2.Subject
|
||||
ExpectFalse(t, strings.Contains(err3.Error(), "foo"))
|
||||
expect.False(t, strings.Contains(err3.Error(), "foo"))
|
||||
}
|
||||
|
||||
func TestErrorStringSimple(t *testing.T) {
|
||||
errFailure := New("generic failure")
|
||||
ne := errFailure.Subject("foo bar")
|
||||
ExpectEqual(t, ansi.StripANSI(ne.Error()), "foo bar: generic failure")
|
||||
expect.Equal(t, ansi.StripANSI(ne.Error()), "foo bar: generic failure")
|
||||
ne = ne.Subject("baz")
|
||||
ExpectEqual(t, ansi.StripANSI(ne.Error()), "baz > foo bar: generic failure")
|
||||
expect.Equal(t, ansi.StripANSI(ne.Error()), "baz > foo bar: generic failure")
|
||||
}
|
||||
|
||||
func TestErrorStringNested(t *testing.T) {
|
||||
@@ -154,5 +154,5 @@ func TestErrorStringNested(t *testing.T) {
|
||||
• action 3 > inner3: generic failure
|
||||
• 3
|
||||
• 3`
|
||||
ExpectEqual(t, ansi.StripANSI(ne.Error()), want)
|
||||
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
)
|
||||
|
||||
func log(_ string, err error, level zerolog.Level, logger ...*zerolog.Logger) {
|
||||
func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger) {
|
||||
var l *zerolog.Logger
|
||||
if len(logger) > 0 {
|
||||
l = logger[0]
|
||||
} else {
|
||||
l = logging.GetLogger()
|
||||
}
|
||||
l.WithLevel(level).Msg(err.Error())
|
||||
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
|
||||
switch level {
|
||||
case zerolog.FatalLevel:
|
||||
os.Exit(1)
|
||||
case zerolog.PanicLevel:
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func LogFatal(msg string, err error, logger ...*zerolog.Logger) {
|
||||
|
||||
@@ -27,7 +27,7 @@ func (m *MultilineError) Adds(s string) *MultilineError {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *MultilineError) AddLines(lines any) *MultilineError {
|
||||
func (m *MultilineError) AddLines(lines ...any) *MultilineError {
|
||||
v := reflect.ValueOf(lines)
|
||||
if v.Kind() == reflect.Slice {
|
||||
for i := range v.Len() {
|
||||
|
||||
@@ -4,18 +4,9 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestMultiline(t *testing.T) {
|
||||
multiline := Multiline()
|
||||
multiline.Addf("line 1 %s", "test")
|
||||
multiline.Adds("line 2")
|
||||
multiline.AddLines([]any{1, "2", 3.0, net.IPv4(127, 0, 0, 1)})
|
||||
t.Error(New("result").With(multiline))
|
||||
t.Error(multiline.Subject("subject").Withf("inner"))
|
||||
}
|
||||
|
||||
func TestWrapMultiline(t *testing.T) {
|
||||
multiline := Multiline()
|
||||
var wrapper error = wrap(multiline)
|
||||
@@ -34,5 +25,5 @@ func TestPrependSubjectMultiline(t *testing.T) {
|
||||
|
||||
builder := NewBuilder()
|
||||
builder.Add(multiline)
|
||||
require.Equal(t, len(builder.errs), len(multiline.Extras), builder.errs)
|
||||
expect.Equal(t, len(multiline.Extras), len(builder.errs))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
)
|
||||
|
||||
@@ -28,10 +28,6 @@ func PrependSubject(subject string, err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if subject == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:errorlint
|
||||
switch err := err.(type) {
|
||||
case *withSubject:
|
||||
@@ -93,13 +89,17 @@ func (err *withSubject) Error() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (err *withSubject) MarshalJSONTo(buf []byte) []byte {
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (err *withSubject) MarshalJSON() ([]byte, error) {
|
||||
subjects := slices.Clone(err.Subjects)
|
||||
slices.Reverse(subjects)
|
||||
|
||||
reversed := map[string]any{
|
||||
"subjects": subjects,
|
||||
"err": err.Err,
|
||||
reversed := struct {
|
||||
Subjects []string `json:"subjects"`
|
||||
Err error `json:"err"`
|
||||
}{
|
||||
Subjects: subjects,
|
||||
Err: err.Err,
|
||||
}
|
||||
return json.MarshalTo(reversed, buf)
|
||||
|
||||
return json.Marshal(reversed)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
stdJSON "encoding/json"
|
||||
)
|
||||
|
||||
func newError(message string) error {
|
||||
@@ -73,7 +72,7 @@ func IsJSONMarshallable(err error) bool {
|
||||
case *baseError:
|
||||
return IsJSONMarshallable(err.Err)
|
||||
default:
|
||||
var v stdJSON.Marshaler
|
||||
var v json.Marshaler
|
||||
return errors.As(err, &v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,8 +25,10 @@ type FetchResult struct {
|
||||
contentType string
|
||||
}
|
||||
|
||||
const faviconFetchTimeout = 3 * time.Second
|
||||
|
||||
func (res *FetchResult) OK() bool {
|
||||
return res.Icon != nil
|
||||
return len(res.Icon) > 0
|
||||
}
|
||||
|
||||
func (res *FetchResult) ContentType() string {
|
||||
@@ -40,39 +43,55 @@ func (res *FetchResult) ContentType() string {
|
||||
|
||||
const maxRedirectDepth = 5
|
||||
|
||||
func FetchFavIconFromURL(iconURL *IconURL) *FetchResult {
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) *FetchResult {
|
||||
switch iconURL.IconSource {
|
||||
case IconSourceAbsolute:
|
||||
return fetchIconAbsolute(iconURL.URL())
|
||||
return fetchIconAbsolute(ctx, iconURL.URL())
|
||||
case IconSourceRelative:
|
||||
return &FetchResult{StatusCode: http.StatusBadRequest, ErrMsg: "unexpected relative icon"}
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
return fetchKnownIcon(iconURL)
|
||||
return fetchKnownIcon(ctx, iconURL)
|
||||
}
|
||||
return &FetchResult{StatusCode: http.StatusBadRequest, ErrMsg: "invalid icon source"}
|
||||
}
|
||||
|
||||
func fetchIconAbsolute(url string) *FetchResult {
|
||||
func fetchIconAbsolute(ctx context.Context, url string) *FetchResult {
|
||||
if result := loadIconCache(url); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
resp, err := gphttp.Get(url)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
if err == nil {
|
||||
err = errors.New(resp.Status)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "request timeout"}
|
||||
}
|
||||
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: err.Error()}
|
||||
}
|
||||
|
||||
resp, err := gphttp.Do(req)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "connection error"}
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
icon, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return &FetchResult{StatusCode: http.StatusInternalServerError, ErrMsg: "internal error"}
|
||||
}
|
||||
|
||||
storeIconCache(url, icon)
|
||||
return &FetchResult{Icon: icon}
|
||||
if len(icon) == 0 {
|
||||
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "empty icon"}
|
||||
}
|
||||
|
||||
res := &FetchResult{Icon: icon}
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
res.contentType = contentType
|
||||
}
|
||||
// else leave it empty
|
||||
storeIconCache(url, res)
|
||||
return res
|
||||
}
|
||||
|
||||
var nameSanitizer = strings.NewReplacer(
|
||||
@@ -86,44 +105,53 @@ func sanitizeName(name string) string {
|
||||
return strings.ToLower(nameSanitizer.Replace(name))
|
||||
}
|
||||
|
||||
func fetchKnownIcon(url *IconURL) *FetchResult {
|
||||
func fetchKnownIcon(ctx context.Context, url *IconURL) *FetchResult {
|
||||
// if icon isn't in the list, no need to fetch
|
||||
if !url.HasIcon() {
|
||||
return &FetchResult{StatusCode: http.StatusNotFound, ErrMsg: "no such icon"}
|
||||
}
|
||||
|
||||
return fetchIconAbsolute(url.URL())
|
||||
return fetchIconAbsolute(ctx, url.URL())
|
||||
}
|
||||
|
||||
func fetchIcon(filetype, filename string) *FetchResult {
|
||||
result := fetchKnownIcon(NewSelfhStIconURL(filename, filetype))
|
||||
if result.Icon == nil {
|
||||
func fetchIcon(ctx context.Context, filetype, filename string) *FetchResult {
|
||||
result := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, filetype))
|
||||
if result.OK() {
|
||||
return result
|
||||
}
|
||||
return fetchKnownIcon(NewWalkXCodeIconURL(filename, filetype))
|
||||
return fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, filetype))
|
||||
}
|
||||
|
||||
func FindIcon(ctx context.Context, r route, uri string) *FetchResult {
|
||||
key := routeKey(r)
|
||||
if result := loadIconCache(key); result != nil {
|
||||
if result := loadIconCache(r.Key()); result != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
result := fetchIcon("png", sanitizeName(r.Reference()))
|
||||
result := fetchIcon(ctx, "png", sanitizeName(r.Reference()))
|
||||
if !result.OK() {
|
||||
if r, ok := r.(httpRoute); ok {
|
||||
// fallback to parse html
|
||||
result = findIconSlow(ctx, r, uri, 0)
|
||||
result = findIconSlow(ctx, r, uri, nil)
|
||||
}
|
||||
}
|
||||
if result.OK() {
|
||||
storeIconCache(key, result.Icon)
|
||||
storeIconCache(r.Key(), result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, depth int) *FetchResult {
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, 3*time.Second, errors.New("favicon request timeout"))
|
||||
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) *FetchResult {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "request timeout"}
|
||||
default:
|
||||
}
|
||||
|
||||
if len(stack) > maxRedirectDepth {
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "too many redirects"}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, faviconFetchTimeout, errors.New("favicon request timeout"))
|
||||
defer cancel()
|
||||
|
||||
newReq, err := http.NewRequestWithContext(ctx, "GET", r.TargetURL().String(), nil)
|
||||
@@ -149,14 +177,13 @@ func findIconSlow(ctx context.Context, r httpRoute, uri string, depth int) *Fetc
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "connection error"}
|
||||
default:
|
||||
if loc := c.Header().Get("Location"); loc != "" {
|
||||
if depth > maxRedirectDepth {
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "too many redirects"}
|
||||
}
|
||||
loc = strutils.SanitizeURI(loc)
|
||||
if loc == "/" || loc == newReq.URL.Path {
|
||||
if loc == "/" || loc == newReq.URL.Path || slices.Contains(stack, loc) {
|
||||
return &FetchResult{StatusCode: http.StatusBadGateway, ErrMsg: "circular redirect"}
|
||||
}
|
||||
return findIconSlow(ctx, r, loc, depth+1)
|
||||
// append current path to stack
|
||||
// handles redirect to the same path with different query
|
||||
return findIconSlow(ctx, r, loc, append(stack, newReq.URL.Path))
|
||||
}
|
||||
}
|
||||
return &FetchResult{StatusCode: c.status, ErrMsg: "upstream error: " + string(c.data)}
|
||||
@@ -188,8 +215,8 @@ func findIconSlow(ctx context.Context, r httpRoute, uri string, depth int) *Fetc
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(href, "http://"), strings.HasPrefix(href, "https://"):
|
||||
return fetchIconAbsolute(href)
|
||||
return fetchIconAbsolute(ctx, href)
|
||||
default:
|
||||
return findIconSlow(ctx, r, href, 0)
|
||||
return findIconSlow(ctx, r, href, append(stack, newReq.URL.Path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
config "github.com/yusing/go-proxy/internal/config/types"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
@@ -43,7 +42,7 @@ func (cfg *ItemConfig) GetOverride(alias string) *ItemConfig {
|
||||
return overrideConfigInstance.GetOverride(alias, cfg)
|
||||
}
|
||||
|
||||
func (item *Item) MarshalJSONTo(buf []byte) []byte {
|
||||
func (item *Item) MarshalJSON() ([]byte, error) {
|
||||
var url *string
|
||||
if !strings.ContainsRune(item.Alias, '.') {
|
||||
godoxyCfg := config.GetInstance().Value()
|
||||
@@ -56,7 +55,7 @@ func (item *Item) MarshalJSONTo(buf []byte) []byte {
|
||||
} else {
|
||||
url = &item.Alias
|
||||
}
|
||||
return json.MarshalTo(map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"show": item.Show,
|
||||
"alias": item.Alias,
|
||||
"provider": item.Provider,
|
||||
@@ -67,7 +66,7 @@ func (item *Item) MarshalJSONTo(buf []byte) []byte {
|
||||
"description": item.Description,
|
||||
"sort_order": item.SortOrder,
|
||||
"widget_config": item.WidgetConfig,
|
||||
}, buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (c Homepage) Add(item *Item) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
)
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
InitOverridesConfig()
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
ItemConfig: &ItemConfig{
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/atomic"
|
||||
)
|
||||
|
||||
type cacheEntry struct {
|
||||
Icon []byte `json:"icon"`
|
||||
LastAccess time.Time `json:"lastAccess"`
|
||||
Icon []byte `json:"icon"`
|
||||
ContentType string `json:"content_type"`
|
||||
LastAccess atomic.Value[time.Time] `json:"last_access"`
|
||||
}
|
||||
|
||||
// cache key can be absolute url or route name.
|
||||
@@ -25,7 +27,9 @@ var (
|
||||
|
||||
const (
|
||||
iconCacheTTL = 3 * 24 * time.Hour
|
||||
cleanUpInterval = time.Hour
|
||||
cleanUpInterval = time.Minute
|
||||
maxCacheSize = 1024 * 1024 // 1MB
|
||||
maxCacheEntries = 100
|
||||
)
|
||||
|
||||
func InitIconCache() {
|
||||
@@ -77,19 +81,29 @@ func pruneExpiredIconCache() {
|
||||
nPruned++
|
||||
}
|
||||
}
|
||||
if len(iconCache) > maxCacheEntries {
|
||||
newIconCache := make(map[string]*cacheEntry, maxCacheEntries)
|
||||
i := 0
|
||||
for key, icon := range iconCache {
|
||||
if i == maxCacheEntries {
|
||||
break
|
||||
}
|
||||
if !icon.IsExpired() {
|
||||
newIconCache[key] = icon
|
||||
i++
|
||||
}
|
||||
}
|
||||
iconCache = newIconCache
|
||||
}
|
||||
if nPruned > 0 {
|
||||
logging.Info().Int("pruned", nPruned).Msg("pruned expired icon cache")
|
||||
}
|
||||
}
|
||||
|
||||
func routeKey(r route) string {
|
||||
return r.ProviderName() + ":" + r.TargetName()
|
||||
}
|
||||
|
||||
func PruneRouteIconCache(route route) {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
delete(iconCache, routeKey(route))
|
||||
delete(iconCache, route.Key())
|
||||
}
|
||||
|
||||
func loadIconCache(key string) *FetchResult {
|
||||
@@ -97,41 +111,49 @@ func loadIconCache(key string) *FetchResult {
|
||||
defer iconCacheMu.RUnlock()
|
||||
|
||||
icon, ok := iconCache[key]
|
||||
if ok && icon != nil {
|
||||
if ok && len(icon.Icon) > 0 {
|
||||
logging.Debug().
|
||||
Str("key", key).
|
||||
Msg("icon found in cache")
|
||||
icon.LastAccess = time.Now()
|
||||
return &FetchResult{Icon: icon.Icon}
|
||||
icon.LastAccess.Store(time.Now())
|
||||
return &FetchResult{Icon: icon.Icon, contentType: icon.ContentType}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func storeIconCache(key string, icon []byte) {
|
||||
func storeIconCache(key string, result *FetchResult) {
|
||||
icon := result.Icon
|
||||
if len(icon) > maxCacheSize {
|
||||
logging.Debug().Int("size", len(icon)).Msg("icon cache size exceeds max cache size")
|
||||
return
|
||||
}
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
iconCache[key] = &cacheEntry{Icon: icon, LastAccess: time.Now()}
|
||||
entry := &cacheEntry{Icon: icon, ContentType: result.contentType}
|
||||
entry.LastAccess.Store(time.Now())
|
||||
iconCache[key] = entry
|
||||
logging.Debug().Str("key", key).Int("size", len(icon)).Msg("stored icon cache")
|
||||
}
|
||||
|
||||
func (e *cacheEntry) IsExpired() bool {
|
||||
return time.Since(e.LastAccess) > iconCacheTTL
|
||||
return time.Since(e.LastAccess.Load()) > iconCacheTTL
|
||||
}
|
||||
|
||||
func (e *cacheEntry) UnmarshalJSON(data []byte) error {
|
||||
attempt := struct {
|
||||
Icon []byte `json:"icon"`
|
||||
LastAccess time.Time `json:"lastAccess"`
|
||||
}{}
|
||||
err := json.Unmarshal(data, &attempt)
|
||||
if err == nil {
|
||||
e.Icon = attempt.Icon
|
||||
e.LastAccess = attempt.LastAccess
|
||||
return nil
|
||||
// check if data is json
|
||||
if json.Valid(data) {
|
||||
err := json.Unmarshal(data, &e)
|
||||
// return only if unmarshal is successful
|
||||
// otherwise fallback to base64
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// fallback to bytes
|
||||
err = json.Unmarshal(data, &e.Icon)
|
||||
// fallback to base64
|
||||
icon, err := base64.StdEncoding.DecodeString(string(data))
|
||||
if err == nil {
|
||||
e.LastAccess = time.Now()
|
||||
e.Icon = icon
|
||||
e.LastAccess.Store(time.Now())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -3,7 +3,7 @@ package homepage
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
expect "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestIconURL(t *testing.T) {
|
||||
@@ -114,11 +114,11 @@ func TestIconURL(t *testing.T) {
|
||||
u := &IconURL{}
|
||||
err := u.Parse(tc.input)
|
||||
if tc.wantErr {
|
||||
ExpectError(t, ErrInvalidIconURL, err)
|
||||
expect.ErrorIs(t, ErrInvalidIconURL, err)
|
||||
} else {
|
||||
tc.wantValue.FullValue = tc.input
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, u, tc.wantValue)
|
||||
expect.NoError(t, err)
|
||||
expect.Equal(t, u, tc.wantValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/pkg/json"
|
||||
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
@@ -60,15 +59,15 @@ func InitIconListCache() {
|
||||
DisplayNames: make(ReferenceDisplayNameMap),
|
||||
IconList: []string{},
|
||||
}
|
||||
// err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
|
||||
// if err != nil {
|
||||
// logging.Error().Err(err).Msg("failed to load icon list cache config")
|
||||
// } else if len(iconsCache.IconList) > 0 {
|
||||
// logging.Info().
|
||||
// Int("icons", len(iconsCache.IconList)).
|
||||
// Int("display_names", len(iconsCache.DisplayNames)).
|
||||
// Msg("icon list cache loaded")
|
||||
// }
|
||||
err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("failed to load icon list cache config")
|
||||
} else if len(iconsCache.IconList) > 0 {
|
||||
logging.Info().
|
||||
Int("icons", len(iconsCache.IconList)).
|
||||
Int("display_names", len(iconsCache.DisplayNames)).
|
||||
Msg("icon list cache loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func ListAvailableIcons() (*Cache, error) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/jsonstore"
|
||||
)
|
||||
|
||||
type OverrideConfig struct {
|
||||
@@ -17,39 +16,19 @@ type OverrideConfig struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var overrideConfigInstance = &OverrideConfig{
|
||||
ItemOverrides: make(map[string]*ItemConfig),
|
||||
DisplayOrder: make(map[string]int),
|
||||
CategoryOrder: make(map[string]int),
|
||||
ItemVisibility: make(map[string]bool),
|
||||
}
|
||||
|
||||
func InitOverridesConfig() {
|
||||
overrideConfigInstance.mu.Lock()
|
||||
defer overrideConfigInstance.mu.Unlock()
|
||||
|
||||
err := utils.LoadJSONIfExist(common.HomepageJSONConfigPath, overrideConfigInstance)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("failed to load homepage overrides config")
|
||||
} else if len(overrideConfigInstance.ItemOverrides) > 0 {
|
||||
logging.Info().
|
||||
Int("count", len(overrideConfigInstance.ItemOverrides)).
|
||||
Msg("homepage overrides config loaded")
|
||||
}
|
||||
task.OnProgramExit("save_homepage_json_config", func() {
|
||||
if len(overrideConfigInstance.ItemOverrides) == 0 {
|
||||
return
|
||||
}
|
||||
if err := utils.SaveJSON(common.HomepageJSONConfigPath, overrideConfigInstance, 0o644); err != nil {
|
||||
logging.Error().Err(err).Msg("failed to save homepage overrides config")
|
||||
}
|
||||
})
|
||||
}
|
||||
var overrideConfigInstance = jsonstore.Object[*OverrideConfig](common.NamespaceHomepageOverrides)
|
||||
|
||||
func GetOverrideConfig() *OverrideConfig {
|
||||
return overrideConfigInstance
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) Initialize() {
|
||||
c.ItemOverrides = make(map[string]*ItemConfig)
|
||||
c.DisplayOrder = make(map[string]int)
|
||||
c.CategoryOrder = make(map[string]int)
|
||||
c.ItemVisibility = make(map[string]bool)
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -59,9 +38,7 @@ func (c *OverrideConfig) OverrideItem(alias string, override *ItemConfig) {
|
||||
func (c *OverrideConfig) OverrideItems(items map[string]*ItemConfig) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for key, value := range items {
|
||||
c.ItemOverrides[key] = value
|
||||
}
|
||||
maps.Copy(c.ItemOverrides, items)
|
||||
}
|
||||
|
||||
func (c *OverrideConfig) GetOverride(alias string, item *ItemConfig) *ItemConfig {
|
||||
|
||||
@@ -2,14 +2,16 @@ package homepage
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
gpnet "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/utils/pool"
|
||||
)
|
||||
|
||||
type route interface {
|
||||
TargetName() string
|
||||
pool.Object
|
||||
ProviderName() string
|
||||
Reference() string
|
||||
TargetURL() *url.URL
|
||||
TargetURL() *gpnet.URL
|
||||
}
|
||||
|
||||
type httpRoute interface {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package idlewatcher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"strconv"
|
||||
|
||||
@@ -11,9 +12,9 @@ type watcherDebug struct {
|
||||
*Watcher
|
||||
}
|
||||
|
||||
func (w watcherDebug) MarshalMap() map[string]any {
|
||||
func (w watcherDebug) MarshalJSON() ([]byte, error) {
|
||||
state := w.state.Load()
|
||||
return map[string]any{
|
||||
return json.Marshal(map[string]any{
|
||||
"name": w.Name(),
|
||||
"state": map[string]string{
|
||||
"status": string(state.status),
|
||||
@@ -23,7 +24,7 @@ func (w watcherDebug) MarshalMap() map[string]any {
|
||||
"expires": strutils.FormatTime(w.expires()),
|
||||
"last_reset": strutils.FormatTime(w.lastReset.Load()),
|
||||
"config": w.cfg,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Watchers() iter.Seq2[string, watcherDebug] {
|
||||
|
||||
@@ -102,8 +102,8 @@ func checkUpdateState(key string) (w *Watcher, ready bool, err error) {
|
||||
return w, false, nil
|
||||
}
|
||||
|
||||
// MarshalMap implements health.HealthMonitor.
|
||||
func (w *Watcher) MarshalMap() map[string]any {
|
||||
// MarshalJSON implements health.HealthMonitor.
|
||||
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
||||
url := w.hc.URL()
|
||||
if url.Port() == "0" {
|
||||
url = nil
|
||||
@@ -118,5 +118,5 @@ func (w *Watcher) MarshalMap() map[string]any {
|
||||
Config: dummyHealthCheckConfig,
|
||||
URL: url,
|
||||
Detail: detail,
|
||||
}).MarshalMap()
|
||||
}).MarshalJSON()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user