Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bb34b8788 | ||
|
|
f6dc432419 | ||
|
|
9b2ee628aa | ||
|
|
357ad26a0e | ||
|
|
a3e705373c | ||
|
|
71ad13256e | ||
|
|
07511281b8 | ||
|
|
7c11c9c91a | ||
|
|
2cabe4c416 | ||
|
|
588dd41244 | ||
|
|
7b86bb262c | ||
|
|
97fa648b2f |
115
README.md
@@ -2,24 +2,24 @@
|
||||
|
||||
# GoDoxy
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||

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

|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
|
||||
[](https://discord.gg/umReR62nRd)
|
||||
|
||||
A lightweight, simple, and [performant](https://github.com/yusing/go-proxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||
A lightweight, simple, and [performant](https://github.com/yusing/godoxy/wiki/Benchmarks) reverse proxy with WebUI.
|
||||
|
||||
For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wiki)**
|
||||
For full documentation, check out **[Wiki](https://github.com/yusing/godoxy/wiki)**
|
||||
|
||||
**EN** | <a href="README_CHT.md">中文</a>
|
||||
|
||||
**Currently working on [feat/godoxy-agent](https://github.com/yusing/go-proxy/tree/feat/godoxy-agent).<br/>Fork this instead of default branch.**
|
||||
**Currently working on [feat/godoxy-agent](https://github.com/yusing/godoxy/tree/feat/godoxy-agent).<br/>For contribution, please fork this instead of default branch.**
|
||||
|
||||
<!-- [](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
|
||||
[](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy) -->
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/4bb371f4-6e4c-425c-89b2-b9e962bdd46f" style="max-width: 650">
|
||||
<img src="screenshots/webui.jpg" style="max-width: 650">
|
||||
|
||||
</div>
|
||||
|
||||
@@ -31,29 +31,29 @@ For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wi
|
||||
- [Table of content](#table-of-content)
|
||||
- [Key Features](#key-features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [How does GoDoxy work](#how-does-godoxy-work)
|
||||
- [Setup](#setup)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Screenshots](#screenshots)
|
||||
- [idlesleeper](#idlesleeper)
|
||||
- [Metrics and Logs](#metrics-and-logs)
|
||||
- [Manual Setup](#manual-setup)
|
||||
- [Folder structrue](#folder-structrue)
|
||||
- [Build it yourself](#build-it-yourself)
|
||||
|
||||
## Key Features
|
||||
|
||||
- Easy to use
|
||||
- Effortless configuration
|
||||
- Simple multi-node setup
|
||||
- Simple multi-node setup with GoDoxy agents
|
||||
- Error messages is clear and detailed, easy troubleshooting
|
||||
- Auto SSL cert management (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- Auto configuration for docker containers
|
||||
- Auto SSL with Let's Encrypt and DNS-01 (See [Supported DNS-01 Challenge Providers](https://github.com/yusing/go-proxy/wiki/Supported-DNS%E2%80%9001-Providers))
|
||||
- Auto hot-reload on container state / config file changes
|
||||
- Create routes dynamically from running docker containers
|
||||
- **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [screenshots](#idlesleeper))_
|
||||
- HTTP(s) reserve proxy
|
||||
- OpenID Connect support
|
||||
- [HTTP middleware support](https://github.com/yusing/go-proxy/wiki/Middlewares)
|
||||
- [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- TCP and UDP port forwarding
|
||||
- **Web UI with App dashboard and config editor**
|
||||
- HTTP reserve proxy and TCP/UDP port forwarding
|
||||
- OpenID Connect integration
|
||||
- [HTTP middleware](https://github.com/yusing/go-proxy/wiki/Middlewares) and [Custom error pages support](https://github.com/yusing/go-proxy/wiki/Middlewares#custom-error-pages)
|
||||
- **Web UI with App dashboard, config editor, _uptime monitor_, _system monitor_, _docker logs viewer_ (available on nightly builds)**
|
||||
- Supports linux/amd64, linux/arm64
|
||||
- Written in **[Go](https://go.dev)**
|
||||
|
||||
@@ -61,42 +61,87 @@ For full documentation, check out **[Wiki](https://github.com/yusing/go-proxy/wi
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
Setup Wildcard DNS Record(s) for machine running `GoDoxy`, e.g.
|
||||
|
||||
- A Record: `*.y.z` -> `10.0.10.1`
|
||||
- AAAA Record: `*.y.z` -> `::ffff:a00:a01`
|
||||
- A Record: `*.domain.com` -> `10.0.10.1`
|
||||
- AAAA Record (if you use IPv6): `*.domain.com` -> `::ffff:a00:a01`
|
||||
|
||||
## How does GoDoxy work
|
||||
|
||||
1. List all the containers
|
||||
2. Read container name, labels and port configurations for each of them
|
||||
3. Create a route if applicable (a route is like a "Virtual Host" in NPM)
|
||||
|
||||
GoDoxy uses the label `proxy.aliases` as the subdomain(s), if unset it defaults to `container_name`.
|
||||
|
||||
For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
|
||||
|
||||
## Setup
|
||||
|
||||
**NOTE:** GoDoxy is designed to be (and only works when) running in `host` network mode, do not change it. To change listening ports, modify `.env`.
|
||||
|
||||
1. Prepare a new directory for docker compose and config files.
|
||||
1. Prepare a new directory for docker compose and config files.
|
||||
|
||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||
2. Run setup script inside the directory, or [set up manually](#manual-setup)
|
||||
|
||||
```shell
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/go-proxy/main/scripts/setup.sh)"
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/yusing/godoxy/main/scripts/setup.sh)"
|
||||
```
|
||||
|
||||
3. Start the container `docker compose up -d` and wait for it to be ready
|
||||
3. Start the container `docker compose up -d` and wait for it to be ready
|
||||
|
||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||
4. You may now do some extra configuration on WebUI `https://godoxy.yourdomain.com`
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
### Manual Setup
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
### Metrics and Logs
|
||||
|
||||
_In development, available on nightly builds._
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>Uptime Monitor</b></td>
|
||||
<td align="center"><b>Docker Logs</b></td>
|
||||
<td align="center"><b>Server Overview</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
|
||||
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b>System Monitor</b></td>
|
||||
<td align="center"><b>Graphs</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Manual Setup
|
||||
|
||||
1. Make `config` directory then grab `config.example.yml` into `config/config.yml`
|
||||
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/go-proxy/main/config.example.yml -O config/config.yml`
|
||||
`mkdir -p config && wget https://raw.githubusercontent.com/yusing/godoxy/main/config.example.yml -O config/config.yml`
|
||||
|
||||
2. Grab `.env.example` into `.env`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/.env.example -O .env`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/.env.example -O .env`
|
||||
|
||||
3. Grab `compose.example.yml` into `compose.yml`
|
||||
|
||||
`wget https://raw.githubusercontent.com/yusing/go-proxy/main/compose.example.yml -O compose.yml`
|
||||
`wget https://raw.githubusercontent.com/yusing/godoxy/main/compose.example.yml -O compose.yml`
|
||||
|
||||
### Folder structrue
|
||||
|
||||
@@ -119,17 +164,9 @@ Setup DNS Records point to machine which runs `GoDoxy`, e.g.
|
||||
└── .env
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### idlesleeper
|
||||
|
||||

|
||||
|
||||
[🔼Back to top](#table-of-content)
|
||||
|
||||
## Build it yourself
|
||||
|
||||
1. Clone the repository `git clone https://github.com/yusing/go-proxy --depth=1`
|
||||
1. Clone the repository `git clone https://github.com/yusing/godoxy --depth=1`
|
||||
|
||||
2. Install / Upgrade [go (>=1.22)](https://go.dev/doc/install) and `make` if not already
|
||||
|
||||
|
||||
14
go.mod
@@ -6,8 +6,8 @@ require (
|
||||
github.com/PuerkitoBio/goquery v1.10.2 // parsing HTML for extract fav icon
|
||||
github.com/coder/websocket v1.8.12 // websocket for API and agent
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 // oidc authentication
|
||||
github.com/docker/cli v27.5.1+incompatible // docker CLI
|
||||
github.com/docker/docker v27.5.1+incompatible // docker daemon
|
||||
github.com/docker/cli v28.0.1+incompatible // docker CLI
|
||||
github.com/docker/docker v28.0.1+incompatible // docker daemon
|
||||
github.com/fsnotify/fsnotify v1.8.0 // file watcher
|
||||
github.com/go-acme/lego/v4 v4.22.2 // acme client
|
||||
github.com/go-playground/validator/v10 v10.25.0 // validator
|
||||
@@ -15,13 +15,13 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // jwt for default auth
|
||||
github.com/gotify/server/v2 v2.6.1 // reference the Message struct for json response
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
|
||||
github.com/prometheus/client_golang v1.20.5 // metrics
|
||||
github.com/prometheus/client_golang v1.21.0 // metrics
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // lock free map for concurrent operations
|
||||
github.com/rs/zerolog v1.33.0 // logging
|
||||
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
|
||||
golang.org/x/crypto v0.33.0 // encrypting password with bcrypt
|
||||
golang.org/x/crypto v0.35.0 // encrypting password with bcrypt
|
||||
golang.org/x/net v0.35.0 // HTTP header utilities
|
||||
golang.org/x/oauth2 v0.26.0 // oauth2 authentication
|
||||
golang.org/x/oauth2 v0.27.0 // oauth2 authentication
|
||||
golang.org/x/text v0.22.0 // string utilities
|
||||
golang.org/x/time v0.10.0 // time utilities
|
||||
gopkg.in/yaml.v3 v3.0.1 // yaml parsing for different config files
|
||||
@@ -40,7 +40,7 @@ require (
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
28
go.sum
@@ -27,10 +27,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
|
||||
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
|
||||
github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
|
||||
github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.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=
|
||||
@@ -43,8 +43,8 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
|
||||
github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -82,8 +82,8 @@ github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nu
|
||||
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -126,8 +126,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
@@ -178,8 +178,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -203,8 +203,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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=
|
||||
|
||||
@@ -36,9 +36,8 @@ func (res *fetchResult) ContentType() string {
|
||||
if res.contentType == "" {
|
||||
if bytes.HasPrefix(res.icon, []byte("<svg")) || bytes.HasPrefix(res.icon, []byte("<?xml")) {
|
||||
return "image/svg+xml"
|
||||
} else {
|
||||
return "image/x-icon"
|
||||
}
|
||||
return "image/x-icon"
|
||||
}
|
||||
return res.contentType
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch what {
|
||||
case ListRoute:
|
||||
if route := listRoute(which); route == nil {
|
||||
route := listRoute(which)
|
||||
if route == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
} else {
|
||||
U.RespondJSON(w, r, route)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
U "github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -19,15 +19,14 @@ type (
|
||||
*client.Client
|
||||
|
||||
key string
|
||||
refCount *U.RefCount
|
||||
|
||||
l zerolog.Logger
|
||||
refCount uint32
|
||||
closedOn int64
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
clientMap = make(map[string]*SharedClient, 5)
|
||||
clientMapMu sync.Mutex
|
||||
clientMapMu sync.RWMutex
|
||||
|
||||
clientOptEnvHost = []client.Opt{
|
||||
client.WithHostFromEnv(),
|
||||
@@ -35,28 +34,61 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
cleanInterval = 10 * time.Second
|
||||
clientTTLSecs = int64(10)
|
||||
)
|
||||
|
||||
func init() {
|
||||
cleaner := task.RootTask("docker_clients_cleaner")
|
||||
go func() {
|
||||
ticker := time.NewTicker(cleanInterval)
|
||||
defer ticker.Stop()
|
||||
defer cleaner.Finish("program exit")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
closeTimedOutClients()
|
||||
case <-cleaner.Context().Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
task.OnProgramExit("docker_clients_cleanup", func() {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
for _, c := range clientMap {
|
||||
if c.Connected() {
|
||||
c.Client.Close()
|
||||
}
|
||||
delete(clientMap, c.key)
|
||||
c.Client.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *SharedClient) Connected() bool {
|
||||
return c != nil && c.Client != nil
|
||||
func closeTimedOutClients() {
|
||||
clientMapMu.Lock()
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
for _, c := range clientMap {
|
||||
if c.closedOn == 0 {
|
||||
continue
|
||||
}
|
||||
if c.refCount == 0 && now-c.closedOn > clientTTLSecs {
|
||||
delete(clientMap, c.key)
|
||||
c.Client.Close()
|
||||
logging.Debug().Str("host", c.key).Msg("docker client closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the client is still referenced, this is no-op.
|
||||
func (c *SharedClient) Close() {
|
||||
if c.Connected() {
|
||||
c.refCount.Sub()
|
||||
}
|
||||
atomic.StoreInt64(&c.closedOn, time.Now().Unix())
|
||||
atomic.AddUint32(&c.refCount, ^uint32(0))
|
||||
}
|
||||
|
||||
// ConnectClient creates a new Docker client connection to the specified host.
|
||||
@@ -74,7 +106,8 @@ func ConnectClient(host string) (*SharedClient, error) {
|
||||
defer clientMapMu.Unlock()
|
||||
|
||||
if client, ok := clientMap[host]; ok {
|
||||
client.refCount.Add()
|
||||
atomic.StoreInt64(&client.closedOn, 0)
|
||||
atomic.AddUint32(&client.refCount, 1)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -119,23 +152,11 @@ func ConnectClient(host string) (*SharedClient, error) {
|
||||
c := &SharedClient{
|
||||
Client: client,
|
||||
key: host,
|
||||
refCount: U.NewRefCounter(),
|
||||
l: logging.With().Str("address", client.DaemonHost()).Logger(),
|
||||
refCount: 1,
|
||||
}
|
||||
c.l.Trace().Msg("client connected")
|
||||
|
||||
clientMap[host] = c
|
||||
defer logging.Debug().Str("host", host).Msg("docker client connected")
|
||||
|
||||
go func() {
|
||||
<-c.refCount.Zero()
|
||||
clientMapMu.Lock()
|
||||
delete(clientMap, c.key)
|
||||
clientMapMu.Unlock()
|
||||
|
||||
if c.Connected() {
|
||||
c.Client.Close()
|
||||
c.l.Trace().Msg("client closed")
|
||||
}
|
||||
}()
|
||||
clientMap[c.key] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -146,9 +146,6 @@ func (w *Watcher) containerStart(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (w *Watcher) containerStatus() (string, error) {
|
||||
if !w.client.Connected() {
|
||||
return "", errors.New("docker client not connected")
|
||||
}
|
||||
ctx, cancel := context.WithTimeoutCause(w.task.Context(), dockerReqTimeout, errors.New("docker request timeout"))
|
||||
defer cancel()
|
||||
json, err := w.client.ContainerInspect(ctx, w.ContainerID)
|
||||
@@ -242,7 +239,7 @@ func (w *Watcher) getEventCh(dockerWatcher watcher.DockerWatcher) (eventCh <-cha
|
||||
// it exits only if the context is canceled, the container is destroyed,
|
||||
// errors occurred on docker client, or route provider died (mainly caused by config reload).
|
||||
func (w *Watcher) watchUntilDestroy() (returnCause error) {
|
||||
dockerWatcher := watcher.NewDockerWatcherWithClient(w.client)
|
||||
dockerWatcher := watcher.NewDockerWatcher(w.client.DaemonHost())
|
||||
dockerEventCh, dockerEventErrCh := w.getEventCh(dockerWatcher)
|
||||
|
||||
for {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -8,22 +9,43 @@ import (
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
)
|
||||
|
||||
type redirectHTTP struct{}
|
||||
type redirectHTTP struct {
|
||||
Bypass struct {
|
||||
UserAgents []string
|
||||
}
|
||||
}
|
||||
|
||||
var RedirectHTTP = NewMiddleware[redirectHTTP]()
|
||||
|
||||
// before implements RequestModifier.
|
||||
func (redirectHTTP) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
func (m *redirectHTTP) before(w http.ResponseWriter, r *http.Request) (proceed bool) {
|
||||
if r.TLS != nil {
|
||||
return true
|
||||
}
|
||||
r.URL.Scheme = "https"
|
||||
host := r.Host
|
||||
if i := strings.Index(host, ":"); i != -1 {
|
||||
host = host[:i] // strip port number if present
|
||||
|
||||
if len(m.Bypass.UserAgents) > 0 {
|
||||
ua := r.UserAgent()
|
||||
for _, uaBypass := range m.Bypass.UserAgents {
|
||||
if strings.Contains(ua, uaBypass) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
r.URL.Host = host + ":" + common.ProxyHTTPSPort
|
||||
logging.Debug().Str("url", r.URL.String()).Msg("redirect to https")
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
|
||||
return true
|
||||
|
||||
r.URL.Scheme = "https"
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
|
||||
if common.ProxyHTTPSPort != "443" {
|
||||
r.URL.Host = host + ":" + common.ProxyHTTPSPort
|
||||
} else {
|
||||
r.URL.Host = host
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently)
|
||||
|
||||
logging.Debug().Str("url", r.URL.String()).Str("user_agent", r.UserAgent()).Msg("redirect to https")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/net/types"
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
@@ -14,8 +13,8 @@ func TestRedirectToHTTPs(t *testing.T) {
|
||||
reqURL: types.MustParseURL("http://example.com"),
|
||||
})
|
||||
ExpectNoError(t, err)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusTemporaryRedirect)
|
||||
ExpectEqual(t, result.ResponseHeaders.Get("Location"), "https://example.com:"+common.ProxyHTTPSPort)
|
||||
ExpectEqual(t, result.ResponseStatus, http.StatusMovedPermanently)
|
||||
ExpectEqual(t, result.ResponseHeaders.Get("Location"), "https://example.com")
|
||||
}
|
||||
|
||||
func TestNoRedirect(t *testing.T) {
|
||||
|
||||
@@ -234,7 +234,8 @@ func TestOnCorrectness(t *testing.T) {
|
||||
|
||||
tests = append(tests, genCorrectnessTestCases("header", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
Header: http.Header{k: []string{v}}}
|
||||
Header: http.Header{k: []string{v}},
|
||||
}
|
||||
})...)
|
||||
tests = append(tests, genCorrectnessTestCases("query", func(k, v string) *http.Request {
|
||||
return &http.Request{
|
||||
|
||||
@@ -6,20 +6,15 @@ import (
|
||||
|
||||
docker_events "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/rs/zerolog"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type (
|
||||
DockerWatcher struct {
|
||||
zerolog.Logger
|
||||
|
||||
host string
|
||||
client *D.SharedClient
|
||||
clientOwned bool
|
||||
host string
|
||||
client *D.SharedClient
|
||||
}
|
||||
DockerListOptions = docker_events.ListOptions
|
||||
)
|
||||
@@ -53,24 +48,7 @@ func DockerFilterContainerNameID(nameOrID string) filters.KeyValuePair {
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
host: host,
|
||||
clientOwned: true,
|
||||
Logger: logging.With().
|
||||
Str("type", "docker").
|
||||
Str("host", host).
|
||||
Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDockerWatcherWithClient(client *D.SharedClient) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
client: client,
|
||||
Logger: logging.With().
|
||||
Str("type", "docker").
|
||||
Str("host", client.DaemonHost()).
|
||||
Logger(),
|
||||
}
|
||||
return DockerWatcher{host: host}
|
||||
}
|
||||
|
||||
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.Error) {
|
||||
@@ -82,36 +60,12 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
errCh := make(chan E.Error)
|
||||
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
defer close(errCh)
|
||||
|
||||
defer func() {
|
||||
if w.clientOwned && w.client.Connected() {
|
||||
w.client.Close()
|
||||
}
|
||||
defer close(eventCh)
|
||||
defer close(errCh)
|
||||
w.client.Close()
|
||||
}()
|
||||
|
||||
if !w.client.Connected() {
|
||||
var err error
|
||||
attempts := 0
|
||||
for {
|
||||
w.client, err = D.ConnectClient(w.host)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
errCh <- E.Errorf("docker connection attempt #%d: %w", attempts, err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
time.Sleep(dockerWatcherRetryInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer w.client.Close()
|
||||
|
||||
cEventCh, cErrCh := w.client.Events(ctx, options)
|
||||
|
||||
for {
|
||||
@@ -124,7 +78,6 @@ func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerList
|
||||
case msg := <-cEventCh:
|
||||
action, ok := events.DockerEventMap[msg.Action]
|
||||
if !ok {
|
||||
w.Debug().Msgf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
|
||||
continue
|
||||
}
|
||||
event := Event{
|
||||
|
||||
BIN
screenshots/docker-logs.jpg
Normal file
|
After Width: | Height: | Size: 516 KiB |
BIN
screenshots/docker.jpg
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
screenshots/system-info-graphs.jpg
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
screenshots/system-monitor.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
screenshots/uptime.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
screenshots/webui.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 286 KiB |
@@ -150,6 +150,22 @@ ask_while_empty() {
|
||||
eval "$var_name=\"$value\""
|
||||
}
|
||||
|
||||
get_timezone() {
|
||||
if [ -f /etc/timezone ]; then
|
||||
TIMEZONE=$(cat /etc/timezone)
|
||||
if [ -n "$TIMEZONE" ]; then
|
||||
echo "$TIMEZONE"
|
||||
fi
|
||||
elif command -v timedatectl >/dev/null 2>&1; then
|
||||
TIMEZONE=$(timedatectl status | grep "Time zone" | awk '{print $3}')
|
||||
if [ -n "$TIMEZONE" ]; then
|
||||
echo "$TIMEZONE"
|
||||
fi
|
||||
else
|
||||
echo "Warning: could not detect timezone, please set it manually"
|
||||
fi
|
||||
}
|
||||
|
||||
check_pkg "openssl" "openssl"
|
||||
check_pkg "docker" "docker-ce"
|
||||
|
||||
@@ -159,9 +175,16 @@ mkdir_if_not_exists "$CONFIG_BASE_PATH"
|
||||
|
||||
# 2. .env file
|
||||
fetch_file "$DOT_ENV_EXAMPLE_PATH" "$DOT_ENV_PATH"
|
||||
|
||||
# set random JWT secret
|
||||
JWT_SECRET=$(openssl rand -base64 32)
|
||||
sed -i "s/GODOXY_API_JWT_SECRET=.*/GODOXY_API_JWT_SECRET=${JWT_SECRET}/" "$DOT_ENV_PATH"
|
||||
sed -i "s|GODOXY_API_JWT_SECRET=.*|GODOXY_API_JWT_SECRET=${JWT_SECRET}|" "$DOT_ENV_PATH"
|
||||
|
||||
# set timezone
|
||||
get_timezone
|
||||
if [ -n "$TIMEZONE" ]; then
|
||||
sed -i "s|TZ=.*|TZ=${TIMEZONE}|" "$DOT_ENV_PATH"
|
||||
fi
|
||||
|
||||
# 3. docker-compose.yml
|
||||
fetch_file "$COMPOSE_EXAMPLE_FILE_NAME" "$COMPOSE_FILE_NAME"
|
||||
@@ -176,8 +199,8 @@ echo "Setting up login user"
|
||||
ask_while_empty "Enter login username: " LOGIN_USERNAME
|
||||
ask_while_empty "Enter login password: " LOGIN_PASSWORD
|
||||
echo "Setting up login user \"$LOGIN_USERNAME\" with password \"$LOGIN_PASSWORD\""
|
||||
sed -i "s/GODOXY_API_USERNAME=.*/GODOXY_API_USERNAME=${LOGIN_USERNAME}/" "$DOT_ENV_PATH"
|
||||
sed -i "s/GODOXY_API_PASSWORD=.*/GODOXY_API_PASSWORD=${LOGIN_PASSWORD}/" "$DOT_ENV_PATH"
|
||||
sed -i "s|GODOXY_API_USERNAME=.*|GODOXY_API_USERNAME=${LOGIN_USERNAME}|" "$DOT_ENV_PATH"
|
||||
sed -i "s|GODOXY_API_PASSWORD=.*|GODOXY_API_PASSWORD=${LOGIN_PASSWORD}|" "$DOT_ENV_PATH"
|
||||
|
||||
# 6. setup autocert
|
||||
|
||||
|
||||