mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-16 07:06:51 +01:00
Compare commits
9 Commits
0.5.0-beta
...
0.5.0-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1120991019 | ||
|
|
c0ebd9f8c0 | ||
|
|
996b418ea9 | ||
|
|
4cddd4ff71 | ||
|
|
7a0478164f | ||
|
|
2e7ba51521 | ||
|
|
5be8659a99 | ||
|
|
719693deb7 | ||
|
|
23e7d06081 |
11
.github/workflows/docker-image.yml
vendored
11
.github/workflows/docker-image.yml
vendored
@@ -8,7 +8,14 @@ jobs:
|
||||
build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build and Push Container to ghcr.io
|
||||
- name: Set up Docker Build and Push
|
||||
id: docker_build_push
|
||||
uses: GlueOps/github-actions-build-push-containers@v0.3.7
|
||||
with:
|
||||
tags: latest,${{ github.ref_name }}
|
||||
tags: ${{ github.ref_name }}
|
||||
|
||||
- name: Tag as latest
|
||||
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||
run: |
|
||||
docker tag ghcr.io/${{ github.repository }}:${{ github.ref_name }} ghcr.io/${{ github.repository }}:latest
|
||||
docker push ghcr.io/${{ github.repository }}:latest
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,4 +13,6 @@ log/
|
||||
|
||||
go.work.sum
|
||||
|
||||
!src/config/
|
||||
!src/config/
|
||||
|
||||
todo.md
|
||||
23
.vscode/settings.example.json
vendored
23
.vscode/settings.example.json
vendored
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||
"providers.example.yml",
|
||||
"*.providers.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
"yaml.schemas": {
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/config.schema.json": [
|
||||
"config.example.yml",
|
||||
"config.yml"
|
||||
],
|
||||
"https://github.com/yusing/go-proxy/raw/main/schema/providers.schema.json": [
|
||||
"providers.example.yml",
|
||||
"*.providers.yml",
|
||||
"providers.yml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22.6-alpine as builder
|
||||
FROM golang:1.23.1-alpine AS builder
|
||||
COPY src /src
|
||||
ENV GOCACHE=/root/.cache/go-build
|
||||
WORKDIR /src
|
||||
@@ -13,13 +13,13 @@ FROM alpine:latest
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
COPY schema/ /app/schema
|
||||
# copy binary
|
||||
COPY --from=builder /src/go-proxy /app/
|
||||
COPY schema/ /app/schema
|
||||
|
||||
RUN chmod +x /app/go-proxy
|
||||
ENV DOCKER_HOST unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG 0
|
||||
ENV DOCKER_HOST=unix:///var/run/docker.sock
|
||||
ENV GOPROXY_DEBUG=0
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8888
|
||||
|
||||
7
Makefile
7
Makefile
@@ -36,4 +36,9 @@ repush:
|
||||
git reset --soft HEAD^
|
||||
git add -A
|
||||
git commit -m "repush"
|
||||
git push gitlab dev --force
|
||||
git push gitlab dev --force
|
||||
|
||||
rapid-crash:
|
||||
sudo docker run --restart=always --name test_crash debian:bookworm-slim /bin/cat &&\
|
||||
sleep 3 &&\
|
||||
sudo docker rm -f test_crash
|
||||
|
||||
49
README.md
49
README.md
@@ -9,7 +9,7 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
- [go-proxy](#go-proxy)
|
||||
- [Key Points](#key-points)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Commands](#commands)
|
||||
- [Commands line arguments](#commands-line-arguments)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Use JSON Schema in VSCode](#use-json-schema-in-vscode)
|
||||
- [Config File](#config-file)
|
||||
@@ -24,7 +24,6 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
- Auto configuration for docker contaienrs
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Support HTTP(s), TCP and UDP
|
||||
- Support HTTP(s) round robin load balancing
|
||||
- Web UI for configuration and monitoring (See [screenshots](screeenshots))
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
@@ -45,20 +44,22 @@ A [lightweight](docs/benchmark_result.md), easy-to-use, and efficient reverse pr
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Commands
|
||||
### Commands line arguments
|
||||
|
||||
- `go-proxy` start proxy server
|
||||
- `go-proxy validate` validate config and exit
|
||||
- `go-proxy reload` trigger a force reload of config
|
||||
| Argument | Description |
|
||||
| ---------- | -------------------------------- |
|
||||
| empty | start proxy server |
|
||||
| `validate` | validate config and exit |
|
||||
| `reload` | trigger a force reload of config |
|
||||
|
||||
**For docker containers, run `docker exec -it go-proxy /app/go-proxy <command>`**
|
||||
**run with `docker exec <container_name> /app/go-proxy <command>`**
|
||||
|
||||
### Environment variables
|
||||
|
||||
Booleans:
|
||||
|
||||
- `GOPROXY_DEBUG` enable debug behaviors
|
||||
- `GOPROXY_NO_SCHEMA_VALIDATION`: disable schema validation **(useful for testing new DNS Challenge providers)**
|
||||
| Environment Variable | Description | Default | Values |
|
||||
| ------------------------------ | ------------------------- | ------- | ------- |
|
||||
| `GOPROXY_NO_SCHEMA_VALIDATION` | disable schema validation | `false` | boolean |
|
||||
| `GOPROXY_DEBUG` | enable debug behaviors | `false` | boolean |
|
||||
|
||||
### Use JSON Schema in VSCode
|
||||
|
||||
@@ -80,12 +81,14 @@ autocert:
|
||||
- ...
|
||||
# reverse proxy providers configuration
|
||||
providers:
|
||||
entry_1:
|
||||
kind: docker
|
||||
value: # `FROM_ENV` or full url to docker host
|
||||
entry_2:
|
||||
kind: file
|
||||
value: # relative path of file to `config/`
|
||||
include:
|
||||
- providers.yml
|
||||
- other_file_1.yml
|
||||
- ...
|
||||
docker:
|
||||
local: $DOCKER_HOST
|
||||
remote-1: tcp://10.0.2.1:2375
|
||||
remote-2: ssh://root:1234@10.0.2.2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
@@ -106,14 +109,16 @@ See [providers.example.yml](providers.example.yml) for examples
|
||||
|
||||
## Build it yourself
|
||||
|
||||
1. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
|
||||
2. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
|
||||
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
|
||||
3. get dependencies with `make get`
|
||||
3. Clear cache if you have built this before (go < 1.22) with `go clean -cache`
|
||||
|
||||
4. build binary with `make build`
|
||||
4. get dependencies with `make get`
|
||||
|
||||
5. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
5. build binary with `make build`
|
||||
|
||||
6. start your container with `make up` (docker) or `bin/go-proxy` (binary)
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -11,22 +11,26 @@
|
||||
# provider: cloudflare
|
||||
# email: # ACME Email
|
||||
# domains: # a list of domains for cert registration
|
||||
# -
|
||||
# - x.y.z
|
||||
# options:
|
||||
# - auth_token: # your zone API token
|
||||
# - auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
|
||||
# 3. other providers, check readme for more
|
||||
|
||||
providers:
|
||||
local:
|
||||
kind: docker
|
||||
include:
|
||||
- providers.yml # config/providers.yml
|
||||
# add some more below if you want
|
||||
# - file1.yml # config/file_1.yml
|
||||
# - file2.yml
|
||||
docker:
|
||||
# for value format, see https://docs.docker.com/reference/cli/dockerd/
|
||||
# i.e. ssh://user@10.0.1.1:22, tcp://10.0.2.1:2375
|
||||
# use FROM_ENV if you have binded docker socket to /var/run/docker.sock
|
||||
value: FROM_ENV
|
||||
providers:
|
||||
kind: file
|
||||
value: providers.yml
|
||||
# $DOCKER_HOST implies unix:///var/run/docker.sock by default
|
||||
local: $DOCKER_HOST
|
||||
# add more docker providers if needed
|
||||
# remote-1: tcp://10.0.2.1:2375
|
||||
# remote-2: ssh://root:1234@10.0.2.2
|
||||
|
||||
# Fixed options (optional, non hot-reloadable)
|
||||
|
||||
# timeout_shutdown: 5
|
||||
|
||||
212
docs/docker.md
212
docs/docker.md
@@ -1,126 +1,166 @@
|
||||
# Docker container guide
|
||||
# Docker compose guide
|
||||
|
||||
## Table of content
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Docker container guide](#docker-container-guide)
|
||||
- [Docker compose guide](#docker-compose-guide)
|
||||
- [Table of content](#table-of-content)
|
||||
- [Setup](#setup)
|
||||
- [Labels](#labels)
|
||||
- [Syntax](#syntax)
|
||||
- [Fields](#fields)
|
||||
- [Key-value mapping example](#key-value-mapping-example)
|
||||
- [List example](#list-example)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Docker compose examples](#docker-compose-examples)
|
||||
- [Local docker provider in bridge network](#local-docker-provider-in-bridge-network)
|
||||
- [Proxy setup](#proxy-setup)
|
||||
- [Services URLs for above examples](#services-urls-for-above-examples)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install `wget` if not already
|
||||
1. Install `wget` if not already
|
||||
|
||||
2. Run setup script
|
||||
- Ubuntu based: `sudo apt install -y wget`
|
||||
- Fedora based: `sudo yum install -y wget`
|
||||
- Arch based: `sudo pacman -Sy wget`
|
||||
|
||||
`bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)`
|
||||
2. Run setup script
|
||||
|
||||
What it does:
|
||||
`bash <(wget -qO- https://github.com/yusing/go-proxy/raw/main/setup-docker.sh)`
|
||||
|
||||
- Create required directories
|
||||
- Setup `config.yml` and `compose.yml`
|
||||
It will setup folder structure and required config files
|
||||
|
||||
3. Verify folder structure and then `cd go-proxy`
|
||||
3. Verify folder structure and then `cd go-proxy`
|
||||
|
||||
```plain
|
||||
go-proxy
|
||||
├── certs
|
||||
├── compose.yml
|
||||
└── config
|
||||
├── config.yml
|
||||
└── providers.yml
|
||||
```
|
||||
```plain
|
||||
go-proxy
|
||||
├── certs
|
||||
├── compose.yml
|
||||
└── config
|
||||
├── config.yml
|
||||
└── providers.yml
|
||||
```
|
||||
|
||||
4. Enable HTTPs _(optional)_
|
||||
4. Enable HTTPs _(optional)_
|
||||
|
||||
- To use autocert feature
|
||||
Mount a folder (to store obtained certs) or (containing existing cert)
|
||||
|
||||
- completing `autocert` section in `config/config.yml`
|
||||
- mount `certs/` to `/app/certs` to store obtained certs
|
||||
```yaml
|
||||
services:
|
||||
go-proxy:
|
||||
...
|
||||
volumes:
|
||||
- ./certs:/app/certs
|
||||
```
|
||||
|
||||
- To use existing certificate
|
||||
To use **autocert**, complete that section in `config.yml`, e.g.
|
||||
|
||||
mount your wildcard (`*.y.z`) SSL cert
|
||||
```yaml
|
||||
autocert:
|
||||
email: john.doe@x.y.z # ACME Email
|
||||
domains: # a list of domains for cert registration
|
||||
- x.y.z
|
||||
provider: cloudflare
|
||||
options:
|
||||
- auth_token: c1234565789-abcdefghijklmnopqrst # your zone API token
|
||||
```
|
||||
|
||||
- cert / chain / fullchain -> `/app/certs/cert.crt`
|
||||
- private key -> `/app/certs/priv.key`
|
||||
To use **existing certificate**, set path for cert and key in `config.yml`, e.g.
|
||||
|
||||
5. Modify `compose.yml` fit your needs
|
||||
```yaml
|
||||
autocert:
|
||||
cert_path: /app/certs/cert.crt
|
||||
key_path: /app/certs/priv.key
|
||||
```
|
||||
|
||||
Add networks to make sure it is in the same network with other containers, or make sure `proxy.<alias>.host` is reachable
|
||||
5. Modify `compose.yml` to fit your needs
|
||||
|
||||
6. Run `docker compose up -d` to start the container
|
||||
6. Run `docker compose up -d` to start the container
|
||||
|
||||
7. Start editing config files in `http://<ip>:8080`
|
||||
7. Navigate to Web panel `http://gp.yourdomain.com` or use **Visual Studio Code (provides schema check)** to edit proxy config
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Labels
|
||||
|
||||
- `proxy.aliases`: comma separated aliases for subdomain matching
|
||||
### Syntax
|
||||
|
||||
- default: container name
|
||||
| Label | Description |
|
||||
| ----------------------- | -------------------------------------------------------- |
|
||||
| `proxy.aliases` | comma separated aliases for subdomain and label matching |
|
||||
| `proxy.<alias>.<field>` | set field for specific alias |
|
||||
| `proxy.*.<field>` | set field for all aliases |
|
||||
|
||||
- `proxy.*.<field>`: wildcard label for all aliases
|
||||
### Fields
|
||||
|
||||
_Labels below should have a **`proxy.<alias>.`** prefix._
|
||||
| Field | Description | Default | Allowed Values / Syntax |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `scheme` | proxy protocol | <ul><li>`http` for numeric port</li><li>`tcp` for `x:y` port</li></ul> | `http`, `https`, `tcp`, `udp` |
|
||||
| `host` | proxy host | <ul><li>Docker: `container_name`</li><li>File: `localhost`</li></ul> | IP address, hostname |
|
||||
| `port` | proxy port **(http/s)** | first port in `ports:` | number in range of `1 - 65535` |
|
||||
| `port` **(required)** | proxy port **(tcp/udp)** | N/A | `x:y` <br><ul><li>x: port for `go-proxy` to listen on</li><li>y: port or [_service name_](../src/common/constants.go#L55) of target container</li></ul> |
|
||||
| `no_tls_verify` | whether skip tls verify **(https only)** | `false` | boolean |
|
||||
| `path_patterns` | proxy path patterns **(http/s only)**<br> only requests that matched a pattern will be proxied | empty **(proxy all requests)** | yaml style list[<sup>1</sup>](#list-example) of path patterns ([syntax](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)) |
|
||||
| `set_headers` | header to set **(http/s only)** | empty | yaml style key-value mapping[<sup>2</sup>](#key-value-mapping-example) of header-value pairs |
|
||||
| `hide_headers` | header to hide **(http/s only)** | empty | yaml style list[<sup>1</sup>](#list-example) of headers |
|
||||
|
||||
_i.e. `proxy.nginx.scheme: http`_
|
||||
#### Key-value mapping example
|
||||
|
||||
- `scheme`: proxy protocol
|
||||
- default:
|
||||
- if `port` is like `x:y`: `tcp`
|
||||
- if `port` is a number: `http`
|
||||
- allowed: `http`, `https`, `tcp`, `udp`
|
||||
- `host`: proxy host
|
||||
- default: `container_name`
|
||||
- allowed: IP address, hostname
|
||||
- `port`: proxy port
|
||||
- default: first port in `ports:`
|
||||
- `http(s)`: number in range og `0 - 65535`
|
||||
- `tcp`, `udp`: `x:y`
|
||||
- `x`: port for `go-proxy` to listen on
|
||||
- `y`: port, or _service name_ of target container
|
||||
see [constants.go:14 for _service names_](../src/common/constants.go#L74)
|
||||
- `no_tls_verify`: whether skip tls verify when scheme is https
|
||||
- default: `false`
|
||||
- `path`: proxy path _(http(s) proxy only)_
|
||||
- default: empty
|
||||
- `path_mode`: mode for path handling
|
||||
Docker Compose
|
||||
|
||||
- default: empty
|
||||
- allowed: empty, `forward`
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
# values from duplicated header keys will be combined
|
||||
proxy.nginx.set_headers: | # remember to add the '|'
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
X-Custom-Header2: value4
|
||||
# X-Custom-Header2 will be "value3, value4"
|
||||
```
|
||||
|
||||
- `empty`: remove path prefix from URL when proxying
|
||||
1. apps.y.z/webdav -> webdav:80
|
||||
2. apps.y.z./webdav/path/to/file -> webdav:80/path/to/file
|
||||
- `forward`: path remain unchanged
|
||||
1. apps.y.z/webdav -> webdav:80/webdav
|
||||
2. apps.y.z./webdav/path/to/file -> webdav:80/webdav/path/to/file
|
||||
File Provider
|
||||
|
||||
- `set_headers`: a list of header to set, (key:value, one by line)
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
set_headers:
|
||||
# do not duplicate header keys, as it is not allowed in YAML
|
||||
X-Custom-Header1: value1, value2
|
||||
X-Custom-Header2: value3
|
||||
```
|
||||
|
||||
Duplicated keys will be treated as multiple-value headers
|
||||
#### List example
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
proxy.app.set_headers: |
|
||||
X-Custom-Header1: value1
|
||||
X-Custom-Header1: value2
|
||||
X-Custom-Header2: value2
|
||||
```
|
||||
Docker Compose
|
||||
|
||||
- `hide_headers`: comma seperated list of headers to hide
|
||||
```yaml
|
||||
services:
|
||||
nginx:
|
||||
...
|
||||
labels:
|
||||
proxy.nginx.path_patterns: | # remember to add the '|'
|
||||
- GET /
|
||||
- POST /auth
|
||||
proxy.nginx.hide_headers: | # remember to add the '|'
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
- `load_balance`: enable load balance
|
||||
- allowed: `1`, `true`
|
||||
File Provider
|
||||
|
||||
```yaml
|
||||
service_a:
|
||||
host: service_a.internal
|
||||
path_patterns:
|
||||
- GET /
|
||||
- POST /auth
|
||||
hide_headers:
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
@@ -146,8 +186,6 @@ _i.e. `proxy.nginx.scheme: http`_
|
||||
|
||||
## Docker compose examples
|
||||
|
||||
### Local docker provider in bridge network
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
adg-work:
|
||||
@@ -220,24 +258,6 @@ services:
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
#### Proxy setup
|
||||
|
||||
```yaml
|
||||
go-proxy:
|
||||
image: ghcr.io/yusing/go-proxy
|
||||
container_name: go-proxy
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
labels:
|
||||
- proxy.aliases=gp
|
||||
- proxy.gp.port=8080
|
||||
```
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Services URLs for above examples
|
||||
|
||||
- `gp.yourdomain.com`: go-proxy web panel
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
example: # matching `app.y.z`
|
||||
# optional, defaults to http
|
||||
scheme: http
|
||||
# required, proxy target
|
||||
scheme: https
|
||||
host: 10.0.0.1
|
||||
# optional, defaults to 80 for http, 443 for https
|
||||
port: "80"
|
||||
# optional, defaults to empty
|
||||
path:
|
||||
# optional (scheme=https only)
|
||||
# no_tls_verify: false
|
||||
# optional headers to set / override (http(s) only)
|
||||
port: 80
|
||||
path_patterns: # Check https://pkg.go.dev/net/http#hdr-Patterns-ServeMux for syntax
|
||||
- GET / # accept any GET request
|
||||
- POST /auth # for /auth and /auth/* accept only POST
|
||||
- GET /home/{$}
|
||||
- /b/{bucket}/o/{any}
|
||||
no_tls_verify: false
|
||||
set_headers:
|
||||
HEADER_A:
|
||||
- VALUE_1
|
||||
- VALUE_2
|
||||
HEADER_B: [VALUE_3]
|
||||
# optional headers to hide (http(s) only)
|
||||
HEADER_A: VALUE_A, VALUE_B
|
||||
HEADER_B: VALUE_C
|
||||
hide_headers:
|
||||
- HEADER_C
|
||||
- HEADER_D
|
||||
app1: # matching `app1.y.z` -> http://some_host
|
||||
app1:
|
||||
host: some_host
|
||||
app2:
|
||||
scheme: tcp
|
||||
|
||||
@@ -23,23 +23,21 @@
|
||||
},
|
||||
"cert_path": {
|
||||
"title": "path of cert file to load/store",
|
||||
"description": "default: certs/cert.crt",
|
||||
"default": "certs/cert.crt",
|
||||
"markdownDescription": "default: `certs/cert.crt`",
|
||||
"type": "string"
|
||||
},
|
||||
"key_path": {
|
||||
"title": "path of key file to load/store",
|
||||
"description": "default: certs/priv.key",
|
||||
"default": "certs/priv.key",
|
||||
"markdownDescription": "default: `certs/priv.key`",
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"title": "DNS Challenge Provider",
|
||||
"default": "local",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"local",
|
||||
"cloudflare",
|
||||
"clouddns",
|
||||
"duckdns"
|
||||
]
|
||||
"enum": ["local", "cloudflare", "clouddns", "duckdns"]
|
||||
},
|
||||
"options": {
|
||||
"title": "Provider specific options",
|
||||
@@ -49,20 +47,16 @@
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"not": true,
|
||||
"const": "local"
|
||||
"not": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "local"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"email",
|
||||
"domains",
|
||||
"provider",
|
||||
"options"
|
||||
]
|
||||
"required": ["email", "domains", "provider", "options"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -76,9 +70,7 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": [
|
||||
"auth_token"
|
||||
],
|
||||
"required": ["auth_token"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"auth_token": {
|
||||
@@ -101,11 +93,7 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": [
|
||||
"client_id",
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"required": ["client_id", "email", "password"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"client_id": {
|
||||
@@ -136,9 +124,7 @@
|
||||
"then": {
|
||||
"properties": {
|
||||
"options": {
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"required": ["token"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"token": {
|
||||
@@ -155,73 +141,54 @@
|
||||
"providers": {
|
||||
"title": "Proxy providers configuration",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_-]+$": {
|
||||
"description": "Proxy provider",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"include": {
|
||||
"title": "Proxy providers configuration files",
|
||||
"description": "relative path to 'config'",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+\\.(yml|yaml)$",
|
||||
"patternErrorMessage": "Invalid file name"
|
||||
}
|
||||
},
|
||||
"docker": {
|
||||
"title": "Docker provider configuration",
|
||||
"description": "docker clients (name-address pairs)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"description": "Proxy provider kind",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9-_]+$": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"docker",
|
||||
"file"
|
||||
"examples": [
|
||||
"unix:///var/run/docker.sock",
|
||||
"tcp://127.0.0.1:2375",
|
||||
"ssh://user@host:port"
|
||||
],
|
||||
"oneOf": [
|
||||
{
|
||||
"const": "$DOCKER_HOST",
|
||||
"description": "Use DOCKER_HOST environment variable"
|
||||
},
|
||||
{
|
||||
"pattern": "^unix://.+$",
|
||||
"description": "A Unix socket for local Docker communication."
|
||||
},
|
||||
{
|
||||
"pattern": "^ssh://.+$",
|
||||
"description": "An SSH connection to a remote Docker host."
|
||||
},
|
||||
{
|
||||
"pattern": "^fd://.+$",
|
||||
"description": "A file descriptor for Docker communication."
|
||||
},
|
||||
{
|
||||
"pattern": "^tcp://.+$",
|
||||
"description": "A TCP connection to a remote Docker host."
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "docker"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"const": "FROM_ENV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "use docker client from environment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "docker client URL",
|
||||
"examples": [
|
||||
"unix:///var/run/docker.sock",
|
||||
"tcp://127.0.0.1:2375",
|
||||
"ssh://user@host:port"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "file path"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -231,12 +198,10 @@
|
||||
"minimum": 0
|
||||
},
|
||||
"redirect_to_https": {
|
||||
"title": "Redirect to HTTPS",
|
||||
"title": "Redirect to HTTPS on HTTP requests",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"providers"
|
||||
]
|
||||
}
|
||||
"required": ["providers"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "go-proxy providers file",
|
||||
"anyOf": [
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
@@ -16,7 +16,7 @@
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"title": "Proxy scheme (http, https, tcp, udp)",
|
||||
"anyOf": [
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -32,12 +32,17 @@
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
"description": "Auto detect base on port number"
|
||||
"description": "Auto detect base on port format"
|
||||
}
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"anyOf": [
|
||||
"default": "localhost",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null",
|
||||
"description": "localhost (default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "ipv4",
|
||||
@@ -56,64 +61,38 @@
|
||||
],
|
||||
"title": "Proxy host (ipv4 / ipv6 / hostname)"
|
||||
},
|
||||
"port": {
|
||||
"title": "Proxy port"
|
||||
},
|
||||
"path": {
|
||||
"title": "Proxy path pattern (See https://pkg.go.dev/net/http#ServeMux)"
|
||||
},
|
||||
"no_tls_verify": {
|
||||
"description": "Disable TLS verification for https proxy",
|
||||
"type": "boolean"
|
||||
},
|
||||
"port": {},
|
||||
"no_tls_verify": {},
|
||||
"path_patterns": {},
|
||||
"set_headers": {},
|
||||
"hide_headers": {}
|
||||
},
|
||||
"required": [
|
||||
"host"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"anyOf": [
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"enum": [
|
||||
"http",
|
||||
"https"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"not": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"anyOf": [
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{1,5}$",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"markdownDescription": "Proxy port from **1** to **65535**",
|
||||
"patternErrorMessage": "'port' must be a number"
|
||||
"pattern": "^\\d{1,5}$",
|
||||
"patternErrorMessage": "`port` must be a number"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
@@ -122,11 +101,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"anyOf": [
|
||||
"path_patterns": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Proxy path"
|
||||
"type": "array",
|
||||
"markdownDescription": "A list of [path patterns](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux)",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/(\\w*|{\\w*}|{\\$}))+/?$",
|
||||
"patternErrorMessage": "invalid path pattern"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null",
|
||||
@@ -138,9 +122,11 @@
|
||||
"type": "object",
|
||||
"description": "Proxy headers to set",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -156,12 +142,15 @@
|
||||
"else": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"markdownDescription": "`listening port`:`proxy port | service name`",
|
||||
"markdownDescription": "`listening port:proxy port` or `listening port:service name`",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\:[0-9a-z]+$",
|
||||
"patternErrorMessage": "'port' must be in the format of '<listening port>:<proxy port | service name>'"
|
||||
"patternErrorMessage": "invalid syntax"
|
||||
},
|
||||
"path": {
|
||||
"no_tls_verify": {
|
||||
"not": true
|
||||
},
|
||||
"path_patterns": {
|
||||
"not": true
|
||||
},
|
||||
"set_headers": {
|
||||
@@ -171,22 +160,27 @@
|
||||
"not": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"port"
|
||||
]
|
||||
"required": ["port"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"not": {
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"const": "https"
|
||||
}
|
||||
"properties": {
|
||||
"scheme": {
|
||||
"const": "https"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"description": "Disable TLS verification for https proxy",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"no_tls_verify": {
|
||||
"not": true
|
||||
@@ -198,4 +192,4 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
||||
114
setup-binary.sh
114
setup-binary.sh
@@ -1,114 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
REPO_URL=https://github.com/yusing/go-proxy
|
||||
BIN_URL="${REPO_URL}/releases/download/${VERSION}/go-proxy"
|
||||
SRC_URL="${REPO_URL}/archive/refs/tags/${VERSION}.tar.gz"
|
||||
APP_ROOT="/opt/go-proxy/${VERSION}"
|
||||
LOG_FILE="/tmp/go-proxy-setup.log"
|
||||
|
||||
if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then
|
||||
VERSION_URL="${REPO_URL}/raw/main/version.txt"
|
||||
VERSION=$(wget -qO- "$VERSION_URL")
|
||||
fi
|
||||
|
||||
if [ -d "$APP_ROOT" ]; then
|
||||
echo "$APP_ROOT already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if wget exists
|
||||
if ! [ -x "$(command -v wget)" ]; then
|
||||
echo "wget is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if make exists
|
||||
if ! [ -x "$(command -v make)" ]; then
|
||||
echo "make is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dl_source() {
|
||||
cd /tmp
|
||||
echo "Downloading go-proxy source ${VERSION}"
|
||||
wget -c "${SRC_URL}" -O go-proxy.tar.gz &> $LOG_FILE
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "Source download failed, check your internet connection and version number"
|
||||
exit 1
|
||||
fi
|
||||
echo "Done"
|
||||
echo "Extracting go-proxy source ${VERSION}"
|
||||
tar xzf go-proxy.tar.gz &> $LOG_FILE
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "failed to untar go-proxy.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
rm go-proxy.tar.gz
|
||||
mkdir -p "$(dirname "${APP_ROOT}")"
|
||||
mv "go-proxy-${VERSION}" "$APP_ROOT"
|
||||
cd "$APP_ROOT"
|
||||
echo "Done"
|
||||
}
|
||||
dl_binary() {
|
||||
mkdir -p bin
|
||||
echo "Downloading go-proxy binary ${VERSION}"
|
||||
wget -c "${BIN_URL}" -O bin/go-proxy &> $LOG_FILE
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "Binary download failed, check your internet connection and version number"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x bin/go-proxy
|
||||
echo "Done"
|
||||
}
|
||||
setup() {
|
||||
make setup &> $LOG_FILE
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "make setup failed"
|
||||
exit 1
|
||||
fi
|
||||
# SETUP_CODEMIRROR = 1
|
||||
if [ "$SETUP_CODEMIRROR" != "0" ]; then
|
||||
make setup-codemirror &> $LOG_FILE || echo "make setup-codemirror failed, ignored"
|
||||
fi
|
||||
}
|
||||
|
||||
dl_source
|
||||
dl_binary
|
||||
setup
|
||||
|
||||
# setup systemd
|
||||
|
||||
# check if systemctl exists
|
||||
if ! command -v systemctl is-system-running > /dev/null 2>&1; then
|
||||
echo "systemctl not found, skipping systemd setup"
|
||||
exit 0
|
||||
fi
|
||||
systemctl_failed() {
|
||||
echo "Failed to enable and start go-proxy"
|
||||
systemctl status go-proxy
|
||||
exit 1
|
||||
}
|
||||
echo "Setting up systemd service"
|
||||
cat <<EOF > /etc/systemd/system/go-proxy.service
|
||||
[Unit]
|
||||
Description=go-proxy reverse proxy
|
||||
After=network-online.target
|
||||
Wants=network-online.target systemd-networkd-wait-online.service
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${APP_ROOT}/bin/go-proxy
|
||||
WorkingDirectory=${APP_ROOT}
|
||||
Environment="GOPROXY_IS_SYSTEMD=1"
|
||||
Restart=on-failure
|
||||
RestartSec=1s
|
||||
KillMode=process
|
||||
KillSignal=SIGINT
|
||||
TimeoutStartSec=5s
|
||||
TimeoutStopSec=5s
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload &>$LOG_FILE || systemctl_failed
|
||||
systemctl enable --now go-proxy &>$LOG_FILE || systemctl_failed
|
||||
echo "Done"
|
||||
echo "Setup complete"
|
||||
@@ -23,20 +23,10 @@ func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) {
|
||||
U.HandleErr(w, r, U.ErrNotFound("target", target), http.StatusNotFound)
|
||||
return
|
||||
case *R.HTTPRoute:
|
||||
path := r.FormValue("path")
|
||||
if path == "" {
|
||||
U.HandleErr(w, r, U.ErrMissingKey("path"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sr, hasSr := route.GetSubroute(path)
|
||||
if !hasSr {
|
||||
U.HandleErr(w, r, U.ErrNotFound("path", path), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ok = U.IsSiteHealthy(sr.TargetURL.String())
|
||||
ok = U.IsSiteHealthy(route.TargetURL.String())
|
||||
case *R.StreamRoute:
|
||||
ok = U.IsStreamHealthy(
|
||||
route.Scheme.ProxyScheme.String(),
|
||||
string(route.Scheme.ProxyScheme),
|
||||
fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func HandleErr(w http.ResponseWriter, r *http.Request, err error, code ...int) {
|
||||
err = E.From(err).Subjectf("%s %s", r.Method, r.URL)
|
||||
logrus.WithField("?", "api").Error(err)
|
||||
logrus.WithField("module", "api").Error(err)
|
||||
if len(code) > 0 {
|
||||
http.Error(w, err.Error(), code[0])
|
||||
return
|
||||
|
||||
@@ -28,4 +28,4 @@ var providersGenMap = map[string]ProviderGenerator{
|
||||
ProviderDuckdns: providerGenerator(duckdns.NewDefaultConfig, duckdns.NewDNSProviderConfig),
|
||||
}
|
||||
|
||||
var Logger = logrus.WithField("?", "autocert")
|
||||
var Logger = logrus.WithField("module", "autocert")
|
||||
|
||||
@@ -255,4 +255,4 @@ func providerGenerator[CT any, PT challenge.Provider](
|
||||
}
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("?", "autocert")
|
||||
var logger = logrus.WithField("module", "autocert")
|
||||
|
||||
@@ -35,7 +35,7 @@ const (
|
||||
ProvidersSchemaPath = SchemaBasePath + "providers.schema.json"
|
||||
)
|
||||
|
||||
const DockerHostFromEnv = "FROM_ENV"
|
||||
const DockerHostFromEnv = "$DOCKER_HOST"
|
||||
|
||||
const (
|
||||
ProxyHTTPPort = ":80"
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var IsRunningAsService = getEnvBool("GOPROXY_IS_SYSTEMD")
|
||||
var NoSchemaValidation = getEnvBool("GOPROXY_NO_SCHEMA_VALIDATION")
|
||||
var IsDebug = getEnvBool("GOPROXY_DEBUG")
|
||||
|
||||
|
||||
@@ -32,14 +32,15 @@ type Config struct {
|
||||
|
||||
func New() (*Config, E.NestedError) {
|
||||
cfg := &Config{
|
||||
l: logrus.WithField("?", "config"),
|
||||
l: logrus.WithField("module", "config"),
|
||||
reader: U.NewFileReader(common.ConfigPath),
|
||||
watcher: W.NewFileWatcher(common.ConfigFileName),
|
||||
reloadReq: make(chan struct{}),
|
||||
reloadReq: make(chan struct{}, 1),
|
||||
}
|
||||
if err := cfg.load(); err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
cfg.startProviders()
|
||||
cfg.watchChanges()
|
||||
return cfg, E.Nil()
|
||||
}
|
||||
@@ -134,16 +135,9 @@ func (cfg *Config) Statistics() map[string]interface{} {
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
})
|
||||
stats["type"] = p.GetType()
|
||||
stats["num_streams"] = nStreams
|
||||
stats["num_reverse_proxies"] = nRPs
|
||||
switch p.ProviderImpl.(type) {
|
||||
case *PR.DockerProvider:
|
||||
stats["type"] = "docker"
|
||||
case *PR.FileProvider:
|
||||
stats["type"] = "file"
|
||||
default:
|
||||
panic("bug: should not reach here")
|
||||
}
|
||||
providerStats[p.GetName()] = stats
|
||||
})
|
||||
|
||||
@@ -202,12 +196,12 @@ func (cfg *Config) load() E.NestedError {
|
||||
}
|
||||
|
||||
if !common.NoSchemaValidation {
|
||||
if err := Validate(data); err.IsNotNil() {
|
||||
if err = Validate(data); err.IsNotNil() {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
warnings := E.NewBuilder("errors validating config")
|
||||
warnings := E.NewBuilder("errors loading config")
|
||||
|
||||
cfg.l.Debug("starting autocert")
|
||||
ap, err := autocert.NewConfig(&model.AutoCert).GetProvider()
|
||||
@@ -218,16 +212,17 @@ func (cfg *Config) load() E.NestedError {
|
||||
}
|
||||
cfg.autocertProvider = ap
|
||||
|
||||
cfg.l.Debug("starting providers")
|
||||
cfg.l.Debug("loading providers")
|
||||
cfg.proxyProviders = F.NewMap[string, *PR.Provider]()
|
||||
for name, pm := range model.Providers {
|
||||
p := PR.NewProvider(name, pm)
|
||||
cfg.proxyProviders.Set(name, p)
|
||||
if err := p.StartAllRoutes(); err.IsNotNil() {
|
||||
warnings.Add(E.Failure("start routes").Subjectf("provider %s", name).With(err))
|
||||
}
|
||||
for _, filename := range model.Providers.Files {
|
||||
p := PR.NewFileProvider(filename)
|
||||
cfg.proxyProviders.Set(p.GetName(), p)
|
||||
}
|
||||
cfg.l.Debug("started providers")
|
||||
for name, dockerHost := range model.Providers.Docker {
|
||||
p := PR.NewDockerProvider(name, dockerHost)
|
||||
cfg.proxyProviders.Set(p.GetName(), p)
|
||||
}
|
||||
cfg.l.Debug("loaded providers")
|
||||
|
||||
cfg.value = model
|
||||
|
||||
@@ -244,7 +239,7 @@ func (cfg *Config) controlProviders(action string, do func(*PR.Provider) E.Neste
|
||||
|
||||
cfg.proxyProviders.EachKVParallel(func(name string, p *PR.Provider) {
|
||||
if err := do(p); err.IsNotNil() {
|
||||
errors.Add(E.From(err).Subjectf("provider %s", name))
|
||||
errors.Add(E.From(err).Subject(p))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -258,5 +253,5 @@ func (cfg *Config) startProviders() {
|
||||
}
|
||||
|
||||
func (cfg *Config) stopProviders() {
|
||||
cfg.controlProviders("stop", (*PR.Provider).StopAllRoutes)
|
||||
cfg.controlProviders("stop routes", (*PR.Provider).StopAllRoutes)
|
||||
}
|
||||
|
||||
@@ -91,4 +91,4 @@ var clientOptEnvHost = []client.Opt{
|
||||
client.WithAPIVersionNegotiation(),
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("?", "docker")
|
||||
var logger = logrus.WithField("module", "docker")
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func setHeadersParser(value string) (any, E.NestedError) {
|
||||
func yamlListParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return []string{}, E.Nil()
|
||||
}
|
||||
var data []string
|
||||
err := E.From(yaml.Unmarshal([]byte(value), &data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
func yamlStringMappingParser(value string) (any, E.NestedError) {
|
||||
value = strings.TrimSpace(value)
|
||||
lines := strings.Split(value, "\n")
|
||||
h := make(http.Header)
|
||||
h := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
@@ -18,7 +28,11 @@ func setHeadersParser(value string) (any, E.NestedError) {
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
h.Add(key, val)
|
||||
if existing, ok := h[key]; ok {
|
||||
h[key] = existing + ", " + val
|
||||
} else {
|
||||
h[key] = val
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
}
|
||||
@@ -47,8 +61,9 @@ const NSProxy = "proxy"
|
||||
var _ = func() int {
|
||||
RegisterNamespace(NSProxy, ValueParserMap{
|
||||
"aliases": commaSepParser,
|
||||
"set_headers": setHeadersParser,
|
||||
"hide_headers": commaSepParser,
|
||||
"path_patterns": yamlListParser,
|
||||
"set_headers": yamlStringMappingParser,
|
||||
"hide_headers": yamlListParser,
|
||||
"no_tls_verify": boolParser,
|
||||
})
|
||||
return 0
|
||||
@@ -2,8 +2,8 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
@@ -33,27 +33,17 @@ func TestHomePageLabel(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStringProxyLabel(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "ip"
|
||||
v := "bar"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "ip"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Target)
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%q, got %s", v, pl.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolProxyLabelValid(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "no_tls_verify"
|
||||
tests := map[string]bool{
|
||||
"true": true,
|
||||
"TRUE": true,
|
||||
@@ -66,16 +56,10 @@ func TestBoolProxyLabelValid(t *testing.T) {
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), k)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "no_tls_verify"), k)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||
}
|
||||
if pl.Value != v {
|
||||
t.Errorf("expected value=%v, got %v", v, pl.Value)
|
||||
}
|
||||
@@ -87,80 +71,81 @@ func TestBoolProxyLabelInvalid(t *testing.T) {
|
||||
field := "no_tls_verify"
|
||||
_, err := ParseLabel(makeLabel(NSProxy, alias, field), "invalid")
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected err InvalidProxyLabel, got %v", reflect.TypeOf(err))
|
||||
t.Errorf("expected err InvalidProxyLabel, got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderProxyLabelValid(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "set_headers"
|
||||
func TestSetHeaderProxyLabelValid(t *testing.T) {
|
||||
v := `
|
||||
X-Custom-Header1: foo
|
||||
X-Custom-Header1: bar
|
||||
X-Custom-Header2: baz
|
||||
`
|
||||
h := make(http.Header, 0)
|
||||
h.Set("X-Custom-Header1", "foo")
|
||||
h.Add("X-Custom-Header1", "bar")
|
||||
h.Set("X-Custom-Header2", "baz")
|
||||
X-Custom-Header1: foo, bar
|
||||
X-Custom-Header1: baz
|
||||
X-Custom-Header2: boo`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
h := map[string]string{
|
||||
"X-Custom-Header1": "foo, bar, baz",
|
||||
"X-Custom-Header2": "boo",
|
||||
}
|
||||
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||
}
|
||||
hGot, ok := pl.Value.(http.Header)
|
||||
hGot, ok := pl.Value.(map[string]string)
|
||||
if !ok {
|
||||
t.Error("value is not http.Header")
|
||||
t.Errorf("value is not a map[string]string, but %T", pl.Value)
|
||||
return
|
||||
}
|
||||
for k, vWant := range h {
|
||||
vGot := hGot[k]
|
||||
if !reflect.DeepEqual(vGot, vWant) {
|
||||
t.Errorf("expected %s=%q, got %q", k, vWant, vGot)
|
||||
if !reflect.DeepEqual(h, hGot) {
|
||||
t.Errorf("expected %v, got %v", h, hGot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeaderProxyLabelInvalid(t *testing.T) {
|
||||
tests := []string{
|
||||
"X-Custom-Header1 = bar",
|
||||
"X-Custom-Header1",
|
||||
"- X-Custom-Header1",
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
_, err := ParseLabel(makeLabel(NSProxy, "foo", "set_headers"), v)
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected invalid err for %q, got %s", v, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderProxyLabelInvalid(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "set_headers"
|
||||
tests := []string{
|
||||
"X-Custom-Header1 = bar",
|
||||
"X-Custom-Header1",
|
||||
func TestHideHeadersProxyLabel(t *testing.T) {
|
||||
v := `
|
||||
- X-Custom-Header1
|
||||
- X-Custom-Header2
|
||||
- X-Custom-Header3
|
||||
`
|
||||
v = strings.TrimPrefix(v, "\n")
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, "foo", "hide_headers"), v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
|
||||
for _, v := range tests {
|
||||
_, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||
if !err.Is(E.ErrInvalid) {
|
||||
t.Errorf("expected err InvalidProxyLabel for %q, got %v", v, err)
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelSingle(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "hide_headers"
|
||||
v := "X-Custom-Header1"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||
v := "a"
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"X-Custom-Header1"}
|
||||
sWant := []string{"a"}
|
||||
if !ok {
|
||||
t.Error("value is not []string")
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
@@ -168,23 +153,15 @@ func TestCommaSepProxyLabelSingle(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommaSepProxyLabelMulti(t *testing.T) {
|
||||
alias := "foo"
|
||||
field := "hide_headers"
|
||||
v := "X-Custom-Header1, X-Custom-Header2,X-Custom-Header3"
|
||||
pl, err := ParseLabel(makeLabel(NSProxy, alias, field), v)
|
||||
pl, err := ParseLabel("proxy.aliases", v)
|
||||
if err.IsNotNil() {
|
||||
t.Errorf("expected err=nil, got %s", err.Error())
|
||||
}
|
||||
if pl.Target != alias {
|
||||
t.Errorf("expected alias=%s, got %s", alias, pl.Target)
|
||||
}
|
||||
if pl.Attribute != field {
|
||||
t.Errorf("expected field=%s, got %s", field, pl.Attribute)
|
||||
}
|
||||
sGot, ok := pl.Value.([]string)
|
||||
sWant := []string{"X-Custom-Header1", "X-Custom-Header2", "X-Custom-Header3"}
|
||||
if !ok {
|
||||
t.Error("value is not []string")
|
||||
t.Errorf("value is not []string, but %T", pl.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(sGot, sWant) {
|
||||
t.Errorf("expected %q, got %q", sWant, sGot)
|
||||
@@ -20,7 +20,7 @@ type (
|
||||
// Caller then should handle the nested error,
|
||||
// and continue with the valid values.
|
||||
NestedError struct {
|
||||
subject any
|
||||
subject string
|
||||
err error // can be nil
|
||||
extras []NestedError
|
||||
}
|
||||
@@ -96,7 +96,14 @@ func (ne NestedError) Extraf(format string, args ...any) NestedError {
|
||||
}
|
||||
|
||||
func (ne NestedError) Subject(s any) NestedError {
|
||||
ne.subject = s
|
||||
switch ss := s.(type) {
|
||||
case string:
|
||||
ne.subject = ss
|
||||
case fmt.Stringer:
|
||||
ne.subject = ss.String()
|
||||
default:
|
||||
ne.subject = fmt.Sprint(s)
|
||||
}
|
||||
return ne
|
||||
}
|
||||
|
||||
@@ -107,7 +114,8 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError {
|
||||
if strings.Contains(format, "%w") {
|
||||
panic("Subjectf format should not contain %w")
|
||||
}
|
||||
return ne.Subject(fmt.Sprintf(format, args...))
|
||||
ne.subject = fmt.Sprintf(format, args...)
|
||||
return ne
|
||||
}
|
||||
|
||||
func (ne NestedError) IsNil() bool {
|
||||
@@ -131,10 +139,13 @@ func (ne *NestedError) writeToSB(sb *strings.Builder, level int, prefix string)
|
||||
ne.writeIndents(sb, level)
|
||||
sb.WriteString(prefix)
|
||||
|
||||
if ne.err != nil {
|
||||
sb.WriteString(ne.err.Error())
|
||||
if ne.IsNil() {
|
||||
sb.WriteString("nil")
|
||||
return
|
||||
}
|
||||
if ne.subject != nil {
|
||||
|
||||
sb.WriteString(ne.err.Error())
|
||||
if ne.subject != "" {
|
||||
if ne.err != nil {
|
||||
sb.WriteString(fmt.Sprintf(" for %q", ne.subject))
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,16 @@ func TestErrorIs(t *testing.T) {
|
||||
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrInvalid), true)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(ErrFailure), false)
|
||||
|
||||
AssertEq(t, Nil().Is(nil), true)
|
||||
AssertEq(t, Nil().Is(ErrInvalid), false)
|
||||
AssertEq(t, Invalid("foo", "bar").Is(nil), false)
|
||||
}
|
||||
|
||||
func TestNil(t *testing.T) {
|
||||
AssertEq(t, Nil().IsNil(), true)
|
||||
AssertEq(t, Nil().IsNotNil(), false)
|
||||
AssertEq(t, Nil().Error(), "nil")
|
||||
}
|
||||
|
||||
func TestErrorSimple(t *testing.T) {
|
||||
|
||||
32
src/go.mod
32
src/go.mod
@@ -2,21 +2,23 @@ module github.com/yusing/go-proxy
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/docker/cli v27.1.1+incompatible
|
||||
github.com/docker/docker v27.1.1+incompatible
|
||||
github.com/docker/cli v27.2.1+incompatible
|
||||
github.com/docker/docker v27.2.1+incompatible
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/go-acme/lego/v4 v4.17.4
|
||||
github.com/go-acme/lego/v4 v4.18.0
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/net v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.101.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
@@ -28,25 +30,25 @@ require (
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
60
src/go.sum
60
src/go.sum
@@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cloudflare/cloudflare-go v0.101.0 h1:SXWNSEDkbdY84iFIZGyTdWQwDfd98ljv0/4UubpleBQ=
|
||||
github.com/cloudflare/cloudflare-go v0.101.0/go.mod h1:xXQHnoXKR48JlWbFS42i2al3nVqimVhcYvKnIdXLw9g=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY=
|
||||
github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -13,10 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
|
||||
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70=
|
||||
github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI=
|
||||
github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -25,8 +25,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-acme/lego/v4 v4.17.4 h1:h0nePd3ObP6o7kAkndtpTzCw8shOZuWckNYeUQwo36Q=
|
||||
github.com/go-acme/lego/v4 v4.17.4/go.mod h1:dU94SvPNqimEeb7EVilGGSnS0nU1O5Exir0pQ4QFL4U=
|
||||
github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
|
||||
github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
|
||||
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=
|
||||
@@ -51,8 +51,8 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
@@ -79,37 +79,37 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
|
||||
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
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.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.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=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
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=
|
||||
@@ -119,20 +119,20 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.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=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
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=
|
||||
|
||||
21
src/main.go
21
src/main.go
@@ -26,25 +26,18 @@ func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
args := common.GetArgs()
|
||||
l := logrus.WithField("?", "init")
|
||||
l := logrus.WithField("module", "main")
|
||||
|
||||
if common.IsDebug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
if common.IsRunningAsService {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
DisableTimestamp: true,
|
||||
DisableSorting: true,
|
||||
})
|
||||
} else {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
}
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
DisableSorting: true,
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
TimestampFormat: "01-02 15:04:05",
|
||||
})
|
||||
|
||||
if args.Command == common.CommandReload {
|
||||
if err := apiUtils.ReloadServer(); err.IsNotNil() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
@@ -9,14 +8,14 @@ import (
|
||||
|
||||
type (
|
||||
ProxyEntry struct {
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // http proxy only
|
||||
Path string `yaml:"path" json:"path"` // http proxy only
|
||||
SetHeaders http.Header `yaml:"set_headers" json:"set_headers"` // http proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http proxy only
|
||||
Alias string `yaml:"-" json:"-"`
|
||||
Scheme string `yaml:"scheme" json:"scheme"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port string `yaml:"port" json:"port"`
|
||||
NoTLSVerify bool `yaml:"no_tls_verify" json:"no_tls_verify"` // https proxy only
|
||||
PathPatterns []string `yaml:"path_patterns" json:"path_patterns"` // http(s) proxy only
|
||||
SetHeaders map[string]string `yaml:"set_headers" json:"set_headers"` // http(s) proxy only
|
||||
HideHeaders []string `yaml:"hide_headers" json:"hide_headers"` // http(s) proxy only
|
||||
}
|
||||
|
||||
ProxyEntries = *F.Map[string, *ProxyEntry]
|
||||
@@ -37,7 +36,15 @@ func (e *ProxyEntry) SetDefaults() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.Path == "" {
|
||||
e.Path = "/"
|
||||
if e.Host == "" {
|
||||
e.Host = "localhost"
|
||||
}
|
||||
if e.Port == "" {
|
||||
switch e.Scheme {
|
||||
case "http":
|
||||
e.Port = "80"
|
||||
case "https":
|
||||
e.Port = "443"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package model
|
||||
|
||||
type (
|
||||
ProxyProvider struct {
|
||||
Kind string `json:"kind"` // docker, file
|
||||
Value string `json:"value"`
|
||||
}
|
||||
ProxyProviders = map[string]ProxyProvider
|
||||
)
|
||||
6
src/models/proxy_providers.go
Normal file
6
src/models/proxy_providers.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type ProxyProviders struct {
|
||||
Files []string `yaml:"include" json:"include"` // docker, file
|
||||
Docker map[string]string `yaml:"docker" json:"docker"`
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
@@ -12,15 +12,15 @@ import (
|
||||
|
||||
type (
|
||||
Entry struct { // real model after validation
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
Path T.Path
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
Alias T.Alias
|
||||
Scheme T.Scheme
|
||||
Host T.Host
|
||||
Port T.Port
|
||||
URL *url.URL
|
||||
NoTLSVerify bool
|
||||
PathPatterns T.PathPatterns
|
||||
SetHeaders http.Header
|
||||
HideHeaders []string
|
||||
}
|
||||
StreamEntry struct {
|
||||
Alias T.Alias `json:"alias"`
|
||||
@@ -39,7 +39,7 @@ func NewEntry(m *M.ProxyEntry) (any, E.NestedError) {
|
||||
if scheme.IsStream() {
|
||||
return validateStreamEntry(m)
|
||||
}
|
||||
return validateEntry(m, *scheme)
|
||||
return validateEntry(m, scheme)
|
||||
}
|
||||
|
||||
func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
@@ -51,24 +51,28 @@ func validateEntry(m *M.ProxyEntry, s T.Scheme) (*Entry, E.NestedError) {
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
path, err := T.NewPath(m.Path)
|
||||
pathPatterns, err := T.NewPathPatterns(m.PathPatterns)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
url, err := E.Check(url.Parse(s.String() + "://" + host.String() + ":" + strconv.Itoa(int(port))))
|
||||
setHeaders, err := T.NewHTTPHeaders(m.SetHeaders)
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
url, err := E.Check(url.Parse(fmt.Sprintf("%s://%s:%d", s, host, port)))
|
||||
if err.IsNotNil() {
|
||||
return nil, err
|
||||
}
|
||||
return &Entry{
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
Path: path,
|
||||
SetHeaders: m.SetHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
Alias: T.NewAlias(m.Alias),
|
||||
Scheme: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
URL: url,
|
||||
NoTLSVerify: m.NoTLSVerify,
|
||||
PathPatterns: pathPatterns,
|
||||
SetHeaders: setHeaders,
|
||||
HideHeaders: m.HideHeaders,
|
||||
}, E.Nil()
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Alias struct{ F.Stringable }
|
||||
type Alias string
|
||||
type Aliases struct{ *F.Slice[Alias] }
|
||||
|
||||
func NewAlias(s string) Alias {
|
||||
return Alias{F.NewStringable(s)}
|
||||
return Alias(s)
|
||||
}
|
||||
|
||||
func NewAliases(s string) Aliases {
|
||||
|
||||
19
src/proxy/fields/headers.go
Normal file
19
src/proxy/fields/headers.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
func NewHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) {
|
||||
h := make(http.Header)
|
||||
for k, v := range headers {
|
||||
vSplit := strings.Split(v, ",")
|
||||
for _, header := range vSplit {
|
||||
h.Add(k, strings.TrimSpace(header))
|
||||
}
|
||||
}
|
||||
return h, E.Nil()
|
||||
}
|
||||
@@ -2,19 +2,11 @@ package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Host struct{ F.Stringable }
|
||||
type Host string
|
||||
type Subdomain = Alias
|
||||
|
||||
func NewHost(s string) (Host, E.NestedError) {
|
||||
return Host{F.NewStringable(s)}, E.Nil()
|
||||
}
|
||||
|
||||
func (h Host) Subdomain() (*Subdomain, E.NestedError) {
|
||||
if i := h.IndexRune(':'); i != -1 {
|
||||
return &Subdomain{h.SubStr(0, i)}, E.Nil()
|
||||
}
|
||||
return nil, E.Invalid("host", h)
|
||||
return Host(s), E.Nil()
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Path struct{ F.Stringable }
|
||||
|
||||
func NewPath(s string) (Path, E.NestedError) {
|
||||
if s == "" || s[0] == '/' {
|
||||
return Path{F.NewStringable(s)}, E.Nil()
|
||||
}
|
||||
return Path{}, E.Invalid("path", s).With("must be empty or start with '/'")
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type PathMode struct{ F.Stringable }
|
||||
type PathMode string
|
||||
|
||||
func NewPathMode(pm string) (PathMode, E.NestedError) {
|
||||
switch pm {
|
||||
case "", "forward":
|
||||
return PathMode{F.NewStringable(pm)}, E.Nil()
|
||||
return PathMode(pm), E.Nil()
|
||||
default:
|
||||
return PathMode{}, E.Invalid("path mode", pm)
|
||||
return "", E.Invalid("path mode", pm)
|
||||
}
|
||||
}
|
||||
|
||||
func (p PathMode) IsRemove() bool {
|
||||
return p.String() == ""
|
||||
return p == ""
|
||||
}
|
||||
|
||||
func (p PathMode) IsForward() bool {
|
||||
return p.String() == "forward"
|
||||
return p == "forward"
|
||||
}
|
||||
|
||||
37
src/proxy/fields/path_pattern.go
Normal file
37
src/proxy/fields/path_pattern.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type PathPattern string
|
||||
type PathPatterns = []PathPattern
|
||||
|
||||
func NewPathPattern(s string) (PathPattern, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return "", E.Invalid("path", "must not be empty")
|
||||
}
|
||||
if !pathPattern.MatchString(string(s)) {
|
||||
return "", E.Invalid("path pattern", s)
|
||||
}
|
||||
return PathPattern(s), E.Nil()
|
||||
}
|
||||
|
||||
func NewPathPatterns(s []string) (PathPatterns, E.NestedError) {
|
||||
if len(s) == 0 {
|
||||
return []PathPattern{"/"}, E.Nil()
|
||||
}
|
||||
pp := make(PathPatterns, len(s))
|
||||
for i, v := range s {
|
||||
if pattern, err := NewPathPattern(v); err.IsNotNil() {
|
||||
return nil, err
|
||||
} else {
|
||||
pp[i] = pattern
|
||||
}
|
||||
}
|
||||
return pp, E.Nil()
|
||||
}
|
||||
|
||||
var pathPattern = regexp.MustCompile("^((GET|POST|DELETE|PUT|PATCH|HEAD|OPTIONS|CONNECT)\\s)?(/\\w*)+/?$")
|
||||
@@ -11,7 +11,7 @@ type Port int
|
||||
func NewPort(v string) (Port, E.NestedError) {
|
||||
p, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return ErrPort, E.From(err)
|
||||
return ErrPort, E.Invalid("port number", v).With(err)
|
||||
}
|
||||
return NewPortInt(p)
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@ import (
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
F "github.com/yusing/go-proxy/utils/functional"
|
||||
)
|
||||
|
||||
type Scheme struct{ F.Stringable }
|
||||
type Scheme string
|
||||
|
||||
func NewScheme(s string) (*Scheme, E.NestedError) {
|
||||
func NewScheme(s string) (Scheme, E.NestedError) {
|
||||
switch s {
|
||||
case "http", "https", "tcp", "udp":
|
||||
return &Scheme{F.NewStringable(s)}, E.Nil()
|
||||
return Scheme(s), E.Nil()
|
||||
}
|
||||
return nil, E.Invalid("scheme", s)
|
||||
return "", E.Invalid("scheme", s)
|
||||
}
|
||||
|
||||
func NewSchemeFromPort(p string) (*Scheme, E.NestedError) {
|
||||
func NewSchemeFromPort(p string) (Scheme, E.NestedError) {
|
||||
var s string
|
||||
switch {
|
||||
case strings.ContainsRune(p, ':'):
|
||||
@@ -27,11 +26,11 @@ func NewSchemeFromPort(p string) (*Scheme, E.NestedError) {
|
||||
default:
|
||||
s = "http"
|
||||
}
|
||||
return &Scheme{F.NewStringable(s)}, E.Nil()
|
||||
return Scheme(s), E.Nil()
|
||||
}
|
||||
|
||||
func (s Scheme) IsHTTP() bool { return s.String() == "http" }
|
||||
func (s Scheme) IsHTTPS() bool { return s.String() == "https" }
|
||||
func (s Scheme) IsTCP() bool { return s.String() == "tcp" }
|
||||
func (s Scheme) IsUDP() bool { return s.String() == "udp" }
|
||||
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() }
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
type StreamScheme struct {
|
||||
ListeningScheme *Scheme `json:"listening"`
|
||||
ProxyScheme *Scheme `json:"proxy"`
|
||||
ListeningScheme Scheme `json:"listening"`
|
||||
ProxyScheme Scheme `json:"proxy"`
|
||||
}
|
||||
|
||||
func NewStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||
@@ -31,12 +32,12 @@ func NewStreamScheme(s string) (ss *StreamScheme, err E.NestedError) {
|
||||
}
|
||||
|
||||
func (s StreamScheme) String() string {
|
||||
return s.ListeningScheme.String() + " -> " + s.ProxyScheme.String()
|
||||
return fmt.Sprintf("%s -> %s", s.ListeningScheme, 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
|
||||
return s.ListeningScheme == s.ProxyScheme
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ type DockerProvider struct {
|
||||
dockerHost string
|
||||
}
|
||||
|
||||
func DockerProviderImpl(model *M.ProxyProvider) ProviderImpl {
|
||||
return &DockerProvider{dockerHost: model.Value}
|
||||
func DockerProviderImpl(dockerHost string) ProviderImpl {
|
||||
return &DockerProvider{dockerHost: dockerHost}
|
||||
}
|
||||
|
||||
// GetProxyEntries returns proxy entries from a docker client.
|
||||
@@ -32,15 +32,16 @@ func DockerProviderImpl(model *M.ProxyProvider) ProviderImpl {
|
||||
// - p: A pointer to the DockerProvider struct.
|
||||
//
|
||||
// Returns:
|
||||
// - P.EntryModelSlice: A slice of EntryModel structs representing the proxy entries.
|
||||
// - P.EntryModelSlice: (non-nil) A slice of EntryModel structs representing the proxy entries.
|
||||
// - error: An error object if there was an error retrieving the docker client information or parsing the labels.
|
||||
func (p DockerProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
|
||||
info, err := D.GetClientInfo(p.dockerHost)
|
||||
if err.IsNotNil() {
|
||||
return nil, E.From(err)
|
||||
return entries, E.From(err)
|
||||
}
|
||||
|
||||
entries := M.NewProxyEntries()
|
||||
errors := E.NewBuilder("errors when parse docker labels for %q", p.dockerHost)
|
||||
|
||||
for _, container := range info.Containers {
|
||||
@@ -99,8 +100,8 @@ func (p *DockerProvider) getEntriesFromLabels(container *types.Container, client
|
||||
|
||||
// init entries map for all aliases
|
||||
aliases.ForEach(func(a PT.Alias) {
|
||||
entries.Set(a.String(), &M.ProxyEntry{
|
||||
Alias: a.String(),
|
||||
entries.Set(string(a), &M.ProxyEntry{
|
||||
Alias: string(a),
|
||||
Host: clientHost,
|
||||
Port: fmt.Sprint(defaultPort),
|
||||
})
|
||||
|
||||
@@ -16,10 +16,10 @@ type FileProvider struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func FileProviderImpl(m *M.ProxyProvider) ProviderImpl {
|
||||
func FileProviderImpl(filename string) ProviderImpl {
|
||||
return &FileProvider{
|
||||
fileName: m.Value,
|
||||
path: path.Join(common.ConfigBasePath, m.Value),
|
||||
fileName: filename,
|
||||
path: path.Join(common.ConfigBasePath, filename),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,17 @@ func Validate(data []byte) E.NestedError {
|
||||
return U.ValidateYaml(U.GetSchema(common.ProvidersSchemaPath), data)
|
||||
}
|
||||
|
||||
func (p *FileProvider) String() string {
|
||||
return p.fileName
|
||||
}
|
||||
|
||||
func (p *FileProvider) GetProxyEntries() (M.ProxyEntries, E.NestedError) {
|
||||
entries := M.NewProxyEntries()
|
||||
data, err := E.Check(os.ReadFile(p.path))
|
||||
if err.IsNotNil() {
|
||||
return entries, E.Failure("read file").Subject(p.fileName).With(err)
|
||||
return entries, E.Failure("read file").Subject(p).With(err)
|
||||
}
|
||||
ne := E.Failure("validation").Subject(p.fileName)
|
||||
ne := E.Failure("validation").Subject(p)
|
||||
if !common.NoSchemaValidation {
|
||||
if err = Validate(data); err.IsNotNil() {
|
||||
return entries, ne.With(err)
|
||||
|
||||
@@ -2,9 +2,11 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
M "github.com/yusing/go-proxy/models"
|
||||
R "github.com/yusing/go-proxy/route"
|
||||
@@ -20,6 +22,7 @@ type Provider struct {
|
||||
ProviderImpl
|
||||
|
||||
name string
|
||||
t ProviderType
|
||||
routes *R.Routes
|
||||
reloadReqCh chan struct{}
|
||||
|
||||
@@ -28,29 +31,56 @@ type Provider struct {
|
||||
watcherCancel context.CancelFunc
|
||||
|
||||
l *logrus.Entry
|
||||
|
||||
cooldownCh chan struct{}
|
||||
}
|
||||
|
||||
func NewProvider(name string, model M.ProxyProvider) (p *Provider) {
|
||||
p = &Provider{
|
||||
type ProviderType string
|
||||
|
||||
const (
|
||||
ProviderTypeDocker ProviderType = "docker"
|
||||
ProviderTypeFile ProviderType = "file"
|
||||
)
|
||||
|
||||
func newProvider(name string, t ProviderType) *Provider {
|
||||
p := &Provider{
|
||||
name: name,
|
||||
t: t,
|
||||
routes: R.NewRoutes(),
|
||||
reloadReqCh: make(chan struct{}, 1),
|
||||
l: logrus.WithField("provider", name),
|
||||
}
|
||||
switch model.Kind {
|
||||
case common.ProviderKind_Docker:
|
||||
p.ProviderImpl = DockerProviderImpl(&model)
|
||||
case common.ProviderKind_File:
|
||||
p.ProviderImpl = FileProviderImpl(&model)
|
||||
cooldownCh: make(chan struct{}, 1),
|
||||
}
|
||||
p.l = logrus.WithField("provider", p)
|
||||
go p.processReloadRequests()
|
||||
return p
|
||||
}
|
||||
func NewFileProvider(filename string) *Provider {
|
||||
name := path.Base(filename)
|
||||
p := newProvider(name, ProviderTypeFile)
|
||||
p.ProviderImpl = FileProviderImpl(filename)
|
||||
p.watcher = p.NewWatcher()
|
||||
return
|
||||
return p
|
||||
}
|
||||
|
||||
func NewDockerProvider(name string, dockerHost string) *Provider {
|
||||
p := newProvider(name, ProviderTypeDocker)
|
||||
p.ProviderImpl = DockerProviderImpl(dockerHost)
|
||||
p.watcher = p.NewWatcher()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Provider) GetType() ProviderType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return fmt.Sprintf("%s: %s", p.t, p.name)
|
||||
}
|
||||
|
||||
func (p *Provider) StartAllRoutes() E.NestedError {
|
||||
err := p.loadRoutes()
|
||||
|
||||
@@ -58,60 +88,53 @@ func (p *Provider) StartAllRoutes() E.NestedError {
|
||||
p.watcherCtx, p.watcherCancel = context.WithCancel(context.Background())
|
||||
go p.watchEvents()
|
||||
|
||||
if err.IsNotNil() {
|
||||
return err
|
||||
}
|
||||
errors := E.NewBuilder("errors starting routes for provider %q", p.name)
|
||||
errors := E.NewBuilder("errors in routes")
|
||||
nStarted := 0
|
||||
nFailed := 0
|
||||
|
||||
if err.IsNotNil() {
|
||||
errors.Add(err)
|
||||
}
|
||||
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Start(); err.IsNotNil() {
|
||||
errors.Add(err.Subject(alias))
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
nStarted++
|
||||
}
|
||||
})
|
||||
if err := errors.Build(); err.IsNotNil() {
|
||||
return err
|
||||
}
|
||||
p.l.Infof("%d routes started", nStarted)
|
||||
return E.Nil()
|
||||
|
||||
p.l.Debugf("%d routes started, %d failed", nStarted, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
func (p *Provider) StopAllRoutes() E.NestedError {
|
||||
defer p.routes.Clear()
|
||||
|
||||
if p.watcherCancel != nil {
|
||||
p.watcherCancel()
|
||||
p.watcherCancel = nil
|
||||
}
|
||||
errors := E.NewBuilder("errors stopping routes for provider %q", p.name)
|
||||
nStopped := 0
|
||||
nFailed := 0
|
||||
p.routes.EachKVParallel(func(alias string, r R.Route) {
|
||||
if err := r.Stop(); err.IsNotNil() {
|
||||
errors.Add(err.Subject(alias))
|
||||
errors.Add(err.Subject(r))
|
||||
nFailed++
|
||||
} else {
|
||||
nStopped++
|
||||
}
|
||||
})
|
||||
if err := errors.Build(); err.IsNotNil() {
|
||||
return err
|
||||
}
|
||||
p.l.Infof("%d routes stopped", nStopped)
|
||||
return E.Nil()
|
||||
p.l.Debugf("%d routes stopped, %d failed", nStopped, nFailed)
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
func (p *Provider) ReloadRoutes() {
|
||||
defer p.l.Info("routes reloaded")
|
||||
|
||||
select {
|
||||
case p.reloadReqCh <- struct{}{}:
|
||||
defer func() {
|
||||
<-p.reloadReqCh
|
||||
}()
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
// Successfully sent reload request
|
||||
default:
|
||||
return
|
||||
// Reload request already in progress, ignore this request
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,46 +144,72 @@ func (p *Provider) GetCurrentRoutes() *R.Routes {
|
||||
|
||||
func (p *Provider) watchEvents() {
|
||||
events, errs := p.watcher.Events(p.watcherCtx)
|
||||
l := logrus.WithField("?", "watcher")
|
||||
l := p.l.WithField("module", "watcher")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.reloadReqCh:
|
||||
p.ReloadRoutes()
|
||||
case <-p.watcherCtx.Done():
|
||||
return
|
||||
case event, ok := <-events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
l.Infof("watcher event: %v", event)
|
||||
p.reloadReqCh <- struct{}{}
|
||||
l.Info(event)
|
||||
p.ReloadRoutes()
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err.Is(context.Canceled) {
|
||||
continue
|
||||
}
|
||||
l.Errorf("watcher error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) processReloadRequests() {
|
||||
for range p.reloadReqCh {
|
||||
// prevent busy loop caused by a container
|
||||
// repeating crashing and restarting
|
||||
select {
|
||||
case p.cooldownCh <- struct{}{}:
|
||||
p.l.Info("Starting to reload routes")
|
||||
|
||||
p.StopAllRoutes()
|
||||
p.loadRoutes()
|
||||
p.StartAllRoutes()
|
||||
|
||||
p.l.Info("Routes reloaded")
|
||||
|
||||
go func() {
|
||||
time.Sleep(reloadCooldown)
|
||||
<-p.cooldownCh
|
||||
}()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadRoutes() E.NestedError {
|
||||
entries, err := p.GetProxyEntries()
|
||||
|
||||
if err.IsNotNil() {
|
||||
p.l.Warn(err.Subjectf("provider %s", p.name))
|
||||
p.l.Warn(err.Subject(p))
|
||||
}
|
||||
p.routes = R.NewRoutes()
|
||||
|
||||
errors := E.NewBuilder("errors loading routes from provider %q", p.name)
|
||||
errors := E.NewBuilder("errors loading routes from %s", p)
|
||||
entries.EachKV(func(a string, e *M.ProxyEntry) {
|
||||
e.Alias = a
|
||||
r, err := R.NewRoute(e)
|
||||
if err.IsNotNil() {
|
||||
errors.Addf("%s: %w", a, err)
|
||||
p.l.Debugf("failed to load route: %s, %s", a, err)
|
||||
errors.Add(err.Subject(a))
|
||||
} else {
|
||||
p.routes.Set(a, r)
|
||||
}
|
||||
})
|
||||
p.l.Debugf("loaded %d routes from %d entries", p.routes.Size(), entries.Size())
|
||||
return errors.Build()
|
||||
}
|
||||
|
||||
const reloadCooldown = 300 * time.Millisecond
|
||||
|
||||
@@ -535,4 +535,4 @@ func IsPrint(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("?", "http")
|
||||
var logger = logrus.WithField("module", "http")
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package proxy
|
||||
|
||||
// import (
|
||||
// "net/http"
|
||||
// "net/url"
|
||||
// "os"
|
||||
// "reflect"
|
||||
// "testing"
|
||||
// "time"
|
||||
// )
|
||||
|
||||
// var proxy Entry
|
||||
// var proxyUrl, _ = url.Parse("http://127.0.0.1:8181")
|
||||
// var proxyServer = NewServer(ServerOptions{
|
||||
// Name: "proxy",
|
||||
// HTTPAddr: ":8080",
|
||||
// Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// NewReverseProxy(proxyUrl, &http.Transport{}, &proxy).ServeHTTP(w, r)
|
||||
// }),
|
||||
// })
|
||||
|
||||
// var testServer = NewServer(ServerOptions{
|
||||
// Name: "test",
|
||||
// HTTPAddr: ":8181",
|
||||
// Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// h := r.Header
|
||||
// for k, vv := range h {
|
||||
// for _, v := range vv {
|
||||
// w.Header().Add(k, v)
|
||||
// }
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// }),
|
||||
// })
|
||||
|
||||
// var httpClient = http.DefaultClient
|
||||
|
||||
// func TestMain(m *testing.M) {
|
||||
// proxyServer.Start()
|
||||
// testServer.Start()
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// code := m.Run()
|
||||
// proxyServer.Stop()
|
||||
// testServer.Stop()
|
||||
// os.Exit(code)
|
||||
// }
|
||||
|
||||
// func TestSetHeader(t *testing.T) {
|
||||
// hWant := http.Header{"X-Test": []string{"foo", "bar"}, "X-Test2": []string{"baz"}}
|
||||
// proxy = Entry{
|
||||
// Alias: "test",
|
||||
// Scheme: "http",
|
||||
// Host: "127.0.0.1",
|
||||
// Port: "8181",
|
||||
// SetHeaders: hWant,
|
||||
// }
|
||||
// req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// resp, err := httpClient.Do(req)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// hGot := resp.Header
|
||||
// t.Log("headers: ", hGot)
|
||||
// for k, v := range hWant {
|
||||
// if !reflect.DeepEqual(hGot[k], v) {
|
||||
// t.Errorf("header %s: expected %v, got %v", k, v, hGot[k])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestHideHeader(t *testing.T) {
|
||||
// hHide := []string{"X-Test", "X-Test2"}
|
||||
// proxy = Entry{
|
||||
// Alias: "test",
|
||||
// Scheme: "http",
|
||||
// Host: "127.0.0.1",
|
||||
// Port: "8181",
|
||||
// HideHeaders: hHide,
|
||||
// }
|
||||
// req, err := http.NewRequest("HEAD", "http://127.0.0.1:8080", nil)
|
||||
// for _, k := range hHide {
|
||||
// req.Header.Set(k, "foo")
|
||||
// }
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// resp, err := httpClient.Do(req)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// hGot := resp.Header
|
||||
// t.Log("headers: ", hGot)
|
||||
// for _, v := range hHide {
|
||||
// _, ok := hGot[v]
|
||||
// if ok {
|
||||
// t.Errorf("header %s: expected hidden, got %v", v, hGot[v])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -19,25 +19,17 @@ import (
|
||||
|
||||
type (
|
||||
HTTPRoute struct {
|
||||
Alias PT.Alias `json:"alias"`
|
||||
Subroutes HTTPSubroutes `json:"subroutes"`
|
||||
Alias PT.Alias `json:"alias"`
|
||||
TargetURL *URL `json:"target_url"`
|
||||
PathPatterns PT.PathPatterns `json:"path_patterns"`
|
||||
|
||||
mux *http.ServeMux
|
||||
mux *http.ServeMux
|
||||
handler *P.ReverseProxy
|
||||
}
|
||||
|
||||
HTTPSubroute struct {
|
||||
TargetURL URL `json:"targetURL"`
|
||||
Path PathKey `json:"path"`
|
||||
|
||||
proxy *P.ReverseProxy
|
||||
}
|
||||
|
||||
URL struct {
|
||||
*url.URL
|
||||
}
|
||||
PathKey = string
|
||||
SubdomainKey = string
|
||||
HTTPSubroutes = map[PathKey]HTTPSubroute
|
||||
URL url.URL
|
||||
PathKey = PT.PathPattern
|
||||
SubdomainKey = PT.Alias
|
||||
)
|
||||
|
||||
var httpRoutes = F.NewMap[SubdomainKey, *HTTPRoute]()
|
||||
@@ -53,38 +45,18 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||
rp := P.NewReverseProxy(entry.URL, tr, entry)
|
||||
|
||||
httpRoutes.Lock()
|
||||
defer httpRoutes.Unlock()
|
||||
|
||||
var r *HTTPRoute
|
||||
r, ok := httpRoutes.UnsafeGet(entry.Alias.String())
|
||||
r, ok := httpRoutes.UnsafeGet(entry.Alias)
|
||||
if !ok {
|
||||
r = &HTTPRoute{
|
||||
Alias: entry.Alias,
|
||||
Subroutes: make(HTTPSubroutes),
|
||||
mux: http.NewServeMux(),
|
||||
Alias: entry.Alias,
|
||||
TargetURL: (*URL)(entry.URL),
|
||||
PathPatterns: entry.PathPatterns,
|
||||
handler: rp,
|
||||
}
|
||||
httpRoutes.UnsafeSet(entry.Alias.String(), r)
|
||||
}
|
||||
|
||||
path := entry.Path.String()
|
||||
if _, exists := r.Subroutes[path]; exists {
|
||||
httpRoutes.Unlock()
|
||||
return nil, E.Duplicated("path", path).Subject(entry.Alias)
|
||||
}
|
||||
r.mux.HandleFunc(path, rp.ServeHTTP)
|
||||
if err := recover(); err != nil {
|
||||
httpRoutes.Unlock()
|
||||
switch t := err.(type) {
|
||||
case error:
|
||||
// NOTE: likely path pattern error
|
||||
return nil, E.From(t).Subject(entry.Alias)
|
||||
default:
|
||||
return nil, E.From(fmt.Errorf("%v", t)).Subject(entry.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
sr := HTTPSubroute{
|
||||
TargetURL: URL{entry.URL},
|
||||
proxy: rp,
|
||||
Path: path,
|
||||
httpRoutes.UnsafeSet(entry.Alias, r)
|
||||
}
|
||||
|
||||
rewrite := rp.Rewrite
|
||||
@@ -92,36 +64,42 @@ func NewHTTPRoute(entry *P.Entry) (*HTTPRoute, E.NestedError) {
|
||||
if logrus.GetLevel() == logrus.DebugLevel {
|
||||
l := logrus.WithField("alias", entry.Alias)
|
||||
|
||||
sr.proxy.Rewrite = func(pr *P.ProxyRequest) {
|
||||
rp.Rewrite = func(pr *P.ProxyRequest) {
|
||||
l.Debug("request URL: ", pr.In.Host, pr.In.URL.Path)
|
||||
l.Debug("request headers: ", pr.In.Header)
|
||||
rewrite(pr)
|
||||
}
|
||||
} else {
|
||||
sr.proxy.Rewrite = rewrite
|
||||
rp.Rewrite = rewrite
|
||||
}
|
||||
|
||||
r.Subroutes[path] = sr
|
||||
httpRoutes.Unlock()
|
||||
return r, E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) String() string {
|
||||
return string(r.Alias)
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Start() E.NestedError {
|
||||
httpRoutes.Set(r.Alias.String(), r)
|
||||
r.mux = http.NewServeMux()
|
||||
for _, p := range r.PathPatterns {
|
||||
r.mux.HandleFunc(string(p), r.handler.ServeHTTP)
|
||||
}
|
||||
httpRoutes.Set(r.Alias, r)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) Stop() E.NestedError {
|
||||
httpRoutes.Delete(r.Alias.String())
|
||||
r.mux = nil
|
||||
httpRoutes.Delete(r.Alias)
|
||||
return E.Nil()
|
||||
}
|
||||
|
||||
func (r *HTTPRoute) GetSubroute(path PathKey) (HTTPSubroute, bool) {
|
||||
sr, ok := r.Subroutes[path]
|
||||
return sr, ok
|
||||
func (u *URL) String() string {
|
||||
return (*url.URL)(u).String()
|
||||
}
|
||||
|
||||
func (u URL) MarshalText() (text []byte, err error) {
|
||||
func (u *URL) MarshalText() (text []byte, err error) {
|
||||
return []byte(u.String()), nil
|
||||
}
|
||||
|
||||
@@ -140,7 +118,7 @@ func ProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func findMux(host string, path PathKey) (*http.ServeMux, error) {
|
||||
sd := strings.Split(host, ".")[0]
|
||||
if r, ok := httpRoutes.UnsafeGet(sd); ok {
|
||||
if r, ok := httpRoutes.UnsafeGet(PT.Alias(sd)); ok {
|
||||
return r.mux, nil
|
||||
}
|
||||
return nil, E.NotExists("route", fmt.Sprintf("subdomain: %s, path: %s", sd, path))
|
||||
|
||||
@@ -11,6 +11,7 @@ type (
|
||||
Route interface {
|
||||
Start() E.NestedError
|
||||
Stop() E.NestedError
|
||||
String() string
|
||||
}
|
||||
Routes = F.Map[string, Route]
|
||||
)
|
||||
|
||||
@@ -39,16 +39,20 @@ func NewStreamRoute(entry *P.StreamEntry) (*StreamRoute, E.NestedError) {
|
||||
wg: sync.WaitGroup{},
|
||||
stopCh: make(chan struct{}, 1),
|
||||
connCh: make(chan any),
|
||||
l: logger.WithField("alias", entry.Alias),
|
||||
}
|
||||
if entry.Scheme.ListeningScheme.IsTCP() {
|
||||
base.StreamImpl = NewTCPRoute(base)
|
||||
} else {
|
||||
base.StreamImpl = NewUDPRoute(base)
|
||||
}
|
||||
base.l = logrus.WithField("route", base.StreamImpl)
|
||||
return base, E.Nil()
|
||||
}
|
||||
|
||||
func (r *StreamRoute) String() string {
|
||||
return fmt.Sprintf("%s-stream: %s", r.Scheme, r.Alias)
|
||||
}
|
||||
|
||||
func (r *StreamRoute) Start() E.NestedError {
|
||||
if r.started.Load() {
|
||||
return E.Invalid("state", "already started")
|
||||
@@ -127,5 +131,3 @@ func (r *StreamRoute) grHandleConnections() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("?", "stream")
|
||||
|
||||
@@ -53,7 +53,7 @@ func (route *TCPRoute) Handle(c interface{}) error {
|
||||
serverAddr := fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort)
|
||||
dialer := &net.Dialer{}
|
||||
|
||||
serverConn, err := dialer.DialContext(ctx, route.Scheme.ProxyScheme.String(), serverAddr)
|
||||
serverConn, err := dialer.DialContext(ctx, string(route.Scheme.ProxyScheme), serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -36,15 +36,15 @@ func NewUDPRoute(base *StreamRoute) StreamImpl {
|
||||
}
|
||||
|
||||
func (route *UDPRoute) Setup() error {
|
||||
laddr, err := net.ResolveUDPAddr(route.Scheme.ListeningScheme.String(), fmt.Sprintf(":%v", route.Port.ProxyPort))
|
||||
laddr, err := net.ResolveUDPAddr(string(route.Scheme.ListeningScheme), fmt.Sprintf(":%v", route.Port.ProxyPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := net.ListenUDP(route.Scheme.ListeningScheme.String(), laddr)
|
||||
source, err := net.ListenUDP(string(route.Scheme.ListeningScheme), laddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raddr, err := net.ResolveUDPAddr(route.Scheme.ProxyScheme.String(), fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort))
|
||||
raddr, err := net.ResolveUDPAddr(string(route.Scheme.ProxyScheme), fmt.Sprintf("%s:%v", route.Host, route.Port.ProxyPort))
|
||||
if err != nil {
|
||||
source.Close()
|
||||
return err
|
||||
|
||||
@@ -158,4 +158,4 @@ func redirectToTLSHandler(port string) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
var logger = logrus.WithField("?", "server")
|
||||
var logger = logrus.WithField("module", "server")
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package functional
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Stringable struct{ string }
|
||||
|
||||
func NewStringable(v any) Stringable {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
return Stringable{vv}
|
||||
case fmt.Stringer:
|
||||
return Stringable{vv.String()}
|
||||
default:
|
||||
return Stringable{fmt.Sprint(vv)}
|
||||
}
|
||||
}
|
||||
|
||||
func (s Stringable) String() string {
|
||||
return s.string
|
||||
}
|
||||
|
||||
func (s Stringable) Len() int {
|
||||
return len(s.string)
|
||||
}
|
||||
|
||||
func (s Stringable) MarshalText() (text []byte, err error) {
|
||||
return []byte(s.string), nil
|
||||
}
|
||||
|
||||
func (s Stringable) SubStr(start int, end int) Stringable {
|
||||
return Stringable{s.string[start:end]}
|
||||
}
|
||||
|
||||
func (s Stringable) HasPrefix(p Stringable) bool {
|
||||
return len(s.string) >= len(p.string) && s.string[0:len(p.string)] == p.string
|
||||
}
|
||||
|
||||
func (s Stringable) HasSuffix(p Stringable) bool {
|
||||
return len(s.string) >= len(p.string) && s.string[len(s.string)-len(p.string):] == p.string
|
||||
}
|
||||
|
||||
func (s Stringable) IsEmpty() bool {
|
||||
return len(s.string) == 0
|
||||
}
|
||||
|
||||
func (s Stringable) IndexRune(r rune) int {
|
||||
return strings.IndexRune(s.string, r)
|
||||
}
|
||||
|
||||
func (s Stringable) ToInt() (int, error) {
|
||||
return strconv.Atoi(s.string)
|
||||
}
|
||||
|
||||
func (s Stringable) Split(sep string) []Stringable {
|
||||
return Stringables(strings.Split(s.string, sep))
|
||||
}
|
||||
|
||||
func Stringables(ss []string) []Stringable {
|
||||
ret := make([]Stringable, len(ss))
|
||||
for i, s := range ss {
|
||||
ret[i] = Stringable{s}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
D "github.com/yusing/go-proxy/docker"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
@@ -30,14 +31,14 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
var err E.NestedError
|
||||
for range 3 {
|
||||
cl, err = D.ConnectClient(w.host)
|
||||
if err.IsNotNil() {
|
||||
if err.IsNil() {
|
||||
break
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
if err.IsNotNil() {
|
||||
errCh <- E.Failure("connect to docker")
|
||||
errCh <- E.Failure("connecting to docker")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,20 +48,28 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
errCh <- E.From(<-cErrCh)
|
||||
if err := <-cErrCh; err != nil {
|
||||
errCh <- E.From(err)
|
||||
}
|
||||
return
|
||||
case msg := <-cEventCh:
|
||||
containerName, ok := msg.Actor.Attributes["name"]
|
||||
if !ok {
|
||||
// NOTE: should not happen
|
||||
// but if it happens, just ignore it
|
||||
continue
|
||||
var Action Action
|
||||
switch msg.Action {
|
||||
case events.ActionStart:
|
||||
Action = ActionCreated
|
||||
case events.ActionDie:
|
||||
Action = ActionDeleted
|
||||
default: // NOTE: should not happen
|
||||
Action = ActionModified
|
||||
}
|
||||
eventCh <- Event{
|
||||
ActorName: containerName,
|
||||
Action: ActionModified,
|
||||
ActorName: fmt.Sprintf("container %q", msg.Actor.Attributes["name"]),
|
||||
Action: Action,
|
||||
}
|
||||
case err := <-cErrCh:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -79,8 +88,8 @@ func (w *DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nest
|
||||
return eventCh, errCh
|
||||
}
|
||||
|
||||
var dwOptions = types.EventsOptions{Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("event", "start"),
|
||||
filters.Arg("event", "die"), // 'stop' already triggering 'die'
|
||||
var dwOptions = events.ListOptions{Filters: filters.NewArgs(
|
||||
filters.Arg("type", string(events.ContainerEventType)),
|
||||
filters.Arg("event", string(events.ActionStart)),
|
||||
filters.Arg("event", string(events.ActionDie)), // 'stop' already triggering 'die'
|
||||
)}
|
||||
|
||||
@@ -12,22 +12,15 @@ type (
|
||||
|
||||
const (
|
||||
ActionModified Action = "MODIFIED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
ActionCreated Action = "CREATED"
|
||||
ActionStarted Action = "STARTED"
|
||||
ActionDeleted Action = "DELETED"
|
||||
)
|
||||
|
||||
func (e *Event) String() string {
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%s %s", e.ActorName, e.Action)
|
||||
}
|
||||
|
||||
func (a Action) IsDelete() bool {
|
||||
return a == ActionDeleted
|
||||
}
|
||||
|
||||
func (a Action) IsModify() bool {
|
||||
return a == ActionModified
|
||||
}
|
||||
|
||||
func (a Action) IsCreate() bool {
|
||||
return a == ActionCreated
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"path"
|
||||
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
@@ -22,4 +23,4 @@ func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Nested
|
||||
return fwHelper.Add(ctx, f)
|
||||
}
|
||||
|
||||
var fwHelper = newFileWatcherHelper()
|
||||
var fwHelper = newFileWatcherHelper(common.ConfigBasePath)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/yusing/go-proxy/common"
|
||||
E "github.com/yusing/go-proxy/error"
|
||||
)
|
||||
|
||||
@@ -26,14 +25,12 @@ type fileWatcherStream struct {
|
||||
errCh chan E.NestedError
|
||||
}
|
||||
|
||||
func newFileWatcherHelper() *fileWatcherHelper {
|
||||
func newFileWatcherHelper(dirPath string) *fileWatcherHelper {
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
// watch config path for all changes
|
||||
err = w.Add(common.ConfigBasePath)
|
||||
if err != nil {
|
||||
if err = w.Add(dirPath); err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
helper := &fileWatcherHelper{
|
||||
@@ -60,33 +57,24 @@ func (h *fileWatcherHelper) Add(ctx context.Context, w *fileWatcher) (<-chan Eve
|
||||
errCh: make(chan E.NestedError),
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
h.Remove(w)
|
||||
return
|
||||
case <-s.stopped:
|
||||
return
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.stopped <- struct{}{}
|
||||
case <-s.stopped:
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
close(s.eventCh)
|
||||
close(s.errCh)
|
||||
delete(h.m, w.filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
h.m[w.filename] = s
|
||||
return s.eventCh, s.errCh
|
||||
}
|
||||
|
||||
func (h *fileWatcherHelper) Remove(w *fileWatcher) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.m[w.filename].stopped <- struct{}{}
|
||||
delete(h.m, w.filename)
|
||||
}
|
||||
|
||||
// deinit closes the fs watcher
|
||||
// and waits for the start() loop to finish
|
||||
func (h *fileWatcherHelper) close() {
|
||||
_ = h.w.Close()
|
||||
h.wg.Wait() // wait for `start()` loop to finish
|
||||
}
|
||||
|
||||
func (h *fileWatcherHelper) start() {
|
||||
defer h.wg.Done()
|
||||
|
||||
@@ -129,4 +117,4 @@ func (h *fileWatcherHelper) start() {
|
||||
}
|
||||
}
|
||||
|
||||
var fsLogger = logrus.WithField("?", "fsnotify")
|
||||
var fsLogger = logrus.WithField("module", "fsnotify")
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.5.0-beta2
|
||||
0.5.0-rc2
|
||||
Reference in New Issue
Block a user