Compare commits

..

12 Commits

Author SHA1 Message Date
yusing
7bb34b8788 fix redirectHTTP middleware test 2025-03-01 15:53:33 +08:00
yusing
f6dc432419 refactor: fix code formatting and return flow 2025-03-01 15:50:50 +08:00
yusing
9b2ee628aa fix docker client data race on Close(), remove SharedClient.IsConnected 2025-03-01 15:47:08 +08:00
yusing
357ad26a0e reduce docker client initiation 2025-03-01 15:39:25 +08:00
yusing
a3e705373c deps upgrade 2025-03-01 15:34:53 +08:00
yusing
71ad13256e fix redirectHTTP middleware, add bypass.user_agents option 2025-03-01 15:29:33 +08:00
dependabot[bot]
07511281b8 Bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5 (#67)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-25 10:23:42 +08:00
yusing
7c11c9c91a update webui screenshot 2025-02-25 06:06:06 +08:00
yusing
2cabe4c416 update README and screenshots 2025-02-25 06:01:44 +08:00
Yuzerion
588dd41244 Update README.md 2025-02-25 00:03:20 +08:00
yusing
7b86bb262c fix setup script 2025-02-24 00:02:50 +08:00
yusing
97fa648b2f fix setup script 2025-02-23 13:47:30 +08:00
19 changed files with 220 additions and 168 deletions

115
README.md
View File

@@ -2,24 +2,24 @@
# GoDoxy
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/go-proxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
![GitHub last commit](https://img.shields.io/github/last-commit/yusing/godoxy)
[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=yusing_godoxy)
[![](https://dcbadge.limes.pink/api/server/umReR62nRd?style=flat)](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.**
<!-- [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=yusing_go-proxy)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=yusing_go-proxy&metric=vulnerabilities)](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
![idlesleeper](screenshots/idlesleeper.webp)
### 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
![idlesleeper](screenshots/idlesleeper.webp)
[🔼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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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{

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

BIN
screenshots/docker.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
screenshots/uptime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
screenshots/webui.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

View File

@@ -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