mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-19 20:32:35 +02:00
added load balance support and verbose level
This commit is contained in:
parent
a5c53a4f4f
commit
2f439233ed
25 changed files with 530 additions and 240 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
compose.yml
|
||||
go-proxy.yml
|
||||
go-proxy.yml
|
||||
bin/go-proxy.bak
|
||||
logs/
|
10
Dockerfile
Executable file → Normal file
10
Dockerfile
Executable file → Normal file
|
@ -2,14 +2,18 @@ FROM alpine:latest
|
|||
|
||||
LABEL maintainer="yusing@6uo.me"
|
||||
|
||||
COPY bin/go-proxy /usr/bin
|
||||
RUN apk add --no-cache bash
|
||||
RUN mkdir /app
|
||||
COPY bin/go-proxy entrypoint.sh /app/
|
||||
COPY templates/ /app/templates
|
||||
|
||||
RUN chmod +rx /usr/bin/go-proxy
|
||||
RUN chmod +x /app/go-proxy /app/entrypoint.sh
|
||||
ENV DOCKER_HOST unix:///var/run/docker.sock
|
||||
ENV VERBOSITY=1
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 8443
|
||||
|
||||
CMD ["go-proxy"]
|
||||
WORKDIR /app
|
||||
ENTRYPOINT /app/entrypoint.sh
|
18
Makefile
Normal file → Executable file
18
Makefile
Normal file → Executable file
|
@ -1,6 +1,6 @@
|
|||
.PHONY: build up restart logs get test-udp-container
|
||||
.PHONY: all build up quick-restart restart logs get udp-server
|
||||
|
||||
all: build up logs
|
||||
all: build quick-restart logs
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
|
@ -9,12 +9,18 @@ build:
|
|||
up:
|
||||
docker compose up -d --build go-proxy
|
||||
|
||||
quick-restart: # quick restart without restarting the container
|
||||
docker cp bin/go-proxy go-proxy:/app/go-proxy
|
||||
docker cp templates/* go-proxy:/app/templates
|
||||
docker cp entrypoint.sh go-proxy:/app/entrypoint.sh
|
||||
docker exec -d go-proxy bash -c "/app/entrypoint.sh restart"
|
||||
|
||||
restart:
|
||||
docker compose down -t 0
|
||||
docker compose up -d
|
||||
docker kill go-proxy
|
||||
docker compose up -d go-proxy
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
docker logs -f go-proxy
|
||||
|
||||
get:
|
||||
go get -d -u ./src/go-proxy
|
||||
|
@ -26,4 +32,4 @@ udp-server:
|
|||
--label proxy.test-udp.port=20003:9999 \
|
||||
--network data_default \
|
||||
--name test-udp \
|
||||
$$(docker build -q -f udp-test-server.Dockerfile .)
|
||||
$$(docker build -q -f udp-test-server.Dockerfile .)
|
||||
|
|
38
README.md
38
README.md
|
@ -13,6 +13,7 @@ In the examples domain `x.y.z` is used, replace them with your domain
|
|||
- [Single Port Configuration](#single-port-configuration-example)
|
||||
- [Multiple Ports Configuration](#multiple-ports-configuration-example)
|
||||
- [TCP/UDP Configuration](#tcpudp-configuration-example)
|
||||
- [Load balancing Configuration](#load-balancing-configuration-example)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Memory usage](#memory-usage)
|
||||
|
@ -25,6 +26,7 @@ In the examples domain `x.y.z` is used, replace them with your domain
|
|||
- path matching
|
||||
- HTTP proxy
|
||||
- TCP/UDP Proxy (experimental, unable to release port on hot-reload)
|
||||
- HTTP round robin load balance support (same subdomain and path across containers replicas)
|
||||
- Auto hot-reload when container start / die / stop.
|
||||
- Simple panel to see all reverse proxies and health (visit port [panel port] of go-proxy `https://*.y.z:[panel port]`)
|
||||
|
||||
|
@ -62,6 +64,11 @@ In the examples domain `x.y.z` is used, replace them with your domain
|
|||
|
||||
8. check the logs with `docker compose logs` or `make logs` to see if there is any error, check panel at [panel port] for active proxies
|
||||
|
||||
## Known issues
|
||||
|
||||
- When a container has replicas, you have to specify `proxy.<alias>.host` to the container_name
|
||||
- UDP proxy does not work properly
|
||||
|
||||
## Configuration
|
||||
|
||||
With container name, no label needs to be added.
|
||||
|
@ -81,6 +88,19 @@ However, there are some labels you can manipulate with:
|
|||
- `targetPort` must be a number, or the predefined names (see [stream.go](src/go-proxy/stream.go#L28))
|
||||
- `proxy.<alias>.path`: path matching (for http proxy only)
|
||||
- defaults to empty
|
||||
- `proxy.<alias>.path_mode`: mode for path handling
|
||||
- defaults to empty
|
||||
- allowed: \<empty>, forward, sub
|
||||
- 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
|
||||
- sub: remove path prefix from both URL and HTML attributes (`src`, `href` and `action`)
|
||||
|
||||
- `proxy.<alias>.load_balance`: enable load balance
|
||||
- allowed: `1`, `true`
|
||||
|
||||
### Single port configuration example
|
||||
|
||||
|
@ -109,9 +129,9 @@ minio:
|
|||
container_name: minio
|
||||
...
|
||||
labels:
|
||||
proxy.aliases: minio,minio-console
|
||||
proxy.minio.port: 9000
|
||||
proxy.minio-console.port: 9001
|
||||
- proxy.aliases=minio,minio-console
|
||||
- proxy.minio.port=9000
|
||||
- proxy.minio-console.port=9001
|
||||
|
||||
# visit https://minio.y.z to access minio
|
||||
# visit https://minio-console.y.z/whoami to access minio console
|
||||
|
@ -144,6 +164,18 @@ go-proxy:
|
|||
# access app-db via <*>.y.z:20000
|
||||
```
|
||||
|
||||
## Load balancing Configuration Example
|
||||
|
||||
```yaml
|
||||
nginx:
|
||||
...
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
labels:
|
||||
- proxy.nginx.load_balance=1 # allowed: [1, true]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Q: How to fix when it shows "no matching route for subdomain \<subdomain>"?
|
||||
|
|
BIN
bin/go-proxy
BIN
bin/go-proxy
Binary file not shown.
|
@ -3,9 +3,13 @@ services:
|
|||
app:
|
||||
build: .
|
||||
container_name: go-proxy
|
||||
hostname: go-proxy # set hostname to prevent adding itself to proxy list
|
||||
restart: always
|
||||
networks: # also add here
|
||||
networks: # ^also add here
|
||||
- default
|
||||
environment:
|
||||
- VERBOSITY=1 # LOG LEVEL (optional, defaults to 1)
|
||||
- DEBUG=1 # (optional enable only for debug)
|
||||
ports:
|
||||
- 80:80 # http
|
||||
- 443:443 # https
|
||||
|
@ -15,14 +19,15 @@ services:
|
|||
volumes:
|
||||
- /path/to/cert.pem:/certs/cert.crt:ro
|
||||
- /path/to/privkey.pem:/certs/priv.key:ro
|
||||
- ./go-proxy/logs:/app/log # path to logs
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
- host.docker.internal:host-gateway # required if you have containers in `host` network_mode
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-file: '1'
|
||||
max-size: 128k
|
||||
networks: # you may add other external networks
|
||||
networks: # ^you may add other external networks
|
||||
default:
|
||||
driver: bridge
|
12
entrypoint.sh
Normal file
12
entrypoint.sh
Normal file
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
if [ "$1" == "restart" ]; then
|
||||
killall go-proxy
|
||||
fi
|
||||
if [ "$DEBUG" == "1" ]; then
|
||||
/app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 &
|
||||
if [ "$1" != "restart" ]; then
|
||||
tail -f /dev/null
|
||||
fi
|
||||
else
|
||||
/app/go-proxy -v=$VERBOSITY -log_dir=log --stderrthreshold=0 &
|
||||
fi
|
15
go.mod
15
go.mod
|
@ -2,10 +2,9 @@ module github.com/yusing/go-proxy
|
|||
|
||||
go 1.21.7
|
||||
|
||||
require (
|
||||
github.com/docker/docker v25.0.3+incompatible
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
require github.com/docker/docker v25.0.3+incompatible
|
||||
|
||||
require github.com/golang/glog v1.2.0
|
||||
|
||||
require (
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
|
@ -13,9 +12,9 @@ require (
|
|||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
golang.org/x/mod v0.15.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
|
@ -35,6 +34,6 @@ require (
|
|||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
)
|
||||
|
|
10
go.sum
10
go.sum
|
@ -25,6 +25,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
|
||||
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
|
@ -74,12 +76,16 @@ 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.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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=
|
||||
|
@ -90,6 +96,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
|
@ -102,6 +110,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
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=
|
||||
|
|
57
src/go-proxy/constants.go
Normal file
57
src/go-proxy/constants.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
ImageNamePortMap = map[string]string{
|
||||
"postgres": "5432",
|
||||
"mysql": "3306",
|
||||
"mariadb": "3306",
|
||||
"redis": "6379",
|
||||
"mssql": "1433",
|
||||
"memcached": "11211",
|
||||
"rabbitmq": "5672",
|
||||
"mongo": "27017",
|
||||
}
|
||||
ExtraNamePortMap = map[string]string{
|
||||
"dns": "53",
|
||||
"ssh": "22",
|
||||
"ftp": "21",
|
||||
"smtp": "25",
|
||||
"pop3": "110",
|
||||
"imap": "143",
|
||||
}
|
||||
NamePortMap = func() map[string]string {
|
||||
m := make(map[string]string)
|
||||
for k, v := range ImageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range ExtraNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return m
|
||||
}()
|
||||
)
|
||||
|
||||
var (
|
||||
StreamSchemes = []string{TCPStreamType, UDPStreamType} // TODO: support "tcp:udp", "udp:tcp"
|
||||
HTTPSchemes = []string{"http", "https"}
|
||||
ValidSchemes = append(StreamSchemes, HTTPSchemes...)
|
||||
)
|
||||
|
||||
const (
|
||||
UDPStreamType = "udp"
|
||||
TCPStreamType = "tcp"
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyPathMode_Forward = "forward"
|
||||
ProxyPathMode_Sub = "sub" // TODO: implement
|
||||
ProxyPathMode_RemovedPath = ""
|
||||
)
|
||||
|
||||
const StreamStopListenTimeout = 1 * time.Second
|
||||
|
||||
const templateFile = "/app/templates/panel.html"
|
||||
|
||||
const udpBufferSize = 1500
|
74
src/go-proxy/docker.go
Normal file → Executable file
74
src/go-proxy/docker.go
Normal file → Executable file
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
@ -12,28 +11,10 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type ProxyConfig struct {
|
||||
id string
|
||||
Alias string
|
||||
Scheme string
|
||||
Host string
|
||||
Port string
|
||||
Path string // http proxy only
|
||||
}
|
||||
|
||||
func NewProxyConfig() ProxyConfig {
|
||||
return ProxyConfig{}
|
||||
}
|
||||
|
||||
func (cfg *ProxyConfig) UpdateId() {
|
||||
cfg.id = fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
|
||||
}
|
||||
|
||||
var dockerClient *client.Client
|
||||
|
||||
func buildContainerRoute(container types.Container) {
|
||||
|
@ -54,8 +35,12 @@ func buildContainerRoute(container types.Container) {
|
|||
for label, value := range container.Labels {
|
||||
if strings.HasPrefix(label, prefix) {
|
||||
field := strings.TrimPrefix(label, prefix)
|
||||
field = cases.Title(language.Und, cases.NoLower).String(field)
|
||||
field = utils.snakeToCamel(field)
|
||||
prop := reflect.ValueOf(&config).Elem().FieldByName(field)
|
||||
if prop.Kind() == 0 {
|
||||
glog.Infof("[Build] %s: ignoring unknown field %s", alias, field)
|
||||
continue
|
||||
}
|
||||
prop.Set(reflect.ValueOf(value))
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +61,7 @@ func buildContainerRoute(container types.Container) {
|
|||
}
|
||||
if config.Port == "" {
|
||||
// no ports exposed or specified
|
||||
glog.Infof("[Build] %s has no port exposed", alias)
|
||||
return
|
||||
}
|
||||
if config.Scheme == "" {
|
||||
|
@ -87,7 +73,7 @@ func buildContainerRoute(container types.Container) {
|
|||
imageSplit := strings.Split(container.Image, "/")
|
||||
imageSplit = strings.Split(imageSplit[len(imageSplit)-1], ":")
|
||||
imageName := imageSplit[0]
|
||||
_, isKnownImage := imageNamePortMap[imageName]
|
||||
_, isKnownImage := ImageNamePortMap[imageName]
|
||||
if isKnownImage {
|
||||
config.Scheme = "tcp"
|
||||
} else {
|
||||
|
@ -96,22 +82,37 @@ func buildContainerRoute(container types.Container) {
|
|||
}
|
||||
}
|
||||
if !isValidScheme(config.Scheme) {
|
||||
log.Printf("%s: unsupported scheme: %s, using http", container_name, config.Scheme)
|
||||
glog.Infof("%s: unsupported scheme: %s, using http", container_name, config.Scheme)
|
||||
config.Scheme = "http"
|
||||
}
|
||||
if config.Host == "" {
|
||||
if container.HostConfig.NetworkMode != "host" {
|
||||
config.Host = container_name
|
||||
} else {
|
||||
switch {
|
||||
case container.HostConfig.NetworkMode == "host":
|
||||
config.Host = "host.docker.internal"
|
||||
case config.LoadBalance == "true":
|
||||
case config.LoadBalance == "1":
|
||||
for _, network := range container.NetworkSettings.Networks {
|
||||
config.Host = network.IPAddress
|
||||
break
|
||||
}
|
||||
default:
|
||||
for _, network := range container.NetworkSettings.Networks {
|
||||
for _, alias := range network.Aliases {
|
||||
config.Host = alias
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.Host == "" {
|
||||
config.Host = container_name
|
||||
}
|
||||
config.Alias = alias
|
||||
config.UpdateId()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
createRoute(&config)
|
||||
CreateRoute(&config)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
@ -119,10 +120,10 @@ func buildContainerRoute(container types.Container) {
|
|||
}
|
||||
|
||||
func buildRoutes() {
|
||||
initRoutes()
|
||||
InitRoutes()
|
||||
containerSlice, err := dockerClient.ContainerList(context.Background(), container.ListOptions{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
glog.Fatal(err)
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
|
@ -130,22 +131,9 @@ func buildRoutes() {
|
|||
}
|
||||
for _, container := range containerSlice {
|
||||
if container.Names[0] == hostname { // skip self
|
||||
glog.Infof("[Build] Skipping %s", container.Names[0])
|
||||
continue
|
||||
}
|
||||
buildContainerRoute(container)
|
||||
}
|
||||
}
|
||||
|
||||
func findHTTPRoute(host string, path string) (*HTTPRoute, error) {
|
||||
subdomain := strings.Split(host, ".")[0]
|
||||
routeMap, ok := routes.HTTPRoutes.TryGet(subdomain)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no matching route for subdomain %s", subdomain)
|
||||
}
|
||||
for _, route := range routeMap {
|
||||
if strings.HasPrefix(path, route.Path) {
|
||||
return &route, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no matching route for path %s for subdomain %s", path, subdomain)
|
||||
}
|
||||
|
|
22
src/go-proxy/http_lbpool.go
Executable file
22
src/go-proxy/http_lbpool.go
Executable file
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
type httpLoadBalancePool struct {
|
||||
pool []*HTTPRoute
|
||||
curentIndex atomic.Int32
|
||||
}
|
||||
|
||||
func NewHTTPLoadBalancePool() *httpLoadBalancePool {
|
||||
return &httpLoadBalancePool{
|
||||
pool: make([]*HTTPRoute, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *httpLoadBalancePool) Add(route *HTTPRoute) {
|
||||
p.pool = append(p.pool, route)
|
||||
}
|
||||
|
||||
func (p *httpLoadBalancePool) Iterator() []*HTTPRoute {
|
||||
return p.pool
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HTTPRoute struct {
|
||||
Url *url.URL
|
||||
Path string
|
||||
Proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
// TODO: default + per proxy
|
||||
var transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
|
||||
func NewHTTPRoute(Url *url.URL, Path string) HTTPRoute {
|
||||
proxy := httputil.NewSingleHostReverseProxy(Url)
|
||||
proxy.Transport = transport
|
||||
return HTTPRoute{Url: Url, Path: Path, Proxy: proxy}
|
||||
}
|
||||
|
||||
func redirectToTLS(w http.ResponseWriter, r *http.Request) {
|
||||
// Redirect to the same host but with HTTPS
|
||||
log.Printf("[Redirect] redirecting to https")
|
||||
var redirectCode int
|
||||
if r.Method == http.MethodGet {
|
||||
redirectCode = http.StatusMovedPermanently
|
||||
} else {
|
||||
redirectCode = http.StatusPermanentRedirect
|
||||
}
|
||||
http.Redirect(w, r, fmt.Sprintf("https://%s%s?%s", r.Host, r.URL.Path, r.URL.RawQuery), redirectCode)
|
||||
}
|
||||
|
||||
func httpProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
route, err := findHTTPRoute(r.Host, r.URL.Path)
|
||||
if err != nil {
|
||||
log.Printf("[Request] failed %s %s%s, error: %v",
|
||||
r.Method,
|
||||
r.Host,
|
||||
r.URL.Path,
|
||||
err,
|
||||
)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
route.Proxy.ServeHTTP(w, r)
|
||||
}
|
160
src/go-proxy/http_route.go
Executable file
160
src/go-proxy/http_route.go
Executable file
|
@ -0,0 +1,160 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type HTTPRoute struct {
|
||||
Url *url.URL
|
||||
Path string
|
||||
PathMode string
|
||||
Proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
func isValidProxyPathMode(mode string) bool {
|
||||
switch mode {
|
||||
case ProxyPathMode_Forward, ProxyPathMode_Sub, ProxyPathMode_RemovedPath:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPRoute(config *ProxyConfig) (*HTTPRoute, error) {
|
||||
url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port))
|
||||
if err != nil {
|
||||
glog.Infoln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
proxy.Transport = transport
|
||||
|
||||
if !isValidProxyPathMode(config.PathMode) {
|
||||
return nil, fmt.Errorf("invalid path mode: %s", config.PathMode)
|
||||
}
|
||||
|
||||
route := &HTTPRoute{
|
||||
Url: url,
|
||||
Path: config.Path,
|
||||
Proxy: proxy,
|
||||
PathMode: config.PathMode,
|
||||
}
|
||||
|
||||
proxy.Director = nil
|
||||
|
||||
initRewrite := func(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(url)
|
||||
pr.SetXForwarded()
|
||||
}
|
||||
rewrite := initRewrite
|
||||
|
||||
switch {
|
||||
case config.Path == "", config.PathMode == ProxyPathMode_Forward:
|
||||
break
|
||||
case config.PathMode == ProxyPathMode_Sub:
|
||||
rewrite = func(pr *httputil.ProxyRequest) {
|
||||
initRewrite(pr)
|
||||
// disable compression
|
||||
pr.Out.Header.Set("Accept-Encoding", "identity")
|
||||
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
|
||||
}
|
||||
route.Proxy.ModifyResponse = func(r *http.Response) error {
|
||||
contentType, ok := r.Header["Content-Type"]
|
||||
if !ok || len(contentType) == 0 {
|
||||
glog.Infof("unknown content type for %s", r.Request.URL.String())
|
||||
return nil
|
||||
}
|
||||
if !strings.HasPrefix(contentType[0], "text/html") {
|
||||
return nil
|
||||
}
|
||||
err := utils.respRemovePath(r, config.Path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to remove path prefix %s: %v", config.Path, err)
|
||||
r.Status = err.Error()
|
||||
r.StatusCode = http.StatusInternalServerError
|
||||
}
|
||||
return err
|
||||
}
|
||||
default:
|
||||
rewrite = func(pr *httputil.ProxyRequest) {
|
||||
initRewrite(pr)
|
||||
pr.Out.URL.Path = strings.TrimPrefix(pr.Out.URL.Path, config.Path)
|
||||
}
|
||||
}
|
||||
|
||||
if glog.V(3) {
|
||||
route.Proxy.Rewrite = func(pr *httputil.ProxyRequest) {
|
||||
rewrite(pr)
|
||||
r := pr.In
|
||||
glog.Infof("[Request] %s %s%s", r.Method, r.Host, r.URL.Path)
|
||||
glog.V(4).InfoDepthf(1, "Headers: %v", r.Header)
|
||||
}
|
||||
} else {
|
||||
route.Proxy.Rewrite = rewrite
|
||||
}
|
||||
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func (p *httpLoadBalancePool) Pick() *HTTPRoute {
|
||||
// round-robin
|
||||
index := int(p.curentIndex.Load())
|
||||
defer p.curentIndex.Add(1)
|
||||
return p.pool[index%len(p.pool)]
|
||||
}
|
||||
|
||||
func redirectToTLS(w http.ResponseWriter, r *http.Request) {
|
||||
// Redirect to the same host but with HTTPS
|
||||
var redirectCode int
|
||||
if r.Method == http.MethodGet {
|
||||
redirectCode = http.StatusMovedPermanently
|
||||
} else {
|
||||
redirectCode = http.StatusPermanentRedirect
|
||||
}
|
||||
http.Redirect(w, r, fmt.Sprintf("https://%s%s?%s", r.Host, r.URL.Path, r.URL.RawQuery), redirectCode)
|
||||
}
|
||||
|
||||
func findHTTPRoute(host string, path string) (*HTTPRoute, error) {
|
||||
subdomain := strings.Split(host, ".")[0]
|
||||
routeMap, ok := routes.HTTPRoutes.UnsafeGet(subdomain)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no matching route for subdomain %s", subdomain)
|
||||
}
|
||||
return routeMap.FindMatch(path)
|
||||
}
|
||||
|
||||
func httpProxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
route, err := findHTTPRoute(r.Host, r.URL.Path)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("[Request] failed %s %s%s, error: %v",
|
||||
r.Method,
|
||||
r.Host,
|
||||
r.URL.Path,
|
||||
err,
|
||||
)
|
||||
glog.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
route.Proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// TODO: default + per proxy
|
||||
var transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
KeepAlive: 60 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
}
|
32
src/go-proxy/main.go
Normal file → Executable file
32
src/go-proxy/main.go
Normal file → Executable file
|
@ -1,28 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"flag"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/golang/glog"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
flag.Parse()
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
glog.Fatal(err)
|
||||
}
|
||||
|
||||
buildRoutes()
|
||||
log.Printf("[Build] built %v reverse proxies", countRoutes())
|
||||
beginListenStreams()
|
||||
glog.Infof("[Build] built %v reverse proxies", CountRoutes())
|
||||
BeginListenStreams()
|
||||
|
||||
go func() {
|
||||
filter := filters.NewArgs(
|
||||
|
@ -37,13 +39,13 @@ func main() {
|
|||
select {
|
||||
case msg := <-msgChan:
|
||||
// TODO: handle actor only
|
||||
log.Printf("[Event] %s %s caused rebuild", msg.Action, msg.Actor.Attributes["name"])
|
||||
endListenStreams()
|
||||
glog.Infof("[Event] %s %s caused rebuild", msg.Action, msg.Actor.Attributes["name"])
|
||||
EndListenStreams()
|
||||
buildRoutes()
|
||||
log.Printf("[Build] rebuilt %v reverse proxies", countRoutes())
|
||||
beginListenStreams()
|
||||
glog.Infof("[Build] rebuilt %v reverse proxies", CountRoutes())
|
||||
BeginListenStreams()
|
||||
case err := <-errChan:
|
||||
log.Printf("[Event] %s", err)
|
||||
glog.Infof("[Event] %s", err)
|
||||
msgChan, errChan = dockerClient.Events(context.Background(), types.EventsOptions{Filters: filter})
|
||||
}
|
||||
}
|
||||
|
@ -53,22 +55,22 @@ func main() {
|
|||
mux.HandleFunc("/", httpProxyHandler)
|
||||
|
||||
go func() {
|
||||
log.Println("Starting HTTP server on port 80")
|
||||
glog.Infoln("Starting HTTP server on port 80")
|
||||
err := http.ListenAndServe(":80", http.HandlerFunc(redirectToTLS))
|
||||
if err != nil {
|
||||
log.Fatal("HTTP server error", err)
|
||||
glog.Fatal("HTTP server error", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
log.Println("Starting HTTPS panel on port 8443")
|
||||
glog.Infoln("Starting HTTPS panel on port 8443")
|
||||
err := http.ListenAndServeTLS(":8443", "/certs/cert.crt", "/certs/priv.key", http.HandlerFunc(panelHandler))
|
||||
if err != nil {
|
||||
log.Fatal("HTTP server error", err)
|
||||
glog.Fatal("HTTP server error", err)
|
||||
}
|
||||
}()
|
||||
log.Println("Starting HTTPS server on port 443")
|
||||
glog.Infoln("Starting HTTPS server on port 443")
|
||||
err = http.ListenAndServeTLS(":443", "/certs/cert.crt", "/certs/priv.key", mux)
|
||||
if err != nil {
|
||||
log.Fatal("HTTPS Server error: ", err)
|
||||
glog.Fatal("HTTPS Server error: ", err)
|
||||
}
|
||||
}
|
||||
|
|
14
src/go-proxy/map.go
Normal file → Executable file
14
src/go-proxy/map.go
Normal file → Executable file
|
@ -16,19 +16,19 @@ type SafeMapInterface[KT comparable, VT interface{}] interface {
|
|||
|
||||
type SafeMap[KT comparable, VT interface{}] struct {
|
||||
SafeMapInterface[KT, VT]
|
||||
m map[KT]VT
|
||||
mutex sync.Mutex
|
||||
m map[KT]VT
|
||||
mutex sync.Mutex
|
||||
defaultFactory func() VT
|
||||
}
|
||||
|
||||
func NewSafeMap[KT comparable, VT interface{}](df... func() VT) *SafeMap[KT, VT] {
|
||||
func NewSafeMap[KT comparable, VT interface{}](df ...func() VT) *SafeMap[KT, VT] {
|
||||
if len(df) == 0 {
|
||||
return &SafeMap[KT, VT]{
|
||||
m: make(map[KT]VT),
|
||||
}
|
||||
}
|
||||
return &SafeMap[KT, VT]{
|
||||
m: make(map[KT]VT),
|
||||
m: make(map[KT]VT),
|
||||
defaultFactory: df[0],
|
||||
}
|
||||
}
|
||||
|
@ -54,10 +54,8 @@ func (m *SafeMap[KT, VT]) Get(key KT) VT {
|
|||
return value
|
||||
}
|
||||
|
||||
func (m *SafeMap[KT, VT]) TryGet(key KT) (VT, bool) {
|
||||
m.mutex.Lock()
|
||||
func (m *SafeMap[KT, VT]) UnsafeGet(key KT) (VT, bool) {
|
||||
value, ok := m.m[key]
|
||||
m.mutex.Unlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
|
@ -91,4 +89,4 @@ func (m *SafeMap[KT, VT]) ForEach(fn func(key KT, value VT)) {
|
|||
|
||||
func (m *SafeMap[KT, VT]) Iterator() map[KT]VT {
|
||||
return m.m
|
||||
}
|
||||
}
|
||||
|
|
7
src/go-proxy/panel.go
Normal file → Executable file
7
src/go-proxy/panel.go
Normal file → Executable file
|
@ -2,14 +2,13 @@ package main
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const templateFile = "/app/templates/panel.html"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var healthCheckHttpClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
|
@ -72,7 +71,7 @@ func panelCheckTargetHealth(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
url, err := url.Parse(targetUrl)
|
||||
if err != nil {
|
||||
log.Printf("[Panel] failed to parse %s, error: %v", targetUrl, err)
|
||||
glog.Infof("[Panel] failed to parse %s, error: %v", targetUrl, err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
30
src/go-proxy/path_pool_map.go
Normal file
30
src/go-proxy/path_pool_map.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type pathPoolMap struct {
|
||||
*SafeMap[string, *httpLoadBalancePool]
|
||||
}
|
||||
|
||||
func newPathPoolMap() pathPoolMap {
|
||||
return pathPoolMap{
|
||||
NewSafeMap[string](NewHTTPLoadBalancePool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m pathPoolMap) Add(path string, route *HTTPRoute) {
|
||||
m.Ensure(path)
|
||||
m.Get(path).Add(route)
|
||||
}
|
||||
|
||||
func (m pathPoolMap) FindMatch(pathGot string) (*HTTPRoute, error) {
|
||||
for pathWant, v := range m.m {
|
||||
if strings.HasPrefix(pathGot, pathWant) {
|
||||
return v.Pick(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no matching route for path %s", pathGot)
|
||||
}
|
22
src/go-proxy/proxy_config.go
Normal file
22
src/go-proxy/proxy_config.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ProxyConfig struct {
|
||||
id string
|
||||
Alias string
|
||||
Scheme string
|
||||
Host string
|
||||
Port string
|
||||
LoadBalance string
|
||||
Path string // http proxy only
|
||||
PathMode string // http proxy only
|
||||
}
|
||||
|
||||
func NewProxyConfig() ProxyConfig {
|
||||
return ProxyConfig{}
|
||||
}
|
||||
|
||||
func (cfg *ProxyConfig) UpdateId() {
|
||||
cfg.id = fmt.Sprintf("%s-%s-%s-%s-%s", cfg.Alias, cfg.Scheme, cfg.Host, cfg.Port, cfg.Path)
|
||||
}
|
39
src/go-proxy/route.go
Normal file → Executable file
39
src/go-proxy/route.go
Normal file → Executable file
|
@ -1,27 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type Routes struct {
|
||||
HTTPRoutes *SafeMap[string, []HTTPRoute] // id -> path
|
||||
HTTPRoutes *SafeMap[string, pathPoolMap] // id -> (path -> routes)
|
||||
StreamRoutes *SafeMap[string, StreamRoute] // id -> target
|
||||
Mutex sync.Mutex
|
||||
}
|
||||
|
||||
var routes = Routes{}
|
||||
|
||||
var streamSchemes = []string{"tcp", "udp"} // TODO: support "tcp:udp", "udp:tcp"
|
||||
var httpSchemes = []string{"http", "https"}
|
||||
|
||||
var validSchemes = append(streamSchemes, httpSchemes...)
|
||||
|
||||
func isValidScheme(scheme string) bool {
|
||||
for _, v := range validSchemes {
|
||||
for _, v := range ValidSchemes {
|
||||
if v == scheme {
|
||||
return true
|
||||
}
|
||||
|
@ -30,7 +24,7 @@ func isValidScheme(scheme string) bool {
|
|||
}
|
||||
|
||||
func isStreamScheme(scheme string) bool {
|
||||
for _, v := range streamSchemes {
|
||||
for _, v := range StreamSchemes {
|
||||
if v == scheme {
|
||||
return true
|
||||
}
|
||||
|
@ -38,40 +32,35 @@ func isStreamScheme(scheme string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func initRoutes() {
|
||||
func InitRoutes() {
|
||||
utils.resetPortsInUse()
|
||||
routes.HTTPRoutes = NewSafeMap[string, []HTTPRoute](
|
||||
func() []HTTPRoute {
|
||||
return make([]HTTPRoute, 0)
|
||||
},
|
||||
)
|
||||
routes.HTTPRoutes = NewSafeMap[string](newPathPoolMap)
|
||||
routes.StreamRoutes = NewSafeMap[string, StreamRoute]()
|
||||
}
|
||||
|
||||
func countRoutes() int {
|
||||
func CountRoutes() int {
|
||||
return routes.HTTPRoutes.Size() + routes.StreamRoutes.Size()
|
||||
}
|
||||
|
||||
func createRoute(config *ProxyConfig) {
|
||||
func CreateRoute(config *ProxyConfig) {
|
||||
if isStreamScheme(config.Scheme) {
|
||||
if routes.StreamRoutes.Contains(config.id) {
|
||||
log.Printf("[Build] Duplicated %s stream %s, ignoring", config.Scheme, config.id)
|
||||
glog.Infof("[Build] Duplicated %s stream %s, ignoring", config.Scheme, config.id)
|
||||
return
|
||||
}
|
||||
route, err := NewStreamRoute(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
glog.Infoln(err)
|
||||
return
|
||||
}
|
||||
routes.StreamRoutes.Set(config.id, route)
|
||||
} else {
|
||||
routes.HTTPRoutes.Ensure(config.Alias)
|
||||
url, err := url.Parse(fmt.Sprintf("%s://%s:%s", config.Scheme, config.Host, config.Port))
|
||||
route, err := NewHTTPRoute(config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
glog.Infoln(err)
|
||||
return
|
||||
}
|
||||
route := NewHTTPRoute(url, config.Path)
|
||||
routes.HTTPRoutes.Set(config.Alias, append(routes.HTTPRoutes.Get(config.Alias), route))
|
||||
routes.HTTPRoutes.Get(config.Alias).Add(config.Path, route)
|
||||
}
|
||||
}
|
||||
|
|
61
src/go-proxy/stream.go → src/go-proxy/stream_route.go
Normal file → Executable file
61
src/go-proxy/stream.go → src/go-proxy/stream_route.go
Normal file → Executable file
|
@ -3,11 +3,12 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type StreamRoute interface {
|
||||
|
@ -46,7 +47,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
|
|||
|
||||
port_split := strings.Split(config.Port, ":")
|
||||
if len(port_split) != 2 {
|
||||
log.Printf(`[Build] %s: Invalid stream port %s, `+
|
||||
glog.Infof(`[Build] %s: Invalid stream port %s, `+
|
||||
`assuming it's targetPort`, config.Alias, config.Port)
|
||||
srcPort = "0"
|
||||
dstPort = config.Port
|
||||
|
@ -55,7 +56,7 @@ func newStreamRouteBase(config *ProxyConfig) (*StreamRouteBase, error) {
|
|||
dstPort = port_split[1]
|
||||
}
|
||||
|
||||
port, hasName := namePortMap[dstPort]
|
||||
port, hasName := NamePortMap[dstPort]
|
||||
if hasName {
|
||||
dstPort = port
|
||||
}
|
||||
|
@ -117,11 +118,16 @@ func (route *StreamRouteBase) PrintError(err error) {
|
|||
if err == nil {
|
||||
return
|
||||
}
|
||||
route.Logf("Error: %s", err.Error())
|
||||
glog.Errorf("[%s -> %s] %s: %v",
|
||||
route.ListeningScheme,
|
||||
route.TargetScheme,
|
||||
route.Alias,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
func (route *StreamRouteBase) Logf(format string, v ...interface{}) {
|
||||
log.Printf("[%s -> %s] %s: "+format,
|
||||
glog.Infof("[%s -> %s] %s: "+format,
|
||||
append([]interface{}{
|
||||
route.ListeningScheme,
|
||||
route.TargetScheme,
|
||||
|
@ -176,14 +182,14 @@ func stopListening(route StreamRoute) {
|
|||
case <-done:
|
||||
route.Logf("Stopped listening")
|
||||
return
|
||||
case <-time.After(streamStopListenTimeout):
|
||||
case <-time.After(StreamStopListenTimeout):
|
||||
route.Logf("timed out waiting for connections")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func allStreamsDo(msg string, fn ...func(StreamRoute)) {
|
||||
log.Printf("[Stream] %s", msg)
|
||||
glog.Infof("[Stream] %s", msg)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
|
@ -198,48 +204,13 @@ func allStreamsDo(msg string, fn ...func(StreamRoute)) {
|
|||
}
|
||||
|
||||
wg.Wait()
|
||||
log.Printf("[Stream] Finished %s", msg)
|
||||
glog.Infof("[Stream] Finished %s", msg)
|
||||
}
|
||||
|
||||
func beginListenStreams() {
|
||||
func BeginListenStreams() {
|
||||
allStreamsDo("Start", StreamRoute.SetupListen, StreamRoute.Listen)
|
||||
}
|
||||
|
||||
func endListenStreams() {
|
||||
func EndListenStreams() {
|
||||
allStreamsDo("Stop", StreamRoute.StopListening)
|
||||
}
|
||||
|
||||
var imageNamePortMap = map[string]string{
|
||||
"postgres": "5432",
|
||||
"mysql": "3306",
|
||||
"mariadb": "3306",
|
||||
"redis": "6379",
|
||||
"mssql": "1433",
|
||||
"memcached": "11211",
|
||||
"rabbitmq": "5672",
|
||||
"mongo": "27017",
|
||||
}
|
||||
var extraNamePortMap = map[string]string{
|
||||
"dns": "53",
|
||||
"ssh": "22",
|
||||
"ftp": "21",
|
||||
"smtp": "25",
|
||||
"pop3": "110",
|
||||
"imap": "143",
|
||||
}
|
||||
var namePortMap = func() map[string]string {
|
||||
m := make(map[string]string)
|
||||
for k, v := range imageNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range extraNamePortMap {
|
||||
m[k] = v
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
const UDPStreamType = "udp"
|
||||
const TCPStreamType = "tcp"
|
||||
|
||||
// const maxQueueSizePerStream = 100
|
||||
const streamStopListenTimeout = 1 * time.Second
|
5
src/go-proxy/tcp.go → src/go-proxy/tcp_route.go
Normal file → Executable file
5
src/go-proxy/tcp.go → src/go-proxy/tcp_route.go
Normal file → Executable file
|
@ -4,10 +4,11 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
const tcpDialTimeout = 5 * time.Second
|
||||
|
@ -100,7 +101,7 @@ func (route *TCPRoute) grHandleConnection(clientConn net.Conn) {
|
|||
dialer := &net.Dialer{}
|
||||
serverConn, err := dialer.DialContext(ctx, route.TargetScheme, serverAddr)
|
||||
if err != nil {
|
||||
log.Printf("[Stream Dial] %v", err)
|
||||
glog.Infof("[Stream Dial] %v", err)
|
||||
return
|
||||
}
|
||||
route.tcpPipe(clientConn, serverConn)
|
5
src/go-proxy/udp.go → src/go-proxy/udp_route.go
Normal file → Executable file
5
src/go-proxy/udp.go → src/go-proxy/udp_route.go
Normal file → Executable file
|
@ -7,11 +7,6 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
const udpBufferSize = 1500
|
||||
|
||||
// const udpListenTimeout = 100 * time.Second
|
||||
// const udpConnectionTimeout = 30 * time.Second
|
||||
|
||||
type UDPRoute struct {
|
||||
*StreamRouteBase
|
||||
|
50
src/go-proxy/utils.go
Normal file → Executable file
50
src/go-proxy/utils.go
Normal file → Executable file
|
@ -1,11 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
xhtml "golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type Utils struct {
|
||||
|
@ -82,3 +87,48 @@ func (*Utils) healthCheckStream(scheme string, host string) error {
|
|||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*Utils) snakeToCamel(s string) string {
|
||||
toHyphenCamel := http.CanonicalHeaderKey(strings.ReplaceAll(s, "_", "-"))
|
||||
return strings.ReplaceAll(toHyphenCamel, "-", "")
|
||||
}
|
||||
|
||||
func htmlNodesSubPath(node *xhtml.Node, path string) {
|
||||
if node.Type == xhtml.ElementNode {
|
||||
for _, attr := range node.Attr {
|
||||
switch attr.Key {
|
||||
case "src": // img, script, etc.
|
||||
case "href": // link
|
||||
case "action": // form
|
||||
if strings.HasPrefix(attr.Val, path) {
|
||||
attr.Val = strings.Replace(attr.Val, path, "", 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := node.FirstChild; c != nil; c = c.NextSibling {
|
||||
htmlNodesSubPath(c, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (*Utils) respRemovePath(r *http.Response, path string) error {
|
||||
// remove all path prefix from relative path in script, img, a, ...
|
||||
doc, err := xhtml.Parse(r.Body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
htmlNodesSubPath(doc, path)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = xhtml.Render(&buf, doc)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Body = io.NopCloser(strings.NewReader(buf.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
8
templates/panel.html
Normal file → Executable file
8
templates/panel.html
Normal file → Executable file
|
@ -105,11 +105,12 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $alias, $httpRoutes := .HTTPRoutes.Iterator}}
|
||||
{{range $route := $httpRoutes}}
|
||||
{{range $alias, $pathPoolMap := .HTTPRoutes.Iterator}}
|
||||
{{range $path, $lbPool := $pathPoolMap.Iterator}}
|
||||
{{range $_, $route := $lbPool.Iterator}}
|
||||
<tr>
|
||||
<td>{{$alias}}</td>
|
||||
<td>{{$route.Path}}</td>
|
||||
<td>{{$path}}</td>
|
||||
<td>{{$route.Url.String}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="health-circle"></div>
|
||||
|
@ -117,6 +118,7 @@
|
|||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue