mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 21:10:30 +01:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5cf867cd9 | ||
|
|
03ea9bb760 | ||
|
|
a1a5bf921e | ||
|
|
f1bfd13da3 | ||
|
|
b8900999a4 | ||
|
|
e6f77376b9 | ||
|
|
b2a6a20f10 | ||
|
|
05cbf99237 | ||
|
|
d5c0e62be1 | ||
|
|
a21bdedbc1 | ||
|
|
797ebd7771 | ||
|
|
04e9ecbc76 | ||
|
|
9626b65593 | ||
|
|
c9b5516330 | ||
|
|
4363ca88aa | ||
|
|
3353060ad4 | ||
|
|
ddc3b8575e | ||
|
|
136a2ec89f | ||
|
|
021c68f2a7 | ||
|
|
989a09274f | ||
|
|
39c5886d7a | ||
|
|
1a5f3735cf | ||
|
|
4d47eb0e91 | ||
|
|
af7c59b5c2 | ||
|
|
693bf68864 | ||
|
|
c9ddf3d165 | ||
|
|
1549b56866 | ||
|
|
2cd1f22e68 | ||
|
|
688f38943d | ||
|
|
043bbd7a11 | ||
|
|
f997423fd7 | ||
|
|
1871ef3d38 | ||
|
|
7c56c88dd4 | ||
|
|
4d7422dd90 | ||
|
|
eccabc0588 | ||
|
|
0c7b188587 | ||
|
|
4c97b79adf | ||
|
|
8ae9573b07 | ||
|
|
43fce6e739 | ||
|
|
78900772bb | ||
|
|
c16a0444ca | ||
|
|
0d518166ee | ||
|
|
6ae391a3c9 | ||
|
|
357897a0cd | ||
|
|
10a0a8fe09 |
@@ -42,8 +42,8 @@ GODOXY_HTTPS_ADDR=:443
|
||||
# API listening address
|
||||
GODOXY_API_ADDR=127.0.0.1:8888
|
||||
|
||||
# Prometheus Metrics listening address (uncomment to enable)
|
||||
#GODOXY_PROMETHEUS_ADDR=:8889
|
||||
# Prometheus Metrics
|
||||
GODOXY_PROMETHEUS_ENABLED=true
|
||||
|
||||
# Debug mode
|
||||
GODOXY_DEBUG=false
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: yusing # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: yusingwysq # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
22
.github/workflows/docker-image-nightly.yml
vendored
Normal file
22
.github/workflows/docker-image-nightly.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Docker Image CI (nightly)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*" # matches every branch that doesn't contain a '/'
|
||||
- "*/*" # matches every branch containing a single '/'
|
||||
- "**" # matches every branch
|
||||
- "!main" # excludes main
|
||||
|
||||
jobs:
|
||||
build-nightly:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
tag: nightly
|
||||
build-nightly-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: nightly
|
||||
agent: true
|
||||
20
.github/workflows/docker-image-prod.yml
vendored
Normal file
20
.github/workflows/docker-image-prod.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build-prod:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy
|
||||
old_image_name: ${{ github.repository_owner }}/go-proxy
|
||||
tag: latest
|
||||
build-prod-agent:
|
||||
uses: ./.github/workflows/docker-image.yml
|
||||
with:
|
||||
image_name: ${{ github.repository_owner }}/godoxy-agent
|
||||
tag: latest
|
||||
agent: true
|
||||
247
.github/workflows/docker-image.yml
vendored
247
.github/workflows/docker-image.yml
vendored
@@ -1,128 +1,163 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*"]
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
required: true
|
||||
type: string
|
||||
old_image_name:
|
||||
required: false
|
||||
type: string
|
||||
agent:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
REGISTRY: ghcr.io
|
||||
MAKE_ARGS: agent=${{ inputs.agent && '1' || '0' }}
|
||||
DIGEST_PATH: /tmp/digests/${{ inputs.agent && 'agent' || 'main' }}
|
||||
DIGEST_NAME_SUFFIX: ${{ inputs.agent && 'agent' || 'main' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build multi-platform Docker image
|
||||
runs-on: ubuntu-22.04
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
platform: linux/amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
# - linux/arm/v6
|
||||
# - linux/arm/v7
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=raw,value=${{ inputs.tag }},event=branch
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ inputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
MAKE_ARGS=${{ env.MAKE_ARGS }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ env.DIGEST_PATH }}
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ env.DIGEST_PATH }}/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}-${{ env.DIGEST_NAME_SUFFIX }}
|
||||
path: ${{ env.DIGEST_PATH }}/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
merge:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ env.DIGEST_PATH }}
|
||||
pattern: digests-*-${{ env.DIGEST_NAME_SUFFIX }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=raw,value=${{ inputs.tag }},event=branch
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: push
|
||||
working-directory: ${{ env.DIGEST_PATH }}
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
|
||||
|
||||
- name: Old image name
|
||||
if: inputs.old_image_name != ''
|
||||
run: |
|
||||
docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}\
|
||||
${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Inspect image (old)
|
||||
if: inputs.old_image_name != ''
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
@@ -9,9 +9,6 @@ linters-settings:
|
||||
- fieldalignment
|
||||
gocyclo:
|
||||
min-complexity: 14
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 4
|
||||
misspell:
|
||||
locale: US
|
||||
funlen:
|
||||
@@ -102,13 +99,14 @@ linters:
|
||||
- depguard # Not relevant
|
||||
- nakedret # Too strict
|
||||
- lll # Not relevant
|
||||
- gocyclo # FIXME must be fixed
|
||||
- gocyclo # must be fixed
|
||||
- gocognit # Too strict
|
||||
- nestif # Too many false-positive.
|
||||
- prealloc # Too many false-positive.
|
||||
- makezero # Not relevant
|
||||
- dupl # Too strict
|
||||
- gci # I don't care
|
||||
- goconst # Too annoying
|
||||
- gosec # Too strict
|
||||
- gochecknoinits
|
||||
- gochecknoglobals
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.22.8
|
||||
version: 1.22.10
|
||||
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.6.6
|
||||
ref: v1.6.7
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||
runtimes:
|
||||
@@ -22,17 +22,17 @@ lint:
|
||||
- yamllint
|
||||
enabled:
|
||||
- hadolint@2.12.1-beta
|
||||
- actionlint@1.7.6
|
||||
- checkov@3.2.352
|
||||
- actionlint@1.7.7
|
||||
- checkov@3.2.370
|
||||
- git-diff-check
|
||||
- gofmt@1.20.4
|
||||
- golangci-lint@1.63.4
|
||||
- golangci-lint@1.64.5
|
||||
- osv-scanner@1.9.2
|
||||
- oxipng@9.1.3
|
||||
- prettier@3.4.2
|
||||
- oxipng@9.1.4
|
||||
- prettier@3.5.1
|
||||
- shellcheck@0.10.0
|
||||
- shfmt@3.6.0
|
||||
- trufflehog@3.88.2
|
||||
- trufflehog@3.88.9
|
||||
actions:
|
||||
disabled:
|
||||
- trunk-announce
|
||||
|
||||
4
.vscode/settings.example.json
vendored
4
.vscode/settings.example.json
vendored
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/v0.8/schemas/config.schema.json": [
|
||||
"https://github.com/yusing/go-proxy/raw/main/schemas/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/v0.8/schemas/routes.schema.json": [
|
||||
"https://github.com/yusing/go-proxy/raw/main/schemas/routes.schema.json": [
|
||||
"providers.example.yml"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Builder
|
||||
FROM golang:1.23.5-alpine AS builder
|
||||
FROM golang:1.23.6-alpine AS builder
|
||||
HEALTHCHECK NONE
|
||||
|
||||
# package version does not matter
|
||||
|
||||
1
Makefile
1
Makefile
@@ -5,6 +5,7 @@ export GOOS = linux
|
||||
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION}
|
||||
|
||||
ifeq ($(trace), 1)
|
||||
debug = 1
|
||||
GODOXY_TRACE ?= 1
|
||||
endif
|
||||
|
||||
|
||||
97
README.md
97
README.md
@@ -1,21 +1,27 @@
|
||||
<div align="center">
|
||||
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
A lightweight, simple, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||
|
||||
For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
**EN** | <a href="README_CHT.md">中文</a>
|
||||
|
||||
**Currently working on [feat/godoxy-agent](https://github.com/yusing/go-proxy/tree/feat/godoxy-agent).<br/>Fork this instead of default branch.**
|
||||
|
||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||
|
||||
[繁體中文文檔請看此](README_CHT.md)
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
|
||||
A lightweight, easy-to-use, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with a Web UI and dashboard.
|
||||
|
||||
**v0.9 will be out soon, with a brand new [WebUI](next-release.md)!!! Below one is the old one**
|
||||
|
||||

|
||||
|
||||
_Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
</div>
|
||||
|
||||
## Table of content
|
||||
|
||||
@@ -24,12 +30,10 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
- [GoDoxy](#godoxy)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Features](#key-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Setup](#setup)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
@@ -45,6 +49,7 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- OpenID Connect support
|
||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP and UDP port forwarding
|
||||
@@ -54,54 +59,28 @@ _Join our [Discord](https://discord.gg/umReR62nRd) for help and discussions_
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Getting Started
|
||||
|
||||
For full documentation, **[See Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
1. Pull the latest docker images
|
||||
**NOTE:** GoDoxy is designed to be (and only works when) running in `host` network mode, do not change it. To change listening ports, modify `.env`.
|
||||
|
||||
1. Prepare a new directory for docker compose and config files.
|
||||
|
||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
2. Create new directory, `cd` into it, then run setup, or [set up manually](#manual-setup)
|
||||
3. Start the container `docker compose up -d` and wait for it to be ready
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. _(Optional)_ setup WebUI login
|
||||
|
||||
- set random JWT secret
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
- change username and password for WebUI authentication
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
|
||||
4. _(Optional)_ setup `docker-socket-proxy` other docker nodes (see [Multi docker nodes setup](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)) then add them inside `config.yml`
|
||||
|
||||
5. Start the container `docker compose up -d`
|
||||
|
||||
6. You may now do some extra configuration
|
||||
- With text editor (e.g. Visual Studio Code)
|
||||
- With Web UI via `https://gp.y.z`
|
||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -109,15 +88,15 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
|
||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/main/config.example.yml -O config/config.yml`
|
||||
|
||||
2. Grab `.env.example` into `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/.env.example -O .env`
|
||||
|
||||
3. Grab `compose.example.yml` into `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/compose.example.yml -O compose.yml`
|
||||
|
||||
### Folder structrue
|
||||
|
||||
@@ -133,15 +112,13 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
│ │ ├── middleware2.yml
|
||||
│ ├── provider1.yml
|
||||
│ └── provider2.yml
|
||||
├── data
|
||||
│ ├── metrics # metrics data
|
||||
│ │ ├── uptime.json
|
||||
│ │ └── system_info.json
|
||||
└── .env
|
||||
```
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
Copy [`.vscode/settings.example.json`](.vscode/settings.example.json) to `.vscode/settings.json` and modify it to fit your needs
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<div align="center">
|
||||
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
輕量、易用、 [高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks),且帶有主頁和配置面板的反向代理
|
||||
|
||||
完整文檔請查閱 **[Wiki](https://github.com/yusing/go-proxy/wiki)**(暫未有中文翻譯)
|
||||
|
||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||
|
||||
[English Documentation](README.md)
|
||||
<a href="README.md">EN</a> | **中文**
|
||||
|
||||
一個輕量級、易於使用且[高效能](https://github.com/yusing/go-proxy/wiki/Benchmarks)的反向代理,具有網頁介面和儀表板。
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
|
||||

|
||||
|
||||
_加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
</div>
|
||||
|
||||
## 目錄
|
||||
|
||||
@@ -22,12 +28,10 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
- [GoDoxy](#godoxy)
|
||||
- [目錄](#目錄)
|
||||
- [主要特點](#主要特點)
|
||||
- [入門指南](#入門指南)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [前置需求](#前置需求)
|
||||
- [安裝](#安裝)
|
||||
- [手動安裝](#手動安裝)
|
||||
- [資料夾結構](#資料夾結構)
|
||||
- [在 VSCode 中使用 JSON Schema](#在-vscode-中使用-json-schema)
|
||||
- [截圖](#截圖)
|
||||
- [閒置休眠](#閒置休眠)
|
||||
- [自行編譯](#自行編譯)
|
||||
@@ -43,6 +47,7 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
- 容器狀態/配置文件變更時自動熱重載
|
||||
- **閒置休眠**:在閒置時停止容器,有流量時喚醒(_可選,參見[截圖](#閒置休眠)_)
|
||||
- HTTP(s) 反向代理
|
||||
- OpenID Connect 支持
|
||||
- [HTTP 中介軟體支援](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [自訂錯誤頁面支援](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP 和 UDP 埠轉發
|
||||
@@ -52,54 +57,28 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
## 入門指南
|
||||
|
||||
完整文檔請參見 **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
|
||||
### 前置需求
|
||||
## 前置需求
|
||||
|
||||
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
|
||||
|
||||
- A 記錄:`*.y.z` -> `10.0.10.1`
|
||||
- AAAA 記錄:`*.y.z` -> `::ffff:a00:a01`
|
||||
|
||||
### 安裝
|
||||
## 安裝
|
||||
|
||||
1. 拉取最新的 Docker 映像
|
||||
**注意:** GoDoxy 設計為(且僅在)`host` 網路模式下運作,請勿更改。如需更改監聽埠,請修改 `.env`。
|
||||
|
||||
1. 準備一個新目錄用於 docker compose 和配置文件。
|
||||
|
||||
2. 在目錄內運行安裝腳本,或[手動安裝](#手動安裝)
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/yusing/go-proxy:latest
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
2. 建立新目錄,`cd` 進入後運行安裝,或[手動安裝](#手動安裝)
|
||||
3. 啟動容器 `docker compose up -d` 並等待就緒
|
||||
|
||||
```shell
|
||||
docker run --rm -v .:/setup ghcr.io/yusing/go-proxy /app/godoxy setup
|
||||
```
|
||||
|
||||
3. _(可選)_ 設置網頁介面登入
|
||||
|
||||
- 設置隨機 JWT 密鑰
|
||||
|
||||
```shell
|
||||
sed -i "s|API_JWT_SECRET=.*|API_JWT_SECRET=$(openssl rand -base64 32)|g" .env
|
||||
```
|
||||
|
||||
- 更改網頁介面認證的使用者名稱和密碼
|
||||
```shell
|
||||
USERNAME=admin
|
||||
PASSWORD=some-password
|
||||
sed -i "s|API_USERNAME=.*|API_USERNAME=${USERNAME}|g" .env
|
||||
sed -i "s|API_PASSWORD=.*|API_PASSWORD=${PASSWORD}|g" .env
|
||||
```
|
||||
|
||||
4. _(可選)_ 設置其他 Docker 節點的 `docker-socket-proxy`(參見 [多 Docker 節點設置](https://github.com/yusing/go-proxy/wiki/Configurations#multi-docker-nodes-setup)),然後在 `config.yml` 中添加它們
|
||||
|
||||
5. 啟動容器 `docker compose up -d`
|
||||
|
||||
6. 現在您可以進行額外的配置
|
||||
- 使用文字編輯器(如 Visual Studio Code)
|
||||
- 通過網頁介面 `https://gp.y.z`
|
||||
4. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
@@ -107,15 +86,15 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
|
||||
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/main/config.example.yml -O config/config.yml`
|
||||
|
||||
2. 將 `.env.example` 下載到 `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/.env.example -O .env`
|
||||
|
||||
3. 將 `compose.example.yml` 下載到 `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/v0.8/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/compose.example.yml -O compose.yml`
|
||||
|
||||
### 資料夾結構
|
||||
|
||||
@@ -131,15 +110,13 @@ _加入我們的 [Discord](https://discord.gg/umReR62nRd) 獲取幫助和討論_
|
||||
│ │ ├── middleware2.yml
|
||||
│ ├── provider1.yml
|
||||
│ └── provider2.yml
|
||||
├── data
|
||||
│ ├── metrics # metrics data
|
||||
│ │ ├── uptime.json
|
||||
│ │ └── system_info.json
|
||||
└── .env
|
||||
```
|
||||
|
||||
### 在 VSCode 中使用 JSON Schema
|
||||
|
||||
複製 [`.vscode/settings.example.json`](.vscode/settings.example.json) 到 `.vscode/settings.json` 並根據需要修改
|
||||
|
||||
[🔼回到頂部](#目錄)
|
||||
|
||||
## 截圖
|
||||
|
||||
### 閒置休眠
|
||||
|
||||
15
cmd/main.go
15
cmd/main.go
@@ -35,9 +35,6 @@ func init() {
|
||||
}
|
||||
logging.InitLogger(out)
|
||||
// logging.AddHook(v1.GetMemLogger())
|
||||
internal.InitIconListCache()
|
||||
homepage.InitOverridesConfig()
|
||||
favicon.InitIconCache()
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -45,9 +42,6 @@ func main() {
|
||||
args := common.GetArgs()
|
||||
|
||||
switch args.Command {
|
||||
case common.CommandSetup:
|
||||
internal.Setup()
|
||||
return
|
||||
case common.CommandReload:
|
||||
if err := query.ReloadServer(); err != nil {
|
||||
E.LogFatal("server reload error", err)
|
||||
@@ -120,16 +114,19 @@ func main() {
|
||||
printJSON(cfg.Value())
|
||||
return
|
||||
case common.CommandDebugListEntries:
|
||||
printJSON(cfg.DumpEntries())
|
||||
printJSON(cfg.DumpRoutes())
|
||||
return
|
||||
case common.CommandDebugListProviders:
|
||||
printJSON(cfg.DumpRouteProviders())
|
||||
return
|
||||
}
|
||||
|
||||
go internal.InitIconListCache()
|
||||
go homepage.InitOverridesConfig()
|
||||
go favicon.InitIconCache()
|
||||
|
||||
cfg.Start(&config.StartServersOptions{
|
||||
Proxy: true,
|
||||
Metrics: true,
|
||||
Proxy: true,
|
||||
})
|
||||
if err := auth.Initialize(); err != nil {
|
||||
logging.Fatal().Err(err).Msg("failed to initialize authentication")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/yusing/go-proxy-frontend:latest
|
||||
image: ghcr.io/yusing/godoxy-frontend:latest
|
||||
container_name: godoxy-frontend
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
@@ -10,17 +10,18 @@ services:
|
||||
- app
|
||||
# modify below to fit your needs
|
||||
labels:
|
||||
proxy.aliases: gp
|
||||
proxy.#1.port: 3000
|
||||
# proxy.#1.middlewares.cidr_whitelist.status: 403
|
||||
# proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
|
||||
# proxy.#1.middlewares.cidr_whitelist.allow: |
|
||||
proxy.aliases: godoxy
|
||||
proxy.godoxy.port: 3000
|
||||
# proxy.godoxy.middlewares.cidr_whitelist: |
|
||||
# status: 403
|
||||
# message: IP not allowed
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - 10.0.0.0/8
|
||||
# - 192.168.0.0/16
|
||||
# - 172.16.0.0/12
|
||||
app:
|
||||
image: ghcr.io/yusing/go-proxy:latest
|
||||
image: ghcr.io/yusing/godoxy:latest
|
||||
container_name: godoxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
@@ -28,15 +29,13 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./config:/app/config
|
||||
- ./logs:/app/logs
|
||||
- ./error_pages:/app/error_pages
|
||||
|
||||
# (Optional) choose one of below to enable https
|
||||
# 1. use existing certificate
|
||||
# To use autocert, certs will be stored in "./certs".
|
||||
# You can also use a docker volume to store it
|
||||
- ./certs:/app/certs
|
||||
|
||||
# remove "./certs:/app/certs" and uncomment below to use existing certificate
|
||||
# - /path/to/certs/cert.crt:/app/certs/cert.crt
|
||||
# - /path/to/certs/priv.key:/app/certs/priv.key
|
||||
|
||||
# 2. use autocert, certs will be stored in ./certs
|
||||
# you can also use a docker volume to store it
|
||||
|
||||
# - ./certs:/app/certs
|
||||
|
||||
@@ -1,78 +1,42 @@
|
||||
# Autocert (choose one below and uncomment to enable)
|
||||
#
|
||||
# 1. use existing cert
|
||||
#
|
||||
|
||||
# autocert:
|
||||
# provider: local
|
||||
#
|
||||
# cert_path: certs/cert.crt # optional, uncomment only if you need to change it
|
||||
# key_path: certs/priv.key # optional, uncomment only if you need to change it
|
||||
#
|
||||
|
||||
# 2. cloudflare
|
||||
#
|
||||
# autocert:
|
||||
# provider: cloudflare
|
||||
# email: abc@gmail.com # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# - "*.y.z" # remember to use double quotes to surround wildcard domain
|
||||
# email: abc@gmail.com # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# - "*.domain.com"
|
||||
# - "domain.com"
|
||||
# options:
|
||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
#
|
||||
# 3. other providers, check docs/dns_providers.md for more
|
||||
# auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, see https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers#supported-dns-01-providers
|
||||
|
||||
entrypoint:
|
||||
middlewares:
|
||||
# this part blocks all non-LAN HTTP traffic
|
||||
# remove if you don't want this
|
||||
- use: CIDRWhitelist
|
||||
allow:
|
||||
- "127.0.0.1"
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
status: 403
|
||||
message: "Forbidden"
|
||||
# end of CIDRWhitelist
|
||||
# Below define an example of middleware config
|
||||
# 1. block non local IP connections
|
||||
# 2. redirect HTTP to HTTPS
|
||||
#
|
||||
# middlewares:
|
||||
# - use: CIDRWhitelist
|
||||
# allow:
|
||||
# - "127.0.0.1"
|
||||
# - "10.0.0.0/8"
|
||||
# - "172.16.0.0/12"
|
||||
# - "192.168.0.0/16"
|
||||
# status: 403
|
||||
# message: "Forbidden"
|
||||
# - use: RedirectHTTP
|
||||
|
||||
# this part redirects HTTP to HTTPS
|
||||
# remove if you don't want this
|
||||
- use: RedirectHTTP
|
||||
|
||||
# access_log:
|
||||
# buffer_size: 1024
|
||||
# path: /var/log/example.log
|
||||
# filters:
|
||||
# status_codes:
|
||||
# values:
|
||||
# - 200-299
|
||||
# - 101
|
||||
# method:
|
||||
# values:
|
||||
# - GET
|
||||
# host:
|
||||
# values:
|
||||
# - example.y.z
|
||||
# headers:
|
||||
# negative: true
|
||||
# values:
|
||||
# - foo=bar
|
||||
# - baz
|
||||
# cidr:
|
||||
# values:
|
||||
# - 192.168.10.0/24
|
||||
# fields:
|
||||
# headers:
|
||||
# default: keep
|
||||
# config:
|
||||
# foo: redact
|
||||
# query:
|
||||
# default: drop
|
||||
# config:
|
||||
# foo: keep
|
||||
# cookies:
|
||||
# default: redact
|
||||
# config:
|
||||
# foo: keep
|
||||
# below enables access log
|
||||
access_log:
|
||||
format: combined
|
||||
path: /app/logs/entrypoint.log
|
||||
|
||||
providers:
|
||||
# include files are standalone yaml files under `config/` directory
|
||||
@@ -84,6 +48,7 @@ providers:
|
||||
docker:
|
||||
# $DOCKER_HOST implies environment variable `DOCKER_HOST` or unix:///var/run/docker.sock by default
|
||||
local: $DOCKER_HOST
|
||||
|
||||
# explicit only mode
|
||||
# only containers with explicit aliases will be proxied
|
||||
# add "!" after provider name to enable explicit only mode
|
||||
@@ -106,28 +71,10 @@ providers:
|
||||
# - name: discord
|
||||
# provider: webhook
|
||||
# url: https://discord.com/api/webhooks/...
|
||||
# template: discord
|
||||
# # payload: | # discord template implies the following
|
||||
# # {
|
||||
# # "embeds": [
|
||||
# # {
|
||||
# # "title": $title,
|
||||
# # "fields": $fields,
|
||||
# # "color": "$color"
|
||||
# # }
|
||||
# # ]
|
||||
# # }
|
||||
# if match_domains not defined
|
||||
# any host = alias+[any domain] will match
|
||||
# i.e. https://app1.y.z will match alias app1 for any domain y.z
|
||||
# but https://app1.node1.y.z will only match alias "app.node1"
|
||||
#
|
||||
# if match_domains defined
|
||||
# only host = alias+[one of match_domains] will match
|
||||
# i.e. match_domains = [node1.my.app, my.site]
|
||||
# https://app1.my.app, https://app1.my.net, etc. will not match even if app1 exists
|
||||
# only https://*.node1.my.app and https://*.my.site will match
|
||||
#
|
||||
# template: discord # this means use payload template from internal/notif/templates/discord.json
|
||||
|
||||
# Check https://github.com/yusing/go-proxy/wiki/Certificates-and-domain-matching#domain-matching
|
||||
# for explaination of `match_domains`
|
||||
#
|
||||
# match_domains:
|
||||
# - my.site
|
||||
|
||||
59
go.mod
59
go.mod
@@ -1,30 +1,30 @@
|
||||
module github.com/yusing/go-proxy
|
||||
|
||||
go 1.23.5
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1
|
||||
github.com/coder/websocket v1.8.12
|
||||
github.com/coreos/go-oidc/v3 v3.12.0
|
||||
github.com/docker/cli v27.5.1+incompatible
|
||||
github.com/docker/docker v27.5.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-acme/lego/v4 v4.21.0
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gotify/server/v2 v2.6.1
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/vincent-petithory/dataurl v1.0.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/time v0.9.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/PuerkitoBio/goquery v1.10.2 // parsing HTML for extract fav icon
|
||||
github.com/coder/websocket v1.8.12 // websocket for API and agent
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 // oidc authentication
|
||||
github.com/docker/cli v27.5.1+incompatible // docker CLI
|
||||
github.com/docker/docker v27.5.1+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.8.0 // file watcher
|
||||
github.com/go-acme/lego/v4 v4.22.2 // acme client
|
||||
github.com/go-playground/validator/v10 v10.25.0 // validator
|
||||
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // jwt for default auth
|
||||
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/prometheus/client_golang v1.20.5 // metrics
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.33.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.33.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.35.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.26.0 // oauth2 authentication
|
||||
golang.org/x/text v0.22.0 // string utilities
|
||||
golang.org/x/time v0.10.0 // time utilities
|
||||
gopkg.in/yaml.v3 v3.0.1 // yaml parsing for different config files
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -57,9 +57,10 @@ require (
|
||||
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/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
@@ -72,11 +73,11 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
60
go.sum
60
go.sum
@@ -2,8 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -41,8 +41,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
|
||||
github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
|
||||
github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
|
||||
github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -56,8 +56,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -113,12 +113,14 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
|
||||
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/ovh/go-ovh v1.6.0 h1:ixLOwxQdzYDx296sXcgS35TOPEahJkpjMGtzPadCjQI=
|
||||
github.com/ovh/go-ovh v1.6.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
|
||||
github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -132,8 +134,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@@ -176,8 +178,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -185,8 +187,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -199,10 +201,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -211,8 +213,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -229,8 +232,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -248,10 +251,11 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -260,8 +264,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -273,8 +277,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -3,10 +3,13 @@ package api
|
||||
import (
|
||||
"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/favicon"
|
||||
"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/utils/strutils"
|
||||
)
|
||||
|
||||
@@ -36,6 +39,11 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||
mux.HandleFunc("GET", "/v1/favicon", auth.RequireAuth(favicon.GetFavIcon))
|
||||
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
mux.Handle("GET /v1/metrics", promhttp.Handler())
|
||||
logging.Info().Msg("prometheus metrics enabled")
|
||||
}
|
||||
|
||||
defaultAuth := auth.GetDefaultAuth()
|
||||
if defaultAuth != nil {
|
||||
mux.HandleFunc("GET", "/v1/auth/redirect", defaultAuth.RedirectLoginPage)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
@@ -227,7 +226,7 @@ func TestOIDCCallbackHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.wantStatus == http.StatusTemporaryRedirect {
|
||||
setCookie := E.Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||
ExpectEqual(t, setCookie.Name, defaultAuth.TokenCookieName())
|
||||
ExpectTrue(t, setCookie.Value != "")
|
||||
ExpectEqual(t, setCookie.Path, "/")
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -17,7 +16,7 @@ import (
|
||||
func newMockUserPassAuth() *UserPassAuth {
|
||||
return &UserPassAuth{
|
||||
username: "username",
|
||||
pwdHash: E.Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)),
|
||||
pwdHash: Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)),
|
||||
secret: []byte("abcdefghijklmnopqrstuvwxyz"),
|
||||
tokenTTL: time.Hour,
|
||||
}
|
||||
@@ -97,13 +96,13 @@ func TestUserPassLoginCallbackHandler(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := &http.Request{
|
||||
Host: "app.example.com",
|
||||
Body: io.NopCloser(bytes.NewReader(E.Must(json.Marshal(tt.creds)))),
|
||||
Body: io.NopCloser(bytes.NewReader(Must(json.Marshal(tt.creds)))),
|
||||
}
|
||||
auth.LoginCallbackHandler(w, req)
|
||||
if tt.wantErr {
|
||||
ExpectEqual(t, w.Code, http.StatusUnauthorized)
|
||||
} else {
|
||||
setCookie := E.Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||
setCookie := Must(http.ParseSetCookie(w.Header().Get("Set-Cookie")))
|
||||
ExpectTrue(t, setCookie.Name == auth.TokenCookieName())
|
||||
ExpectTrue(t, setCookie.Value != "")
|
||||
ExpectEqual(t, setCookie.Domain, "example.com")
|
||||
|
||||
@@ -29,6 +29,9 @@ const (
|
||||
)
|
||||
|
||||
func InitIconCache() {
|
||||
iconCacheMu.Lock()
|
||||
defer iconCacheMu.Unlock()
|
||||
|
||||
err := utils.LoadJSONIfExist(common.IconCachePath, &iconCache)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("failed to load icon cache")
|
||||
@@ -78,7 +81,7 @@ func pruneExpiredIconCache() {
|
||||
}
|
||||
|
||||
func routeKey(r route.HTTPRoute) string {
|
||||
return r.RawEntry().Provider + ":" + r.TargetName()
|
||||
return r.ProviderName() + ":" + r.TargetName()
|
||||
}
|
||||
|
||||
func PruneRouteIconCache(route route.HTTPRoute) {
|
||||
|
||||
@@ -87,7 +87,7 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
var result *fetchResult
|
||||
hp := r.RawEntry().Homepage.GetOverride()
|
||||
hp := r.HomepageConfig().GetOverride()
|
||||
if !hp.IsEmpty() && hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result = findIcon(r, req, hp.Icon.Value)
|
||||
@@ -189,7 +189,7 @@ func findIcon(r route.HTTPRoute, req *http.Request, uri string) *fetchResult {
|
||||
}
|
||||
|
||||
result := fetchIcon("png", sanitizeName(r.TargetName()))
|
||||
cont := r.RawEntry().Container
|
||||
cont := r.ContainerInfo()
|
||||
if !result.OK() && cont != nil {
|
||||
result = fetchIcon("png", sanitizeName(cont.ImageName))
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
U.RespondError(w, err)
|
||||
return
|
||||
}
|
||||
if icons == nil {
|
||||
icons = []string{}
|
||||
}
|
||||
U.RespondJSON(w, r, icons)
|
||||
case ListTasks:
|
||||
U.RespondJSON(w, r, task.DebugTaskList())
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,6 +21,7 @@ const (
|
||||
ProviderClouddns = "clouddns"
|
||||
ProviderDuckdns = "duckdns"
|
||||
ProviderOVH = "ovh"
|
||||
ProviderPorkbun = "porkbun"
|
||||
)
|
||||
|
||||
var providersGenMap = map[string]ProviderGenerator{
|
||||
@@ -28,4 +30,5 @@ var providersGenMap = map[string]ProviderGenerator{
|
||||
ProviderClouddns: providerGenerator(clouddns.NewDefaultConfig, clouddns.NewDNSProviderConfig),
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
ProviderOVH: providerGenerator(ovh.NewDefaultConfig, ovh.NewDNSProviderConfig),
|
||||
ProviderPorkbun: providerGenerator(porkbun.NewDefaultConfig, porkbun.NewDNSProviderConfig),
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ type Args struct {
|
||||
|
||||
const (
|
||||
CommandStart = ""
|
||||
CommandSetup = "setup"
|
||||
CommandValidate = "validate"
|
||||
CommandListConfigs = "ls-config"
|
||||
CommandListRoutes = "ls-routes"
|
||||
@@ -25,7 +24,6 @@ const (
|
||||
|
||||
var ValidCommands = []string{
|
||||
CommandStart,
|
||||
CommandSetup,
|
||||
CommandValidate,
|
||||
CommandListConfigs,
|
||||
CommandListRoutes,
|
||||
@@ -36,6 +34,15 @@ var ValidCommands = []string{
|
||||
CommandDebugListMTrace,
|
||||
}
|
||||
|
||||
func validateArg(arg string) error {
|
||||
for _, v := range ValidCommands {
|
||||
if arg == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid command %q", arg)
|
||||
}
|
||||
|
||||
func GetArgs() Args {
|
||||
var args Args
|
||||
flag.Parse()
|
||||
@@ -45,12 +52,3 @@ func GetArgs() Args {
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func validateArg(arg string) error {
|
||||
for _, v := range ValidCommands {
|
||||
if arg == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid command %q", arg)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ const (
|
||||
|
||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||
|
||||
SchemasBasePath = "schemas"
|
||||
ConfigSchemaPath = SchemasBasePath + "/config.schema.json"
|
||||
FileProviderSchemaPath = SchemasBasePath + "/providers.schema.json"
|
||||
|
||||
ComposeFileName = "compose.yml"
|
||||
ComposeExampleFileName = "compose.example.yml"
|
||||
|
||||
@@ -38,7 +34,6 @@ const (
|
||||
|
||||
var RequiredDirectories = []string{
|
||||
ConfigBasePath,
|
||||
SchemasBasePath,
|
||||
ErrorPagesBasePath,
|
||||
MiddlewareComposeBasePath,
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ var (
|
||||
APIHTTPPort,
|
||||
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
|
||||
|
||||
MetricsHTTPAddr,
|
||||
MetricsHTTPHost,
|
||||
MetricsHTTPPort,
|
||||
MetricsHTTPURL = GetAddrEnv("PROMETHEUS_ADDR", "", "http")
|
||||
PrometheusEnabled = MetricsHTTPURL != ""
|
||||
PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", false)
|
||||
|
||||
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
|
||||
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/entrypoint"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
"github.com/yusing/go-proxy/internal/net/http/server"
|
||||
"github.com/yusing/go-proxy/internal/notif"
|
||||
proxy "github.com/yusing/go-proxy/internal/route/provider"
|
||||
@@ -104,7 +103,6 @@ func OnConfigChange(ev []events.Event) {
|
||||
}
|
||||
|
||||
if err := Reload(); err != nil {
|
||||
logging.Warn().Msg("using last config")
|
||||
// recovered in event queue
|
||||
panic(err)
|
||||
}
|
||||
@@ -119,14 +117,14 @@ func Reload() E.Error {
|
||||
err := newCfg.load()
|
||||
if err != nil {
|
||||
newCfg.task.Finish(err)
|
||||
return err
|
||||
return E.New("using last config").With(err)
|
||||
}
|
||||
|
||||
// cancel all current subtasks -> wait
|
||||
// -> replace config -> start new subtasks
|
||||
instance.task.Finish("config changed")
|
||||
instance = newCfg
|
||||
instance.Start()
|
||||
instance.Start(StartAllServers)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -182,9 +180,11 @@ func (cfg *Config) StartProxyProviders() {
|
||||
}
|
||||
|
||||
type StartServersOptions struct {
|
||||
Proxy, API, Metrics bool
|
||||
Proxy, API bool
|
||||
}
|
||||
|
||||
var StartAllServers = &StartServersOptions{true, true}
|
||||
|
||||
func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
if len(opts) == 0 {
|
||||
opts = append(opts, &StartServersOptions{})
|
||||
@@ -207,14 +207,6 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
|
||||
Handler: api.NewHandler(cfg),
|
||||
})
|
||||
}
|
||||
if opt.Metrics && common.PrometheusEnabled {
|
||||
server.StartServer(cfg.task, server.Options{
|
||||
Name: "metrics",
|
||||
CertProvider: cfg.AutoCertProvider(),
|
||||
HTTPAddr: common.MetricsHTTPAddr,
|
||||
Handler: metrics.NewHandler(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) load() E.Error {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
route "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
func (cfg *Config) DumpEntries() map[string]*types.RawEntry {
|
||||
entries := make(map[string]*types.RawEntry)
|
||||
func (cfg *Config) DumpRoutes() map[string]*route.Route {
|
||||
entries := make(map[string]*route.Route)
|
||||
cfg.providers.RangeAll(func(_ string, p *provider.Provider) {
|
||||
p.RangeRoutes(func(alias string, r *route.Route) {
|
||||
entries[alias] = r.Entry
|
||||
entries[alias] = r
|
||||
})
|
||||
})
|
||||
return entries
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
PortMapping = map[string]types.Port
|
||||
PortMapping = map[int]types.Port
|
||||
Container struct {
|
||||
_ U.NoCopy
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
|
||||
if v.PublicPort == 0 {
|
||||
continue
|
||||
}
|
||||
res[strutils.PortString(v.PublicPort)] = v
|
||||
res[int(v.PublicPort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (c containerHelper) getPublicPortMapping() PortMapping {
|
||||
func (c containerHelper) getPrivatePortMapping() PortMapping {
|
||||
res := make(PortMapping)
|
||||
for _, v := range c.Ports {
|
||||
res[strutils.PortString(v.PrivatePort)] = v
|
||||
res[int(v.PrivatePort)] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -66,14 +66,6 @@ var databaseMPs = map[string]struct{}{
|
||||
"/var/lib/rabbitmq": {},
|
||||
}
|
||||
|
||||
var databasePrivPorts = map[uint16]struct{}{
|
||||
5432: {}, // postgres
|
||||
3306: {}, // mysql, mariadb
|
||||
6379: {}, // redis
|
||||
11211: {}, // memcached
|
||||
27017: {}, // mongodb
|
||||
}
|
||||
|
||||
func (c containerHelper) isDatabase() bool {
|
||||
for _, m := range c.Mounts {
|
||||
if _, ok := databaseMPs[m.Destination]; ok {
|
||||
@@ -82,7 +74,9 @@ func (c containerHelper) isDatabase() bool {
|
||||
}
|
||||
|
||||
for _, v := range c.Ports {
|
||||
if _, ok := databasePrivPorts[v.PrivatePort]; ok {
|
||||
switch v.PrivatePort {
|
||||
// postgres, mysql or mariadb, redis, memcached, mongodb
|
||||
case 5432, 3306, 6379, 11211, 27017:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,32 +38,32 @@ const (
|
||||
|
||||
// TODO: support stream
|
||||
|
||||
func newWaker(parent task.Parent, entry route.Entry, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
||||
hcCfg := entry.RawEntry().HealthCheck
|
||||
func newWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy, stream net.Stream) (Waker, E.Error) {
|
||||
hcCfg := route.HealthCheckConfig()
|
||||
hcCfg.Timeout = idleWakerCheckTimeout
|
||||
|
||||
waker := &waker{
|
||||
rp: rp,
|
||||
stream: stream,
|
||||
}
|
||||
task := parent.Subtask("idlewatcher." + entry.TargetName())
|
||||
watcher, err := registerWatcher(task, entry, waker)
|
||||
task := parent.Subtask("idlewatcher." + route.TargetName())
|
||||
watcher, err := registerWatcher(task, route, waker)
|
||||
if err != nil {
|
||||
return nil, E.Errorf("register watcher: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case rp != nil:
|
||||
waker.hc = monitor.NewHTTPHealthChecker(entry.TargetURL(), hcCfg)
|
||||
waker.hc = monitor.NewHTTPHealthChecker(route.TargetURL(), hcCfg)
|
||||
case stream != nil:
|
||||
waker.hc = monitor.NewRawHealthChecker(entry.TargetURL(), hcCfg)
|
||||
waker.hc = monitor.NewRawHealthChecker(route.TargetURL(), hcCfg)
|
||||
default:
|
||||
panic("both nil")
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
m := metrics.GetServiceMetrics()
|
||||
fqn := parent.Name() + "/" + entry.TargetName()
|
||||
fqn := parent.Name() + "/" + route.TargetName()
|
||||
waker.metric = m.HealthStatus.With(metrics.HealthMetricLabels(fqn))
|
||||
waker.metric.Set(float64(watcher.Status()))
|
||||
}
|
||||
@@ -71,12 +71,12 @@ func newWaker(parent task.Parent, entry route.Entry, rp *reverseproxy.ReversePro
|
||||
}
|
||||
|
||||
// lifetime should follow route provider.
|
||||
func NewHTTPWaker(parent task.Parent, entry route.Entry, rp *reverseproxy.ReverseProxy) (Waker, E.Error) {
|
||||
return newWaker(parent, entry, rp, nil)
|
||||
func NewHTTPWaker(parent task.Parent, route route.Route, rp *reverseproxy.ReverseProxy) (Waker, E.Error) {
|
||||
return newWaker(parent, route, rp, nil)
|
||||
}
|
||||
|
||||
func NewStreamWaker(parent task.Parent, entry route.Entry, stream net.Stream) (Waker, E.Error) {
|
||||
return newWaker(parent, entry, nil, stream)
|
||||
func NewStreamWaker(parent task.Parent, route route.Route, stream net.Stream) (Waker, E.Error) {
|
||||
return newWaker(parent, route, nil, stream)
|
||||
}
|
||||
|
||||
// Start implements health.HealthMonitor.
|
||||
@@ -155,7 +155,7 @@ func (w *Watcher) getStatusUpdateReady() health.Status {
|
||||
|
||||
// MarshalJSON implements health.HealthMonitor.
|
||||
func (w *Watcher) MarshalJSON() ([]byte, error) {
|
||||
var url net.URL
|
||||
var url *net.URL
|
||||
if w.hc.URL().Port() != "0" {
|
||||
url = w.hc.URL()
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ var (
|
||||
|
||||
const dockerReqTimeout = 3 * time.Second
|
||||
|
||||
func registerWatcher(watcherTask *task.Task, entry route.Entry, waker *waker) (*Watcher, error) {
|
||||
cfg := entry.IdlewatcherConfig()
|
||||
func registerWatcher(watcherTask *task.Task, route route.Route, waker *waker) (*Watcher, error) {
|
||||
cfg := route.IdlewatcherConfig()
|
||||
|
||||
if cfg.IdleTimeout == 0 {
|
||||
panic(errShouldNotReachHere)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
r route.HTTPRoute
|
||||
r route.ReveseProxyRoute
|
||||
ep = NewEntrypoint()
|
||||
)
|
||||
|
||||
|
||||
@@ -75,8 +75,10 @@ func (err *nestedError) Error() string {
|
||||
lines := make([]string, 0, 1+len(err.Extras))
|
||||
if err.Err != nil {
|
||||
lines = append(lines, makeLine(err.Err.Error(), 0))
|
||||
lines = append(lines, makeLines(err.Extras, 1)...)
|
||||
} else {
|
||||
lines = append(lines, makeLines(err.Extras, 0)...)
|
||||
}
|
||||
lines = append(lines, makeLines(err.Extras, 1)...)
|
||||
return strutils.JoinLines(lines)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,6 @@ func From(err error) Error {
|
||||
return &baseError{err}
|
||||
}
|
||||
|
||||
func Must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
LogPanic("must failed", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func Join(errors ...error) Error {
|
||||
n := 0
|
||||
for _, err := range errors {
|
||||
|
||||
@@ -17,15 +17,17 @@ type OverrideConfig struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var overrideConfigInstance *OverrideConfig
|
||||
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 = &OverrideConfig{
|
||||
ItemOverrides: make(map[string]*ItemConfig),
|
||||
DisplayOrder: make(map[string]int),
|
||||
CategoryOrder: make(map[string]int),
|
||||
ItemVisibility: make(map[string]bool),
|
||||
}
|
||||
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")
|
||||
|
||||
@@ -52,6 +52,9 @@ const (
|
||||
)
|
||||
|
||||
func InitIconListCache() {
|
||||
iconsCahceMu.Lock()
|
||||
defer iconsCahceMu.Unlock()
|
||||
|
||||
iconsCache = &Cache{
|
||||
WalkxCode: make(IconsMap),
|
||||
Selfhst: make(IconsMap),
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
@@ -30,7 +29,7 @@ const (
|
||||
|
||||
var (
|
||||
testTask = task.RootTask("test", false)
|
||||
testURL = E.Must(url.Parse("http://" + host + uri))
|
||||
testURL = Must(url.Parse("http://" + host + uri))
|
||||
req = &http.Request{
|
||||
RemoteAddr: remote,
|
||||
Method: method,
|
||||
|
||||
@@ -4,14 +4,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestRebalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
for range 10 {
|
||||
lb.AddServer(types.TestNewServer(0))
|
||||
}
|
||||
@@ -19,7 +18,7 @@ func TestRebalance(t *testing.T) {
|
||||
ExpectEqual(t, lb.sumWeight, maxWeight)
|
||||
})
|
||||
t.Run("less", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
|
||||
@@ -30,7 +29,7 @@ func TestRebalance(t *testing.T) {
|
||||
ExpectEqual(t, lb.sumWeight, maxWeight)
|
||||
})
|
||||
t.Run("more", func(t *testing.T) {
|
||||
lb := New(new(loadbalance.Config))
|
||||
lb := New(new(types.Config))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
|
||||
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
@@ -15,7 +15,7 @@ type (
|
||||
_ U.NoCopy
|
||||
|
||||
name string
|
||||
url types.URL
|
||||
url *net.URL
|
||||
weight Weight
|
||||
|
||||
http.Handler `json:"-"`
|
||||
@@ -26,7 +26,7 @@ type (
|
||||
http.Handler
|
||||
health.HealthMonitor
|
||||
Name() string
|
||||
URL() types.URL
|
||||
URL() *net.URL
|
||||
Weight() Weight
|
||||
SetWeight(weight Weight)
|
||||
TryWake() error
|
||||
@@ -37,7 +37,7 @@ type (
|
||||
|
||||
var NewServerPool = F.NewMap[Pool]
|
||||
|
||||
func NewServer(name string, url types.URL, weight Weight, handler http.Handler, healthMon health.HealthMonitor) Server {
|
||||
func NewServer(name string, url *net.URL, weight Weight, handler http.Handler, healthMon health.HealthMonitor) Server {
|
||||
srv := &server{
|
||||
name: name,
|
||||
url: url,
|
||||
@@ -59,7 +59,7 @@ func (srv *server) Name() string {
|
||||
return srv.name
|
||||
}
|
||||
|
||||
func (srv *server) URL() types.URL {
|
||||
func (srv *server) URL() *net.URL {
|
||||
return srv.url
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
var validMethods = map[string]struct{}{
|
||||
http.MethodGet: {},
|
||||
http.MethodHead: {},
|
||||
http.MethodPost: {},
|
||||
http.MethodPut: {},
|
||||
http.MethodPatch: {},
|
||||
http.MethodDelete: {},
|
||||
http.MethodConnect: {},
|
||||
http.MethodOptions: {},
|
||||
http.MethodTrace: {},
|
||||
}
|
||||
|
||||
func IsMethodValid(method string) bool {
|
||||
_, ok := validMethods[method]
|
||||
return ok
|
||||
switch method {
|
||||
case http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodOptions,
|
||||
http.MethodTrace:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ const (
|
||||
var (
|
||||
cfCIDRsLastUpdate time.Time
|
||||
cfCIDRsMu sync.Mutex
|
||||
|
||||
// RFC 1918.
|
||||
localCIDRs = []*types.CIDR{
|
||||
{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}, // 127.0.0.1/32
|
||||
{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(255, 0, 0, 0)}, // 10.0.0.0/8
|
||||
{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(255, 240, 0, 0)}, // 172.16.0.0/12
|
||||
{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 0, 0)}, // 192.168.0.0/16
|
||||
}
|
||||
)
|
||||
|
||||
var CloudflareRealIP = NewMiddleware[cloudflareRealIP]()
|
||||
@@ -37,7 +45,7 @@ var CloudflareRealIP = NewMiddleware[cloudflareRealIP]()
|
||||
// setup implements MiddlewareWithSetup.
|
||||
func (cri *cloudflareRealIP) setup() {
|
||||
cri.realIP.RealIPOpts = RealIPOpts{
|
||||
Header: "Cf-Connecting-Ip",
|
||||
Header: "CF-Connecting-IP",
|
||||
Recursive: cri.Recursive,
|
||||
}
|
||||
}
|
||||
@@ -72,12 +80,7 @@ func tryFetchCFCIDR() (cfCIDRs []*types.CIDR) {
|
||||
}
|
||||
|
||||
if common.IsTest {
|
||||
cfCIDRs = []*types.CIDR{
|
||||
{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||
{IP: net.IPv4(10, 0, 0, 0), Mask: net.IPv4Mask(255, 0, 0, 0)},
|
||||
{IP: net.IPv4(172, 16, 0, 0), Mask: net.IPv4Mask(255, 255, 0, 0)},
|
||||
{IP: net.IPv4(192, 168, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)},
|
||||
}
|
||||
cfCIDRs = localCIDRs
|
||||
} else {
|
||||
cfCIDRs = make([]*types.CIDR, 0, 30)
|
||||
err := errors.Join(
|
||||
@@ -122,6 +125,6 @@ func fetchUpdateCFIPRange(endpoint string, cfCIDRs *[]*types.CIDR) error {
|
||||
|
||||
*cfCIDRs = append(*cfCIDRs, (*types.CIDR)(cidr))
|
||||
}
|
||||
|
||||
*cfCIDRs = append(*cfCIDRs, localCIDRs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package metricslogger
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
)
|
||||
|
||||
type MetricsLogger struct {
|
||||
ServiceName string `json:"service_name"`
|
||||
}
|
||||
|
||||
func NewMetricsLogger(serviceName string) *MetricsLogger {
|
||||
return &MetricsLogger{serviceName}
|
||||
}
|
||||
|
||||
func (m *MetricsLogger) GetHandler(next http.Handler) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
m.ServeHTTP(rw, req, next.ServeHTTP)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetricsLogger) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
|
||||
visitorIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
visitorIP = req.RemoteAddr
|
||||
}
|
||||
|
||||
// req.RemoteAddr had been modified by middleware (if any)
|
||||
lbls := &metrics.HTTPRouteMetricLabels{
|
||||
Service: m.ServiceName,
|
||||
Method: req.Method,
|
||||
Host: req.Host,
|
||||
Visitor: visitorIP,
|
||||
Path: req.URL.Path,
|
||||
}
|
||||
|
||||
next.ServeHTTP(newHTTPMetricLogger(rw, lbls), req)
|
||||
}
|
||||
|
||||
func (m *MetricsLogger) ResetMetrics() {
|
||||
metrics.GetRouteMetrics().UnregisterService(m.ServiceName)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package metricslogger
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
)
|
||||
|
||||
type httpMetricLogger struct {
|
||||
http.ResponseWriter
|
||||
timestamp time.Time
|
||||
labels *metrics.HTTPRouteMetricLabels
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (l *httpMetricLogger) WriteHeader(status int) {
|
||||
l.ResponseWriter.WriteHeader(status)
|
||||
duration := time.Since(l.timestamp)
|
||||
go func() {
|
||||
m := metrics.GetRouteMetrics()
|
||||
m.HTTPReqTotal.Inc()
|
||||
m.HTTPReqElapsed.With(l.labels).Set(float64(duration.Milliseconds()))
|
||||
|
||||
// ignore 1xx
|
||||
switch {
|
||||
case status >= 500:
|
||||
m.HTTP5xx.With(l.labels).Inc()
|
||||
case status >= 400:
|
||||
m.HTTP4xx.With(l.labels).Inc()
|
||||
case status >= 200:
|
||||
m.HTTP2xx3xx.With(l.labels).Inc()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (l *httpMetricLogger) Unwrap() http.ResponseWriter {
|
||||
return l.ResponseWriter
|
||||
}
|
||||
|
||||
func newHTTPMetricLogger(w http.ResponseWriter, labels *metrics.HTTPRouteMetricLabels) *httpMetricLogger {
|
||||
return &httpMetricLogger{
|
||||
ResponseWriter: w,
|
||||
timestamp: time.Now(),
|
||||
labels: labels,
|
||||
}
|
||||
}
|
||||
@@ -196,34 +196,6 @@ func (m *Middleware) ServeHTTP(next http.HandlerFunc, w http.ResponseWriter, r *
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
// TODO: check conflict or duplicates.
|
||||
func compileMiddlewares(middlewaresMap map[string]OptionsRaw) ([]*Middleware, E.Error) {
|
||||
middlewares := make([]*Middleware, 0, len(middlewaresMap))
|
||||
|
||||
errs := E.NewBuilder("middlewares compile error")
|
||||
invalidOpts := E.NewBuilder("options compile error")
|
||||
|
||||
for name, opts := range middlewaresMap {
|
||||
m, err := Get(name)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
|
||||
m, err = m.New(opts)
|
||||
if err != nil {
|
||||
invalidOpts.Add(err.Subject(name))
|
||||
continue
|
||||
}
|
||||
middlewares = append(middlewares, m)
|
||||
}
|
||||
|
||||
if invalidOpts.HasError() {
|
||||
errs.Add(invalidOpts.Error())
|
||||
}
|
||||
return middlewares, errs.Error()
|
||||
}
|
||||
|
||||
func PatchReverseProxy(rp *ReverseProxy, middlewaresMap map[string]OptionsRaw) (err E.Error) {
|
||||
var middlewares []*Middleware
|
||||
middlewares, err = compileMiddlewares(middlewaresMap)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -39,6 +40,43 @@ func BuildMiddlewaresFromYAML(source string, data []byte, eb *E.Builder) map[str
|
||||
return middlewares
|
||||
}
|
||||
|
||||
func compileMiddlewares(middlewaresMap map[string]OptionsRaw) ([]*Middleware, E.Error) {
|
||||
middlewares := make([]*Middleware, 0, len(middlewaresMap))
|
||||
|
||||
errs := E.NewBuilder("middlewares compile error")
|
||||
invalidOpts := E.NewBuilder("options compile error")
|
||||
|
||||
for name, opts := range middlewaresMap {
|
||||
m, err := Get(name)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
continue
|
||||
}
|
||||
|
||||
m, err = m.New(opts)
|
||||
if err != nil {
|
||||
invalidOpts.Add(err.Subject(name))
|
||||
continue
|
||||
}
|
||||
middlewares = append(middlewares, m)
|
||||
}
|
||||
|
||||
if invalidOpts.HasError() {
|
||||
errs.Add(invalidOpts.Error())
|
||||
}
|
||||
sort.Sort(ByPriority(middlewares))
|
||||
return middlewares, errs.Error()
|
||||
}
|
||||
|
||||
func BuildMiddlewareFromMap(name string, middlewaresMap map[string]OptionsRaw) (*Middleware, E.Error) {
|
||||
compiled, err := compileMiddlewares(middlewaresMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewMiddlewareChain(name, compiled), nil
|
||||
}
|
||||
|
||||
// TODO: check conflict or duplicates.
|
||||
func BuildMiddlewareFromChainRaw(name string, defs []map[string]any) (*Middleware, E.Error) {
|
||||
chainErr := E.NewBuilder("")
|
||||
chain := make([]*Middleware, 0, len(defs))
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestBuild(t *testing.T) {
|
||||
errs := E.NewBuilder("")
|
||||
middlewares := BuildMiddlewaresFromYAML("", testMiddlewareCompose, errs)
|
||||
ExpectNoError(t, errs.Error())
|
||||
E.Must(json.MarshalIndent(middlewares, "", " "))
|
||||
Must(json.MarshalIndent(middlewares, "", " "))
|
||||
// t.Log(string(data))
|
||||
// TODO: test
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/reverseproxy"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
//go:embed test_data/sample_headers.json
|
||||
@@ -79,11 +80,11 @@ type TestResult struct {
|
||||
|
||||
type testArgs struct {
|
||||
middlewareOpt OptionsRaw
|
||||
upstreamURL types.URL
|
||||
upstreamURL *types.URL
|
||||
|
||||
realRoundTrip bool
|
||||
|
||||
reqURL types.URL
|
||||
reqURL *types.URL
|
||||
reqMethod string
|
||||
headers http.Header
|
||||
body []byte
|
||||
@@ -94,14 +95,14 @@ type testArgs struct {
|
||||
}
|
||||
|
||||
func (args *testArgs) setDefaults() {
|
||||
if args.reqURL.Nil() {
|
||||
args.reqURL = E.Must(types.ParseURL("https://example.com"))
|
||||
if args.reqURL == nil {
|
||||
args.reqURL = Must(types.ParseURL("https://example.com"))
|
||||
}
|
||||
if args.reqMethod == "" {
|
||||
args.reqMethod = http.MethodGet
|
||||
}
|
||||
if args.upstreamURL.Nil() {
|
||||
args.upstreamURL = E.Must(types.ParseURL("https://10.0.0.1:8443")) // dummy url, no actual effect
|
||||
if args.upstreamURL == nil {
|
||||
args.upstreamURL = Must(types.ParseURL("https://10.0.0.1:8443")) // dummy url, no actual effect
|
||||
}
|
||||
if args.respHeaders == nil {
|
||||
args.respHeaders = http.Header{}
|
||||
|
||||
@@ -23,12 +23,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/metrics"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
@@ -96,38 +93,7 @@ type ReverseProxy struct {
|
||||
HandlerFunc http.HandlerFunc
|
||||
|
||||
TargetName string
|
||||
TargetURL types.URL
|
||||
}
|
||||
|
||||
type httpMetricLogger struct {
|
||||
http.ResponseWriter
|
||||
timestamp time.Time
|
||||
labels *metrics.HTTPRouteMetricLabels
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter.
|
||||
func (l *httpMetricLogger) WriteHeader(status int) {
|
||||
l.ResponseWriter.WriteHeader(status)
|
||||
duration := time.Since(l.timestamp)
|
||||
go func() {
|
||||
m := metrics.GetRouteMetrics()
|
||||
m.HTTPReqTotal.Inc()
|
||||
m.HTTPReqElapsed.With(l.labels).Set(float64(duration.Milliseconds()))
|
||||
|
||||
// ignore 1xx
|
||||
switch {
|
||||
case status >= 500:
|
||||
m.HTTP5xx.With(l.labels).Inc()
|
||||
case status >= 400:
|
||||
m.HTTP4xx.With(l.labels).Inc()
|
||||
case status >= 200:
|
||||
m.HTTP2xx3xx.With(l.labels).Inc()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (l *httpMetricLogger) Unwrap() http.ResponseWriter {
|
||||
return l.ResponseWriter
|
||||
TargetURL *types.URL
|
||||
}
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
@@ -167,7 +133,7 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
||||
// URLs to the scheme, host, and base path provided in target. If the
|
||||
// target's path is "/base" and the incoming request was for "/dir",
|
||||
// the target request will be for /base/dir.
|
||||
func NewReverseProxy(name string, target types.URL, transport http.RoundTripper) *ReverseProxy {
|
||||
func NewReverseProxy(name string, target *types.URL, transport http.RoundTripper) *ReverseProxy {
|
||||
if transport == nil {
|
||||
panic("nil transport")
|
||||
}
|
||||
@@ -181,15 +147,11 @@ func NewReverseProxy(name string, target types.URL, transport http.RoundTripper)
|
||||
return rp
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) UnregisterMetrics() {
|
||||
metrics.GetRouteMetrics().UnregisterService(p.TargetName)
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) rewriteRequestURL(req *http.Request) {
|
||||
targetQuery := p.TargetURL.RawQuery
|
||||
req.URL.Scheme = p.TargetURL.Scheme
|
||||
req.URL.Host = p.TargetURL.Host
|
||||
req.URL.Path, req.URL.RawPath = joinURLPath(p.TargetURL.URL, req.URL)
|
||||
req.URL.Path, req.URL.RawPath = joinURLPath(&p.TargetURL.URL, req.URL)
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
@@ -255,28 +217,6 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
visitorIP, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
visitorIP = req.RemoteAddr
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
t := time.Now()
|
||||
// req.RemoteAddr had been modified by middleware (if any)
|
||||
lbls := &metrics.HTTPRouteMetricLabels{
|
||||
Service: p.TargetName,
|
||||
Method: req.Method,
|
||||
Host: req.Host,
|
||||
Visitor: visitorIP,
|
||||
Path: req.URL.Path,
|
||||
}
|
||||
rw = &httpMetricLogger{
|
||||
ResponseWriter: rw,
|
||||
timestamp: t,
|
||||
labels: lbls,
|
||||
}
|
||||
}
|
||||
|
||||
transport := p.Transport
|
||||
|
||||
ctx := req.Context()
|
||||
@@ -360,7 +300,11 @@ func (p *ReverseProxy) handler(rw http.ResponseWriter, req *http.Request) {
|
||||
// separated list and fold multiple headers into one.
|
||||
prior, ok := outreq.Header[gphttp.HeaderXForwardedFor]
|
||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||
xff := visitorIP
|
||||
|
||||
xff, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
xff = req.RemoteAddr
|
||||
}
|
||||
if len(prior) > 0 {
|
||||
xff = strings.Join(prior, ", ") + ", " + xff
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package types
|
||||
|
||||
import (
|
||||
urlPkg "net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type URL struct {
|
||||
*urlPkg.URL
|
||||
_ utils.NoCopy
|
||||
urlPkg.URL
|
||||
}
|
||||
|
||||
func MustParseURL(url string) URL {
|
||||
func MustParseURL(url string) *URL {
|
||||
u, err := ParseURL(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -16,40 +19,38 @@ func MustParseURL(url string) URL {
|
||||
return u
|
||||
}
|
||||
|
||||
func ParseURL(url string) (URL, error) {
|
||||
u, err := urlPkg.Parse(url)
|
||||
func ParseURL(url string) (*URL, error) {
|
||||
u := &URL{}
|
||||
return u, u.Parse(url)
|
||||
}
|
||||
|
||||
func NewURL(url *urlPkg.URL) *URL {
|
||||
return &URL{URL: *url}
|
||||
}
|
||||
|
||||
func (u *URL) Parse(url string) error {
|
||||
uu, err := urlPkg.Parse(url)
|
||||
if err != nil {
|
||||
return URL{}, err
|
||||
return err
|
||||
}
|
||||
return URL{URL: u}, nil
|
||||
u.URL = *uu
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewURL(url *urlPkg.URL) URL {
|
||||
return URL{url}
|
||||
}
|
||||
|
||||
func (u URL) Nil() bool {
|
||||
return u.URL == nil
|
||||
}
|
||||
|
||||
func (u URL) String() string {
|
||||
if u.URL == nil {
|
||||
func (u *URL) String() string {
|
||||
if u == nil {
|
||||
return "nil"
|
||||
}
|
||||
return u.URL.String()
|
||||
}
|
||||
|
||||
func (u URL) MarshalJSON() (text []byte, err error) {
|
||||
if u.URL == nil {
|
||||
func (u *URL) MarshalJSON() (text []byte, err error) {
|
||||
if u == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return []byte("\"" + u.URL.String() + "\""), nil
|
||||
}
|
||||
|
||||
func (u URL) Equals(other *URL) bool {
|
||||
return u.URL == other.URL || u.String() == other.String()
|
||||
}
|
||||
|
||||
func (u URL) JoinPath(path string) URL {
|
||||
return URL{u.URL.JoinPath(path)}
|
||||
func (u *URL) Equals(other *URL) bool {
|
||||
return u.String() == other.String()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package notif
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@@ -45,3 +47,23 @@ func (base *ProviderBase) GetURL() string {
|
||||
func (base *ProviderBase) GetToken() string {
|
||||
return base.Token
|
||||
}
|
||||
|
||||
func (base *ProviderBase) GetMethod() string {
|
||||
return http.MethodPost
|
||||
}
|
||||
|
||||
func (base *ProviderBase) GetMIMEType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
func (base *ProviderBase) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
// no-op by default
|
||||
}
|
||||
|
||||
func (base *ProviderBase) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
return E.Errorf("%s status %d: %s", base.Name, resp.StatusCode, body)
|
||||
}
|
||||
return E.Errorf("%s status %d", base.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import "fmt"
|
||||
type Color uint
|
||||
|
||||
const (
|
||||
Red Color = 0xff0000
|
||||
Green Color = 0x00ff00
|
||||
Blue Color = 0x0000ff
|
||||
ColorError Color = 0xff0000
|
||||
ColorSuccess Color = 0x00ff00
|
||||
ColorInfo Color = 0x0000ff
|
||||
)
|
||||
|
||||
func (c Color) HexString() string {
|
||||
|
||||
@@ -38,6 +38,8 @@ func (cfg *NotificationConfig) UnmarshalMap(m map[string]any) (err E.Error) {
|
||||
cfg.Provider = &Webhook{}
|
||||
case ProviderGotify:
|
||||
cfg.Provider = &GotifyClient{}
|
||||
case ProviderNtfy:
|
||||
cfg.Provider = &Ntfy{}
|
||||
default:
|
||||
return ErrUnknownNotifProvider.
|
||||
Subject(cfg.ProviderName).
|
||||
|
||||
@@ -14,10 +14,15 @@ type (
|
||||
logCh chan *LogMessage
|
||||
providers F.Set[Provider]
|
||||
}
|
||||
LogField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
LogFields []LogField
|
||||
LogMessage struct {
|
||||
Level zerolog.Level
|
||||
Title string
|
||||
Extras map[string]any
|
||||
Extras LogFields
|
||||
Color Color
|
||||
}
|
||||
)
|
||||
@@ -48,6 +53,10 @@ func Notify(msg *LogMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *LogFields) Add(name, value string) {
|
||||
*f = append(*f, LogField{Name: name, Value: value})
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) RegisterProvider(cfg *NotificationConfig) {
|
||||
disp.providers.Add(cfg.Provider)
|
||||
}
|
||||
@@ -74,6 +83,9 @@ func (disp *Dispatcher) start() {
|
||||
}
|
||||
|
||||
func (disp *Dispatcher) dispatch(msg *LogMessage) {
|
||||
if true {
|
||||
return
|
||||
}
|
||||
task := disp.task.Subtask("dispatcher")
|
||||
defer task.Finish("notif dispatched")
|
||||
|
||||
|
||||
@@ -3,32 +3,22 @@ package notif
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func formatMarkdown(extras map[string]interface{}) string {
|
||||
func formatMarkdown(extras LogFields) string {
|
||||
msg := bytes.NewBufferString("")
|
||||
for k, v := range extras {
|
||||
for _, field := range extras {
|
||||
msg.WriteString("#### ")
|
||||
msg.WriteString(k)
|
||||
msg.WriteString(field.Name)
|
||||
msg.WriteRune('\n')
|
||||
msg.WriteString(fmt.Sprintf("%v", v))
|
||||
msg.WriteString(field.Value)
|
||||
msg.WriteRune('\n')
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func formatDiscord(extras map[string]interface{}) (string, error) {
|
||||
fieldsMap := make([]map[string]any, len(extras))
|
||||
i := 0
|
||||
for k, extra := range extras {
|
||||
fieldsMap[i] = map[string]any{
|
||||
"name": k,
|
||||
"value": extra,
|
||||
}
|
||||
i++
|
||||
}
|
||||
fields, err := json.Marshal(fieldsMap)
|
||||
func formatDiscord(extras LogFields) (string, error) {
|
||||
fields, err := json.Marshal(extras)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -24,16 +24,6 @@ func (client *GotifyClient) GetURL() string {
|
||||
return client.URL + gotifyMsgEndpoint
|
||||
}
|
||||
|
||||
// GetMethod implements Provider.
|
||||
func (client *GotifyClient) GetMethod() string {
|
||||
return http.MethodPost
|
||||
}
|
||||
|
||||
// GetMIMEType implements Provider.
|
||||
func (client *GotifyClient) GetMIMEType() string {
|
||||
return "application/json"
|
||||
}
|
||||
|
||||
// MakeBody implements Provider.
|
||||
func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
var priority int
|
||||
@@ -71,7 +61,7 @@ func (client *GotifyClient) makeRespError(resp *http.Response) error {
|
||||
var errm model.Error
|
||||
err := json.NewDecoder(resp.Body).Decode(&errm)
|
||||
if err != nil {
|
||||
return fmt.Errorf(ProviderGotify+" status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, but failed to decode err response: %w", client.Name, resp.StatusCode, err)
|
||||
}
|
||||
return fmt.Errorf(ProviderGotify+" status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
return fmt.Errorf("%s status %d %s: %s", client.Name, resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||
}
|
||||
|
||||
89
internal/notif/ntfy.go
Normal file
89
internal/notif/ntfy.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package notif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
// See https://docs.ntfy.sh/publish
|
||||
type Ntfy struct {
|
||||
ProviderBase
|
||||
Topic string `json:"topic"`
|
||||
Style NtfyStyle `json:"style"`
|
||||
}
|
||||
|
||||
type NtfyStyle string
|
||||
|
||||
const (
|
||||
NtfyStyleMarkdown NtfyStyle = "markdown"
|
||||
NtfyStylePlain NtfyStyle = "plain"
|
||||
)
|
||||
|
||||
func (n *Ntfy) Validate() E.Error {
|
||||
if n.URL == "" {
|
||||
return E.New("url is required")
|
||||
}
|
||||
if n.Topic == "" {
|
||||
return E.New("topic is required")
|
||||
}
|
||||
if n.Topic[0] == '/' {
|
||||
return E.New("topic should not start with a slash")
|
||||
}
|
||||
switch n.Style {
|
||||
case "":
|
||||
n.Style = NtfyStyleMarkdown
|
||||
case NtfyStyleMarkdown, NtfyStylePlain:
|
||||
default:
|
||||
return E.Errorf("invalid style, expecting %q or %q, got %q", NtfyStyleMarkdown, NtfyStylePlain, n.Style)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetURL() string {
|
||||
if n.URL[len(n.URL)-1] == '/' {
|
||||
return n.URL + n.Topic
|
||||
}
|
||||
return n.URL + "/" + n.Topic
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetMIMEType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *Ntfy) GetToken() string {
|
||||
return n.Token
|
||||
}
|
||||
|
||||
func (n *Ntfy) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
switch n.Style {
|
||||
case NtfyStyleMarkdown:
|
||||
return strings.NewReader(formatMarkdown(logMsg.Extras)), nil
|
||||
default:
|
||||
return &bytes.Buffer{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Ntfy) SetHeaders(logMsg *LogMessage, headers http.Header) {
|
||||
headers.Set("Title", logMsg.Title)
|
||||
|
||||
switch logMsg.Level {
|
||||
// warning (or other unspecified) uses default priority
|
||||
case zerolog.FatalLevel:
|
||||
headers.Set("Priority", "urgent")
|
||||
case zerolog.ErrorLevel:
|
||||
headers.Set("Priority", "high")
|
||||
case zerolog.InfoLevel:
|
||||
headers.Set("Priority", "low")
|
||||
case zerolog.DebugLevel:
|
||||
headers.Set("Priority", "min")
|
||||
}
|
||||
|
||||
if n.Style == NtfyStyleMarkdown {
|
||||
headers.Set("Markdown", "yes")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
@@ -21,6 +22,7 @@ type (
|
||||
GetMIMEType() string
|
||||
|
||||
MakeBody(logMsg *LogMessage) (io.Reader, error)
|
||||
SetHeaders(logMsg *LogMessage, headers http.Header)
|
||||
|
||||
makeRespError(resp *http.Response) error
|
||||
}
|
||||
@@ -30,6 +32,7 @@ type (
|
||||
|
||||
const (
|
||||
ProviderGotify = "gotify"
|
||||
ProviderNtfy = "ntfy"
|
||||
ProviderWebhook = "webhook"
|
||||
)
|
||||
|
||||
@@ -38,6 +41,10 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
||||
if err != nil {
|
||||
return E.PrependSubject(provider.GetName(), err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
@@ -52,6 +59,7 @@ func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) err
|
||||
if provider.GetToken() != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+provider.GetToken())
|
||||
}
|
||||
provider.SetHeaders(msg, req.Header)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -92,12 +92,12 @@ func (webhook *Webhook) GetMIMEType() string {
|
||||
func (webhook *Webhook) makeRespError(resp *http.Response) error {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webhook status %d, failed to read body: %w", resp.StatusCode, err)
|
||||
return fmt.Errorf("%s status %d, failed to read body: %w", webhook.Name, resp.StatusCode, err)
|
||||
}
|
||||
if len(body) > 0 {
|
||||
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, body)
|
||||
return fmt.Errorf("%s status %d: %s", webhook.Name, resp.StatusCode, body)
|
||||
}
|
||||
return fmt.Errorf("webhook status %d", resp.StatusCode)
|
||||
return fmt.Errorf("%s status %d", webhook.Name, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
type Entry = route.Entry
|
||||
|
||||
func ValidateEntry(m *route.RawEntry) (Entry, E.Error) {
|
||||
scheme, err := route.NewScheme(m.Scheme)
|
||||
if err != nil {
|
||||
return nil, E.From(err)
|
||||
}
|
||||
|
||||
var entry Entry
|
||||
errs := E.NewBuilder("entry validation failed")
|
||||
if scheme.IsStream() {
|
||||
entry = validateStreamEntry(m, errs)
|
||||
} else {
|
||||
entry = validateRPEntry(m, scheme, errs)
|
||||
}
|
||||
if errs.HasError() {
|
||||
return nil, errs.Error()
|
||||
}
|
||||
if !UseHealthCheck(entry) && (UseLoadBalance(entry) || UseIdleWatcher(entry)) {
|
||||
return nil, E.New("healthCheck.disable cannot be true when loadbalancer or idlewatcher is enabled")
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func IsDocker(entry Entry) bool {
|
||||
iw := entry.IdlewatcherConfig()
|
||||
return iw != nil && iw.ContainerID != ""
|
||||
}
|
||||
|
||||
func IsZeroPort(entry Entry) bool {
|
||||
return entry.TargetURL().Port() == "0"
|
||||
}
|
||||
|
||||
func ShouldNotServe(entry Entry) bool {
|
||||
return IsZeroPort(entry) && !UseIdleWatcher(entry)
|
||||
}
|
||||
|
||||
func UseLoadBalance(entry Entry) bool {
|
||||
lb := entry.RawEntry().LoadBalance
|
||||
return lb != nil && lb.Link != ""
|
||||
}
|
||||
|
||||
func UseIdleWatcher(entry Entry) bool {
|
||||
iw := entry.IdlewatcherConfig()
|
||||
return iw != nil && iw.IdleTimeout > 0
|
||||
}
|
||||
|
||||
func UseHealthCheck(entry Entry) bool {
|
||||
hc := entry.RawEntry().HealthCheck
|
||||
return hc != nil && !hc.Disable
|
||||
}
|
||||
|
||||
func UseAccessLog(entry Entry) bool {
|
||||
return entry.RawEntry().AccessLog != nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
type ReverseProxyEntry struct { // real model after validation
|
||||
Raw *route.RawEntry `json:"raw"`
|
||||
URL net.URL `json:"url"`
|
||||
|
||||
/* Docker only */
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) TargetName() string {
|
||||
return rp.Raw.Alias
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) TargetURL() net.URL {
|
||||
return rp.URL
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) RawEntry() *route.RawEntry {
|
||||
return rp.Raw
|
||||
}
|
||||
|
||||
func (rp *ReverseProxyEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return rp.Idlewatcher
|
||||
}
|
||||
|
||||
func validateRPEntry(m *route.RawEntry, s route.Scheme, errs *E.Builder) *ReverseProxyEntry {
|
||||
cont := m.Container
|
||||
if cont == nil {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
if m.LoadBalance != nil && m.LoadBalance.Link == "" {
|
||||
m.LoadBalance = nil
|
||||
}
|
||||
|
||||
port := E.Collect(errs, route.ValidatePort, m.Port)
|
||||
url := E.Collect(errs, url.Parse, fmt.Sprintf("%s://%s:%d", s, m.Host, port))
|
||||
iwCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
|
||||
|
||||
if errs.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ReverseProxyEntry{
|
||||
Raw: m,
|
||||
URL: net.NewURL(url),
|
||||
Idlewatcher: iwCfg,
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
)
|
||||
|
||||
type StreamEntry struct {
|
||||
Raw *route.RawEntry `json:"raw"`
|
||||
|
||||
Scheme route.StreamScheme `json:"scheme"`
|
||||
URL net.URL `json:"url"`
|
||||
ListenURL net.URL `json:"listening_url"`
|
||||
Port route.StreamPort `json:"port,omitempty"`
|
||||
|
||||
/* Docker only */
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StreamEntry) TargetName() string {
|
||||
return s.Raw.Alias
|
||||
}
|
||||
|
||||
func (s *StreamEntry) TargetURL() net.URL {
|
||||
return s.URL
|
||||
}
|
||||
|
||||
func (s *StreamEntry) RawEntry() *route.RawEntry {
|
||||
return s.Raw
|
||||
}
|
||||
|
||||
func (s *StreamEntry) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return s.Idlewatcher
|
||||
}
|
||||
|
||||
func validateStreamEntry(m *route.RawEntry, errs *E.Builder) *StreamEntry {
|
||||
cont := m.Container
|
||||
if cont == nil {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
port := E.Collect(errs, route.ValidateStreamPort, m.Port)
|
||||
scheme := E.Collect(errs, route.ValidateStreamScheme, m.Scheme)
|
||||
url := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", scheme.ProxyScheme, m.Host, port.ProxyPort))
|
||||
listenURL := E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://:%d", scheme.ListeningScheme, port.ListeningPort))
|
||||
idleWatcherCfg := E.Collect(errs, idlewatcher.ValidateConfig, cont)
|
||||
|
||||
if errs.HasError() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StreamEntry{
|
||||
Raw: m,
|
||||
Scheme: *scheme,
|
||||
URL: url,
|
||||
ListenURL: listenURL,
|
||||
Port: port,
|
||||
Idlewatcher: idleWatcherCfg,
|
||||
}
|
||||
}
|
||||
134
internal/route/fileserver.go
Normal file
134
internal/route/fileserver.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
metricslogger "github.com/yusing/go-proxy/internal/net/http/middleware/metrics_logger"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type (
|
||||
FileServer struct {
|
||||
*Route
|
||||
|
||||
Health *monitor.FileServerHealthMonitor `json:"health"`
|
||||
|
||||
task *task.Task
|
||||
middleware *middleware.Middleware
|
||||
handler http.Handler
|
||||
accessLogger *accesslog.AccessLogger
|
||||
}
|
||||
)
|
||||
|
||||
func handler(root string) http.Handler {
|
||||
return http.FileServer(http.Dir(root))
|
||||
}
|
||||
|
||||
func NewFileServer(base *Route) (*FileServer, E.Error) {
|
||||
s := &FileServer{Route: base}
|
||||
|
||||
s.Root = filepath.Clean(s.Root)
|
||||
if !path.IsAbs(s.Root) {
|
||||
return nil, E.New("`root` must be an absolute path")
|
||||
}
|
||||
|
||||
s.handler = handler(s.Root)
|
||||
|
||||
if len(s.Middlewares) > 0 {
|
||||
mid, err := middleware.BuildMiddlewareFromMap(s.Alias, s.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.middleware = mid
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (s *FileServer) Start(parent task.Parent) E.Error {
|
||||
s.task = parent.Subtask("fileserver."+s.TargetName(), false)
|
||||
|
||||
pathPatterns := s.PathPatterns
|
||||
switch {
|
||||
case len(pathPatterns) == 0:
|
||||
case len(pathPatterns) == 1 && pathPatterns[0] == "/":
|
||||
default:
|
||||
mux := gphttp.NewServeMux()
|
||||
patErrs := E.NewBuilder("invalid path pattern(s)")
|
||||
for _, p := range pathPatterns {
|
||||
patErrs.Add(mux.Handle(p, s.handler))
|
||||
}
|
||||
if err := patErrs.Error(); err != nil {
|
||||
s.task.Finish(err)
|
||||
return err
|
||||
}
|
||||
s.handler = mux
|
||||
}
|
||||
|
||||
if s.middleware != nil {
|
||||
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s.middleware.ServeHTTP(s.handler.ServeHTTP, w, r)
|
||||
})
|
||||
}
|
||||
|
||||
if s.UseAccessLog() {
|
||||
var err error
|
||||
s.accessLogger, err = accesslog.NewFileAccessLogger(s.task, s.AccessLog)
|
||||
if err != nil {
|
||||
s.task.Finish(err)
|
||||
return E.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
metricsLogger := metricslogger.NewMetricsLogger(s.TargetName())
|
||||
s.handler = metricsLogger.GetHandler(s.handler)
|
||||
s.task.OnCancel("reset_metrics", metricsLogger.ResetMetrics)
|
||||
}
|
||||
|
||||
if s.UseHealthCheck() {
|
||||
s.Health = monitor.NewFileServerHealthMonitor(s.TargetName(), s.HealthCheck, s.Root)
|
||||
if err := s.Health.Start(s.task); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
routes.SetHTTPRoute(s.TargetName(), s)
|
||||
s.task.OnCancel("entrypoint_remove_route", func() {
|
||||
routes.DeleteHTTPRoute(s.TargetName())
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileServer) Task() *task.Task {
|
||||
return s.task
|
||||
}
|
||||
|
||||
// Finish implements task.TaskFinisher.
|
||||
func (s *FileServer) Finish(reason any) {
|
||||
s.task.Finish(reason)
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (s *FileServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
s.handler.ServeHTTP(w, req)
|
||||
if s.accessLogger != nil {
|
||||
s.accessLogger.Log(req, req.Response)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileServer) HealthMonitor() health.HealthMonitor {
|
||||
return s.Health
|
||||
}
|
||||
122
internal/route/fileserver_test.go
Normal file
122
internal/route/fileserver_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
//nolint:gofumpt
|
||||
package route
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestPathTraversalAttack(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
root := filepath.Join(tmp, "static")
|
||||
if err := os.Mkdir(root, 0755); err != nil {
|
||||
t.Fatalf("Failed to create root directory: %v", err)
|
||||
}
|
||||
|
||||
// Create a file inside the root
|
||||
validPath := "test.txt"
|
||||
validContent := "test content"
|
||||
if err := os.WriteFile(filepath.Join(root, validPath), []byte(validContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// create one at ..
|
||||
secretFile := "secret.txt"
|
||||
if err := os.WriteFile(filepath.Join(tmp, secretFile), []byte(validContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
traversals := []string{
|
||||
"../",
|
||||
"./../",
|
||||
"./.././",
|
||||
"..%2f",
|
||||
".%2f..%2f",
|
||||
".%2f%2e%2e",
|
||||
".%2e",
|
||||
".%2e/",
|
||||
".%2e%2f",
|
||||
"%2e.",
|
||||
"%2e%2e",
|
||||
}
|
||||
|
||||
for _, traversal := range traversals {
|
||||
traversals = append(traversals, "%2f"+traversal)
|
||||
traversals = append(traversals, traversal+"%2f")
|
||||
traversals = append(traversals, "%2f"+traversal+"%2f")
|
||||
traversals = append(traversals, "/"+traversal)
|
||||
traversals = append(traversals, traversal+"/")
|
||||
traversals = append(traversals, "/"+traversal+"/")
|
||||
}
|
||||
|
||||
// Setup the FileServer
|
||||
fs, err := NewFileServer(&Route{Root: root})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create FileServer: %v", err)
|
||||
}
|
||||
|
||||
// Create a test server with the handler
|
||||
ts := httptest.NewServer(fs.handler)
|
||||
defer ts.Close()
|
||||
|
||||
// Test valid path
|
||||
t.Run("valid path", func(t *testing.T) {
|
||||
validURL := ts.URL + "/" + validPath
|
||||
resp, err := http.Get(validURL)
|
||||
if err != nil {
|
||||
t.Errorf("Error making request to %s: %v", validURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected 200 OK, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading response body: %v", err)
|
||||
}
|
||||
|
||||
if string(body) != validContent {
|
||||
t.Errorf("Expected %q, got %q", validContent, string(body))
|
||||
}
|
||||
})
|
||||
|
||||
// Test ../ path
|
||||
// tsURL := Must(url.Parse(ts.URL))
|
||||
for _, traversal := range traversals {
|
||||
p := traversal + secretFile
|
||||
t.Run(p, func(t *testing.T) {
|
||||
u := &url.URL{Scheme: "http", Host: ts.Listener.Addr().String(), Path: p}
|
||||
resp, err := http.DefaultClient.Do(&http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: u,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Error making request to %s: %v", p, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound && resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 404 or 400, got %d in url %s", resp.StatusCode, u.String())
|
||||
}
|
||||
|
||||
u = Must(url.Parse(ts.URL + "/" + p))
|
||||
resp, err = http.DefaultClient.Do(&http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: u,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Error making request to %s: %v", u.String(), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package provider
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -62,15 +61,13 @@ func (p *DockerProvider) NewWatcher() watcher.Watcher {
|
||||
}
|
||||
|
||||
func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
||||
routes := route.NewRoutes()
|
||||
entries := route.NewProxyEntries()
|
||||
|
||||
containers, err := docker.ListContainers(p.dockerHost)
|
||||
if err != nil {
|
||||
return routes, E.From(err)
|
||||
return nil, E.From(err)
|
||||
}
|
||||
|
||||
errs := E.NewBuilder("")
|
||||
routes := make(route.Routes)
|
||||
|
||||
for _, c := range containers {
|
||||
container := docker.FromDocker(&c, p.dockerHost)
|
||||
@@ -78,47 +75,38 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
||||
continue
|
||||
}
|
||||
|
||||
newEntries, err := p.entriesFromContainerLabels(container)
|
||||
newEntries, err := p.routesFromContainerLabels(container)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(container.ContainerName))
|
||||
}
|
||||
// although err is not nil
|
||||
// there may be some valid entries in `en`
|
||||
dups := entries.MergeFrom(newEntries)
|
||||
// add the duplicate proxy entries to the error
|
||||
dups.RangeAll(func(k string, v *route.RawEntry) {
|
||||
errs.Addf("duplicated alias %s", k)
|
||||
})
|
||||
for k, v := range newEntries {
|
||||
if routes.Contains(k) {
|
||||
errs.Addf("duplicated alias %s", k)
|
||||
} else {
|
||||
routes[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routes, err = route.FromEntries(p.ShortName(), entries)
|
||||
errs.Add(err)
|
||||
|
||||
return routes, errs.Error()
|
||||
}
|
||||
|
||||
func (p *DockerProvider) shouldIgnore(container *docker.Container) bool {
|
||||
return container.IsExcluded ||
|
||||
!container.IsExplicit && p.IsExplicitOnly() ||
|
||||
!container.IsExplicit && container.IsDatabase ||
|
||||
strings.HasSuffix(container.ContainerName, "-old")
|
||||
}
|
||||
|
||||
// Returns a list of proxy entries for a container.
|
||||
// Always non-nil.
|
||||
func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries route.RawEntries, _ E.Error) {
|
||||
entries = route.NewProxyEntries()
|
||||
|
||||
if p.shouldIgnore(container) {
|
||||
return
|
||||
func (p *DockerProvider) routesFromContainerLabels(container *docker.Container) (route.Routes, E.Error) {
|
||||
if !container.IsExplicit && p.IsExplicitOnly() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
routes := make(route.Routes, len(container.Aliases))
|
||||
|
||||
// init entries map for all aliases
|
||||
for _, a := range container.Aliases {
|
||||
entries.Store(a, &route.RawEntry{
|
||||
Alias: a,
|
||||
Container: container,
|
||||
})
|
||||
routes[a] = &route.Route{
|
||||
Metadata: route.Metadata{
|
||||
Container: container,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
errs := E.NewBuilder("label errors")
|
||||
@@ -170,32 +158,32 @@ func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container)
|
||||
}
|
||||
|
||||
// init entry if not exist
|
||||
en, ok := entries.Load(alias)
|
||||
r, ok := routes[alias]
|
||||
if !ok {
|
||||
en = &route.RawEntry{
|
||||
Alias: alias,
|
||||
Container: container,
|
||||
r = &route.Route{
|
||||
Metadata: route.Metadata{
|
||||
Container: container,
|
||||
},
|
||||
}
|
||||
entries.Store(alias, en)
|
||||
routes[alias] = r
|
||||
}
|
||||
|
||||
// deserialize map into entry object
|
||||
err := U.Deserialize(entryMap, en)
|
||||
err := U.Deserialize(entryMap, r)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(alias))
|
||||
} else {
|
||||
entries.Store(alias, en)
|
||||
routes[alias] = r
|
||||
}
|
||||
}
|
||||
if wildcardProps != nil {
|
||||
entries.Range(func(alias string, re *route.RawEntry) bool {
|
||||
for _, re := range routes {
|
||||
if err := U.Deserialize(wildcardProps, re); err != nil {
|
||||
errs.Add(err.Subject(docker.WildcardAlias))
|
||||
return false
|
||||
break
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return entries, errs.Error()
|
||||
return routes, errs.Error()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestParseDockerLabels(t *testing.T) {
|
||||
labels := make(map[string]string)
|
||||
ExpectNoError(t, yaml.Unmarshal(testDockerLabelsYAML, &labels))
|
||||
|
||||
routes, err := provider.entriesFromContainerLabels(
|
||||
routes, err := provider.routesFromContainerLabels(
|
||||
docker.FromDocker(&types.Container{
|
||||
Names: []string{"container"},
|
||||
Labels: labels,
|
||||
@@ -31,6 +31,6 @@ func TestParseDockerLabels(t *testing.T) {
|
||||
}, "/var/run/docker.sock"),
|
||||
)
|
||||
ExpectNoError(t, err)
|
||||
ExpectTrue(t, routes.Has("app"))
|
||||
ExpectTrue(t, routes.Has("app1"))
|
||||
ExpectTrue(t, routes.Contains("app"))
|
||||
ExpectTrue(t, routes.Contains("app1"))
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
T "github.com/yusing/go-proxy/internal/route/types"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
@@ -23,7 +21,7 @@ const (
|
||||
testDockerIP = "172.17.0.123"
|
||||
)
|
||||
|
||||
func makeEntries(cont *types.Container, dockerHostIP ...string) route.RawEntries {
|
||||
func makeRoutes(cont *types.Container, dockerHostIP ...string) route.Routes {
|
||||
var p DockerProvider
|
||||
var host string
|
||||
if len(dockerHostIP) > 0 {
|
||||
@@ -31,12 +29,13 @@ func makeEntries(cont *types.Container, dockerHostIP ...string) route.RawEntries
|
||||
} else {
|
||||
host = client.DefaultDockerHost
|
||||
}
|
||||
cont.ID = "test"
|
||||
p.name = "test"
|
||||
entries := E.Must(p.entriesFromContainerLabels(D.FromDocker(cont, host)))
|
||||
entries.RangeAll(func(k string, v *route.RawEntry) {
|
||||
v.Finalize()
|
||||
})
|
||||
return entries
|
||||
routes := Must(p.routesFromContainerLabels(D.FromDocker(cont, host)))
|
||||
for _, r := range routes {
|
||||
r.Finalize()
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func TestExplicitOnly(t *testing.T) {
|
||||
@@ -57,43 +56,41 @@ func TestApplyLabel(t *testing.T) {
|
||||
"GET /static",
|
||||
}
|
||||
middlewaresExpect := map[string]map[string]any{
|
||||
"middleware1": {
|
||||
"prop1": "value1",
|
||||
"prop2": "value2",
|
||||
},
|
||||
"middleware2": {
|
||||
"prop3": "value3",
|
||||
"prop4": "value4",
|
||||
"request": {
|
||||
"set_headers": map[string]any{
|
||||
"X-Header": "value1",
|
||||
},
|
||||
"add_headers": map[string]any{
|
||||
"X-Header2": "value2",
|
||||
},
|
||||
},
|
||||
}
|
||||
entries := makeEntries(&types.Container{
|
||||
entries := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: "",
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.middleware1.prop1": "value1",
|
||||
"proxy.a.middlewares.middleware1.prop2": "value2",
|
||||
"proxy.a.middlewares.middleware2.prop3": "value3",
|
||||
"proxy.a.middlewares.middleware2.prop4": "value4",
|
||||
"proxy.a.homepage.show": "true",
|
||||
"proxy.a.homepage.icon": "png/adguard-home.png",
|
||||
"proxy.a.healthcheck.path": "/ping",
|
||||
"proxy.a.healthcheck.interval": "10s",
|
||||
D.LabelAliases: "a,b",
|
||||
D.LabelIdleTimeout: "",
|
||||
D.LabelStopMethod: common.StopMethodDefault,
|
||||
D.LabelStopSignal: "SIGTERM",
|
||||
D.LabelStopTimeout: common.StopTimeoutDefault,
|
||||
D.LabelWakeTimeout: common.WakeTimeoutDefault,
|
||||
"proxy.*.no_tls_verify": "true",
|
||||
"proxy.*.scheme": "https",
|
||||
"proxy.*.host": "app",
|
||||
"proxy.*.port": "4567",
|
||||
"proxy.a.path_patterns": pathPatterns,
|
||||
"proxy.a.middlewares.request.set_headers.X-Header": "value1",
|
||||
"proxy.a.middlewares.request.add_headers.X-Header2": "value2",
|
||||
"proxy.a.homepage.show": "true",
|
||||
"proxy.a.homepage.icon": "png/adguard-home.png",
|
||||
"proxy.a.healthcheck.path": "/ping",
|
||||
"proxy.a.healthcheck.interval": "10s",
|
||||
},
|
||||
})
|
||||
|
||||
a, ok := entries.Load("a")
|
||||
a, ok := entries["a"]
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
b, ok := entries["b"]
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "https")
|
||||
@@ -102,8 +99,8 @@ func TestApplyLabel(t *testing.T) {
|
||||
ExpectEqual(t, a.Host, "app")
|
||||
ExpectEqual(t, b.Host, "app")
|
||||
|
||||
ExpectEqual(t, a.Port, "4567")
|
||||
ExpectEqual(t, b.Port, "4567")
|
||||
ExpectEqual(t, a.Port.Proxy, 4567)
|
||||
ExpectEqual(t, b.Port.Proxy, 4567)
|
||||
|
||||
ExpectTrue(t, a.NoTLSVerify)
|
||||
ExpectTrue(t, b.NoTLSVerify)
|
||||
@@ -139,7 +136,7 @@ func TestApplyLabel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApplyLabelWithAlias(t *testing.T) {
|
||||
entries := makeEntries(&types.Container{
|
||||
entries := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
@@ -150,23 +147,23 @@ func TestApplyLabelWithAlias(t *testing.T) {
|
||||
"proxy.c.scheme": "https",
|
||||
},
|
||||
})
|
||||
a, ok := entries.Load("a")
|
||||
a, ok := entries["a"]
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
b, ok := entries["b"]
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
c, ok := entries["c"]
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Port, "3333")
|
||||
ExpectEqual(t, a.Port.Proxy, 3333)
|
||||
ExpectEqual(t, a.NoTLSVerify, true)
|
||||
ExpectEqual(t, b.Scheme, "http")
|
||||
ExpectEqual(t, b.Port, "1234")
|
||||
ExpectEqual(t, b.Port.Proxy, 1234)
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRef(t *testing.T) {
|
||||
entries := makeEntries(&types.Container{
|
||||
entries := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
State: "running",
|
||||
Labels: map[string]string{
|
||||
@@ -178,19 +175,19 @@ func TestApplyLabelWithRef(t *testing.T) {
|
||||
"proxy.#3.scheme": "https",
|
||||
},
|
||||
})
|
||||
a, ok := entries.Load("a")
|
||||
a, ok := entries["a"]
|
||||
ExpectTrue(t, ok)
|
||||
b, ok := entries.Load("b")
|
||||
b, ok := entries["b"]
|
||||
ExpectTrue(t, ok)
|
||||
c, ok := entries.Load("c")
|
||||
c, ok := entries["c"]
|
||||
ExpectTrue(t, ok)
|
||||
|
||||
ExpectEqual(t, a.Scheme, "http")
|
||||
ExpectEqual(t, a.Host, "localhost")
|
||||
ExpectEqual(t, a.Port, "4444")
|
||||
ExpectEqual(t, b.Port, "9999")
|
||||
ExpectEqual(t, a.Port.Proxy, 4444)
|
||||
ExpectEqual(t, b.Port.Proxy, 9999)
|
||||
ExpectEqual(t, c.Scheme, "https")
|
||||
ExpectEqual(t, c.Port, "1111")
|
||||
ExpectEqual(t, c.Port.Proxy, 1111)
|
||||
}
|
||||
|
||||
func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
@@ -200,11 +197,12 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a,b",
|
||||
"proxy.#1.host": "localhost",
|
||||
"proxy.*.port": "4444",
|
||||
"proxy.#4.scheme": "https",
|
||||
},
|
||||
}, "")
|
||||
var p DockerProvider
|
||||
_, err := p.entriesFromContainerLabels(c)
|
||||
_, err := p.routesFromContainerLabels(c)
|
||||
ExpectError(t, ErrAliasRefIndexOutOfRange, err)
|
||||
|
||||
c = D.FromDocker(&types.Container{
|
||||
@@ -215,7 +213,7 @@ func TestApplyLabelWithRefIndexError(t *testing.T) {
|
||||
"proxy.#0.host": "localhost",
|
||||
},
|
||||
}, "")
|
||||
_, err = p.entriesFromContainerLabels(c)
|
||||
_, err = p.routesFromContainerLabels(c)
|
||||
ExpectError(t, ErrAliasRefIndexOutOfRange, err)
|
||||
}
|
||||
|
||||
@@ -229,17 +227,17 @@ func TestDynamicAliases(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
entries := makeEntries(c)
|
||||
entries := makeRoutes(c)
|
||||
|
||||
raw, ok := entries.Load("app1")
|
||||
r, ok := entries["app1"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Scheme, "http")
|
||||
ExpectEqual(t, raw.Port, "1234")
|
||||
ExpectEqual(t, r.Scheme, "http")
|
||||
ExpectEqual(t, r.Port.Proxy, 1234)
|
||||
|
||||
raw, ok = entries.Load("app1_backend")
|
||||
r, ok = entries["app1_backend"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Scheme, "http")
|
||||
ExpectEqual(t, raw.Port, "5678")
|
||||
ExpectEqual(t, r.Scheme, "http")
|
||||
ExpectEqual(t, r.Port.Proxy, 5678)
|
||||
}
|
||||
|
||||
func TestDisableHealthCheck(t *testing.T) {
|
||||
@@ -251,22 +249,22 @@ func TestDisableHealthCheck(t *testing.T) {
|
||||
"proxy.a.port": "1234",
|
||||
},
|
||||
}
|
||||
raw, ok := makeEntries(c).Load("a")
|
||||
r, ok := makeRoutes(c)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.HealthCheck, nil)
|
||||
ExpectFalse(t, r.UseHealthCheck())
|
||||
}
|
||||
|
||||
func TestPublicIPLocalhost(t *testing.T) {
|
||||
c := &types.Container{Names: dummyNames, State: "running"}
|
||||
raw, ok := makeEntries(c).Load("a")
|
||||
r, ok := makeRoutes(c)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PublicIP, "127.0.0.1")
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
ExpectEqual(t, r.Container.PublicIP, "127.0.0.1")
|
||||
ExpectEqual(t, r.Host, r.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestPublicIPRemote(t *testing.T) {
|
||||
c := &types.Container{Names: dummyNames, State: "running"}
|
||||
raw, ok := makeEntries(c, testIP).Load("a")
|
||||
raw, ok := makeRoutes(c, testIP)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PublicIP, testIP)
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
@@ -283,10 +281,10 @@ func TestPrivateIPLocalhost(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
raw, ok := makeEntries(c).Load("a")
|
||||
r, ok := makeRoutes(c)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PrivateIP, testDockerIP)
|
||||
ExpectEqual(t, raw.Host, raw.Container.PrivateIP)
|
||||
ExpectEqual(t, r.Container.PrivateIP, testDockerIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PrivateIP)
|
||||
}
|
||||
|
||||
func TestPrivateIPRemote(t *testing.T) {
|
||||
@@ -301,11 +299,11 @@ func TestPrivateIPRemote(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
raw, ok := makeEntries(c, testIP).Load("a")
|
||||
r, ok := makeRoutes(c, testIP)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectEqual(t, raw.Container.PrivateIP, "")
|
||||
ExpectEqual(t, raw.Container.PublicIP, testIP)
|
||||
ExpectEqual(t, raw.Host, raw.Container.PublicIP)
|
||||
ExpectEqual(t, r.Container.PrivateIP, "")
|
||||
ExpectEqual(t, r.Container.PublicIP, testIP)
|
||||
ExpectEqual(t, r.Host, r.Container.PublicIP)
|
||||
}
|
||||
|
||||
func TestStreamDefaultValues(t *testing.T) {
|
||||
@@ -328,59 +326,58 @@ func TestStreamDefaultValues(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("local", func(t *testing.T) {
|
||||
raw, ok := makeEntries(cont).Load("a")
|
||||
r, ok := makeRoutes(cont)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
en := E.Must(entry.ValidateEntry(raw))
|
||||
a := ExpectType[*entry.StreamEntry](t, en)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.URL.Hostname(), privIP)
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(privPort))
|
||||
ExpectNoError(t, r.Validate())
|
||||
ExpectEqual(t, r.Scheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, r.TargetURL().Hostname(), privIP)
|
||||
ExpectEqual(t, r.Port.Listening, 0)
|
||||
ExpectEqual(t, r.Port.Proxy, int(privPort))
|
||||
})
|
||||
|
||||
t.Run("remote", func(t *testing.T) {
|
||||
raw, ok := makeEntries(cont, testIP).Load("a")
|
||||
r, ok := makeRoutes(cont, testIP)["a"]
|
||||
ExpectTrue(t, ok)
|
||||
en := E.Must(entry.ValidateEntry(raw))
|
||||
a := ExpectType[*entry.StreamEntry](t, en)
|
||||
ExpectEqual(t, a.Scheme.ListeningScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.Scheme.ProxyScheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, a.URL.Hostname(), testIP)
|
||||
ExpectEqual(t, a.Port.ListeningPort, 0)
|
||||
ExpectEqual(t, a.Port.ProxyPort, T.Port(pubPort))
|
||||
ExpectNoError(t, r.Validate())
|
||||
ExpectEqual(t, r.Scheme, T.Scheme("udp"))
|
||||
ExpectEqual(t, r.TargetURL().Hostname(), testIP)
|
||||
ExpectEqual(t, r.Port.Listening, 0)
|
||||
ExpectEqual(t, r.Port.Proxy, int(pubPort))
|
||||
})
|
||||
}
|
||||
|
||||
func TestExplicitExclude(t *testing.T) {
|
||||
_, ok := makeEntries(&types.Container{
|
||||
r, ok := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
Labels: map[string]string{
|
||||
D.LabelAliases: "a",
|
||||
D.LabelExclude: "true",
|
||||
"proxy.a.no_tls_verify": "true",
|
||||
},
|
||||
}, "").Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
}, "")["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectTrue(t, r.ShouldExclude())
|
||||
}
|
||||
|
||||
func TestImplicitExcludeDatabase(t *testing.T) {
|
||||
t.Run("mount path detection", func(t *testing.T) {
|
||||
_, ok := makeEntries(&types.Container{
|
||||
r, ok := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
Mounts: []types.MountPoint{
|
||||
{Source: "/data", Destination: "/var/lib/postgresql/data"},
|
||||
},
|
||||
}).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectTrue(t, r.ShouldExclude())
|
||||
})
|
||||
t.Run("exposed port detection", func(t *testing.T) {
|
||||
_, ok := makeEntries(&types.Container{
|
||||
r, ok := makeRoutes(&types.Container{
|
||||
Names: dummyNames,
|
||||
Ports: []types.Port{
|
||||
{Type: "tcp", PrivatePort: 5432, PublicPort: 5432},
|
||||
},
|
||||
}).Load("a")
|
||||
ExpectFalse(t, ok)
|
||||
})["a"]
|
||||
ExpectTrue(t, ok)
|
||||
ExpectTrue(t, r.ShouldExclude())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher"
|
||||
@@ -31,10 +30,10 @@ func (p *Provider) newEventHandler() *EventHandler {
|
||||
|
||||
func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event) {
|
||||
oldRoutes := handler.provider.routes
|
||||
newRoutes, err := handler.provider.loadRoutesImpl()
|
||||
newRoutes, err := handler.provider.loadRoutes()
|
||||
if err != nil {
|
||||
handler.errs.Add(err)
|
||||
if newRoutes.Size() == 0 {
|
||||
if len(newRoutes) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -47,34 +46,32 @@ func (handler *EventHandler) Handle(parent task.Parent, events []watcher.Event)
|
||||
E.LogDebug(eventsLog.About(), eventsLog.Error(), handler.provider.Logger())
|
||||
|
||||
oldRoutesLog := E.NewBuilder("old routes")
|
||||
oldRoutes.RangeAllParallel(func(k string, r *route.Route) {
|
||||
for k := range oldRoutes {
|
||||
oldRoutesLog.Adds(k)
|
||||
})
|
||||
}
|
||||
E.LogDebug(oldRoutesLog.About(), oldRoutesLog.Error(), handler.provider.Logger())
|
||||
|
||||
newRoutesLog := E.NewBuilder("new routes")
|
||||
newRoutes.RangeAllParallel(func(k string, r *route.Route) {
|
||||
for k := range newRoutes {
|
||||
newRoutesLog.Adds(k)
|
||||
})
|
||||
}
|
||||
E.LogDebug(newRoutesLog.About(), newRoutesLog.Error(), handler.provider.Logger())
|
||||
}
|
||||
|
||||
oldRoutes.RangeAll(func(k string, oldr *route.Route) {
|
||||
newr, ok := newRoutes.Load(k)
|
||||
for k, oldr := range oldRoutes {
|
||||
newr, ok := newRoutes[k]
|
||||
switch {
|
||||
case !ok:
|
||||
handler.Remove(oldr)
|
||||
case handler.matchAny(events, newr):
|
||||
handler.Update(parent, oldr, newr)
|
||||
case entry.ShouldNotServe(newr):
|
||||
handler.Remove(oldr)
|
||||
}
|
||||
})
|
||||
newRoutes.RangeAll(func(k string, newr *route.Route) {
|
||||
if !(oldRoutes.Has(k) || entry.ShouldNotServe(newr)) {
|
||||
}
|
||||
for k, newr := range newRoutes {
|
||||
if _, ok := oldRoutes[k]; !ok {
|
||||
handler.Add(parent, newr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route) bool {
|
||||
@@ -89,8 +86,8 @@ func (handler *EventHandler) matchAny(events []watcher.Event, route *route.Route
|
||||
func (handler *EventHandler) match(event watcher.Event, route *route.Route) bool {
|
||||
switch handler.provider.GetType() {
|
||||
case types.ProviderTypeDocker:
|
||||
return route.Entry.Container.ContainerID == event.ActorID ||
|
||||
route.Entry.Container.ContainerName == event.ActorName
|
||||
return route.Container.ContainerID == event.ActorID ||
|
||||
route.Container.ContainerName == event.ActorName
|
||||
case types.ProviderTypeFile:
|
||||
return true
|
||||
}
|
||||
@@ -103,14 +100,14 @@ func (handler *EventHandler) Add(parent task.Parent, route *route.Route) {
|
||||
if err != nil {
|
||||
handler.errs.Add(err.Subject("add"))
|
||||
} else {
|
||||
handler.added.Adds(route.Entry.Alias)
|
||||
handler.added.Adds(route.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Remove(route *route.Route) {
|
||||
route.Finish("route removed")
|
||||
handler.provider.routes.Delete(route.Entry.Alias)
|
||||
handler.removed.Adds(route.Entry.Alias)
|
||||
delete(handler.provider.routes, route.Alias)
|
||||
handler.removed.Adds(route.Alias)
|
||||
}
|
||||
|
||||
func (handler *EventHandler) Update(parent task.Parent, oldRoute *route.Route, newRoute *route.Route) {
|
||||
@@ -119,7 +116,7 @@ func (handler *EventHandler) Update(parent task.Parent, oldRoute *route.Route, n
|
||||
if err != nil {
|
||||
handler.errs.Add(err.Subject("update"))
|
||||
} else {
|
||||
handler.updated.Adds(newRoute.Entry.Alias)
|
||||
handler.updated.Adds(newRoute.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,16 +33,13 @@ func FileProviderImpl(filename string) (ProviderImpl, error) {
|
||||
return impl, nil
|
||||
}
|
||||
|
||||
func validate(provider string, data []byte) (route.Routes, E.Error) {
|
||||
entries, err := utils.DeserializeYAMLMap[*route.RawEntry](data)
|
||||
if err != nil {
|
||||
return route.NewRoutes(), err
|
||||
}
|
||||
return route.FromEntries(provider, entries)
|
||||
func validate(data []byte) (routes route.Routes, err E.Error) {
|
||||
err = utils.DeserializeYAML(data, &routes)
|
||||
return
|
||||
}
|
||||
|
||||
func Validate(data []byte) (err E.Error) {
|
||||
_, err = validate("", data)
|
||||
_, err = validate(data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,14 +60,15 @@ func (p *FileProvider) Logger() *zerolog.Logger {
|
||||
}
|
||||
|
||||
func (p *FileProvider) loadRoutesImpl() (route.Routes, E.Error) {
|
||||
routes := route.NewRoutes()
|
||||
|
||||
data, err := os.ReadFile(p.path)
|
||||
if err != nil {
|
||||
return routes, E.From(err)
|
||||
return nil, E.Wrap(err)
|
||||
}
|
||||
|
||||
return validate(p.ShortName(), data)
|
||||
routes, err := validate(data)
|
||||
if err != nil && len(routes) == 0 {
|
||||
return nil, E.Wrap(err)
|
||||
}
|
||||
return routes, E.Wrap(err)
|
||||
}
|
||||
|
||||
func (p *FileProvider) NewWatcher() W.Watcher {
|
||||
|
||||
@@ -12,6 +12,6 @@ import (
|
||||
var testAllFieldsYAML []byte
|
||||
|
||||
func TestFile(t *testing.T) {
|
||||
_, err := validate("", testAllFieldsYAML)
|
||||
_, err := validate(testAllFieldsYAML)
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
R "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
W "github.com/yusing/go-proxy/internal/watcher"
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
|
||||
type (
|
||||
Provider struct {
|
||||
ProviderImpl `json:"-"`
|
||||
ProviderImpl
|
||||
|
||||
t types.ProviderType
|
||||
routes R.Routes
|
||||
routes route.Routes
|
||||
|
||||
watcher W.Watcher
|
||||
}
|
||||
@@ -28,7 +28,7 @@ type (
|
||||
fmt.Stringer
|
||||
ShortName() string
|
||||
IsExplicitOnly() bool
|
||||
loadRoutesImpl() (R.Routes, E.Error)
|
||||
loadRoutesImpl() (route.Routes, E.Error)
|
||||
NewWatcher() W.Watcher
|
||||
Logger() *zerolog.Logger
|
||||
}
|
||||
@@ -41,10 +41,7 @@ const (
|
||||
var ErrEmptyProviderName = errors.New("empty provider name")
|
||||
|
||||
func newProvider(t types.ProviderType) *Provider {
|
||||
return &Provider{
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
}
|
||||
return &Provider{t: t}
|
||||
}
|
||||
|
||||
func NewFileProvider(filename string) (p *Provider, err error) {
|
||||
@@ -84,13 +81,13 @@ func (p *Provider) MarshalText() ([]byte, error) {
|
||||
return []byte(p.String()), nil
|
||||
}
|
||||
|
||||
func (p *Provider) startRoute(parent task.Parent, r *R.Route) E.Error {
|
||||
func (p *Provider) startRoute(parent task.Parent, r *route.Route) E.Error {
|
||||
err := r.Start(parent)
|
||||
if err != nil {
|
||||
return err.Subject(r.Entry.Alias)
|
||||
delete(p.routes, r.Alias)
|
||||
return err.Subject(r.Alias)
|
||||
}
|
||||
|
||||
p.routes.Store(r.Entry.Alias, r)
|
||||
p.routes[r.Alias] = r
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,11 +95,10 @@ func (p *Provider) startRoute(parent task.Parent, r *R.Route) E.Error {
|
||||
func (p *Provider) Start(parent task.Parent) E.Error {
|
||||
t := parent.Subtask("provider."+p.String(), false)
|
||||
|
||||
// routes and event queue will stop on config reload
|
||||
errs := p.routes.CollectErrorsParallel(
|
||||
func(alias string, r *R.Route) error {
|
||||
return p.startRoute(t, r)
|
||||
})
|
||||
errs := E.NewBuilder("routes error")
|
||||
for _, r := range p.routes {
|
||||
errs.Add(p.startRoute(t, r))
|
||||
}
|
||||
|
||||
eventQueue := events.NewEventQueue(
|
||||
t.Subtask("event_queue", false),
|
||||
@@ -119,32 +115,52 @@ func (p *Provider) Start(parent task.Parent) E.Error {
|
||||
)
|
||||
eventQueue.Start(p.watcher.Events(t.Context()))
|
||||
|
||||
if err := E.Join(errs...); err != nil {
|
||||
if err := errs.Error(); err != nil {
|
||||
return err.Subject(p.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) RangeRoutes(do func(string, *R.Route)) {
|
||||
p.routes.RangeAll(do)
|
||||
func (p *Provider) RangeRoutes(do func(string, *route.Route)) {
|
||||
for alias, r := range p.routes {
|
||||
do(alias, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) GetRoute(alias string) (*R.Route, bool) {
|
||||
return p.routes.Load(alias)
|
||||
func (p *Provider) GetRoute(alias string) (r *route.Route, ok bool) {
|
||||
r, ok = p.routes[alias]
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() E.Error {
|
||||
var err E.Error
|
||||
p.routes, err = p.loadRoutesImpl()
|
||||
if p.routes.Size() > 0 {
|
||||
return err
|
||||
func (p *Provider) loadRoutes() (routes route.Routes, err E.Error) {
|
||||
routes, err = p.loadRoutesImpl()
|
||||
if err != nil && len(routes) == 0 {
|
||||
return route.Routes{}, err
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
errs := E.NewBuilder("routes error")
|
||||
errs.Add(err)
|
||||
// check for exclusion
|
||||
// set alias and provider, then validate
|
||||
for alias, r := range routes {
|
||||
r.Alias = alias
|
||||
r.Provider = p.ShortName()
|
||||
if err := r.Validate(); err != nil {
|
||||
errs.Add(err.Subject(alias))
|
||||
delete(routes, alias)
|
||||
continue
|
||||
}
|
||||
if r.ShouldExclude() {
|
||||
delete(routes, alias)
|
||||
}
|
||||
}
|
||||
return err
|
||||
return routes, errs.Error()
|
||||
}
|
||||
|
||||
func (p *Provider) LoadRoutes() (err E.Error) {
|
||||
p.routes, err = p.loadRoutes()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Provider) NumRoutes() int {
|
||||
return p.routes.Size()
|
||||
return len(p.routes)
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ func (stats *RouteStats) AddOther(other RouteStats) {
|
||||
|
||||
func (p *Provider) Statistics() ProviderStats {
|
||||
var rps, streams RouteStats
|
||||
p.routes.RangeAll(func(_ string, r *R.Route) {
|
||||
switch r.Type {
|
||||
case route.RouteTypeReverseProxy:
|
||||
for _, r := range p.routes {
|
||||
switch r.Type() {
|
||||
case route.RouteTypeHTTP:
|
||||
rps.Add(r)
|
||||
case route.RouteTypeStream:
|
||||
streams.Add(r)
|
||||
}
|
||||
})
|
||||
}
|
||||
return ProviderStats{
|
||||
Total: rps.Total + streams.Total,
|
||||
RPs: rps,
|
||||
|
||||
@@ -3,7 +3,6 @@ package route
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/api/v1/favicon"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
@@ -15,37 +14,33 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
"github.com/yusing/go-proxy/internal/net/http/middleware"
|
||||
metricslogger "github.com/yusing/go-proxy/internal/net/http/middleware/metrics_logger"
|
||||
"github.com/yusing/go-proxy/internal/net/http/reverseproxy"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
"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/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||
)
|
||||
|
||||
type (
|
||||
HTTPRoute struct {
|
||||
*entry.ReverseProxyEntry
|
||||
ReveseProxyRoute struct {
|
||||
*Route
|
||||
|
||||
HealthMon health.HealthMonitor `json:"health,omitempty"`
|
||||
|
||||
loadBalancer *loadbalancer.LoadBalancer
|
||||
server loadbalancer.Server
|
||||
handler http.Handler
|
||||
rp *reverseproxy.ReverseProxy
|
||||
|
||||
task *task.Task
|
||||
|
||||
l zerolog.Logger
|
||||
}
|
||||
)
|
||||
|
||||
// var globalMux = http.NewServeMux() // TODO: support regex subdomain matching.
|
||||
|
||||
func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) {
|
||||
func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, E.Error) {
|
||||
trans := gphttp.DefaultTransport
|
||||
httpConfig := entry.Raw.HTTPConfig
|
||||
httpConfig := base.HTTPConfig
|
||||
|
||||
if httpConfig.NoTLSVerify {
|
||||
trans = gphttp.DefaultTransportNoTLS
|
||||
@@ -55,65 +50,57 @@ func NewHTTPRoute(entry *entry.ReverseProxyEntry) (impl, E.Error) {
|
||||
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
|
||||
}
|
||||
|
||||
service := entry.TargetName()
|
||||
rp := reverseproxy.NewReverseProxy(service, entry.URL, trans)
|
||||
service := base.TargetName()
|
||||
rp := reverseproxy.NewReverseProxy(service, base.ProxyURL, trans)
|
||||
|
||||
if len(entry.Raw.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, entry.Raw.Middlewares)
|
||||
if len(base.Middlewares) > 0 {
|
||||
err := middleware.PatchReverseProxy(rp, base.Middlewares)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
r := &HTTPRoute{
|
||||
ReverseProxyEntry: entry,
|
||||
rp: rp,
|
||||
l: logging.With().
|
||||
Str("type", entry.URL.Scheme).
|
||||
Str("name", service).
|
||||
Logger(),
|
||||
r := &ReveseProxyRoute{
|
||||
Route: base,
|
||||
rp: rp,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) String() string {
|
||||
func (r *ReveseProxyRoute) String() string {
|
||||
return r.TargetName()
|
||||
}
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
||||
if entry.ShouldNotServe(r) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReveseProxyRoute) Start(parent task.Parent) E.Error {
|
||||
r.task = parent.Subtask("http."+r.TargetName(), false)
|
||||
|
||||
switch {
|
||||
case entry.UseIdleWatcher(r):
|
||||
waker, err := idlewatcher.NewHTTPWaker(parent, r.ReverseProxyEntry, r.rp)
|
||||
case r.UseIdleWatcher():
|
||||
waker, err := idlewatcher.NewHTTPWaker(parent, r, r.rp)
|
||||
if err != nil {
|
||||
r.task.Finish(err)
|
||||
return err
|
||||
}
|
||||
r.handler = waker
|
||||
r.HealthMon = waker
|
||||
case entry.UseHealthCheck(r):
|
||||
if entry.IsDocker(r) {
|
||||
case r.UseHealthCheck():
|
||||
if r.IsDocker() {
|
||||
client, err := docker.ConnectClient(r.Idlewatcher.DockerHost)
|
||||
if err == nil {
|
||||
fallback := monitor.NewHTTPHealthChecker(r.rp.TargetURL, r.Raw.HealthCheck)
|
||||
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.Idlewatcher.ContainerID, r.TargetName(), r.Raw.HealthCheck, fallback)
|
||||
fallback := monitor.NewHTTPHealthChecker(r.rp.TargetURL, r.HealthCheck)
|
||||
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.Idlewatcher.ContainerID, r.TargetName(), r.HealthCheck, fallback)
|
||||
r.task.OnCancel("close_docker_client", client.Close)
|
||||
}
|
||||
}
|
||||
if r.HealthMon == nil {
|
||||
r.HealthMon = monitor.NewHTTPHealthMonitor(r.rp.TargetURL, r.Raw.HealthCheck)
|
||||
r.HealthMon = monitor.NewHTTPHealthMonitor(r.rp.TargetURL, r.HealthCheck)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.UseAccessLog(r) {
|
||||
if r.UseAccessLog() {
|
||||
var err error
|
||||
r.rp.AccessLogger, err = accesslog.NewFileAccessLogger(r.task, r.Raw.AccessLog)
|
||||
r.rp.AccessLogger, err = accesslog.NewFileAccessLogger(r.task, r.AccessLog)
|
||||
if err != nil {
|
||||
r.task.Finish(err)
|
||||
return E.From(err)
|
||||
@@ -121,7 +108,7 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
||||
}
|
||||
|
||||
if r.handler == nil {
|
||||
pathPatterns := r.Raw.PathPatterns
|
||||
pathPatterns := r.PathPatterns
|
||||
switch {
|
||||
case len(pathPatterns) == 0:
|
||||
r.handler = r.rp
|
||||
@@ -130,7 +117,7 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
||||
default:
|
||||
logging.Warn().
|
||||
Str("route", r.TargetName()).
|
||||
Msg("`path_patterns` is deprecated. Use `rules` instead.")
|
||||
Msg("`path_patterns` for reverse proxy is deprecated. Use `rules` instead.")
|
||||
mux := gphttp.NewServeMux()
|
||||
patErrs := E.NewBuilder("invalid path pattern(s)")
|
||||
for _, p := range pathPatterns {
|
||||
@@ -144,17 +131,23 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Raw.Rules) > 0 {
|
||||
r.handler = r.Raw.Rules.BuildHandler(r.TargetName(), r.handler)
|
||||
if len(r.Rules) > 0 {
|
||||
r.handler = r.Rules.BuildHandler(r.TargetName(), r.handler)
|
||||
}
|
||||
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
E.LogWarn("health monitor error", err, &r.l)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if entry.UseLoadBalance(r) {
|
||||
if common.PrometheusEnabled {
|
||||
metricsLogger := metricslogger.NewMetricsLogger(r.TargetName())
|
||||
r.handler = metricsLogger.GetHandler(r.handler)
|
||||
r.task.OnCancel("reset_metrics", metricsLogger.ResetMetrics)
|
||||
}
|
||||
|
||||
if r.UseLoadBalance() {
|
||||
r.addToLoadBalancer(parent)
|
||||
} else {
|
||||
routes.SetHTTPRoute(r.TargetName(), r)
|
||||
@@ -163,55 +156,47 @@ func (r *HTTPRoute) Start(parent task.Parent) E.Error {
|
||||
})
|
||||
}
|
||||
|
||||
if common.PrometheusEnabled {
|
||||
r.task.OnCancel("metrics_cleanup", r.rp.UnregisterMetrics)
|
||||
}
|
||||
|
||||
r.task.OnCancel("reset_favicon", func() { favicon.PruneRouteIconCache(r) })
|
||||
return nil
|
||||
}
|
||||
|
||||
// Task implements task.TaskStarter.
|
||||
func (r *HTTPRoute) Task() *task.Task {
|
||||
func (r *ReveseProxyRoute) Task() *task.Task {
|
||||
return r.task
|
||||
}
|
||||
|
||||
// Finish implements task.TaskFinisher.
|
||||
func (r *HTTPRoute) Finish(reason any) {
|
||||
func (r *ReveseProxyRoute) Finish(reason any) {
|
||||
r.task.Finish(reason)
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
func (r *ReveseProxyRoute) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) HealthMonitor() health.HealthMonitor {
|
||||
func (r *ReveseProxyRoute) HealthMonitor() health.HealthMonitor {
|
||||
return r.HealthMon
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) addToLoadBalancer(parent task.Parent) {
|
||||
func (r *ReveseProxyRoute) addToLoadBalancer(parent task.Parent) {
|
||||
var lb *loadbalancer.LoadBalancer
|
||||
cfg := r.Raw.LoadBalance
|
||||
cfg := r.LoadBalance
|
||||
l, ok := routes.GetHTTPRoute(cfg.Link)
|
||||
var linked *HTTPRoute
|
||||
var linked *ReveseProxyRoute
|
||||
if ok {
|
||||
linked = l.(*HTTPRoute)
|
||||
linked = l.(*ReveseProxyRoute)
|
||||
lb = linked.loadBalancer
|
||||
lb.UpdateConfigIfNeeded(cfg)
|
||||
if linked.Raw.Homepage.IsEmpty() && !r.Raw.Homepage.IsEmpty() {
|
||||
linked.Raw.Homepage = r.Raw.Homepage
|
||||
if linked.Homepage.IsEmpty() && !r.Homepage.IsEmpty() {
|
||||
linked.Homepage = r.Homepage
|
||||
}
|
||||
} else {
|
||||
lb = loadbalancer.New(cfg)
|
||||
if err := lb.Start(parent); err != nil {
|
||||
panic(err) // should always return nil
|
||||
}
|
||||
linked = &HTTPRoute{
|
||||
ReverseProxyEntry: &entry.ReverseProxyEntry{
|
||||
Raw: &route.RawEntry{
|
||||
Alias: cfg.Link,
|
||||
Homepage: r.Raw.Homepage,
|
||||
},
|
||||
_ = lb.Start(parent) // always return nil
|
||||
linked = &ReveseProxyRoute{
|
||||
Route: &Route{
|
||||
Alias: cfg.Link,
|
||||
Homepage: r.Homepage,
|
||||
},
|
||||
HealthMon: lb,
|
||||
loadBalancer: lb,
|
||||
@@ -220,9 +205,10 @@ func (r *HTTPRoute) addToLoadBalancer(parent task.Parent) {
|
||||
routes.SetHTTPRoute(cfg.Link, linked)
|
||||
}
|
||||
r.loadBalancer = lb
|
||||
r.server = loadbalance.NewServer(r.task.Name(), r.rp.TargetURL, r.Raw.LoadBalance.Weight, r.handler, r.HealthMon)
|
||||
lb.AddServer(r.server)
|
||||
|
||||
server := loadbalance.NewServer(r.task.Name(), r.rp.TargetURL, r.LoadBalance.Weight, r.handler, r.HealthMon)
|
||||
lb.AddServer(server)
|
||||
r.task.OnCancel("lb_remove_server", func() {
|
||||
lb.RemoveServer(r.server)
|
||||
lb.RemoveServer(server)
|
||||
})
|
||||
}
|
||||
409
internal/route/route.go
Executable file → Normal file
409
internal/route/route.go
Executable file → Normal file
@@ -1,104 +1,363 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
url "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
|
||||
dockertypes "github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
"github.com/yusing/go-proxy/internal/route/rules"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
Route struct {
|
||||
_ U.NoCopy
|
||||
impl
|
||||
Type types.RouteType
|
||||
Entry *RawEntry
|
||||
}
|
||||
Routes = F.Map[string, *Route]
|
||||
_ utils.NoCopy
|
||||
|
||||
impl interface {
|
||||
types.Route
|
||||
task.TaskStarter
|
||||
task.TaskFinisher
|
||||
String() string
|
||||
TargetURL() url.URL
|
||||
Alias string `json:"alias"`
|
||||
Scheme types.Scheme `json:"scheme,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port types.Port `json:"port,omitempty"`
|
||||
Root string `json:"root,omitempty"`
|
||||
|
||||
types.HTTPConfig
|
||||
PathPatterns []string `json:"path_patterns,omitempty"`
|
||||
Rules rules.Rules `json:"rules,omitempty" validate:"omitempty,unique=Name"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty"`
|
||||
AccessLog *accesslog.Config `json:"access_log,omitempty"`
|
||||
|
||||
Metadata `deserialize:"-"`
|
||||
}
|
||||
RawEntry = types.RawEntry
|
||||
RawEntries = types.RawEntries
|
||||
|
||||
Metadata struct {
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
// private fields
|
||||
LisURL *net.URL `json:"lurl,omitempty"`
|
||||
ProxyURL *net.URL `json:"purl,omitempty"`
|
||||
Idlewatcher *idlewatcher.Config `json:"idlewatcher,omitempty"`
|
||||
|
||||
impl types.Route
|
||||
isValidated bool
|
||||
}
|
||||
Routes map[string]*Route
|
||||
)
|
||||
|
||||
// function alias.
|
||||
var (
|
||||
NewRoutes = F.NewMap[Routes]
|
||||
NewProxyEntries = types.NewProxyEntries
|
||||
)
|
||||
|
||||
func (rt *Route) Container() *docker.Container {
|
||||
if rt.Entry.Container == nil {
|
||||
return docker.DummyContainer
|
||||
}
|
||||
return rt.Entry.Container
|
||||
func (r Routes) Contains(alias string) bool {
|
||||
_, ok := r[alias]
|
||||
return ok
|
||||
}
|
||||
|
||||
func NewRoute(raw *RawEntry) (*Route, E.Error) {
|
||||
raw.Finalize()
|
||||
en, err := entry.ValidateEntry(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (r *Route) Validate() (err E.Error) {
|
||||
if r.isValidated {
|
||||
return nil
|
||||
}
|
||||
r.isValidated = true
|
||||
r.Finalize()
|
||||
|
||||
var t types.RouteType
|
||||
var rt impl
|
||||
errs := E.NewBuilder("entry validation failed")
|
||||
|
||||
switch e := en.(type) {
|
||||
case *entry.StreamEntry:
|
||||
t = types.RouteTypeStream
|
||||
rt, err = NewStreamRoute(e)
|
||||
case *entry.ReverseProxyEntry:
|
||||
t = types.RouteTypeReverseProxy
|
||||
rt, err = NewHTTPRoute(e)
|
||||
switch r.Scheme {
|
||||
case types.SchemeFileServer:
|
||||
r.impl, err = NewFileServer(r)
|
||||
if err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
case types.SchemeHTTP, types.SchemeHTTPS:
|
||||
if r.Port.Listening != 0 {
|
||||
errs.Addf("unexpected listening port for %s scheme", r.Scheme)
|
||||
}
|
||||
fallthrough
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
r.LisURL = E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Listening))
|
||||
fallthrough
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
if r.LoadBalance != nil && r.LoadBalance.Link == "" {
|
||||
r.LoadBalance = nil
|
||||
}
|
||||
r.ProxyURL = E.Collect(errs, net.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
|
||||
r.Idlewatcher = E.Collect(errs, idlewatcher.ValidateConfig, r.Container)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
if !r.UseHealthCheck() && (r.UseLoadBalance() || r.UseIdleWatcher()) {
|
||||
errs.Adds("healthCheck.disable cannot be true when loadbalancer or idlewatcher is enabled")
|
||||
}
|
||||
return &Route{
|
||||
impl: rt,
|
||||
Type: t,
|
||||
Entry: raw,
|
||||
}, nil
|
||||
|
||||
if errs.HasError() {
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
switch r.Scheme {
|
||||
case types.SchemeFileServer:
|
||||
r.impl, err = NewFileServer(r)
|
||||
case types.SchemeHTTP, types.SchemeHTTPS:
|
||||
r.impl, err = NewReverseProxyRoute(r)
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
r.impl, err = NewStreamRoute(r)
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func FromEntries(provider string, entries RawEntries) (Routes, E.Error) {
|
||||
b := E.NewBuilder("errors in routes")
|
||||
func (r *Route) Start(parent task.Parent) (err E.Error) {
|
||||
if r.impl == nil {
|
||||
return E.New("route not initialized")
|
||||
}
|
||||
return r.impl.Start(parent)
|
||||
}
|
||||
|
||||
routes := NewRoutes()
|
||||
entries.RangeAllParallel(func(alias string, en *RawEntry) {
|
||||
if en == nil {
|
||||
en = new(RawEntry)
|
||||
}
|
||||
en.Alias = alias
|
||||
en.Provider = provider
|
||||
if strings.HasPrefix(alias, "x-") { // x properties
|
||||
return
|
||||
}
|
||||
r, err := NewRoute(en)
|
||||
func (r *Route) Finish(reason any) {
|
||||
if r.impl == nil {
|
||||
return
|
||||
}
|
||||
r.impl.Finish(reason)
|
||||
r.impl = nil
|
||||
}
|
||||
|
||||
func (r *Route) Started() bool {
|
||||
return r.impl != nil
|
||||
}
|
||||
|
||||
func (r *Route) ProviderName() string {
|
||||
return r.Provider
|
||||
}
|
||||
|
||||
func (r *Route) TargetName() string {
|
||||
return r.Alias
|
||||
}
|
||||
|
||||
func (r *Route) TargetURL() *net.URL {
|
||||
return r.ProxyURL
|
||||
}
|
||||
|
||||
func (r *Route) Type() types.RouteType {
|
||||
switch r.Scheme {
|
||||
case types.SchemeHTTP, types.SchemeHTTPS, types.SchemeFileServer:
|
||||
return types.RouteTypeHTTP
|
||||
case types.SchemeTCP, types.SchemeUDP:
|
||||
return types.RouteTypeStream
|
||||
}
|
||||
panic(fmt.Errorf("unexpected scheme %s for alias %s", r.Scheme, r.Alias))
|
||||
}
|
||||
|
||||
func (r *Route) HealthMonitor() health.HealthMonitor {
|
||||
return r.impl.HealthMonitor()
|
||||
}
|
||||
|
||||
func (r *Route) IdlewatcherConfig() *idlewatcher.Config {
|
||||
return r.Idlewatcher
|
||||
}
|
||||
|
||||
func (r *Route) HealthCheckConfig() *health.HealthCheckConfig {
|
||||
return r.HealthCheck
|
||||
}
|
||||
|
||||
func (r *Route) LoadBalanceConfig() *loadbalance.Config {
|
||||
return r.LoadBalance
|
||||
}
|
||||
|
||||
func (r *Route) HomepageConfig() *homepage.Item {
|
||||
return r.Homepage
|
||||
}
|
||||
|
||||
func (r *Route) ContainerInfo() *docker.Container {
|
||||
return r.Container
|
||||
}
|
||||
|
||||
func (r *Route) IsDocker() bool {
|
||||
if r.Container == nil {
|
||||
return false
|
||||
}
|
||||
return r.Container.ContainerID != ""
|
||||
}
|
||||
|
||||
func (r *Route) IsZeroPort() bool {
|
||||
return r.Port.Proxy == 0
|
||||
}
|
||||
|
||||
func (r *Route) ShouldExclude() bool {
|
||||
if r.Container != nil {
|
||||
switch {
|
||||
case err != nil:
|
||||
b.Add(err.Subject(alias))
|
||||
case entry.ShouldNotServe(r):
|
||||
return
|
||||
default:
|
||||
routes.Store(alias, r)
|
||||
case r.Container.IsExcluded:
|
||||
return true
|
||||
case r.IsZeroPort() && !r.UseIdleWatcher():
|
||||
return true
|
||||
case r.Container.IsDatabase && !r.Container.IsExplicit:
|
||||
return true
|
||||
case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
return routes, b.Error()
|
||||
} else if r.IsZeroPort() {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(r.Alias, "x-") ||
|
||||
strings.HasSuffix(r.Alias, "-old") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Route) UseLoadBalance() bool {
|
||||
return r.LoadBalance != nil && r.LoadBalance.Link != ""
|
||||
}
|
||||
|
||||
func (r *Route) UseIdleWatcher() bool {
|
||||
return r.Idlewatcher != nil && r.Idlewatcher.IdleTimeout > 0
|
||||
}
|
||||
|
||||
func (r *Route) UseHealthCheck() bool {
|
||||
return !r.HealthCheck.Disable
|
||||
}
|
||||
|
||||
func (r *Route) UseAccessLog() bool {
|
||||
return r.AccessLog != nil
|
||||
}
|
||||
|
||||
func (r *Route) Finalize() {
|
||||
r.Alias = strings.ToLower(strings.TrimSpace(r.Alias))
|
||||
r.Host = strings.ToLower(strings.TrimSpace(r.Host))
|
||||
|
||||
isDocker := r.Container != nil
|
||||
cont := r.Container
|
||||
|
||||
if r.Host == "" {
|
||||
switch {
|
||||
case !isDocker:
|
||||
r.Host = "localhost"
|
||||
case cont.PrivateIP != "":
|
||||
r.Host = cont.PrivateIP
|
||||
case cont.PublicIP != "":
|
||||
r.Host = cont.PublicIP
|
||||
}
|
||||
}
|
||||
|
||||
lp, pp := r.Port.Listening, r.Port.Proxy
|
||||
|
||||
if isDocker {
|
||||
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
if r.Scheme == "" {
|
||||
r.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
|
||||
if pp == 0 {
|
||||
pp = port
|
||||
}
|
||||
if r.Scheme == "" {
|
||||
r.Scheme = "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pp == 0 {
|
||||
switch {
|
||||
case r.Scheme == "https":
|
||||
pp = 443
|
||||
case !isDocker:
|
||||
pp = 80
|
||||
default:
|
||||
pp = lowestPort(cont.PrivatePortMapping)
|
||||
if pp == 0 {
|
||||
pp = lowestPort(cont.PublicPortMapping)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isDocker {
|
||||
// replace private port with public port if using public IP.
|
||||
if r.Host == cont.PublicIP {
|
||||
if p, ok := cont.PrivatePortMapping[pp]; ok {
|
||||
pp = int(p.PublicPort)
|
||||
}
|
||||
}
|
||||
// replace public port with private port if using private IP.
|
||||
if r.Host == cont.PrivateIP {
|
||||
if p, ok := cont.PublicPortMapping[pp]; ok {
|
||||
pp = int(p.PrivatePort)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Scheme == "" {
|
||||
switch {
|
||||
case r.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
case r.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
|
||||
r.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Scheme == "" {
|
||||
switch {
|
||||
case lp != 0:
|
||||
r.Scheme = "tcp"
|
||||
case strings.HasSuffix(strconv.Itoa(pp), "443"):
|
||||
r.Scheme = "https"
|
||||
default: // assume its http
|
||||
r.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
r.Port.Listening, r.Port.Proxy = lp, pp
|
||||
|
||||
if r.HealthCheck == nil {
|
||||
r.HealthCheck = health.DefaultHealthConfig
|
||||
}
|
||||
|
||||
if !r.HealthCheck.Disable {
|
||||
if r.HealthCheck.Interval == 0 {
|
||||
r.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if r.HealthCheck.Timeout == 0 {
|
||||
r.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
}
|
||||
}
|
||||
|
||||
if isDocker && cont.IdleTimeout != "" {
|
||||
if cont.WakeTimeout == "" {
|
||||
cont.WakeTimeout = common.WakeTimeoutDefault
|
||||
}
|
||||
if cont.StopTimeout == "" {
|
||||
cont.StopTimeout = common.StopTimeoutDefault
|
||||
}
|
||||
if cont.StopMethod == "" {
|
||||
cont.StopMethod = common.StopMethodDefault
|
||||
}
|
||||
}
|
||||
|
||||
if r.Homepage.IsEmpty() {
|
||||
r.Homepage = homepage.NewItem(r.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
func lowestPort(ports map[int]dockertypes.Port) (res int) {
|
||||
cmp := (uint16)(65535)
|
||||
for port, v := range ports {
|
||||
if v.PrivatePort < cmp {
|
||||
cmp = v.PrivatePort
|
||||
res = port
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/yusing/go-proxy/internal"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
||||
"github.com/yusing/go-proxy/internal/route/routes"
|
||||
route "github.com/yusing/go-proxy/internal/route/types"
|
||||
@@ -44,15 +43,15 @@ func HomepageCategories() []string {
|
||||
check := make(map[string]struct{})
|
||||
categories := make([]string, 0)
|
||||
routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) {
|
||||
en := r.RawEntry()
|
||||
if en.Homepage.IsEmpty() || en.Homepage.Category == "" {
|
||||
homepage := r.HomepageConfig()
|
||||
if homepage.IsEmpty() || homepage.Category == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := check[en.Homepage.Category]; ok {
|
||||
if _, ok := check[homepage.Category]; ok {
|
||||
return
|
||||
}
|
||||
check[en.Homepage.Category] = struct{}{}
|
||||
categories = append(categories, en.Homepage.Category)
|
||||
check[homepage.Category] = struct{}{}
|
||||
categories = append(categories, homepage.Category)
|
||||
})
|
||||
return categories
|
||||
}
|
||||
@@ -61,8 +60,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
hpCfg := homepage.NewHomePageConfig()
|
||||
|
||||
routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) {
|
||||
en := r.RawEntry()
|
||||
item := en.Homepage
|
||||
item := r.HomepageConfig()
|
||||
|
||||
if item.IsEmpty() {
|
||||
item = homepage.NewItem(alias)
|
||||
@@ -78,7 +76,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
}
|
||||
|
||||
item.Alias = alias
|
||||
item.Provider = r.RawEntry().Provider
|
||||
item.Provider = r.ProviderName()
|
||||
|
||||
if providerFilter != "" && item.Provider != providerFilter {
|
||||
return
|
||||
@@ -86,7 +84,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
|
||||
if item.Name == "" {
|
||||
reference := r.TargetName()
|
||||
cont := r.RawEntry().Container
|
||||
cont := r.ContainerInfo()
|
||||
if cont != nil {
|
||||
reference = cont.ImageName
|
||||
}
|
||||
@@ -104,8 +102,9 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
}
|
||||
|
||||
if useDefaultCategories {
|
||||
if en.Container != nil && item.Category == "" {
|
||||
if category, ok := homepage.PredefinedCategories[en.Container.ImageName]; ok {
|
||||
container := r.ContainerInfo()
|
||||
if container != nil && item.Category == "" {
|
||||
if category, ok := homepage.PredefinedCategories[container.ImageName]; ok {
|
||||
item.Category = category
|
||||
}
|
||||
}
|
||||
@@ -122,12 +121,12 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
}
|
||||
|
||||
switch {
|
||||
case entry.IsDocker(r):
|
||||
case r.IsDocker():
|
||||
if item.Category == "" {
|
||||
item.Category = "Docker"
|
||||
}
|
||||
item.SourceType = string(provider.ProviderTypeDocker)
|
||||
case entry.UseLoadBalance(r):
|
||||
case r.UseLoadBalance():
|
||||
if item.Category == "" {
|
||||
item.Category = "Load-balanced"
|
||||
}
|
||||
@@ -148,11 +147,11 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||
func RoutesByAlias(typeFilter ...route.RouteType) map[string]route.Route {
|
||||
rts := make(map[string]route.Route)
|
||||
if len(typeFilter) == 0 || typeFilter[0] == "" {
|
||||
typeFilter = []route.RouteType{route.RouteTypeReverseProxy, route.RouteTypeStream}
|
||||
typeFilter = []route.RouteType{route.RouteTypeHTTP, route.RouteTypeStream}
|
||||
}
|
||||
for _, t := range typeFilter {
|
||||
switch t {
|
||||
case route.RouteTypeReverseProxy:
|
||||
case route.RouteTypeHTTP:
|
||||
routes.GetHTTPRoutes().RangeAll(func(alias string, r route.HTTPRoute) {
|
||||
rts[alias] = r
|
||||
})
|
||||
|
||||
@@ -30,7 +30,8 @@ const (
|
||||
CommandSet = "set"
|
||||
CommandAdd = "add"
|
||||
CommandRemove = "remove"
|
||||
CommandBypass = "bypass"
|
||||
CommandPass = "pass"
|
||||
CommandPassAlt = "bypass"
|
||||
)
|
||||
|
||||
var commands = map[string]struct {
|
||||
@@ -94,7 +95,7 @@ var commands = map[string]struct {
|
||||
},
|
||||
validate: validateURL,
|
||||
build: func(args any) CommandHandler {
|
||||
target := args.(types.URL).String()
|
||||
target := args.(*types.URL).String()
|
||||
return ReturningCommand(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
|
||||
})
|
||||
@@ -159,7 +160,7 @@ var commands = map[string]struct {
|
||||
},
|
||||
validate: validateAbsoluteURL,
|
||||
build: func(args any) CommandHandler {
|
||||
target := args.(types.URL)
|
||||
target := args.(*types.URL)
|
||||
if target.Scheme == "" {
|
||||
target.Scheme = "http"
|
||||
}
|
||||
@@ -231,7 +232,7 @@ func (cmd *Command) Parse(v string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if directive == CommandBypass {
|
||||
if directive == CommandPass || directive == CommandPassAlt {
|
||||
if len(args) != 0 {
|
||||
return ErrInvalidArguments.Subject(directive)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ var (
|
||||
ErrInvalidCommandSequence = E.New("invalid command sequence")
|
||||
ErrInvalidSetTarget = E.New("invalid `rule.set` target")
|
||||
|
||||
ErrExpectNoArg = ErrInvalidArguments.Withf("expect no arg")
|
||||
ErrExpectOneArg = ErrInvalidArguments.Withf("expect 1 arg")
|
||||
ErrExpectTwoArgs = ErrInvalidArguments.Withf("expect 2 args")
|
||||
ErrExpectNoArg = E.Wrap(ErrInvalidArguments, "expect no arg")
|
||||
ErrExpectOneArg = E.Wrap(ErrInvalidArguments, "expect 1 arg")
|
||||
ErrExpectTwoArgs = E.Wrap(ErrInvalidArguments, "expect 2 args")
|
||||
ErrExpectKVOptionalV = E.Wrap(ErrInvalidArguments, "expect 'key' or 'key value'")
|
||||
)
|
||||
|
||||
@@ -20,9 +20,8 @@ func (h *Help) String() string {
|
||||
sb.WriteString(h.command)
|
||||
sb.WriteString(" ")
|
||||
for arg := range h.args {
|
||||
sb.WriteRune('<')
|
||||
sb.WriteString(arg)
|
||||
sb.WriteString("> ")
|
||||
sb.WriteString(strings.ToUpper(arg))
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
if h.description != "" {
|
||||
sb.WriteString("\n\t")
|
||||
@@ -32,7 +31,7 @@ func (h *Help) String() string {
|
||||
sb.WriteRune('\n')
|
||||
for arg, desc := range h.args {
|
||||
sb.WriteRune('\t')
|
||||
sb.WriteString(arg)
|
||||
sb.WriteString(strings.ToUpper(arg))
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(desc)
|
||||
sb.WriteRune('\n')
|
||||
|
||||
@@ -34,15 +34,25 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnHeader,
|
||||
args: map[string]string{
|
||||
"key": "the header key",
|
||||
"value": "the header value",
|
||||
"key": "the header key",
|
||||
"[value]": "the header value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return len(r.Header[k]) > 0
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.Header.Get(k) == v
|
||||
for _, vv := range r.Header[k] {
|
||||
if v == vv {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -50,13 +60,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnQuery,
|
||||
args: map[string]string{
|
||||
"key": "the query key",
|
||||
"value": "the query value",
|
||||
"key": "the query key",
|
||||
"[value]": "the query value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return len(cached.GetQueries(r)[k]) > 0
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
queries := cached.GetQueries(r)[k]
|
||||
for _, query := range queries {
|
||||
@@ -72,13 +87,24 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnCookie,
|
||||
args: map[string]string{
|
||||
"key": "the cookie key",
|
||||
"value": "the cookie value",
|
||||
"key": "the cookie key",
|
||||
"[value]": "the cookie value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
cookies := cached.GetCookies(r)
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
cookies := cached.GetCookies(r)
|
||||
for _, cookie := range cookies {
|
||||
@@ -95,13 +121,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnForm,
|
||||
args: map[string]string{
|
||||
"key": "the form key",
|
||||
"value": "the form value",
|
||||
"key": "the form key",
|
||||
"[value]": "the form value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.FormValue(k) != ""
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.FormValue(k) == v
|
||||
}
|
||||
@@ -111,13 +142,18 @@ var checkers = map[string]struct {
|
||||
help: Help{
|
||||
command: OnPostForm,
|
||||
args: map[string]string{
|
||||
"key": "the form key",
|
||||
"value": "the form value",
|
||||
"key": "the form key",
|
||||
"[value]": "the form value",
|
||||
},
|
||||
},
|
||||
validate: toStrTuple,
|
||||
validate: toKVOptionalV,
|
||||
builder: func(args any) CheckFunc {
|
||||
k, v := args.(*StrTuple).Unpack()
|
||||
if v == "" {
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.PostFormValue(k) != ""
|
||||
}
|
||||
}
|
||||
return func(cached Cache, r *http.Request) bool {
|
||||
return r.PostFormValue(k) == v
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestParseOn(t *testing.T) {
|
||||
@@ -15,25 +20,50 @@ func TestParseOn(t *testing.T) {
|
||||
}{
|
||||
// header
|
||||
{
|
||||
name: "header_valid",
|
||||
name: "header_valid_kv",
|
||||
input: "header Connection Upgrade",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "header_invalid",
|
||||
name: "header_valid_k",
|
||||
input: "header Connection",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "header_missing_arg",
|
||||
input: "header",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
// query
|
||||
{
|
||||
name: "query_valid",
|
||||
name: "query_valid_kv",
|
||||
input: "query key value",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "query_invalid",
|
||||
name: "query_valid_k",
|
||||
input: "query key",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "query_missing_arg",
|
||||
input: "query",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
{
|
||||
name: "cookie_valid_kv",
|
||||
input: "cookie key value",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cookie_valid_k",
|
||||
input: "cookie key",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cookie_missing_arg",
|
||||
input: "cookie",
|
||||
wantErr: ErrExpectKVOptionalV,
|
||||
},
|
||||
// method
|
||||
{
|
||||
@@ -43,9 +73,14 @@ func TestParseOn(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "method_invalid",
|
||||
input: "method",
|
||||
input: "method invalid",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "method_missing_arg",
|
||||
input: "method",
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
// path
|
||||
{
|
||||
name: "path_valid",
|
||||
@@ -53,9 +88,9 @@ func TestParseOn(t *testing.T) {
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "path_invalid",
|
||||
name: "path_missing_arg",
|
||||
input: "path",
|
||||
wantErr: ErrInvalidArguments,
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
// remote
|
||||
{
|
||||
@@ -65,9 +100,14 @@ func TestParseOn(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "remote_invalid",
|
||||
input: "remote",
|
||||
input: "remote abcd",
|
||||
wantErr: ErrInvalidArguments,
|
||||
},
|
||||
{
|
||||
name: "remote_missing_arg",
|
||||
input: "remote",
|
||||
wantErr: ErrExpectOneArg,
|
||||
},
|
||||
{
|
||||
name: "unknown_target",
|
||||
input: "unknown",
|
||||
@@ -87,3 +127,152 @@ func TestParseOn(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testCorrectness struct {
|
||||
name string
|
||||
checker string
|
||||
input *http.Request
|
||||
want bool
|
||||
}
|
||||
|
||||
func genCorrectnessTestCases(field string, genRequest func(k, v string) *http.Request) []testCorrectness {
|
||||
return []testCorrectness{
|
||||
{
|
||||
name: field + "_match",
|
||||
checker: field + " foo bar",
|
||||
input: genRequest("foo", "bar"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: field + "_no_match",
|
||||
checker: field + " foo baz",
|
||||
input: genRequest("foo", "bar"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: field + "_exists",
|
||||
checker: field + " foo",
|
||||
input: genRequest("foo", "abcd"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: field + "_not_exists",
|
||||
checker: field + " foo",
|
||||
input: genRequest("bar", "abcd"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnCorrectness(t *testing.T) {
|
||||
tests := []testCorrectness{
|
||||
{
|
||||
name: "method_match",
|
||||
checker: "method GET",
|
||||
input: &http.Request{Method: http.MethodGet},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "method_no_match",
|
||||
checker: "method GET",
|
||||
input: &http.Request{Method: http.MethodPost},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "path_exact_match",
|
||||
checker: "path /example",
|
||||
input: &http.Request{
|
||||
URL: &url.URL{Path: "/example"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "path_wildcard_match",
|
||||
checker: "path /example/*",
|
||||
input: &http.Request{
|
||||
URL: &url.URL{Path: "/example/123"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "remote_match",
|
||||
checker: "remote 192.168.1.0/24",
|
||||
input: &http.Request{
|
||||
RemoteAddr: "192.168.1.5",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "remote_no_match",
|
||||
checker: "remote 192.168.1.0/24",
|
||||
input: &http.Request{
|
||||
RemoteAddr: "192.168.2.5",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "basic_auth_correct",
|
||||
checker: "basic_auth user " + string(Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost))),
|
||||
input: &http.Request{
|
||||
Header: http.Header{
|
||||
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user:password"))}, // "user:password"
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "basic_auth_incorrect",
|
||||
checker: "basic_auth user " + string(Must(bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost))),
|
||||
input: &http.Request{
|
||||
Header: http.Header{
|
||||
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user:incorrect"))}, // "user:wrong"
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
tests = append(tests, genCorrectnessTestCases("header", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{k: []string{v}}}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("query", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
URL: &url.URL{
|
||||
RawQuery: fmt.Sprintf("%s=%s", k, v),
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("cookie", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{
|
||||
"Cookie": {fmt.Sprintf("%s=%s", k, v)},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("form", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Form: url.Values{
|
||||
k: []string{v},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("postform", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
PostForm: url.Values{
|
||||
k: []string{v},
|
||||
},
|
||||
}
|
||||
})...)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
on, err := parseOn(tt.checker)
|
||||
ExpectNoError(t, err)
|
||||
got := on.Check(Cache{}, tt.input)
|
||||
if tt.want != got {
|
||||
t.Errorf("want %v, got %v", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type (
|
||||
one match means this line is matched.
|
||||
*/
|
||||
Rule struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Name string `json:"name"`
|
||||
On RuleOn `json:"on"`
|
||||
Do Command `json:"do"`
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ func toStrTuple(args []string) (any, E.Error) {
|
||||
return &StrTuple{args[0], args[1]}, nil
|
||||
}
|
||||
|
||||
// toKVOptionalV returns *StrTuple that value is optional.
|
||||
func toKVOptionalV(args []string) (any, E.Error) {
|
||||
switch len(args) {
|
||||
case 1:
|
||||
return &StrTuple{args[0], ""}, nil
|
||||
case 2:
|
||||
return &StrTuple{args[0], args[1]}, nil
|
||||
default:
|
||||
return nil, ErrExpectKVOptionalV
|
||||
}
|
||||
}
|
||||
|
||||
// validateURL returns types.URL with the URL validated.
|
||||
func validateURL(args []string) (any, E.Error) {
|
||||
if len(args) != 1 {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/route/entry"
|
||||
"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/watcher/health"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
// TODO: support stream load balance.
|
||||
type StreamRoute struct {
|
||||
*entry.StreamEntry
|
||||
*Route
|
||||
|
||||
net.Stream `json:"-"`
|
||||
|
||||
@@ -30,16 +30,13 @@ type StreamRoute struct {
|
||||
l zerolog.Logger
|
||||
}
|
||||
|
||||
func NewStreamRoute(entry *entry.StreamEntry) (impl, E.Error) {
|
||||
func NewStreamRoute(base *Route) (route.Route, E.Error) {
|
||||
// TODO: support non-coherent scheme
|
||||
if !entry.Scheme.IsCoherent() {
|
||||
return nil, E.Errorf("unsupported scheme: %v -> %v", entry.Scheme.ListeningScheme, entry.Scheme.ProxyScheme)
|
||||
}
|
||||
return &StreamRoute{
|
||||
StreamEntry: entry,
|
||||
Route: base,
|
||||
l: logging.With().
|
||||
Str("type", string(entry.Scheme.ListeningScheme)).
|
||||
Str("name", entry.TargetName()).
|
||||
Str("type", string(base.Scheme)).
|
||||
Str("name", base.TargetName()).
|
||||
Logger(),
|
||||
}, nil
|
||||
}
|
||||
@@ -50,10 +47,6 @@ func (r *StreamRoute) String() string {
|
||||
|
||||
// Start implements task.TaskStarter.
|
||||
func (r *StreamRoute) Start(parent task.Parent) E.Error {
|
||||
if entry.ShouldNotServe(r) {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.task = parent.Subtask("stream." + r.TargetName())
|
||||
r.Stream = NewStream(r)
|
||||
parent.OnCancel("finish", func() {
|
||||
@@ -61,25 +54,25 @@ func (r *StreamRoute) Start(parent task.Parent) E.Error {
|
||||
})
|
||||
|
||||
switch {
|
||||
case entry.UseIdleWatcher(r):
|
||||
waker, err := idlewatcher.NewStreamWaker(parent, r.StreamEntry, r.Stream)
|
||||
case r.UseIdleWatcher():
|
||||
waker, err := idlewatcher.NewStreamWaker(parent, r, r.Stream)
|
||||
if err != nil {
|
||||
r.task.Finish(err)
|
||||
return err
|
||||
}
|
||||
r.Stream = waker
|
||||
r.HealthMon = waker
|
||||
case entry.UseHealthCheck(r):
|
||||
if entry.IsDocker(r) {
|
||||
client, err := docker.ConnectClient(r.Idlewatcher.DockerHost)
|
||||
case r.UseHealthCheck():
|
||||
if r.IsDocker() {
|
||||
client, err := docker.ConnectClient(r.IdlewatcherConfig().DockerHost)
|
||||
if err == nil {
|
||||
fallback := monitor.NewRawHealthChecker(r.TargetURL(), r.Raw.HealthCheck)
|
||||
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.Idlewatcher.ContainerID, r.TargetName(), r.Raw.HealthCheck, fallback)
|
||||
fallback := monitor.NewRawHealthChecker(r.TargetURL(), r.HealthCheck)
|
||||
r.HealthMon = monitor.NewDockerHealthMonitor(client, r.IdlewatcherConfig().ContainerID, r.TargetName(), r.HealthCheck, fallback)
|
||||
r.task.OnCancel("close_docker_client", client.Close)
|
||||
}
|
||||
}
|
||||
if r.HealthMon == nil {
|
||||
r.HealthMon = monitor.NewRawHealthMonitor(r.TargetURL(), r.Raw.HealthCheck)
|
||||
r.HealthMon = monitor.NewRawHealthMonitor(r.TargetURL(), r.HealthCheck)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,9 +81,7 @@ func (r *StreamRoute) Start(parent task.Parent) E.Error {
|
||||
return E.From(err)
|
||||
}
|
||||
|
||||
r.l.Info().
|
||||
Int("port", int(r.Port.ListeningPort)).
|
||||
Msg("listening")
|
||||
r.l.Info().Int("port", r.Port.Listening).Msg("listening")
|
||||
|
||||
if r.HealthMon != nil {
|
||||
if err := r.HealthMon.Start(r.task); err != nil {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
T "github.com/yusing/go-proxy/internal/route/types"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
@@ -45,25 +44,25 @@ func (stream *Stream) Setup() error {
|
||||
|
||||
ctx := stream.task.Context()
|
||||
|
||||
switch stream.Scheme.ListeningScheme {
|
||||
switch stream.Scheme {
|
||||
case "tcp":
|
||||
stream.targetAddr, err = net.ResolveTCPAddr("tcp", stream.URL.Host)
|
||||
stream.targetAddr, err = net.ResolveTCPAddr("tcp", stream.ProxyURL.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tcpListener, err := lcfg.Listen(ctx, "tcp", stream.ListenURL.Host)
|
||||
tcpListener, err := lcfg.Listen(ctx, "tcp", stream.LisURL.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// in case ListeningPort was zero, get the actual port
|
||||
stream.Port.ListeningPort = T.Port(tcpListener.Addr().(*net.TCPAddr).Port)
|
||||
stream.Port.Listening = tcpListener.Addr().(*net.TCPAddr).Port
|
||||
stream.listener = types.NetListener(tcpListener)
|
||||
case "udp":
|
||||
stream.targetAddr, err = net.ResolveUDPAddr("udp", stream.URL.Host)
|
||||
stream.targetAddr, err = net.ResolveUDPAddr("udp", stream.ProxyURL.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
udpListener, err := lcfg.ListenPacket(ctx, "udp", stream.ListenURL.Host)
|
||||
udpListener, err := lcfg.ListenPacket(ctx, "udp", stream.LisURL.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +71,7 @@ func (stream *Stream) Setup() error {
|
||||
udpListener.Close()
|
||||
return errors.New("udp listener is not *net.UDPConn")
|
||||
}
|
||||
stream.Port.ListeningPort = T.Port(udpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||
stream.Port.Listening = udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||
stream.listener = NewUDPForwarder(ctx, udpConn, stream.targetAddr)
|
||||
default:
|
||||
panic("should not reach here")
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
)
|
||||
|
||||
type Entry interface {
|
||||
TargetName() string
|
||||
TargetURL() net.URL
|
||||
RawEntry() *RawEntry
|
||||
IdlewatcherConfig() *idlewatcher.Config
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
func ValidateHTTPHeaders(headers map[string]string) (http.Header, E.Error) {
|
||||
h := make(http.Header)
|
||||
for k, v := range headers {
|
||||
vSplit := strutils.CommaSeperatedList(v)
|
||||
for _, header := range vSplit {
|
||||
h.Add(k, header)
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package types
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/route"
|
||||
"github.com/yusing/go-proxy/internal/route/types"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
@@ -12,14 +14,14 @@ func TestHTTPConfigDeserialize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]any
|
||||
expected HTTPConfig
|
||||
expected types.HTTPConfig
|
||||
}{
|
||||
{
|
||||
name: "no_tls_verify",
|
||||
input: map[string]any{
|
||||
"no_tls_verify": "true",
|
||||
},
|
||||
expected: HTTPConfig{
|
||||
expected: types.HTTPConfig{
|
||||
NoTLSVerify: true,
|
||||
},
|
||||
},
|
||||
@@ -28,7 +30,7 @@ func TestHTTPConfigDeserialize(t *testing.T) {
|
||||
input: map[string]any{
|
||||
"response_header_timeout": "1s",
|
||||
},
|
||||
expected: HTTPConfig{
|
||||
expected: types.HTTPConfig{
|
||||
ResponseHeaderTimeout: 1 * time.Second,
|
||||
},
|
||||
},
|
||||
@@ -36,12 +38,12 @@ func TestHTTPConfigDeserialize(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := RawEntry{}
|
||||
cfg := Route{}
|
||||
err := utils.Deserialize(tt.input, &cfg)
|
||||
if err != nil {
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
ExpectDeepEqual(t, cfg.HTTPConfig, &tt.expected)
|
||||
ExpectDeepEqual(t, cfg.HTTPConfig, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,37 +7,55 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type Port int
|
||||
type Port struct {
|
||||
Listening int `json:"listening"`
|
||||
Proxy int `json:"proxy"`
|
||||
}
|
||||
|
||||
var ErrPortOutOfRange = E.New("port out of range")
|
||||
var (
|
||||
ErrInvalidPortSyntax = E.New("invalid port syntax, expect [listening_port:]target_port")
|
||||
ErrPortOutOfRange = E.New("port out of range")
|
||||
)
|
||||
|
||||
// Parse implements strutils.Parser.
|
||||
func (p *Port) Parse(v string) (err error) {
|
||||
parts := strutils.SplitRune(v, ':')
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
p.Listening = 0
|
||||
p.Proxy, err = strconv.Atoi(v)
|
||||
case 2:
|
||||
var err2 error
|
||||
p.Listening, err = strconv.Atoi(parts[0])
|
||||
p.Proxy, err2 = strconv.Atoi(parts[1])
|
||||
err = E.Join(err, err2)
|
||||
default:
|
||||
return ErrInvalidPortSyntax.Subject(v)
|
||||
}
|
||||
|
||||
func ValidatePort[String ~string](v String) (Port, error) {
|
||||
p, err := strutils.Atoi(string(v))
|
||||
if err != nil {
|
||||
return ErrPort, err
|
||||
return err
|
||||
}
|
||||
return ValidatePortInt(p)
|
||||
}
|
||||
|
||||
func ValidatePortInt[Int int | uint16](v Int) (Port, error) {
|
||||
p := Port(v)
|
||||
if !p.inBound() {
|
||||
return ErrPort, ErrPortOutOfRange.Subject(strconv.Itoa(int(p)))
|
||||
if p.Listening < MinPort || p.Listening > MaxPort {
|
||||
return ErrPortOutOfRange.Subjectf("%d", p.Listening)
|
||||
}
|
||||
return p, nil
|
||||
|
||||
if p.Proxy < MinPort || p.Proxy > MaxPort {
|
||||
return ErrPortOutOfRange.Subjectf("%d", p.Proxy)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Port) inBound() bool {
|
||||
return p >= MinPort && p <= MaxPort
|
||||
}
|
||||
|
||||
func (p Port) String() string {
|
||||
return strconv.Itoa(int(p))
|
||||
func (p *Port) String() string {
|
||||
if p.Listening == 0 {
|
||||
return strconv.Itoa(p.Proxy)
|
||||
}
|
||||
return strconv.Itoa(p.Listening) + ":" + strconv.Itoa(p.Proxy)
|
||||
}
|
||||
|
||||
const (
|
||||
MinPort = 0
|
||||
MaxPort = 65535
|
||||
ErrPort = Port(-1)
|
||||
NoPort = Port(0)
|
||||
)
|
||||
|
||||
106
internal/route/types/port_test.go
Normal file
106
internal/route/types/port_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var invalidPorts = []string{
|
||||
"",
|
||||
"123:",
|
||||
"0:",
|
||||
":1234",
|
||||
"qwerty",
|
||||
"asdfgh:asdfgh",
|
||||
"1234:asdfgh",
|
||||
}
|
||||
|
||||
var tooManyColonsPorts = []string{
|
||||
"1234:1234:1234",
|
||||
}
|
||||
|
||||
var outOfRangePorts = []string{
|
||||
"-1:1234",
|
||||
"1234:-1",
|
||||
"65536",
|
||||
"0:65536",
|
||||
}
|
||||
|
||||
func TestPortInvalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs []string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid",
|
||||
inputs: invalidPorts,
|
||||
wantErr: strconv.ErrSyntax,
|
||||
},
|
||||
|
||||
{
|
||||
name: "too many colons",
|
||||
inputs: tooManyColonsPorts,
|
||||
wantErr: ErrInvalidPortSyntax,
|
||||
},
|
||||
{
|
||||
name: "out of range",
|
||||
inputs: outOfRangePorts,
|
||||
wantErr: ErrPortOutOfRange,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
for _, input := range tc.inputs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := &Port{}
|
||||
err := p.Parse(input)
|
||||
if !errors.Is(err, tc.wantErr) {
|
||||
t.Errorf("expected error %v, got %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs string
|
||||
expect Port
|
||||
}{
|
||||
{
|
||||
name: "valid_lp",
|
||||
inputs: "1234:5678",
|
||||
expect: Port{
|
||||
Listening: 1234,
|
||||
Proxy: 5678,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid_p",
|
||||
inputs: "5678",
|
||||
expect: Port{
|
||||
Listening: 0,
|
||||
Proxy: 5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := &Port{}
|
||||
err := p.Parse(tc.inputs)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got %v", err)
|
||||
}
|
||||
if p.Listening != tc.expect.Listening {
|
||||
t.Errorf("expected listening port %d, got %d", tc.expect.Listening, p.Listening)
|
||||
}
|
||||
if p.Proxy != tc.expect.Proxy {
|
||||
t.Errorf("expected proxy port %d, got %d", tc.expect.Proxy, p.Proxy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
//nolint:goconst
|
||||
package types
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/net/http/accesslog"
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
"github.com/yusing/go-proxy/internal/route/rules"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
)
|
||||
|
||||
type (
|
||||
RawEntry struct {
|
||||
_ U.NoCopy
|
||||
|
||||
// raw entry object before validation
|
||||
// loaded from docker labels or yaml file
|
||||
Alias string `json:"alias"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
|
||||
HTTPConfig
|
||||
PathPatterns []string `json:"path_patterns,omitempty"`
|
||||
Rules rules.Rules `json:"rules,omitempty" validate:"omitempty,unique=Name"`
|
||||
HealthCheck *health.HealthCheckConfig `json:"healthcheck,omitempty"`
|
||||
LoadBalance *loadbalance.Config `json:"load_balance,omitempty"`
|
||||
Middlewares map[string]docker.LabelMap `json:"middlewares,omitempty"`
|
||||
Homepage *homepage.Item `json:"homepage,omitempty"`
|
||||
AccessLog *accesslog.Config `json:"access_log,omitempty"`
|
||||
|
||||
/* Docker only */
|
||||
Container *docker.Container `json:"container,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
|
||||
finalized bool
|
||||
}
|
||||
|
||||
RawEntries = F.Map[string, *RawEntry]
|
||||
)
|
||||
|
||||
var NewProxyEntries = F.NewMapOf[string, *RawEntry]
|
||||
|
||||
func (e *RawEntry) Finalize() {
|
||||
if e.finalized {
|
||||
return
|
||||
}
|
||||
|
||||
isDocker := e.Container != nil
|
||||
cont := e.Container
|
||||
if !isDocker {
|
||||
cont = docker.DummyContainer
|
||||
}
|
||||
|
||||
if e.Host == "" {
|
||||
switch {
|
||||
case cont.PrivateIP != "":
|
||||
e.Host = cont.PrivateIP
|
||||
case cont.PublicIP != "":
|
||||
e.Host = cont.PublicIP
|
||||
case !isDocker:
|
||||
e.Host = "localhost"
|
||||
}
|
||||
}
|
||||
|
||||
lp, pp, extra := e.splitPorts()
|
||||
|
||||
if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "tcp"
|
||||
}
|
||||
} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
|
||||
if pp == "" {
|
||||
pp = strconv.Itoa(port)
|
||||
}
|
||||
if e.Scheme == "" {
|
||||
e.Scheme = "http"
|
||||
}
|
||||
} else if pp == "" && e.Scheme == "https" {
|
||||
pp = "443"
|
||||
} else if pp == "" {
|
||||
if p := lowestPort(cont.PrivatePortMapping); p != "" {
|
||||
pp = p
|
||||
} else if p := lowestPort(cont.PublicPortMapping); p != "" {
|
||||
pp = p
|
||||
} else if !isDocker {
|
||||
pp = "80"
|
||||
} else {
|
||||
logging.Debug().Msg("no port found for " + e.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
// replace private port with public port if using public IP.
|
||||
if e.Host == cont.PublicIP {
|
||||
if p, ok := cont.PrivatePortMapping[pp]; ok {
|
||||
pp = strutils.PortString(p.PublicPort)
|
||||
}
|
||||
}
|
||||
// replace public port with private port if using private IP.
|
||||
if e.Host == cont.PrivateIP {
|
||||
if p, ok := cont.PublicPortMapping[pp]; ok {
|
||||
pp = strutils.PortString(p.PrivatePort)
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" && isDocker {
|
||||
switch {
|
||||
case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
|
||||
e.Scheme = "udp"
|
||||
case e.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
|
||||
e.Scheme = "udp"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Scheme == "" {
|
||||
switch {
|
||||
case lp != "":
|
||||
e.Scheme = "tcp"
|
||||
case strings.HasSuffix(pp, "443"):
|
||||
e.Scheme = "https"
|
||||
default: // assume its http
|
||||
e.Scheme = "http"
|
||||
}
|
||||
}
|
||||
|
||||
if e.HealthCheck == nil {
|
||||
e.HealthCheck = new(health.HealthCheckConfig)
|
||||
}
|
||||
|
||||
if e.HealthCheck.Disable {
|
||||
e.HealthCheck = nil
|
||||
} else {
|
||||
if e.HealthCheck.Interval == 0 {
|
||||
e.HealthCheck.Interval = common.HealthCheckIntervalDefault
|
||||
}
|
||||
if e.HealthCheck.Timeout == 0 {
|
||||
e.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
|
||||
}
|
||||
}
|
||||
|
||||
if cont.IdleTimeout != "" {
|
||||
if cont.WakeTimeout == "" {
|
||||
cont.WakeTimeout = common.WakeTimeoutDefault
|
||||
}
|
||||
if cont.StopTimeout == "" {
|
||||
cont.StopTimeout = common.StopTimeoutDefault
|
||||
}
|
||||
if cont.StopMethod == "" {
|
||||
cont.StopMethod = common.StopMethodDefault
|
||||
}
|
||||
}
|
||||
|
||||
e.Port = joinPorts(lp, pp, extra)
|
||||
|
||||
if e.Port == "" || e.Host == "" {
|
||||
if lp != "" {
|
||||
e.Port = lp + ":0"
|
||||
} else {
|
||||
e.Port = "0"
|
||||
}
|
||||
}
|
||||
|
||||
if e.Homepage.IsEmpty() {
|
||||
e.Homepage = homepage.NewItem(e.Alias)
|
||||
}
|
||||
|
||||
e.finalized = true
|
||||
}
|
||||
|
||||
func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
|
||||
portSplit := strutils.SplitRune(e.Port, ':')
|
||||
if len(portSplit) == 1 {
|
||||
pp = portSplit[0]
|
||||
} else {
|
||||
lp = portSplit[0]
|
||||
pp = portSplit[1]
|
||||
if len(portSplit) > 2 {
|
||||
extra = strutils.JoinRune(portSplit[2:], ':')
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func joinPorts(lp string, pp string, extra string) string {
|
||||
s := make([]string, 0, 3)
|
||||
if lp != "" {
|
||||
s = append(s, lp)
|
||||
}
|
||||
if pp != "" {
|
||||
s = append(s, pp)
|
||||
}
|
||||
if extra != "" {
|
||||
s = append(s, extra)
|
||||
}
|
||||
return strutils.JoinRune(s, ':')
|
||||
}
|
||||
|
||||
func lowestPort(ports map[string]types.Port) string {
|
||||
var cmp uint16
|
||||
var res string
|
||||
for port, v := range ports {
|
||||
if v.PrivatePort < cmp || cmp == 0 {
|
||||
cmp = v.PrivatePort
|
||||
res = port
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -3,14 +3,39 @@ package types
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/docker"
|
||||
idlewatcher "github.com/yusing/go-proxy/internal/docker/idlewatcher/types"
|
||||
"github.com/yusing/go-proxy/internal/homepage"
|
||||
net "github.com/yusing/go-proxy/internal/net/types"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/watcher/health"
|
||||
|
||||
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
|
||||
)
|
||||
|
||||
type (
|
||||
//nolint:interfacebloat // this is for avoiding circular imports
|
||||
Route interface {
|
||||
Entry
|
||||
task.TaskStarter
|
||||
task.TaskFinisher
|
||||
ProviderName() string
|
||||
TargetName() string
|
||||
TargetURL() *net.URL
|
||||
HealthMonitor() health.HealthMonitor
|
||||
|
||||
Started() bool
|
||||
|
||||
IdlewatcherConfig() *idlewatcher.Config
|
||||
HealthCheckConfig() *health.HealthCheckConfig
|
||||
LoadBalanceConfig() *loadbalance.Config
|
||||
HomepageConfig() *homepage.Item
|
||||
ContainerInfo() *docker.Container
|
||||
|
||||
IsDocker() bool
|
||||
UseLoadBalance() bool
|
||||
UseIdleWatcher() bool
|
||||
UseHealthCheck() bool
|
||||
UseAccessLog() bool
|
||||
}
|
||||
HTTPRoute interface {
|
||||
Route
|
||||
|
||||
@@ -3,6 +3,6 @@ package types
|
||||
type RouteType string
|
||||
|
||||
const (
|
||||
RouteTypeStream RouteType = "stream"
|
||||
RouteTypeReverseProxy RouteType = "reverse_proxy"
|
||||
RouteTypeStream RouteType = "stream"
|
||||
RouteTypeHTTP RouteType = "http"
|
||||
)
|
||||
|
||||
@@ -8,16 +8,22 @@ type Scheme string
|
||||
|
||||
var ErrInvalidScheme = E.New("invalid scheme")
|
||||
|
||||
func NewScheme(s string) (Scheme, error) {
|
||||
const (
|
||||
SchemeHTTP Scheme = "http"
|
||||
SchemeHTTPS Scheme = "https"
|
||||
SchemeTCP Scheme = "tcp"
|
||||
SchemeUDP Scheme = "udp"
|
||||
SchemeFileServer Scheme = "fileserver"
|
||||
)
|
||||
|
||||
func (s Scheme) Validate() E.Error {
|
||||
switch s {
|
||||
case "http", "https", "tcp", "udp":
|
||||
return Scheme(s), nil
|
||||
case SchemeHTTP, SchemeHTTPS,
|
||||
SchemeTCP, SchemeUDP, SchemeFileServer:
|
||||
return nil
|
||||
}
|
||||
return "", ErrInvalidScheme.Subject(s)
|
||||
return ErrInvalidScheme.Subject(string(s))
|
||||
}
|
||||
|
||||
func (s Scheme) IsHTTP() bool { return s == "http" }
|
||||
func (s Scheme) IsHTTPS() bool { return s == "https" }
|
||||
func (s Scheme) IsTCP() bool { return s == "tcp" }
|
||||
func (s Scheme) IsUDP() bool { return s == "udp" }
|
||||
func (s Scheme) IsStream() bool { return s.IsTCP() || s.IsUDP() }
|
||||
func (s Scheme) IsReverseProxy() bool { return s == SchemeHTTP || s == SchemeHTTPS }
|
||||
func (s Scheme) IsStream() bool { return s == SchemeTCP || s == SchemeUDP }
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type StreamPort struct {
|
||||
ListeningPort Port `json:"listening"`
|
||||
ProxyPort Port `json:"proxy"`
|
||||
}
|
||||
|
||||
var ErrStreamPortTooManyColons = E.New("too many colons")
|
||||
|
||||
func ValidateStreamPort(p string) (StreamPort, error) {
|
||||
split := strutils.SplitRune(p, ':')
|
||||
|
||||
switch len(split) {
|
||||
case 1:
|
||||
split = []string{"0", split[0]}
|
||||
case 2:
|
||||
break
|
||||
default:
|
||||
return StreamPort{}, ErrStreamPortTooManyColons.Subject(p)
|
||||
}
|
||||
|
||||
listeningPort, lErr := ValidatePort(split[0])
|
||||
proxyPort, pErr := ValidatePort(split[1])
|
||||
if err := E.Join(lErr, pErr); err != nil {
|
||||
return StreamPort{}, err
|
||||
}
|
||||
|
||||
return StreamPort{listeningPort, proxyPort}, nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var validPorts = []string{
|
||||
"1234:5678",
|
||||
"0:2345",
|
||||
"2345",
|
||||
}
|
||||
|
||||
var invalidPorts = []string{
|
||||
"",
|
||||
"123:",
|
||||
"0:",
|
||||
":1234",
|
||||
"qwerty",
|
||||
"asdfgh:asdfgh",
|
||||
"1234:asdfgh",
|
||||
}
|
||||
|
||||
var outOfRangePorts = []string{
|
||||
"-1:1234",
|
||||
"1234:-1",
|
||||
"65536",
|
||||
"0:65536",
|
||||
}
|
||||
|
||||
var tooManyColonsPorts = []string{
|
||||
"1234:1234:1234",
|
||||
}
|
||||
|
||||
func TestStreamPort(t *testing.T) {
|
||||
for _, port := range validPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
for _, port := range invalidPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, strconv.ErrSyntax, err)
|
||||
}
|
||||
for _, port := range outOfRangePorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, ErrPortOutOfRange, err)
|
||||
}
|
||||
for _, port := range tooManyColonsPorts {
|
||||
_, err := ValidateStreamPort(port)
|
||||
ExpectError2(t, port, ErrStreamPortTooManyColons, err)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type StreamScheme struct {
|
||||
ListeningScheme Scheme `json:"listening"`
|
||||
ProxyScheme Scheme `json:"proxy"`
|
||||
}
|
||||
|
||||
func ValidateStreamScheme(s string) (*StreamScheme, error) {
|
||||
ss := &StreamScheme{}
|
||||
parts := strutils.SplitRune(s, ':')
|
||||
if len(parts) == 1 {
|
||||
parts = []string{s, s}
|
||||
} else if len(parts) != 2 {
|
||||
return nil, ErrInvalidScheme.Subject(s)
|
||||
}
|
||||
|
||||
var lErr, pErr error
|
||||
ss.ListeningScheme, lErr = NewScheme(parts[0])
|
||||
ss.ProxyScheme, pErr = NewScheme(parts[1])
|
||||
|
||||
if err := E.Join(lErr, pErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func (s StreamScheme) String() string {
|
||||
return string(s.ListeningScheme) + " -> " + string(s.ProxyScheme)
|
||||
}
|
||||
|
||||
// IsCoherent checks if the ListeningScheme and ProxyScheme of the StreamScheme are equal.
|
||||
//
|
||||
// It returns a boolean value indicating whether the ListeningScheme and ProxyScheme are equal.
|
||||
func (s StreamScheme) IsCoherent() bool {
|
||||
return s.ListeningScheme == s.ProxyScheme
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
validStreamSchemes = []string{
|
||||
"tcp:tcp",
|
||||
"tcp:udp",
|
||||
"udp:tcp",
|
||||
"udp:udp",
|
||||
"tcp",
|
||||
"udp",
|
||||
}
|
||||
|
||||
invalidStreamSchemes = []string{
|
||||
"tcp:tcp:",
|
||||
"tcp:",
|
||||
":udp:",
|
||||
":udp",
|
||||
"top",
|
||||
}
|
||||
)
|
||||
|
||||
func TestNewStreamScheme(t *testing.T) {
|
||||
for _, s := range validStreamSchemes {
|
||||
_, err := ValidateStreamScheme(s)
|
||||
ExpectNoError(t, err)
|
||||
}
|
||||
for _, s := range invalidStreamSchemes {
|
||||
_, err := ValidateStreamScheme(s)
|
||||
ExpectError(t, ErrInvalidScheme, err)
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var (
|
||||
branch = common.GetEnvString("BRANCH", "v0.8")
|
||||
baseURL = "https://github.com/yusing/go-proxy/raw/" + branch
|
||||
requiredConfigs = []Config{
|
||||
{common.ConfigBasePath, true, false, ""},
|
||||
{common.DotEnvPath, false, true, common.DotEnvExamplePath},
|
||||
{common.ComposeFileName, false, true, common.ComposeExampleFileName},
|
||||
{path.Join(common.ConfigBasePath, common.ConfigFileName), false, true, common.ConfigExampleFileName},
|
||||
}
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Pathname string
|
||||
IsDir bool
|
||||
NeedDownload bool
|
||||
DownloadFileName string
|
||||
}
|
||||
|
||||
func Setup() {
|
||||
log.Println("setting up go-proxy")
|
||||
log.Println("branch:", branch)
|
||||
|
||||
if err := os.Chdir("/setup"); err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
|
||||
for _, config := range requiredConfigs {
|
||||
config.setup()
|
||||
}
|
||||
|
||||
log.Println("setup finished")
|
||||
}
|
||||
|
||||
func (c *Config) setup() {
|
||||
if c.IsDir {
|
||||
mkdir(c.Pathname)
|
||||
return
|
||||
}
|
||||
if !c.NeedDownload {
|
||||
touch(c.Pathname)
|
||||
return
|
||||
}
|
||||
|
||||
fetch(c.DownloadFileName, c.Pathname)
|
||||
}
|
||||
|
||||
func hasFileOrDir(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func mkdir(pathname string) {
|
||||
_, err := os.Stat(pathname)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
log.Printf("creating directory %q\n", pathname)
|
||||
err := os.MkdirAll(pathname, 0o755)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func touch(pathname string) {
|
||||
if hasFileOrDir(pathname) {
|
||||
return
|
||||
}
|
||||
log.Printf("creating file %q\n", pathname)
|
||||
_, err := os.Create(pathname)
|
||||
if err != nil {
|
||||
log.Fatalf("failed: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(remoteFilename string, outFileName string) {
|
||||
if hasFileOrDir(outFileName) {
|
||||
if remoteFilename == outFileName {
|
||||
log.Printf("%q already exists, not overwriting\n", outFileName)
|
||||
return
|
||||
}
|
||||
log.Printf("%q already exists, downloading to %q\n", outFileName, remoteFilename)
|
||||
outFileName = remoteFilename
|
||||
}
|
||||
log.Printf("downloading %q to %q\n", remoteFilename, outFileName)
|
||||
|
||||
url, err := url.JoinPath(baseURL, remoteFilename)
|
||||
if err != nil {
|
||||
log.Fatalf("unexpected error: %s\n", err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("http request failed: %s\n", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
log.Fatalf("error reading response body: %s\n", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(outFileName, body, 0o644)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
log.Fatalf("failed to write to file: %s\n", err)
|
||||
}
|
||||
|
||||
log.Print("done")
|
||||
|
||||
resp.Body.Close()
|
||||
}
|
||||
@@ -31,6 +31,13 @@ var (
|
||||
ErrUnknownField = E.New("unknown field")
|
||||
)
|
||||
|
||||
var (
|
||||
tagDeserialize = "deserialize" // `deserialize:"-"` to exclude from deserialization
|
||||
tagJSON = "json" // share between Deserialize and json.Marshal
|
||||
tagValidate = "validate" // uses go-playground/validator
|
||||
tagAliases = "aliases" // declare aliases for fields
|
||||
)
|
||||
|
||||
var mapUnmarshalerType = reflect.TypeFor[MapUnmarshaller]()
|
||||
|
||||
var defaultValues = functional.NewMapOf[reflect.Type, func() any]()
|
||||
@@ -67,6 +74,9 @@ func extractFields(t reflect.Type) (all, anonymous []reflect.StructField) {
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
if field.Tag.Get(tagDeserialize) == "-" {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous {
|
||||
f1, f2 := extractFields(field.Type)
|
||||
fields = append(fields, f1...)
|
||||
@@ -97,6 +107,33 @@ func ValidateWithFieldTags(s any) E.Error {
|
||||
return errs.Error()
|
||||
}
|
||||
|
||||
func ValidateWithCustomValidator(v reflect.Value) E.Error {
|
||||
isStruct := false
|
||||
for {
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer, reflect.Interface:
|
||||
if v.IsNil() {
|
||||
return E.Errorf("validate: v is %w", ErrNilValue)
|
||||
}
|
||||
if validate, ok := v.Interface().(CustomValidator); ok {
|
||||
return validate.Validate()
|
||||
}
|
||||
if isStruct {
|
||||
return nil
|
||||
}
|
||||
v = v.Elem()
|
||||
case reflect.Struct:
|
||||
if !v.CanAddr() {
|
||||
return nil
|
||||
}
|
||||
v = v.Addr()
|
||||
isStruct = true
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dive(dst reflect.Value) (v reflect.Value, t reflect.Type, err E.Error) {
|
||||
dstT := dst.Type()
|
||||
for {
|
||||
@@ -186,7 +223,7 @@ func Deserialize(src SerializedObject, dst any) (err E.Error) {
|
||||
}
|
||||
for _, field := range fields {
|
||||
var key string
|
||||
if jsonTag, ok := field.Tag.Lookup("json"); ok {
|
||||
if jsonTag, ok := field.Tag.Lookup(tagJSON); ok {
|
||||
if jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
@@ -198,10 +235,10 @@ func Deserialize(src SerializedObject, dst any) (err E.Error) {
|
||||
mapping[key] = dstV.FieldByName(field.Name)
|
||||
|
||||
if !hasValidateTag {
|
||||
_, hasValidateTag = field.Tag.Lookup("validate")
|
||||
_, hasValidateTag = field.Tag.Lookup(tagValidate)
|
||||
}
|
||||
|
||||
aliases, ok := field.Tag.Lookup("aliases")
|
||||
aliases, ok := field.Tag.Lookup(tagAliases)
|
||||
if ok {
|
||||
for _, alias := range strutils.CommaSeperatedList(aliases) {
|
||||
mapping[alias] = dstV.FieldByName(field.Name)
|
||||
@@ -220,34 +257,28 @@ func Deserialize(src SerializedObject, dst any) (err E.Error) {
|
||||
}
|
||||
if hasValidateTag {
|
||||
errs.Add(ValidateWithFieldTags(dstV.Interface()))
|
||||
} else {
|
||||
if dstV.CanAddr() {
|
||||
dstV = dstV.Addr()
|
||||
}
|
||||
if validator, ok := dstV.Interface().(CustomValidator); ok {
|
||||
errs.Add(validator.Validate())
|
||||
}
|
||||
}
|
||||
if err := ValidateWithCustomValidator(dstV); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
return errs.Error()
|
||||
case reflect.Map:
|
||||
if dstV.IsNil() {
|
||||
dstV.Set(reflect.MakeMap(dstT))
|
||||
}
|
||||
for k := range src {
|
||||
for k, v := range src {
|
||||
mapVT := dstT.Elem()
|
||||
tmp := New(mapVT).Elem()
|
||||
err := Convert(reflect.ValueOf(src[k]), tmp)
|
||||
if err == nil {
|
||||
dstV.SetMapIndex(reflect.ValueOf(k), tmp)
|
||||
} else {
|
||||
err := Convert(reflect.ValueOf(v), tmp)
|
||||
if err != nil {
|
||||
errs.Add(err.Subject(k))
|
||||
continue
|
||||
}
|
||||
if err := ValidateWithCustomValidator(tmp.Addr()); err != nil {
|
||||
errs.Add(err.Subject(k))
|
||||
} else {
|
||||
dstV.SetMapIndex(reflect.ValueOf(k), tmp)
|
||||
}
|
||||
}
|
||||
if dstV.CanAddr() {
|
||||
dstV = dstV.Addr()
|
||||
}
|
||||
if validator, ok := dstV.Interface().(CustomValidator); ok {
|
||||
errs.Add(validator.Validate())
|
||||
if err := ValidateWithCustomValidator(dstV); err != nil {
|
||||
errs.Add(err)
|
||||
}
|
||||
return errs.Error()
|
||||
default:
|
||||
@@ -428,7 +459,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr E.E
|
||||
src = strings.TrimSpace(src)
|
||||
isMultiline := strings.ContainsRune(src, '\n')
|
||||
// one liner is comma separated list
|
||||
if !isMultiline {
|
||||
if !isMultiline && src[0] != '-' {
|
||||
values := strutils.CommaSeperatedList(src)
|
||||
dst.Set(reflect.MakeSlice(dst.Type(), len(values), len(values)))
|
||||
errs := E.NewBuilder("invalid slice values")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user