Compare commits

26 Commits

Author SHA1 Message Date
LittleSheep
0b38c4a470 Expand config search area 2024-02-14 15:06:51 +08:00
LittleSheep
515f086f19 Compress 2024-02-14 14:51:00 +08:00
LittleSheep
d75ac2999b 🗑️ Remove test data 2024-02-14 14:31:29 +08:00
LittleSheep
7eee10c4ff WebSocket Support 2024-02-14 13:53:43 +08:00
LittleSheep
e40fe6049f 🐛 Fix http proxy 2024-02-13 22:56:22 +08:00
LittleSheep
ed9434b85a 💚 Fix dockerfile missing build-base 2024-02-13 21:03:33 +08:00
LittleSheep
804108a209 💚 Fix dockerfile missing openssl during build 2024-02-13 20:51:39 +08:00
LittleSheep
46736c12b9 🔨 Update dockerfile 2024-02-13 20:48:40 +08:00
LittleSheep
1a562fbee8 🔨 Add rust version as a pre-release version 2024-02-13 20:44:52 +08:00
7796ee3554 🔀 Merge pull request '♻️ 使用 Actix RS 重构' (#8) from refactor/actix-rs into refactor/rust
Reviewed-on: https://code.smartsheep.studio/Goatworks/RoadSign/pulls/8
2024-02-13 12:39:08 +00:00
LittleSheep
12add73ecb Multiple listeners 2024-02-13 20:32:13 +08:00
LittleSheep
3fbe1db1ef TLS 2024-02-13 18:27:01 +08:00
LittleSheep
e27023c130 More detailed error 2024-02-13 01:42:03 +08:00
LittleSheep
2478a05c89 More details trace 2024-02-13 01:20:27 +08:00
LittleSheep
cb8eab6c1b 🛂 Secured sideload 2024-02-13 01:04:43 +08:00
LittleSheep
ae3894bea6 Sideload 2024-02-13 00:34:54 +08:00
LittleSheep
b7d4a54d62 ♻️ Migrated to actix rs 2024-02-13 00:01:39 +08:00
LittleSheep
ead748a508 Warden process manager 2024-01-15 23:22:53 +08:00
LittleSheep
4c08d78bed Add headers in reserve proxy 2024-01-14 19:12:38 +08:00
LittleSheep
a088f6224e Basic Auth 2024-01-14 18:39:22 +08:00
LittleSheep
f02977b7d7 Metrics 2024-01-14 17:55:14 +08:00
LittleSheep
905b70349b Clean url with suffix 2024-01-14 14:52:19 +08:00
LittleSheep
91ecf9d7bb Static files 2024-01-14 13:14:41 +08:00
LittleSheep
c991d0b54a Route and forward for http(s) 2024-01-14 02:01:56 +08:00
LittleSheep
5de1d13907 Config loader 2024-01-13 14:46:26 +08:00
LittleSheep
bf7004c89c 🎉 Start developing! 2024-01-13 12:35:59 +08:00
97 changed files with 3629 additions and 4186 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -3,8 +3,9 @@
{
"type": "go",
"name": "Run RoadSign",
"goExecPath": "/opt/homebrew/bin/go",
"buildParams": ["code.smartsheep.studio/goatworks/roadsign/pkg/cmd/server"],
"goExecPath": "C:\\Tools\\Scoop\\shims\\go.exe",
"buildParams": ["code.smartsheep.studio/goatworks/roadsign/pkg/cmd"],
},
]
}

View File

@@ -2,27 +2,27 @@ name: release-nightly
on:
push:
branches: [ master ]
branches: [ refactor/rust ]
jobs:
build-docker:
runs-on: edge
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: xsheep2010/roadsign:nightly
file: ./Dockerfile
tags: xsheep2010/roadsign:sigma

7
.gitignore vendored
View File

@@ -1,3 +1,8 @@
/config
/certs
/test/data
/letsencrypt
.DS_Store
# Added by cargo
/target

5
.idea/RoadSign.iml generated
View File

@@ -7,7 +7,10 @@
</component>
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />

6
.idea/misc.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.9" />
</component>
</project>

2182
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
Cargo.toml Normal file
View File

@@ -0,0 +1,38 @@
[package]
name = "roadsign"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.5"
actix-proxy = "0.2.0"
actix-web = { version = "4.5.1", features = ["rustls-0_22"] }
actix-web-httpauth = "0.8.1"
awc = { version = "3.4.0", features = ["tls-rustls-0_22"] }
config = { version = "0.14.0", features = ["toml"] }
lazy_static = "1.4.0"
mime = "0.3.17"
percent-encoding = "2.3.1"
queryst = "3.0.0"
rand = "0.8.5"
regex = "1.10.2"
serde = "1.0.195"
serde_json = "1.0.111"
tokio = { version = "1.35.1", features = [
"rt-multi-thread",
"macros",
"time",
"full",
] }
toml = "0.8.8"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
wildmatch = "2.3.0"
derive_more = "0.99.17"
rustls = "0.22.2"
rustls-pemfile = "2.0.0"
futures = "0.3.30"
actix-web-actors = "4.3.0"
actix = "0.13.3"

View File

@@ -1,21 +1,13 @@
# Building Backend
FROM golang:alpine as roadsign-server
FROM rust:alpine as roadsign-server
RUN apk add nodejs npm
RUN apk add libressl-dev build-base
WORKDIR /source
COPY . .
WORKDIR /source/pkg/sideload/view
RUN npm install
RUN npm run build
WORKDIR /source
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/server/main.go
# Runtime
FROM golang:alpine
COPY --from=roadsign-server /dist /roadsign/server
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN cargo build --release
EXPOSE 81
CMD ["/roadsign/server"]
CMD ["/source/target/release/roadsign"]

View File

@@ -5,19 +5,15 @@ A blazing fast reverse proxy with a lot of shining features.
## Features
1. Reverse proxy
2. HTTP2 Support
3. WebSocket Support
4. Static File Hosting
5. Low Configuration
6. Analytics and Metrics
7. Integrate with CI/CD
8. Web management panel (Work in progres for v2, available in v1)
9. One-liner CLI
10. Open-source and free
11. **Blazing fast ⚡**
2. Static file hosting
3. ~~Analytics and Metrics~~
4. Integrate with CI/CD
5. Webhook integration
6. ~~Web management panel~~
7. One-liner CLI
8. **Blazing fast ⚡**
> [!IMPORTANT]
> Currently roadsign haven't supported for server-side events. We are working on it.
> Deleted item means under construction, check out our roadmap!
### How fast is it?
@@ -46,7 +42,7 @@ We strongly recommend you install RoadSign via docker compose.
version: "3"
services:
roadsign:
image: xsheep2010/roadsign:nightly
image: code.smartsheep.studio/goatworks/roadsign:nightly
restart: always
volumes:
- "./certs:/certs" # Optional, use for storage certificates
@@ -59,11 +55,12 @@ services:
- "81:81"
```
After that, you can manage your roadsign instance with RoadSign CLI aka. RDC.
After that, you can manage your roadsign instance with RoadSign CLI aka. RDS CLI.
To install it, run this command. (Make sure you have golang toolchain on your computer)
```shell
go install code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rdc@latest
go install -buildvcs code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest
# Tips: Add `buildvsc` flag to provide more detail compatibility check.
```
## Usage
@@ -76,10 +73,10 @@ After configure, you need sync your config to remote server. Before that, add a
rds cli with this command.
```shell
rdc connect <id> <url> <password>
# ID will allow you find this server in after commands.
# URL is to your roadsign server sideload api.
# Password is your roadsign server credential.
rds connect <id> <url> <password>
# ID will allow you find this server.py.rs in after commands.
# URL is to your roadsign server.py.rs sideload api.
# Password is your roadsign server.py.rs credential.
# ======================================================================
# !WARNING! All these things will storage in your $HOME/.roadsignrc.yaml
# ======================================================================
@@ -88,8 +85,8 @@ rdc connect <id> <url> <password>
Then, sync your local config to remote.
```shell
rdc sync <server id> <region id> <config file>
# Server ID is your server added by last command.
rds sync <server.py.rs id> <site id> <config file>
# Server ID is your server.py.rs added by last command.
# Site ID is your new site id or old site id if you need update it.
# Config File is your local config file path.
```

17
Settings.toml Normal file
View File

@@ -0,0 +1,17 @@
regions = "./regions"
secret = "aEXcED5xJ3"
[sideload]
bind_addr = "0.0.0.0:81"
[[proxies.bind]]
addr = "0.0.0.0:80"
tls = false
[[proxies.bind]]
addr = "0.0.0.0:443"
tls = false
[[certificates]]
domain = "localhost"
certs = "certs/fullchain.pem"
key = "certs/privkey.pem"

View File

@@ -1,31 +0,0 @@
id = "example-region"
[[locations]]
id = "example-websocket"
host = ["localhost:8000"]
path = ["/ws"]
[[locations.destinations]]
id = "example-websocket-destination"
uri = "http://localhost:8765"
[[locations]]
id = "example-warden"
host = ["localhost:4321"]
path = ["/"]
[[locations.destinations]]
id = "example-warden-destination"
uri = "http://localhost:4321"
[[applications]]
id = "example-warden-app"
workdir = "test/data/warden"
command = ["node", "dist/server/entry.mjs"]
environment = ["PUBLIC_CMS=https://smartsheep.studio"]
[[locations]]
id = "example-static"
host = ["localhost:8000"]
path = ["/roadsign"]
[[locations.destinations]]
id = "example-static-destination"
uri = "files://test/data"

61
go.mod
View File

@@ -1,61 +0,0 @@
module code.smartsheep.studio/goatworks/roadsign
go 1.21.4
require (
github.com/gofiber/contrib/websocket v1.3.0
github.com/gofiber/fiber/v2 v2.52.0
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.38.1
github.com/saracen/fastzip v0.1.11
github.com/spf13/viper v1.17.0
github.com/urfave/cli/v2 v2.26.0
github.com/valyala/fasthttp v1.51.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/fasthttp/websocket v1.5.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sync v0.5.0 // indirect
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.1
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

170
go.sum
View File

@@ -1,170 +0,0 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.7 h1:0a6o2OfeATvtGgoMKleURhLT6JqWPg7fYfWnH4KHau4=
github.com/fasthttp/websocket v1.5.7/go.mod h1:bC4fxSono9czeXHQUVKxsC0sNjbm7lPJR04GDFqClfU=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/contrib/websocket v1.3.0 h1:XADFAGorer1VJ1bqC4UkCjqS37kwRTV0415+050NrMk=
github.com/gofiber/contrib/websocket v1.3.0/go.mod h1:xguaOzn2ZZ759LavtosEP+rcxIgBEE/rdumPINhR+Xo=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/saracen/fastzip v0.1.11 h1:NnExbTEJbya7148cov09BCxwfur9tQ5BQ1QyQH6XleA=
github.com/saracen/fastzip v0.1.11/go.mod h1:/lN5BiU451/OZMS+hfhVsSDj/RNrxYmO9EYxCtMrFrY=
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea h1:8czYLkvzZRE+AElIQeDffQdgR+CC3wKEFILYU/1PeX4=
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea/go.mod h1:hnzuad9d2wdd3z8fC6UouHQK5qZxqv3F/E6MMzXc7q0=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,89 +0,0 @@
package conn
import (
"encoding/json"
"fmt"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
)
var CliCommands = []*cli.Command{
{
Name: "list",
Aliases: []string{"ls"},
Description: "List all connected remote server",
Action: func(ctx *cli.Context) error {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
log.Info().Msgf("There are %d server(s) connected in total.", len(servers))
for idx, server := range servers {
log.Info().Msgf("%d) %s: %s", idx+1, server.ID, server.Url)
}
return nil
},
},
{
Name: "connect",
Aliases: []string{"add"},
Description: "Connect and save configuration of remote server",
ArgsUsage: "<id> <server url> <credential>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 3 {
return fmt.Errorf("must have three arguments: <id> <server url> <credential>")
}
c := CliConnection{
ID: ctx.Args().Get(0),
Url: ctx.Args().Get(1),
Credential: ctx.Args().Get(2),
}
if err := c.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
} else {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
viper.Set("servers", append(servers, c))
if err := viper.WriteConfig(); err != nil {
return err
} else {
log.Info().Msg("Successfully connected a new remote server, enter \"rds ls\" to get more info.")
return nil
}
}
},
},
{
Name: "disconnect",
Aliases: []string{"remove"},
Description: "Remove a remote server configuration",
ArgsUsage: "<id>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 1 {
return fmt.Errorf("must have more one arguments: <server url>")
}
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
viper.Set("servers", lo.Filter(servers, func(item CliConnection, idx int) bool {
return item.ID != ctx.Args().Get(0)
}))
if err := viper.WriteConfig(); err != nil {
return err
} else {
log.Info().Msg("Successfully disconnected a remote server, enter \"rds ls\" to get more info.")
return nil
}
},
},
}

View File

@@ -1,42 +0,0 @@
package conn
import (
"encoding/json"
"fmt"
"strings"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type CliConnection struct {
ID string `json:"id"`
Url string `json:"url"`
Credential string `json:"credential"`
}
func (v CliConnection) CheckConnectivity() error {
client := fiber.Get(v.Url + "/cgi/metadata")
client.BasicAuth("RoadSign CLI", v.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("couldn't connect to server: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, may cause by invalid credential")
} else {
var resp fiber.Map
if err := json.Unmarshal(data, &resp); err != nil {
return err
} else if resp["server"] != "RoadSign" {
return fmt.Errorf("remote server isn't roadsign")
} else if resp["version"] != roadsign.AppVersion {
if strings.Contains(roadsign.AppVersion, "#") {
return fmt.Errorf("remote server version mismatch client version, update or downgrade client required")
} else {
log.Warn().Msg("RoadSign CLI didn't complied with vcs information, compatibility was disabled. To enable it, reinstall cli with -buildvcs flag.")
}
}
}
return nil
}

View File

@@ -1,17 +0,0 @@
package conn
import (
"encoding/json"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func GetConnection(id string) (CliConnection, bool) {
var servers []CliConnection
raw, _ := json.Marshal(viper.Get("servers"))
_ = json.Unmarshal(raw, &servers)
return lo.Find(servers, func(item CliConnection) bool {
return item.ID == id
})
}

View File

@@ -1,98 +0,0 @@
package deploy
import (
"fmt"
"io"
"os"
"strings"
jsoniter "github.com/json-iterator/go"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rdc/conn"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
)
var DeployCommands = []*cli.Command{
{
Name: "deploy",
Aliases: []string{"dp"},
ArgsUsage: "<server> <site> <upstream> [path]",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 4 {
return fmt.Errorf("must have four arguments: <server> <site> <upstream> <path>")
}
if !strings.HasSuffix(ctx.Args().Get(3), ".zip") {
return fmt.Errorf("input file must be a zip file and ends with .zip")
}
server, ok := conn.GetConnection(ctx.Args().Get(0))
if !ok {
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
// Send request
log.Info().Msg("Now publishing to remote server...")
url := fmt.Sprintf("/webhooks/publish/%s/%s?mimetype=%s", ctx.Args().Get(1), ctx.Args().Get(2), "application/zip")
client := fiber.Put(server.Url+url).
SendFile(ctx.Args().Get(3), "attachments").
MultipartForm(nil).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to publish to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site is successfully published! 🎉")
return nil
},
},
{
Name: "sync",
ArgsUsage: "<server> <site> <configuration path>",
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() < 3 {
return fmt.Errorf("must have three arguments: <server> <site> <configuration path>")
}
server, ok := conn.GetConnection(ctx.Args().Get(0))
if !ok {
return fmt.Errorf("server was not found, use \"rds connect\" add one first")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
var raw []byte
if file, err := os.Open(ctx.Args().Get(2)); err != nil {
return err
} else {
raw, _ = io.ReadAll(file)
}
url := fmt.Sprintf("/webhooks/sync/%s", ctx.Args().Get(1))
client := fiber.Put(server.Url+url).
JSONEncoder(jsoniter.ConfigCompatibleWithStandardLibrary.Marshal).
JSONDecoder(jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal).
Body(raw).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to sync to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site configuration is up-to-date! 🎉")
return nil
},
},
}

View File

@@ -1,48 +0,0 @@
package main
import (
"os"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rdc/conn"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rdc/deploy"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
)
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
}
func main() {
// Configure settings
viper.AddConfigPath("$HOME")
viper.SetConfigName(".roadsignrc")
viper.SetConfigType("toml")
// Load settings
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
viper.SafeWriteConfig()
viper.ReadInConfig()
} else {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
}
// Configure CLI
app := &cli.App{
Name: "RoadSign CLI",
Version: roadsign.AppVersion,
Suggest: true,
Commands: append(append([]*cli.Command{}, conn.CliCommands...), deploy.DeployCommands...),
}
// Run CLI
if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("An error occurred when running cli.")
}
}

View File

@@ -1,81 +0,0 @@
package main
import (
"os"
"os/signal"
"strings"
"syscall"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/hypertext"
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"code.smartsheep.studio/goatworks/roadsign/pkg/sideload"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
}
func main() {
// Configure settings
viper.AddConfigPath(".")
viper.AddConfigPath("..")
viper.SetConfigName("settings")
viper.SetConfigType("toml")
// Load settings
if err := viper.ReadInConfig(); err != nil {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
// Present settings
if len(viper.GetString("security.credential")) <= 0 {
credential := strings.ReplaceAll(uuid.NewString(), "-", "")
viper.Set("security.credential", credential)
_ = viper.WriteConfig()
log.Warn().Msg("There isn't any api credential configured in settings.yml, auto generated a credential for api accessing.")
log.Warn().Msgf("RoadSign auto generated api credential is %s", credential)
}
// Load & init navigator
if err := navi.ReadInConfig(viper.GetString("paths.configs")); err != nil {
log.Panic().Err(err).Msg("An error occurred when loading configurations.")
} else {
log.Info().Int("count", len(navi.R.Regions)).Msg("All configuration has been loaded.")
}
// Init warden
navi.InitializeWarden(navi.R.Regions)
// Init hypertext server
hypertext.RunServer(
hypertext.InitServer(),
viper.GetStringSlice("hypertext.ports"),
viper.GetStringSlice("hypertext.secured_ports"),
viper.GetString("hypertext.certificate.pem"),
viper.GetString("hypertext.certificate.key"),
)
// Init sideload server
hypertext.RunServer(
sideload.InitSideload(),
viper.GetStringSlice("hypertext.sideload_ports"),
viper.GetStringSlice("hypertext.sideload_secured_ports"),
viper.GetString("hypertext.certificate.sideload_pem"),
viper.GetString("hypertext.certificate.sideload_key"),
)
log.Info().Msgf("RoadSign v%s is started...", roadsign.AppVersion)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info().Msgf("RoadSign v%s is quitting...", roadsign.AppVersion)
}

View File

@@ -1,139 +0,0 @@
package hypertext
import (
"github.com/spf13/viper"
"math/rand"
"regexp"
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func UseProxies(app *fiber.App) {
app.All("/*", func(ctx *fiber.Ctx) error {
host := ctx.Hostname()
path := ctx.Path()
queries := ctx.Queries()
headers := ctx.GetReqHeaders()
// Filtering sites
for _, region := range navi.R.Regions {
// Matching rules
for _, location := range region.Locations {
if !lo.Contains(location.Host, host) {
continue
}
if !func() bool {
flag := false
for _, pattern := range location.Path {
if ok, _ := regexp.MatchString(pattern, path); ok {
flag = true
break
}
}
return flag
}() {
continue
}
// Filter query strings
flag := true
for rk, rv := range location.Queries {
for ik, iv := range queries {
if rk != ik && rv != iv {
flag = false
break
}
}
if !flag {
break
}
}
if !flag {
continue
}
// Filter headers
for rk, rv := range location.Headers {
for ik, iv := range headers {
if rk == ik {
for _, ov := range iv {
if !lo.Contains(rv, ov) {
flag = false
break
}
}
}
if !flag {
break
}
}
if !flag {
break
}
}
if !flag {
continue
}
idx := rand.Intn(len(location.Destinations))
dest := location.Destinations[idx]
// Passing all the rules means the site is what we are looking for.
// Let us respond to our client!
return makeResponse(ctx, region, &location, &dest)
}
}
// There is no site available for this request.
// Just ignore it and give our client a not found status.
// Do not care about the user experience, we can do it in custom error handler.
return fiber.ErrNotFound
})
}
func makeResponse(ctx *fiber.Ctx, region *navi.Region, location *navi.Location, dest *navi.Destination) error {
uri := ctx.Request().URI().String()
// Modify request
for _, transformer := range dest.Transformers {
if err := transformer.TransformRequest(ctx); err != nil {
return err
}
}
// Forward
err := navi.R.Forward(ctx, dest)
// Modify response
for _, transformer := range dest.Transformers {
if err := transformer.TransformResponse(ctx); err != nil {
return err
}
}
// Collect trace
if viper.GetBool("telemetry.capture_traces") {
var message string
if err != nil {
message = err.Error()
}
go navi.R.AddTrace(navi.RoadTrace{
Region: region.ID,
Location: location.ID,
Destination: dest.ID,
Uri: uri,
IpAddress: ctx.IP(),
UserAgent: ctx.Get(fiber.HeaderUserAgent),
Error: navi.RoadTraceError{
IsNull: err == nil,
Message: message,
},
})
}
return err
}

View File

@@ -1,79 +0,0 @@
package hypertext
import (
jsoniter "github.com/json-iterator/go"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func InitServer() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign",
ServerHeader: "RoadSign",
DisableStartupMessage: true,
EnableIPValidation: true,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
Prefork: viper.GetBool("performance.prefork"),
BodyLimit: viper.GetInt("hypertext.limitation.max_body_size"),
})
if viper.GetBool("performance.request_logging") {
app.Use(logger.New(logger.Config{
Output: log.Logger,
Format: "[Proxies] [${time}] ${status} - ${latency} ${method} ${path}\n",
}))
}
if viper.GetInt("hypertext.limitation.max_qps") > 0 {
app.Use(limiter.New(limiter.Config{
Max: viper.GetInt("hypertext.limitation.max_qps"),
Expiration: 1 * time.Second,
}))
}
UseProxies(app)
return app
}
func RunServer(app *fiber.App, ports []string, securedPorts []string, pem string, key string) {
for _, port := range ports {
port := port
go func() {
if viper.GetBool("hypertext.certificate.redirect") {
redirector := fiber.New(fiber.Config{
AppName: "RoadSign",
ServerHeader: "RoadSign",
DisableStartupMessage: true,
EnableIPValidation: true,
})
redirector.All("/", func(c *fiber.Ctx) error {
return c.Redirect(strings.ReplaceAll(string(c.Request().URI().FullURI()), "http", "https"))
})
if err := redirector.Listen(port); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext common ports.")
}
} else {
if err := app.Listen(port); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext common ports.")
}
}
}()
}
for _, port := range securedPorts {
port := port
go func() {
if err := app.ListenTLS(port, pem, key); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.")
}
}()
}
}

View File

@@ -1,17 +0,0 @@
package roadsign
import (
"runtime/debug"
)
func init() {
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
AppVersion += "#" + setting.Value
}
}
}
}
var AppVersion = "2.0.0-delta1"

View File

@@ -1,52 +0,0 @@
package navi
import (
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
"github.com/pelletier/go-toml/v2"
)
var R *RoadApp
func ReadInConfig(root string) error {
instance := &RoadApp{
Regions: make([]*Region, 0),
Traces: make([]RoadTrace, 0, viper.GetInt("performance.traces_limit")),
}
if err := filepath.Walk(root, func(fp string, info os.FileInfo, _ error) error {
var region Region
if info.IsDir() {
return nil
} else if !strings.HasSuffix(info.Name(), ".toml") {
return nil
} else if file, err := os.OpenFile(fp, os.O_RDONLY, 0755); err != nil {
return err
} else if data, err := io.ReadAll(file); err != nil {
return err
} else if err := toml.Unmarshal(data, &region); err != nil {
return err
} else {
defer file.Close()
if region.Disabled {
return nil
}
instance.Regions = append(instance.Regions, &region)
}
return nil
}); err != nil {
return err
}
R = instance
return nil
}

View File

@@ -1,25 +0,0 @@
package navi
import "github.com/spf13/viper"
type RoadTrace struct {
Region string `json:"region"`
Location string `json:"location"`
Destination string `json:"destination"`
Uri string `json:"uri"`
IpAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Error RoadTraceError `json:"error"`
}
type RoadTraceError struct {
IsNull bool `json:"is_null"`
Message string `json:"message"`
}
func (v *RoadApp) AddTrace(trace RoadTrace) {
v.Traces = append(v.Traces, trace)
if len(v.Traces) > viper.GetInt("performance.traces_limit") {
v.Traces = v.Traces[1:]
}
}

View File

@@ -1,204 +0,0 @@
package navi
import (
"errors"
"fmt"
"github.com/fasthttp/websocket"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
"github.com/gofiber/fiber/v2/utils"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"github.com/valyala/fasthttp"
"io/fs"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
)
func makeUnifiedResponse(c *fiber.Ctx, dest *Destination) error {
if websocket.FastHTTPIsWebSocketUpgrade(c.Context()) {
// Handle websocket
return makeWebsocketResponse(c, dest)
} else {
_, queries := dest.GetRawUri()
if len(queries.Get("sse")) > 0 {
// Handle server-side event
return makeSeverSideEventResponse(c, dest)
} else {
// Handle normal http request
return makeHypertextResponse(c, dest)
}
}
}
func makeHypertextResponse(c *fiber.Ctx, dest *Destination) error {
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
return proxy.Do(c, dest.MakeUri(c), &fasthttp.Client{
ReadTimeout: timeout,
WriteTimeout: timeout,
})
}
var wsUpgrader = websocket.FastHTTPUpgrader{}
func makeWebsocketResponse(c *fiber.Ctx, dest *Destination) error {
uri := dest.MakeWebsocketUri(c)
// Upgrade connection
return wsUpgrader.Upgrade(c.Context(), func(conn *websocket.Conn) {
// Dial the destination
remote, _, err := websocket.DefaultDialer.Dial(uri, nil)
if err != nil {
return
}
defer remote.Close()
// Read messages from remote
disconnect := make(chan struct{})
signal := make(chan struct {
head int
data []byte
})
go func() {
defer close(disconnect)
for {
mode, message, err := remote.ReadMessage()
if err != nil {
log.Warn().Err(err).Msg("An error occurred during the websocket proxying...")
return
} else {
signal <- struct {
head int
data []byte
}{head: mode, data: message}
}
}
}()
// Relay the destination websocket to client
for {
select {
case <-disconnect:
case val := <-signal:
if err := conn.WriteMessage(val.head, val.data); err != nil {
return
}
default:
if head, data, err := conn.ReadMessage(); err != nil {
return
} else {
remote.WriteMessage(head, data)
}
}
}
})
}
func makeSeverSideEventResponse(c *fiber.Ctx, dest *Destination) error {
// TODO Impl SSE with https://github.com/gofiber/recipes/blob/master/sse/main.go
return fiber.NewError(fiber.StatusNotImplemented, "Server-side-events was not available now.")
}
func makeFileResponse(c *fiber.Ctx, dest *Destination) error {
uri, queries := dest.GetRawUri()
root := http.Dir(uri)
method := c.Method()
// We only serve static assets for GET and HEAD methods
if method != fiber.MethodGet && method != fiber.MethodHead {
return c.Next()
}
// Strip prefix
prefix := c.Route().Path
path := strings.TrimPrefix(c.Path(), prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Add prefix
if queries.Get("prefix") != "" {
path = queries.Get("prefix") + path
}
if len(path) > 1 {
path = utils.TrimRight(path, '/')
}
file, err := root.Open(path)
if err != nil && errors.Is(err, fs.ErrNotExist) {
if queries.Get("suffix") != "" {
file, err = root.Open(path + queries.Get("suffix"))
}
if err != nil && queries.Get("fallback") != "" {
file, err = root.Open(queries.Get("fallback"))
}
}
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return fiber.ErrNotFound
}
return fmt.Errorf("failed to open: %w", err)
}
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to stat: %w", err)
}
// Serve index if path is directory
if stat.IsDir() {
indexFile := lo.Ternary(len(queries.Get("index")) > 0, queries.Get("index"), "index.html")
indexPath := utils.TrimRight(path, '/') + indexFile
index, err := root.Open(indexPath)
if err == nil {
indexStat, err := index.Stat()
if err == nil {
file = index
stat = indexStat
}
}
}
c.Status(fiber.StatusOK)
modTime := stat.ModTime()
contentLength := int(stat.Size())
// Set Content-Type header
if queries.Get("charset") == "" {
c.Type(filepath.Ext(stat.Name()))
} else {
c.Type(filepath.Ext(stat.Name()), queries.Get("charset"))
}
// Set Last-Modified header
if !modTime.IsZero() {
c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
}
if method == fiber.MethodGet {
maxAge, err := strconv.Atoi(queries.Get("maxAge"))
if lo.Ternary(err != nil, maxAge, 0) > 0 {
c.Set(fiber.HeaderCacheControl, "public, max-age="+queries.Get("maxAge"))
}
c.Response().SetBodyStream(file, contentLength)
return nil
}
if method == fiber.MethodHead {
c.Request().ResetBody()
c.Response().SkipBody = true
c.Response().Header.SetContentLength(contentLength)
if err := file.Close(); err != nil {
return fmt.Errorf("failed to close: %w", err)
}
return nil
}
return fiber.ErrNotFound
}

View File

@@ -1,25 +0,0 @@
package navi
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/navi/transformers"
"github.com/gofiber/fiber/v2"
)
type RoadApp struct {
Regions []*Region `json:"regions"`
Traces []RoadTrace `json:"traces"`
}
func (v *RoadApp) Forward(ctx *fiber.Ctx, dest *Destination) error {
switch dest.GetType() {
case DestinationHypertext:
return makeUnifiedResponse(ctx, dest)
case DestinationStaticFile:
return makeFileResponse(ctx, dest)
default:
return fiber.ErrBadGateway
}
}
type RequestTransformerConfig = transformers.TransformerConfig

View File

@@ -1,87 +0,0 @@
package navi
import (
"fmt"
"net/url"
"strings"
"code.smartsheep.studio/goatworks/roadsign/pkg/navi/transformers"
"code.smartsheep.studio/goatworks/roadsign/pkg/warden"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
type Region struct {
ID string `json:"id" toml:"id"`
Disabled bool `json:"disabled" toml:"disabled"`
Locations []Location `json:"locations" toml:"locations"`
Applications []warden.Application `json:"applications" toml:"applications"`
}
type Location struct {
ID string `json:"id" toml:"id"`
Host []string `json:"host" toml:"host"`
Path []string `json:"path" toml:"path"`
Queries map[string]string `json:"queries" toml:"queries"`
Headers map[string][]string `json:"headers" toml:"headers"`
Destinations []Destination `json:"destinations" toml:"destinations"`
}
type DestinationType = int8
const (
DestinationHypertext = DestinationType(iota)
DestinationStaticFile
DestinationUnknown
)
type Destination struct {
ID string `json:"id" toml:"id"`
Uri string `json:"uri" toml:"uri"`
Transformers []transformers.TransformerConfig `json:"transformers" toml:"transformers"`
}
func (v *Destination) GetProtocol() string {
return strings.SplitN(v.Uri, "://", 2)[0]
}
func (v *Destination) GetType() DestinationType {
protocol := v.GetProtocol()
switch protocol {
case "http", "https":
return DestinationHypertext
case "file", "files":
return DestinationStaticFile
}
return DestinationUnknown
}
func (v *Destination) GetRawUri() (string, url.Values) {
uri := strings.SplitN(v.Uri, "://", 2)[1]
data := strings.SplitN(uri, "?", 2)
data = append(data, " ") // Make data array least have two element
qs, _ := url.ParseQuery(data[1])
return data[0], qs
}
func (v *Destination) MakeUri(ctx *fiber.Ctx) string {
var queries []string
for k, v := range ctx.Queries() {
parsed, _ := url.QueryUnescape(v)
value := url.QueryEscape(parsed)
queries = append(queries, fmt.Sprintf("%s=%s", k, value))
}
path := string(ctx.Request().URI().Path())
hash := string(ctx.Request().URI().Hash())
uri, _ := v.GetRawUri()
return uri + path +
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
lo.Ternary(len(hash) > 0, "#"+hash, "")
}
func (v *Destination) MakeWebsocketUri(ctx *fiber.Ctx) string {
return strings.Replace(v.MakeUri(ctx), "http", "ws", 1)
}

View File

@@ -1,41 +0,0 @@
package transformers
import (
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
)
var CompressResponse = Transformer{
ModifyResponse: func(options any, ctx *fiber.Ctx) error {
opts := DeserializeOptions[struct {
Level int `json:"level" toml:"level"`
}](options)
fctx := func(c *fasthttp.RequestCtx) {}
var compressor fasthttp.RequestHandler
switch opts.Level {
// Best Speed Mode
case 1:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliBestSpeed,
fasthttp.CompressBestSpeed,
)
// Best Compression Mode
case 2:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliBestCompression,
fasthttp.CompressBestCompression,
)
// Default Mode
default:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliDefaultCompression,
fasthttp.CompressDefaultCompression,
)
}
compressor(ctx.Context())
return nil
},
}

View File

@@ -1,61 +0,0 @@
package transformers
import (
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
)
// Definitions
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type Transformer struct {
ModifyRequest func(options any, ctx *fiber.Ctx) error
ModifyResponse func(options any, ctx *fiber.Ctx) error
}
type TransformerConfig struct {
Type string `json:"type" toml:"type"`
Options any `json:"options" toml:"options"`
}
func (v *TransformerConfig) TransformRequest(ctx *fiber.Ctx) error {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyRequest != nil {
return f.ModifyRequest(v.Options, ctx)
}
break
}
}
return nil
}
func (v *TransformerConfig) TransformResponse(ctx *fiber.Ctx) error {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyResponse != nil {
return f.ModifyResponse(v.Options, ctx)
}
break
}
}
return nil
}
// Helpers
func DeserializeOptions[T any](data any) T {
var out T
raw, _ := json.Marshal(data)
_ = json.Unmarshal(raw, &out)
return out
}
// Map of Transformers
// Every transformer need to be mapped here so that they can get work.
var Transformers = map[string]Transformer{
"replacePath": ReplacePath,
"compressResponse": CompressResponse,
}

View File

@@ -1,26 +0,0 @@
package transformers
import (
"regexp"
"strings"
"github.com/gofiber/fiber/v2"
)
var ReplacePath = Transformer{
ModifyRequest: func(options any, ctx *fiber.Ctx) error {
opts := DeserializeOptions[struct {
Pattern string `json:"pattern" toml:"pattern"`
Value string `json:"value" toml:"value"`
Repl string `json:"repl" toml:"repl"` // Use when complex mode(regexp) enabled
Complex bool `json:"complex" toml:"complex"`
}](options)
path := string(ctx.Request().URI().Path())
if !opts.Complex {
ctx.Path(strings.ReplaceAll(path, opts.Pattern, opts.Value))
} else if ex := regexp.MustCompile(opts.Pattern); ex != nil {
ctx.Path(ex.ReplaceAllString(path, opts.Repl))
}
return nil
},
}

View File

@@ -1,17 +0,0 @@
package navi
import "code.smartsheep.studio/goatworks/roadsign/pkg/warden"
func InitializeWarden(regions []*Region) {
for _, region := range regions {
for _, application := range region.Applications {
warden.InstancePool = append(warden.InstancePool, &warden.AppInstance{
Manifest: application,
})
}
}
for _, instance := range warden.InstancePool {
instance.Wake()
}
}

View File

@@ -1,26 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"code.smartsheep.studio/goatworks/roadsign/pkg/warden"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getApplications(c *fiber.Ctx) error {
applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.Application {
return item.Applications
})
return c.JSON(applications)
}
func getApplicationLogs(c *fiber.Ctx) error {
if instance, ok := lo.Find(warden.InstancePool, func(item *warden.AppInstance) bool {
return item.Manifest.ID == c.Params("id")
}); !ok {
return fiber.NewError(fiber.StatusNotFound)
} else {
return c.SendString(instance.Logs())
}
}

View File

@@ -1,13 +0,0 @@
package sideload
import (
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
)
func getMetadata(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"server": "RoadSign",
"version": roadsign.AppVersion,
})
}

View File

@@ -1,10 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"github.com/gofiber/fiber/v2"
)
func getTraces(c *fiber.Ctx) error {
return c.JSON(navi.R.Traces)
}

View File

@@ -1,91 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/warden"
"context"
"os"
"path/filepath"
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/samber/lo"
"github.com/saracen/fastzip"
)
func doPublish(c *fiber.Ctx) error {
var workdir string
var destination *navi.Destination
var application *warden.Application
for _, item := range navi.R.Regions {
if item.ID == c.Params("site") {
for _, location := range item.Locations {
for _, dest := range location.Destinations {
if dest.ID == c.Params("slug") {
destination = &dest
workdir, _ = dest.GetRawUri()
break
}
}
}
for _, app := range item.Applications {
if app.ID == c.Params("slug") {
application = &app
workdir = app.Workdir
break
}
}
break
}
}
var instance *warden.AppInstance
if application != nil {
if instance = warden.GetFromPool(application.ID); instance != nil {
instance.Stop()
}
} else if destination != nil && destination.GetType() != navi.DestinationStaticFile {
return fiber.ErrUnprocessableEntity
} else {
return fiber.ErrNotFound
}
if c.Query("overwrite", "yes") == "yes" {
files, _ := filepath.Glob(filepath.Join(workdir, "*"))
for _, file := range files {
_ = os.Remove(file)
}
}
if form, err := c.MultipartForm(); err == nil {
files := form.File["attachments"]
for _, file := range files {
mimetype := lo.Ternary(len(c.Query("mimetype")) > 0, c.Query("mimetype"), file.Header["Content-Type"][0])
switch mimetype {
case "application/zip":
dst := filepath.Join(os.TempDir(), uuid.NewString()+".zip")
if err := c.SaveFile(file, dst); err != nil {
return err
} else {
if ex, err := fastzip.NewExtractor(dst, workdir); err != nil {
return err
} else if err = ex.Extract(context.Background()); err != nil {
defer ex.Close()
return err
}
}
default:
dst := filepath.Join(workdir, file.Filename)
if err := c.SaveFile(file, dst); err != nil {
return err
}
}
}
}
if instance != nil {
instance.Wake()
}
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,67 +0,0 @@
package sideload
import (
"fmt"
"os"
"path/filepath"
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"code.smartsheep.studio/goatworks/roadsign/pkg/warden"
"github.com/gofiber/fiber/v2"
"github.com/pelletier/go-toml/v2"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func getRegions(c *fiber.Ctx) error {
return c.JSON(navi.R.Regions)
}
func getRegionConfig(c *fiber.Ctx) error {
fp := filepath.Join(viper.GetString("paths.configs"), c.Params("id"))
var err error
var data []byte
if data, err = os.ReadFile(fp + ".toml"); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.Type("toml").SendString(string(data))
}
func doSync(c *fiber.Ctx) error {
req := string(c.Body())
id := c.Params("slug")
path := filepath.Join(viper.GetString("paths.configs"), fmt.Sprintf("%s.toml", id))
if file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil {
return fiber.NewError(fiber.ErrInternalServerError.Code, err.Error())
} else {
raw, _ := toml.Marshal(req)
file.Write(raw)
defer file.Close()
}
var rebootQueue []*warden.AppInstance
if region, ok := lo.Find(navi.R.Regions, func(item *navi.Region) bool {
return item.ID == id
}); ok {
for _, application := range region.Applications {
if instance := warden.GetFromPool(application.ID); instance != nil {
instance.Stop()
rebootQueue = append(rebootQueue, instance)
}
}
}
// Reload
navi.ReadInConfig(viper.GetString("paths.configs"))
// Reboot
for _, instance := range rebootQueue {
instance.Wake()
}
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,71 +0,0 @@
package sideload
import (
"fmt"
"net/http"
"code.smartsheep.studio/goatworks/roadsign/pkg/sideload/view"
"github.com/gofiber/fiber/v2/middleware/filesystem"
jsoniter "github.com/json-iterator/go"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func InitSideload() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign Sideload",
ServerHeader: "RoadSign Sideload",
DisableStartupMessage: true,
EnableIPValidation: true,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
TrustedProxies: viper.GetStringSlice("security.sideload_trusted_proxies"),
BodyLimit: viper.GetInt("hypertext.limitation.max_body_size"),
})
if viper.GetBool("performance.request_logging") {
app.Use(logger.New(logger.Config{
Output: log.Logger,
Format: "[Sideload] [${time}] ${status} - ${latency} ${method} ${path}\n",
}))
}
app.Use(basicauth.New(basicauth.Config{
Realm: fmt.Sprintf("RoadSign v%s", roadsign.AppVersion),
Authorizer: func(_, password string) bool {
return password == viper.GetString("security.credential")
},
}))
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(view.FS),
PathPrefix: "dist",
Index: "index.html",
NotFoundFile: "index.html",
}))
cgi := app.Group("/cgi").Name("CGI")
{
cgi.Get("/metadata", getMetadata)
cgi.Get("/traces", getTraces)
cgi.Get("/stats", getStats)
cgi.Get("/regions", getRegions)
cgi.Get("/regions/cfg/:id", getRegionConfig)
cgi.Get("/applications", getApplications)
cgi.Get("/applications/logs/:id", getApplicationLogs)
}
webhooks := app.Group("/webhooks").Name("WebHooks")
{
webhooks.Put("/publish/:site/:slug", doPublish)
webhooks.Put("/sync/:slug", doSync)
}
return app
}

View File

@@ -1,27 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/navi"
"code.smartsheep.studio/goatworks/roadsign/pkg/warden"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getStats(c *fiber.Ctx) error {
locations := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []navi.Location {
return item.Locations
})
destinations := lo.FlatMap(locations, func(item navi.Location, idx int) []navi.Destination {
return item.Destinations
})
applications := lo.FlatMap(navi.R.Regions, func(item *navi.Region, idx int) []warden.Application {
return item.Applications
})
return c.JSON(fiber.Map{
"regions": len(navi.R.Regions),
"locations": len(locations),
"destinations": len(destinations),
"applications": len(applications),
})
}

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,6 +0,0 @@
package view
import "embed"
//go:embed all:dist
var FS embed.FS

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RoadSign Sideload</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -1,24 +0,0 @@
{
"name": "roadsign-sideload",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@solidjs/router": "^0.10.10",
"solid-js": "^1.8.7"
},
"devDependencies": {
"autoprefixer": "^10.4.17",
"daisyui": "^4.6.0",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.0.8",
"vite-plugin-solid": "^2.8.0"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,17 +0,0 @@
import "./index.css";
/* @refresh reload */
import { render } from "solid-js/web";
import { Route, Router } from "@solidjs/router";
import RootLayout from "./layouts/RootLayout";
import Dashboard from "./pages/dashboard";
const root = document.getElementById("root");
render(() => (
<Router root={RootLayout}>
<Route path="/" component={Dashboard} />
</Router>
), root!);

View File

@@ -1,11 +0,0 @@
import Navbar from "./shared/Navbar";
export default function RootLayout(props: any) {
return (
<div>
<Navbar />
<main class="h-[calc(100vh-64px)]">{props.children}</main>
</div>
);
}

View File

@@ -1,62 +0,0 @@
import { For } from "solid-js";
interface MenuItem {
label: string;
href: string;
}
export default function Navbar() {
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }];
return (
<div class="navbar bg-base-100 shadow-md">
<div class="navbar-start">
<div class="dropdown">
<div tabIndex={0} role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</div>
<ul
tabIndex={0}
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<For each={nav}>
{(item) => (
<li>
<a href={item.href}>{item.label}</a>
</li>
)}
</For>
</ul>
</div>
<a href="/" class="btn btn-ghost text-xl">
RoadSign
</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<For each={nav}>
{(item) => (
<li>
<a href={item.href}>{item.label}</a>
</li>
)}
</For>
</ul>
</div>
<div class="navbar-end"></div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
export default function Dashboard() {
return (
<div class="h-full w-full flex justify-center items-center">
<div class="max-w-96 text-center">
<h1 class="text-2xl font-bold">Hold on</h1>
<p>Our brand new sideload administration panel is still in progress. For now, you can use sideload api and roadsign cli.</p>
</div>
</div>
)
}

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,44 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
daisyui: {
themes: [
{
light: {
...require("daisyui/src/theming/themes")["light"],
primary: "#4750a3",
secondary: "#93c5fd",
accent: "#0f766e",
info: "#67e8f9",
success: "#15803d",
warning: "#f97316",
error: "#dc2626",
"--rounded-box": "0",
"--rounded-btn": "0",
"--rounded-badge": "0",
"--tab-radius": "0",
},
},
{
dark: {
...require("daisyui/src/theming/themes")["dark"],
primary: "#4750a3",
secondary: "#93c5fd",
accent: "#0f766e",
info: "#67e8f9",
success: "#15803d",
warning: "#f97316",
error: "#dc2626",
"--rounded-box": "0",
"--rounded-btn": "0",
"--rounded-badge": "0",
"--tab-radius": "0",
},
},
],
},
plugins: [require("daisyui")],
};

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,6 +0,0 @@
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +0,0 @@
package warden
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/samber/lo"
)
var InstancePool []*AppInstance
func GetFromPool(id string) *AppInstance {
val, ok := lo.Find(InstancePool, func(item *AppInstance) bool {
return item.Manifest.ID == id
})
return lo.Ternary(ok, val, nil)
}
func StartPool() []error {
var errors []error
for _, instance := range InstancePool {
if err := instance.Wake(); err != nil {
errors = append(errors, err)
}
}
return errors
}
type AppStatus = int8
const (
AppCreated = AppStatus(iota)
AppStarting
AppStarted
AppExited
AppFailure
)
type AppInstance struct {
Manifest Application `json:"manifest"`
Cmd *exec.Cmd `json:"-"`
Logger strings.Builder `json:"-"`
Status AppStatus `json:"status"`
}
func (v *AppInstance) Wake() error {
if v.Cmd != nil {
return nil
}
if v.Cmd == nil {
return v.Start()
}
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
return v.Start()
}
if v.Cmd.ProcessState.Exited() {
return v.Start()
} else if v.Cmd.ProcessState.Exited() {
return fmt.Errorf("process already dead")
}
if v.Cmd.ProcessState.Exited() {
return fmt.Errorf("cannot start process")
} else {
return nil
}
}
func (v *AppInstance) Start() error {
manifest := v.Manifest
if len(manifest.Command) <= 0 {
return fmt.Errorf("you need set the command for %s to enable process manager", manifest.ID)
}
v.Cmd = exec.Command(manifest.Command[0], manifest.Command[1:]...)
v.Cmd.Dir = filepath.Join(manifest.Workdir)
v.Cmd.Env = append(v.Cmd.Env, manifest.Environment...)
v.Cmd.Stdout = &v.Logger
v.Cmd.Stderr = &v.Logger
// Monitor
go func() {
for {
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
v.Status = AppStarting
} else if !v.Cmd.ProcessState.Exited() {
v.Status = AppStarted
} else {
v.Status = lo.Ternary(v.Cmd.ProcessState.Success(), AppExited, AppFailure)
return
}
time.Sleep(100 * time.Millisecond)
}
}()
return v.Cmd.Start()
}
func (v *AppInstance) Stop() error {
if v.Cmd != nil && v.Cmd.Process != nil {
if err := v.Cmd.Process.Signal(os.Interrupt); err != nil {
v.Cmd.Process.Kill()
return err
} else {
v.Cmd = nil
}
}
return nil
}
func (v *AppInstance) Logs() string {
return v.Logger.String()
}

View File

@@ -1,8 +0,0 @@
package warden
type Application struct {
ID string `json:"id" toml:"id"`
Workdir string `json:"workdir" toml:"workdir"`
Command []string `json:"command" toml:"command"`
Environment []string `json:"environment" toml:"environment"`
}

12
regions/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello, World!</title>
</head>
<body>
<p>Hello, there!</p>
<p>Here's the roadsign benchmarking test data!</p>
</body>
</html>

21
regions/index.toml Normal file
View File

@@ -0,0 +1,21 @@
id = "index"
[[locations]]
id = "root"
hosts = ["localhost"]
paths = ["/"]
[[locations.destinations]]
id = "websocket"
uri = "http://localhost:8765"
# [[locations.destinations]]
# id = "hypertext"
# uri = "https://example.com"
# [[locations.destinations]]
# id = "static"
# uri = "files://regions?index=index.html"
# [[applications]]
# id = "script"
# exe = "./script.sh"
# workdir = "regions"

1
regions/kokodayo.txt Normal file
View File

@@ -0,0 +1 @@
Ko Ko Da Yo~

3
regions/script.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
echo "Good morning!" > ./kokodayo.txt

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hello, World!</title>
</head>
<body>
<p>Hello, there!</p>
<p>
Here's the roadsign benchmarking test data! And you are in the subfolder
now!
</p>
</body>
</html>

View File

@@ -1,35 +0,0 @@
[debug]
print_routes = false
[hypertext]
sideload_ports = [":81"]
sideload_secured_ports = []
ports = [":8000"]
secured_ports = []
[hypertext.certificate]
redirect = false
sideload_key = "./cert.key"
sideload_pem = "./cert.pem"
key = "./cert.key"
pem = "./cert.pem"
[hypertext.limitation]
max_body_size = 549_755_813_888 # 512 GiB
max_qps = -1
[paths]
configs = "./config"
[telemetry]
request_logging = true
capture_traces = true
[performance]
traces_limit = 256
network_timeout = 3_000
prefork = false
[security]
sideload_trusted_proxies = ["localhost"]
credential = "e81f43f32d934271af6322e5376f5f59"

10
src/config/loader.rs Normal file
View File

@@ -0,0 +1,10 @@
use config::Config;
pub fn load_settings() -> Config {
Config::builder()
.add_source(config::File::with_name("Settings"))
.add_source(config::File::with_name("/Settings"))
.add_source(config::Environment::with_prefix("ROADSIGN"))
.build()
.unwrap()
}

11
src/config/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
use config::Config;
use lazy_static::lazy_static;
use tokio::sync::RwLock;
use crate::config::loader::load_settings;
pub mod loader;
lazy_static! {
pub static ref CFG: RwLock<Config> = RwLock::new(load_settings());
}

69
src/main.rs Normal file
View File

@@ -0,0 +1,69 @@
extern crate core;
mod config;
mod proxies;
mod sideload;
mod warden;
mod server;
pub mod tls;
use std::error;
use lazy_static::lazy_static;
use proxies::RoadInstance;
use tokio::sync::Mutex;
use tokio::task::JoinSet;
use tracing::{error, info, Level};
use crate::proxies::server::build_proxies;
use crate::sideload::server::build_sideload;
lazy_static! {
static ref ROAD: Mutex<RoadInstance> = Mutex::new(RoadInstance::new());
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn error::Error>> {
// Setting up logging
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
// Prepare all the stuff
info!("Loading proxy regions...");
match proxies::loader::scan_regions(
config::CFG
.read()
.await
.get_string("regions")?
) {
Err(_) => error!("Loading proxy regions... failed"),
Ok((regions, count)) => {
ROAD.lock().await.regions = regions;
info!(count, "Loading proxy regions... done")
}
};
let mut server_set = JoinSet::new();
// Proxies
for server in build_proxies().await? {
server_set.spawn(server);
}
// Sideload
server_set.spawn(build_sideload().await?);
// Process manager
{
let mut app = ROAD.lock().await;
{
let reg = app.regions.clone();
app.warden.scan(reg);
}
app.warden.start().await;
}
// Wait for web servers
server_set.join_next().await;
Ok(())
}

117
src/proxies/config.rs Normal file
View File

@@ -0,0 +1,117 @@
use std::collections::HashMap;
use queryst::parse;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::warden::Application;
use super::responder::StaticResponderConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Region {
pub id: String,
pub locations: Vec<Location>,
pub applications: Option<Vec<Application>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
pub id: String,
pub hosts: Vec<String>,
pub paths: Vec<String>,
pub headers: Option<HashMap<String, String>>,
pub queries: Option<Vec<String>>,
pub methods: Option<Vec<String>>,
pub destinations: Vec<Destination>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Destination {
pub id: String,
pub uri: String,
pub timeout: Option<u32>,
pub weight: Option<u32>,
}
pub enum DestinationType {
Hypertext,
StaticFiles,
Unknown,
}
impl Destination {
pub fn get_type(&self) -> DestinationType {
match self.get_protocol() {
"http" | "https" => DestinationType::Hypertext,
"file" | "files" => DestinationType::StaticFiles,
_ => DestinationType::Unknown,
}
}
pub fn get_protocol(&self) -> &str {
self.uri.as_str().splitn(2, "://").collect::<Vec<_>>()[0]
}
pub fn get_queries(&self) -> &str {
self.uri
.as_str()
.splitn(2, '?')
.collect::<Vec<_>>()
.get(1)
.unwrap_or(&"")
}
pub fn get_host(&self) -> &str {
self
.uri
.as_str()
.splitn(2, "://")
.collect::<Vec<_>>()
.get(1)
.unwrap_or(&"")
.splitn(2, '?')
.collect::<Vec<_>>()[0]
}
pub fn get_hypertext_uri(&self) -> Result<String, ()> {
match self.get_protocol() {
"http" => Ok("http://".to_string() + self.get_host()),
"https" => Ok("https://".to_string() + self.get_host()),
_ => Err(()),
}
}
pub fn get_static_config(&self) -> Result<StaticResponderConfig, ()> {
match self.get_protocol() {
"file" | "files" => {
let queries = parse(self.get_queries()).unwrap_or(json!({}));
Ok(StaticResponderConfig {
uri: self.get_host().to_string(),
utf8: queries
.get("utf8")
.and_then(|val| val.as_bool())
.unwrap_or(false),
browse: queries
.get("browse")
.and_then(|val| val.as_bool())
.unwrap_or(false),
with_slash: queries
.get("slash")
.and_then(|val| val.as_bool())
.unwrap_or(false),
index: queries
.get("index")
.and_then(|val| val.as_str().map(str::to_string)),
fallback: queries
.get("fallback")
.and_then(|val| val.as_str().map(str::to_string)),
suffix: queries
.get("suffix")
.and_then(|val| val.as_str().map(str::to_string)),
})
}
_ => Err(()),
}
}
}

55
src/proxies/loader.rs Normal file
View File

@@ -0,0 +1,55 @@
use std::ffi::OsStr;
use std::fs::{self, DirEntry};
use std::io;
use tracing::warn;
use crate::proxies::config;
pub fn scan_regions(basepath: String) -> io::Result<(Vec<config::Region>, u32)> {
let mut count: u32 = 0;
let mut result = vec![];
for entry in fs::read_dir(basepath)? {
if let Ok(val) = load_region(entry.unwrap()) {
result.push(val);
count += 1;
};
}
Ok((result, count))
}
pub fn load_region(file: DirEntry) -> Result<config::Region, String> {
if file.metadata().map(|val| val.is_dir()).unwrap()
|| file.path().extension().and_then(OsStr::to_str).unwrap() != "toml"
{
return Err("File entry wasn't toml file".to_string());
}
let fp = file.path();
let content = match fs::read_to_string(fp.clone()) {
Ok(val) => val,
Err(err) => {
warn!(
err = format!("{:?}", err),
filepath = fp.clone().to_str(),
"An error occurred when loading region, skipped."
);
return Err("Failed to load file".to_string());
}
};
let data: config::Region = match toml::from_str(&content) {
Ok(val) => val,
Err(err) => {
warn!(
err = format!("{:?}", err),
filepath = fp.clone().to_str(),
"An error occurred when parsing region, skipped."
);
return Err("Failed to parse file".to_string());
}
};
Ok(data)
}

111
src/proxies/metrics.rs Normal file
View File

@@ -0,0 +1,111 @@
use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
use super::config::{Destination, Location, Region};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RoadTrace {
pub region: String,
pub location: String,
pub destination: String,
pub ip_address: String,
pub user_agent: String,
pub error: Option<String>,
}
impl RoadTrace {
pub fn from_structs(
ip: String,
ua: String,
reg: Region,
loc: Location,
end: Destination,
) -> RoadTrace {
RoadTrace {
ip_address: ip,
user_agent: ua,
region: reg.id,
location: loc.id,
destination: end.id,
error: None,
}
}
pub fn from_structs_with_error(
ip: String,
ua: String,
reg: Region,
loc: Location,
end: Destination,
err: String,
) -> RoadTrace {
let mut trace = Self::from_structs(ip, ua, reg, loc, end);
trace.error = Some(err);
trace
}
}
#[derive(Debug, Clone)]
pub struct RoadMetrics {
pub requests_count: u64,
pub failures_count: u64,
pub recent_successes: VecDeque<RoadTrace>,
pub recent_errors: VecDeque<RoadTrace>,
}
const MAX_TRACE_COUNT: usize = 32;
impl RoadMetrics {
pub fn new() -> RoadMetrics {
RoadMetrics {
requests_count: 0,
failures_count: 0,
recent_successes: VecDeque::new(),
recent_errors: VecDeque::new(),
}
}
pub fn get_success_rate(&self) -> f64 {
if self.requests_count > 0 {
(self.requests_count - self.failures_count) as f64 / self.requests_count as f64
} else {
0.0
}
}
pub fn add_success_request(
&mut self,
ip: String,
ua: String,
reg: Region,
loc: Location,
end: Destination,
) {
self.requests_count += 1;
self.recent_successes
.push_back(RoadTrace::from_structs(ip, ua, reg, loc, end));
if self.recent_successes.len() > MAX_TRACE_COUNT {
self.recent_successes.pop_front();
}
}
pub fn add_failure_request(
&mut self,
ip: String,
ua: String,
reg: Region,
loc: Location,
end: Destination,
err: String, // For some reason error is rarely cloneable, so we use preformatted message
) {
self.requests_count += 1;
self.failures_count += 1;
self.recent_errors
.push_back(RoadTrace::from_structs_with_error(ip, ua, reg, loc, end, err));
if self.recent_errors.len() > MAX_TRACE_COUNT {
self.recent_errors.pop_front();
}
}
}

136
src/proxies/mod.rs Normal file
View File

@@ -0,0 +1,136 @@
use actix_web::http::header::{ContentType, HeaderMap};
use actix_web::http::{Method, StatusCode, Uri};
use regex::Regex;
use wildmatch::WildMatch;
use actix_web::{error, HttpResponse};
use derive_more::{Display};
use crate::warden::WardenInstance;
use self::{
config::{Location, Region},
metrics::RoadMetrics,
};
pub mod config;
pub mod loader;
pub mod metrics;
pub mod responder;
pub mod route;
pub mod server;
#[derive(Debug, Display)]
pub enum ProxyError {
#[display(fmt = "Upgrade required for this connection")]
UpgradeRequired,
#[display(fmt = "Remote gateway issue")]
BadGateway,
#[display(fmt = "No configured able to process this request")]
NoGateway,
#[display(fmt = "Not found")]
NotFound,
#[display(fmt = "Only accepts method GET")]
MethodGetOnly,
#[display(fmt = "Invalid request path")]
InvalidRequestPath,
#[display(fmt = "Upstream does not support protocol you used")]
NotImplemented,
}
impl error::ResponseError for ProxyError {
fn status_code(&self) -> StatusCode {
match *self {
ProxyError::UpgradeRequired => StatusCode::UPGRADE_REQUIRED,
ProxyError::BadGateway => StatusCode::BAD_GATEWAY,
ProxyError::NoGateway => StatusCode::NOT_FOUND,
ProxyError::NotFound => StatusCode::NOT_FOUND,
ProxyError::MethodGetOnly => StatusCode::METHOD_NOT_ALLOWED,
ProxyError::InvalidRequestPath => StatusCode::BAD_REQUEST,
ProxyError::NotImplemented => StatusCode::NOT_IMPLEMENTED,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
}
#[derive(Debug, Clone)]
pub struct RoadInstance {
pub regions: Vec<Region>,
pub metrics: RoadMetrics,
pub warden: WardenInstance,
}
impl RoadInstance {
pub fn new() -> RoadInstance {
RoadInstance {
regions: vec![],
warden: WardenInstance {
applications: vec![],
},
metrics: RoadMetrics::new(),
}
}
pub fn filter(
&self,
uri: &Uri,
method: &Method,
headers: &HeaderMap,
) -> Option<(&Region, &Location)> {
self.regions.iter().find_map(|region| {
let location = region.locations.iter().find(|location| {
let mut hosts = location.hosts.iter();
if !hosts.any(|item| {
WildMatch::new(item.as_str()).matches(uri.host().unwrap_or("localhost"))
}) {
return false;
}
let mut paths = location.paths.iter();
if !paths.any(|item| {
uri.path().starts_with(item)
|| Regex::new(item.as_str()).unwrap().is_match(uri.path())
}) {
return false;
}
if let Some(val) = location.methods.clone() {
if !val.iter().any(|item| *item == method.to_string()) {
return false;
}
}
if let Some(val) = location.headers.clone() {
match !val.keys().all(|item| {
headers.get(item).unwrap()
== location.headers.clone().unwrap().get(item).unwrap()
}) {
true => return false,
false => (),
}
};
if let Some(val) = location.queries.clone() {
let queries: Vec<&str> = uri.query().unwrap_or("").split('&').collect();
if !val.iter().all(|item| queries.contains(&item.as_str())) {
return false;
}
}
true
});
location.map(|location| (region, location))
})
}
}

288
src/proxies/responder.rs Normal file
View File

@@ -0,0 +1,288 @@
use crate::proxies::ProxyError;
use crate::proxies::ProxyError::{BadGateway, UpgradeRequired};
use actix_files::NamedFile;
use actix_web::http::{header, Method};
use actix_web::{web, HttpRequest, HttpResponse};
use awc::error::HeaderValue;
use awc::http::Uri;
use awc::Client;
use futures::Sink;
use futures::stream::StreamExt;
use std::str::FromStr;
use std::time::Duration;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use actix::io::{SinkWrite, WriteHandler};
use actix::{Actor, ActorContext, AsyncContext, StreamHandler};
use actix_web_actors::ws;
use actix_web_actors::ws::{CloseReason, handshake, ProtocolError, WebsocketContext};
use tracing::log::warn;
pub async fn respond_hypertext(
uri: String,
req: HttpRequest,
payload: web::Payload,
client: web::Data<Client>,
) -> Result<HttpResponse, ProxyError> {
let mut append_part = req.uri().to_string();
if let Some(stripped_uri) = append_part.strip_prefix('/') {
append_part = stripped_uri.to_string();
}
let uri = Uri::from_str(uri.as_str()).expect("Invalid upstream");
let target_url = format!("{}{}", uri, append_part);
let forwarded_req = client
.request_from(target_url.as_str(), req.head())
.insert_header((header::HOST, uri.host().expect("Invalid upstream")));
let forwarded_req = match req.connection_info().realip_remote_addr() {
Some(addr) => forwarded_req
.insert_header((header::X_FORWARDED_FOR, addr))
.insert_header((header::X_FORWARDED_PROTO, req.connection_info().scheme()))
.insert_header((header::X_FORWARDED_HOST, req.connection_info().host()))
.insert_header((
header::FORWARDED,
format!(
"by={};for={};host={};proto={}",
addr,
addr,
req.connection_info().host(),
req.connection_info().scheme()
),
)),
None => forwarded_req,
};
if req
.headers()
.get(header::UPGRADE)
.unwrap_or(&HeaderValue::from_static(""))
.to_str()
.unwrap_or("")
.to_lowercase()
== "websocket"
{
let uri = uri.to_string().replacen("http", "ws", 1);
return respond_websocket(uri, req, payload).await;
}
let res = forwarded_req
.timeout(Duration::from_secs(1800))
.send_stream(payload)
.await
.map_err(|err| {
warn!("Remote gateway issue... {}", err);
BadGateway
})?;
let mut client_resp = HttpResponse::build(res.status());
for (header_name, header_value) in res
.headers()
.iter()
.filter(|(h, _)| *h != header::CONNECTION && *h != header::CONTENT_ENCODING)
{
client_resp.insert_header((header_name.clone(), header_value.clone()));
}
Ok(client_resp.streaming(res))
}
pub struct WebsocketProxy<S>
where
S: Unpin + Sink<ws::Message>,
{
send: SinkWrite<ws::Message, S>,
}
impl<S> WriteHandler<ProtocolError> for WebsocketProxy<S>
where
S: Unpin + 'static + Sink<ws::Message>,
{
fn error(&mut self, err: ProtocolError, ctx: &mut Self::Context) -> actix::Running {
self.error(err, ctx);
actix::Running::Stop
}
}
impl<S> Actor for WebsocketProxy<S>
where
S: Unpin + 'static + Sink<ws::Message>,
{
type Context = WebsocketContext<Self>;
}
impl<S> StreamHandler<Result<ws::Frame, ProtocolError>> for WebsocketProxy<S>
where
S: Unpin + Sink<ws::Message> + 'static,
{
fn handle(&mut self, item: Result<ws::Frame, ProtocolError>, ctx: &mut Self::Context) {
let frame = match item {
Ok(frame) => frame,
Err(err) => return self.error(err, ctx),
};
let msg = match frame {
ws::Frame::Text(t) => match t.try_into() {
Ok(t) => ws::Message::Text(t),
Err(e) => {
self.error(e, ctx);
return;
}
},
ws::Frame::Binary(b) => ws::Message::Binary(b),
ws::Frame::Continuation(c) => ws::Message::Continuation(c),
ws::Frame::Ping(p) => ws::Message::Ping(p),
ws::Frame::Pong(p) => ws::Message::Pong(p),
ws::Frame::Close(r) => ws::Message::Close(r),
};
ctx.write_raw(msg)
}
}
impl<S> StreamHandler<Result<ws::Message, ProtocolError>> for WebsocketProxy<S>
where
S: Unpin + Sink<ws::Message> + 'static,
{
fn handle(&mut self, item: Result<ws::Message, ProtocolError>, ctx: &mut Self::Context) {
let msg = match item {
Ok(msg) => msg,
Err(err) => return self.error(err, ctx),
};
let _ = self.send.write(msg);
}
}
impl<S> WebsocketProxy<S>
where
S: Unpin + Sink<ws::Message> + 'static,
{
fn error<E>(&mut self, err: E, ctx: &mut <Self as Actor>::Context)
where
E: std::error::Error,
{
let reason = Some(CloseReason {
code: ws::CloseCode::Error,
description: Some(err.to_string()),
});
ctx.close(reason.clone());
let _ = self.send.write(ws::Message::Close(reason));
self.send.close();
ctx.stop();
}
}
pub async fn respond_websocket(
uri: String,
req: HttpRequest,
payload: web::Payload,
) -> Result<HttpResponse, ProxyError> {
let mut res = handshake(&req).map_err(|_| UpgradeRequired)?;
let (_, conn) = awc::Client::new()
.ws(uri)
.connect()
.await
.map_err(|_| BadGateway)?;
let (send, recv) = conn.split();
let out = WebsocketContext::with_factory(payload, |ctx| {
ctx.add_stream(recv);
WebsocketProxy {
send: SinkWrite::new(send, ctx),
}
});
Ok(res.streaming(out))
}
pub struct StaticResponderConfig {
pub uri: String,
pub utf8: bool,
pub browse: bool,
pub with_slash: bool,
pub index: Option<String>,
pub fallback: Option<String>,
pub suffix: Option<String>,
}
pub async fn respond_static(
cfg: StaticResponderConfig,
req: HttpRequest,
) -> Result<HttpResponse, ProxyError> {
if req.method() != Method::GET {
return Err(ProxyError::MethodGetOnly);
}
let path = req
.uri()
.path()
.trim_start_matches('/')
.trim_end_matches('/');
let path = match percent_encoding::percent_decode_str(path).decode_utf8() {
Ok(val) => val,
Err(_) => {
return Err(ProxyError::NotFound);
}
};
let base_path = cfg.uri.parse::<PathBuf>().unwrap();
let mut file_path = base_path.clone();
for p in Path::new(&*path) {
if p == OsStr::new(".") {
continue;
} else if p == OsStr::new("..") {
file_path.pop();
} else {
file_path.push(p);
}
}
if !file_path.starts_with(cfg.uri) {
return Err(ProxyError::InvalidRequestPath);
}
if !file_path.exists() {
if let Some(suffix) = cfg.suffix {
let file_name = file_path
.file_name()
.and_then(OsStr::to_str)
.unwrap()
.to_string();
file_path.pop();
file_path.push((file_name + &suffix).as_str());
if file_path.is_file() {
return Ok(NamedFile::open(file_path).unwrap().into_response(&req));
}
}
if let Some(file) = cfg.fallback {
let fallback_path = base_path.join(file);
if fallback_path.is_file() {
return Ok(NamedFile::open(fallback_path).unwrap().into_response(&req));
}
}
return Err(ProxyError::NotFound);
}
if file_path.is_file() {
Ok(NamedFile::open(file_path).unwrap().into_response(&req))
} else {
if let Some(index_file) = &cfg.index {
let index_path = file_path.join(index_file);
if index_path.is_file() {
return Ok(NamedFile::open(index_path).unwrap().into_response(&req));
}
}
Err(ProxyError::NotFound)
}
}

87
src/proxies/route.rs Normal file
View File

@@ -0,0 +1,87 @@
use actix_web::{HttpRequest, HttpResponse, ResponseError, web};
use actix_web::http::header;
use awc::Client;
use rand::seq::SliceRandom;
use crate::{
proxies::{
config::{Destination, DestinationType},
responder,
},
ROAD,
};
use crate::proxies::ProxyError;
pub async fn handle(req: HttpRequest, payload: web::Payload, client: web::Data<Client>) -> HttpResponse {
let readable_app = ROAD.lock().await;
let (region, location) = match readable_app.filter(req.uri(), req.method(), req.headers()) {
Some(val) => val,
None => {
return ProxyError::NoGateway.error_response();
}
};
let destination = location
.destinations
.choose_weighted(&mut rand::thread_rng(), |item| item.weight.unwrap_or(1))
.unwrap();
async fn forward(
end: &Destination,
req: HttpRequest,
payload: web::Payload,
client: web::Data<Client>,
) -> Result<HttpResponse, ProxyError> {
// Handle normal web request
match end.get_type() {
DestinationType::Hypertext => {
let Ok(uri) = end.get_hypertext_uri() else {
return Err(ProxyError::NotImplemented);
};
responder::respond_hypertext(uri, req, payload, client).await
}
DestinationType::StaticFiles => {
let Ok(cfg) = end.get_static_config() else {
return Err(ProxyError::NotImplemented);
};
responder::respond_static(cfg, req).await
}
_ => Err(ProxyError::NotImplemented)
}
}
let reg = region.clone();
let loc = location.clone();
let end = destination.clone();
let ip = match req.connection_info().realip_remote_addr() {
None => "unknown".to_string(),
Some(val) => val.to_string(),
};
let ua = match req.headers().get(header::USER_AGENT) {
None => "unknown".to_string(),
Some(val) => val.to_str().unwrap().to_string(),
};
match forward(&end, req, payload, client).await {
Ok(resp) => {
tokio::spawn(async move {
let writable_app = &mut ROAD.lock().await;
writable_app.metrics.add_success_request(ip, ua, reg, loc, end);
});
resp
}
Err(resp) => {
let message = resp.to_string();
tokio::spawn(async move {
let writable_app = &mut ROAD.lock().await;
writable_app
.metrics
.add_failure_request(ip, ua, reg, loc, end, message);
});
resp.error_response()
}
}
}

40
src/proxies/server.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::error;
use actix_web::{App, HttpServer, web};
use actix_web::dev::Server;
use actix_web::middleware::{Compress, Logger};
use awc::Client;
use crate::config::CFG;
use crate::proxies::route;
use crate::server::ServerBindConfig;
use crate::tls::{load_certificates, use_rustls};
pub async fn build_proxies() -> Result<Vec<Server>, Box<dyn error::Error>> {
load_certificates().await?;
let cfg = CFG
.read()
.await
.get::<Vec<ServerBindConfig>>("proxies.bind")?;
let mut tasks = Vec::new();
for item in cfg {
tasks.push(build_single_proxy(item)?);
}
Ok(tasks)
}
pub fn build_single_proxy(cfg: ServerBindConfig) -> Result<Server, Box<dyn error::Error>> {
let server = HttpServer::new(|| {
App::new()
.wrap(Logger::default())
.wrap(Compress::default())
.app_data(web::Data::new(Client::default()))
.default_service(web::to(route::handle))
});
if cfg.tls {
Ok(server.bind_rustls_0_22(cfg.addr, use_rustls()?)?.run())
} else {
Ok(server.bind(cfg.addr)?.run())
}
}

7
src/server.rs Normal file
View File

@@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ServerBindConfig {
pub addr: String,
pub tls: bool,
}

15
src/sideload/mod.rs Normal file
View File

@@ -0,0 +1,15 @@
use actix_web::{Scope, web};
use crate::sideload::overview::get_overview;
use crate::sideload::regions::list_region;
mod overview;
mod regions;
pub mod server;
static ROOT: &str = "";
pub fn service() -> Scope {
web::scope("/cgi")
.route(ROOT, web::get().to(get_overview))
.route("/regions", web::get().to(list_region))
}

52
src/sideload/overview.rs Normal file
View File

@@ -0,0 +1,52 @@
use actix_web::web;
use serde::Serialize;
use crate::proxies::config::{Destination, Location};
use crate::proxies::metrics::RoadTrace;
use crate::ROAD;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct OverviewData {
regions: usize,
locations: usize,
destinations: usize,
requests_count: u64,
failures_count: u64,
successes_count: u64,
success_rate: f64,
recent_successes: Vec<RoadTrace>,
recent_errors: Vec<RoadTrace>,
}
pub async fn get_overview() -> web::Json<OverviewData> {
let locked_app = ROAD.lock().await;
let regions = locked_app.regions.clone();
let locations = regions
.iter()
.flat_map(|item| item.locations.clone())
.collect::<Vec<Location>>();
let destinations = locations
.iter()
.flat_map(|item| item.destinations.clone())
.collect::<Vec<Destination>>();
web::Json(OverviewData {
regions: regions.len(),
locations: locations.len(),
destinations: destinations.len(),
requests_count: locked_app.metrics.requests_count,
successes_count: locked_app.metrics.requests_count - locked_app.metrics.failures_count,
failures_count: locked_app.metrics.failures_count,
success_rate: locked_app.metrics.get_success_rate(),
recent_successes: locked_app
.metrics
.recent_successes
.clone()
.into_iter()
.collect::<Vec<_>>(),
recent_errors: locked_app
.metrics
.recent_errors
.clone()
.into_iter()
.collect::<Vec<_>>(),
})
}

9
src/sideload/regions.rs Normal file
View File

@@ -0,0 +1,9 @@
use actix_web::web;
use crate::proxies::config::Region;
use crate::ROAD;
pub async fn list_region() -> web::Json<Vec<Region>> {
let locked_app = ROAD.lock().await;
web::Json(locked_app.regions.clone())
}

35
src/sideload/server.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::error;
use actix_web::dev::Server;
use actix_web::{App, HttpServer};
use actix_web_httpauth::extractors::AuthenticationError;
use actix_web_httpauth::headers::www_authenticate::basic::Basic;
use actix_web_httpauth::middleware::HttpAuthentication;
use crate::sideload;
pub async fn build_sideload() -> Result<Server, Box<dyn error::Error>> {
Ok(
HttpServer::new(|| {
App::new()
.wrap(HttpAuthentication::basic(|req, credentials| async move {
let password = match crate::config::CFG
.read()
.await
.get_string("secret") {
Ok(val) => val,
Err(_) => return Err((AuthenticationError::new(Basic::new()).into(), req))
};
if credentials.password().unwrap_or("") != password {
Err((AuthenticationError::new(Basic::new()).into(), req))
} else {
Ok(req)
}
}))
.service(sideload::service())
}).bind(
crate::config::CFG
.read()
.await
.get_string("sideload.bind_addr")?
)?.workers(1).run()
)
}

76
src/tls.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::fs::File;
use std::{error};
use std::io::BufReader;
use std::sync::Arc;
use config::ConfigError;
use lazy_static::lazy_static;
use rustls::crypto::ring::sign::RsaSigningKey;
use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use wildmatch::WildMatch;
lazy_static! {
static ref CERTS: Mutex<Vec<CertificateConfig>> = Mutex::new(Vec::new());
}
#[derive(Debug)]
struct ProxyCertResolver;
impl ResolvesServerCert for ProxyCertResolver {
fn resolve(&self, handshake: ClientHello) -> Option<Arc<CertifiedKey>> {
let domain = handshake.server_name()?;
let certs = CERTS.lock().unwrap();
for cert in certs.iter() {
if WildMatch::new(cert.domain.as_str()).matches(domain) {
return match cert.clone().load() {
Ok(val) => Some(val),
Err(_) => None
};
}
}
None
}
}
#[derive(Clone, Serialize, Deserialize)]
struct CertificateConfig {
pub domain: String,
pub certs: String,
pub key: String,
}
impl CertificateConfig {
pub fn load(self) -> Result<Arc<CertifiedKey>, Box<dyn error::Error>> {
let certs =
rustls_pemfile::certs(&mut BufReader::new(&mut File::open(self.certs)?))
.collect::<Result<Vec<_>, _>>()?;
let key =
rustls_pemfile::private_key(&mut BufReader::new(&mut File::open(self.key)?))?
.unwrap();
let sign = RsaSigningKey::new(&key)?;
Ok(Arc::new(CertifiedKey::new(certs, Arc::new(sign))))
}
}
pub async fn load_certificates() -> Result<(), ConfigError> {
let certs = crate::config::CFG
.read()
.await
.get::<Vec<CertificateConfig>>("certificates")?;
CERTS.lock().unwrap().clone_from(&certs);
Ok(())
}
pub fn use_rustls() -> Result<rustls::ServerConfig, ConfigError> {
Ok(
rustls::ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(ProxyCertResolver))
)
}

72
src/warden/mod.rs Normal file
View File

@@ -0,0 +1,72 @@
pub mod runner;
use std::collections::HashMap;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::{debug, warn};
use crate::proxies::config::Region;
use self::runner::AppInstance;
lazy_static! {
static ref INSTANCES: Mutex<HashMap<String, AppInstance>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WardenInstance {
pub applications: Vec<Application>,
}
impl WardenInstance {
pub fn new() -> WardenInstance {
WardenInstance {
applications: vec![],
}
}
pub fn scan(&mut self, regions: Vec<Region>) {
self.applications = regions
.iter()
.flat_map(|item| item.applications.clone().unwrap_or_default())
.collect::<Vec<Application>>();
debug!(
applications = format!("{:?}", self.applications),
"Warden scan accomplished."
)
}
pub async fn start(&self) {
for item in self.applications.iter() {
let mut instance = AppInstance::new();
match instance.start(item.clone()).await {
Ok(_) => {
debug!(id = item.id, "Warden successfully created instance for");
INSTANCES.lock().await.insert(item.clone().id, instance);
}
Err(err) => warn!(
id = item.id,
err = format!("{:?}", err),
"Warden failed to create an instance for"
),
};
}
}
}
impl Default for WardenInstance {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Application {
pub id: String,
pub exe: String,
pub args: Option<Vec<String>>,
pub env: Option<HashMap<String, String>>,
pub workdir: String,
}

104
src/warden/runner.rs Normal file
View File

@@ -0,0 +1,104 @@
use std::{borrow::BorrowMut, collections::HashMap, io};
use super::Application;
use lazy_static::lazy_static;
use tokio::{
io::{AsyncBufReadExt, BufReader},
process::{Child, Command},
};
use tokio::sync::Mutex;
lazy_static! {
static ref STDOUT: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
static ref STDERR: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}
#[derive(Debug)]
pub struct AppInstance {
pub app: Option<Application>,
pub program: Option<Child>,
}
impl AppInstance {
pub fn new() -> AppInstance {
AppInstance {
app: None,
program: None,
}
}
pub async fn start(&mut self, app: Application) -> io::Result<()> {
return match Command::new(app.exe.clone())
.args(app.args.clone().unwrap_or_default())
.envs(app.env.clone().unwrap_or_default())
.current_dir(app.workdir.clone())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(mut child) => {
let stderr_reader = BufReader::new(child.stderr.take().unwrap());
let stdout_reader = BufReader::new(child.stdout.take().unwrap());
tokio::spawn(read_stream_and_capture(stderr_reader, app.id.clone(), true));
tokio::spawn(read_stream_and_capture(
stdout_reader,
app.id.clone(),
false,
));
self.app = Some(app.clone());
self.program = Some(child);
Ok(())
}
Err(err) => Err(err),
};
}
pub async fn stop(&mut self) -> Result<(), io::Error> {
if let Some(child) = self.program.borrow_mut() {
return child.kill().await;
}
Ok(())
}
pub async fn get_stdout(&self) -> Option<String> {
if let Some(app) = self.app.clone() {
STDOUT.lock().await.get(&app.id).cloned()
} else {
None
}
}
pub async fn get_stderr(&self) -> Option<String> {
if let Some(app) = self.app.clone() {
STDERR.lock().await.get(&app.id).cloned()
} else {
None
}
}
}
impl Default for AppInstance {
fn default() -> Self {
Self::new()
}
}
async fn read_stream_and_capture<R>(reader: R, id: String, is_err: bool) -> io::Result<()>
where
R: tokio::io::AsyncBufRead + Unpin,
{
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
if !is_err {
if let Some(out) = STDOUT.lock().await.get_mut(&id) {
out.push_str(&line);
}
} else if let Some(out) = STDERR.lock().await.get_mut(&id) {
out.push_str(&line);
}
}
Ok(())
}

View File

@@ -1,85 +0,0 @@
# Benchmark
This result is design for test the performance of the roadsign.
Welcome to contribute more tests of others reverse proxy software!
## Platform
All tests are running on my workstation:
```text
..' littlesheep@LittleSheepdeMacBook-Pro
,xNMM. ------------------------------------
.OMMMMo OS: macOS Sonoma 14.1 23B2073 arm64
lMM" Host: MacBook Pro (14-inch, Nov 2023, Three Thunderbolt 4 ports)
.;loddo:. .olloddol;. Kernel: 23.1.0
cKMMMMMMMMMMNWMMMMMMMMMM0: Uptime: 2 days, 1 hour, 16 mins
.KMMMMMMMMMMMMMMMMMMMMMMMWd. Packages: 63 (brew), 4 (brew-cask)
XMMMMMMMMMMMMMMMMMMMMMMMX. Shell: zsh 5.9
;MMMMMMMMMMMMMMMMMMMMMMMM: Display (Color LCD): 3024x1964 @ 120Hz (as 1512x982) [Built-in]
:MMMMMMMMMMMMMMMMMMMMMMMM: DE: Aqua
.MMMMMMMMMMMMMMMMMMMMMMMMX. WM: Quartz Compositor
kMMMMMMMMMMMMMMMMMMMMMMMMWd. WM Theme: Multicolor (Dark)
'XMMMMMMMMMMMMMMMMMMMMMMMMMMk Font: .AppleSystemUIFont [System], Helvetica [User]
'XMMMMMMMMMMMMMMMMMMMMMMMMK. Cursor: Fill - Black, Outline - White (32px)
kMMMMMMMMMMMMMMMMMMMMMMd Terminal: iTerm 3.4.22
;KMMMMMMMWXXWMMMMMMMk. Terminal Font: MesloLGMNFM-Regular (12pt)
"cooc*" "*coo'" CPU: Apple M3 Max (14) @ 4.06 GHz
GPU: Apple M3 Max (30) [Integrated]
Memory: 18.45 GiB / 36.00 GiB (51%)
Swap: Disabled
Disk (/): 72.52 GiB / 926.35 GiB (8%) - apfs [Read-only]
Local IP (en0): 192.168.50.0/24 *
Battery: 100% [AC connected]
Power Adapter: 96W USB-C Power Adapter
Locale: zh_CN.UTF-8
```
## Results
The tests are run in the order `nginx -> roadsign without prefork -> roadsign with prefork`. There is no reason why nginx performance should be affected by hardware temperature.
### Nginx
```shell
go-wrk -c 60 -d 120 http://localhost:8001
# => Running 120s test @ http://localhost:8001
# => 60 goroutine(s) running concurrently
# => 515749 requests in 1m59.953302003s, 245.92MB read
# => Requests/sec: 4299.58
# => Transfer/sec: 2.05MB
# => Avg Req Time: 13.954846ms
# => Fastest Request: 0s
# => Slowest Request: 410.6972ms
# => Number of Errors: 0
```
### RoadSign
```shell
go-wrk -c 60 -d 120 http://localhost:8000
# => Running 120s test @ http://localhost:8000
# => 60 goroutine(s) running concurrently
# => 8905230 requests in 1m56.215762709s, 3.52GB read
# => Requests/sec: 76626.70
# => Transfer/sec: 30.98MB
# => Avg Req Time: 783.016µs
# => Fastest Request: 28.542µs
# => Slowest Request: 46.773083ms
# => Number of Errors: 0
```
### RoadSign w/ Prefork
```shell
go-wrk -c 60 -d 120 http://localhost:8000
# => Running 120s test @ http://localhost:8000
# => 60 goroutine(s) running concurrently
# => 4784308 requests in 1m59.100307178s, 1.89GB read
# => Requests/sec: 40170.41
# => Transfer/sec: 16.24MB
# => Avg Req Time: 1.493636ms
# => Fastest Request: 34.291µs
# => Slowest Request: 8.727666ms
# => Number of Errors: 0
```

View File

@@ -1 +0,0 @@
/warden

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello, World!</title>
</head>
<body>
<p>Hello, there!</p>
<p>Here's the roadsign vs. nginx benchmarking test data!</p>
</body>
</html>

View File

@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events Example</h1>
<div id="sse-data"></div>
<script>
const eventSource = new EventSource("/sse")
const sseDataElement = document.getElementById("sse-data")
eventSource.onmessage = (event) => {
sseDataElement.innerText = `Data from server: ${event.data}`
}
eventSource.onerror = (error) => {
console.error("EventSource failed:", error)
eventSource.close()
}
</script>
</body>
</html>

View File

@@ -1,28 +0,0 @@
import time
from flask import Flask, render_template, Response
app = Flask(__name__, template_folder=".")
# Generator function to simulate real-time updates
def event_stream():
count = 0
while True:
time.sleep(1)
count += 1
yield f"data: {count}\n\n"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/sse')
def sse():
return Response(event_stream(), content_type='text/event-stream')
if __name__ == '__main__':
app.run(debug=True, threaded=True)

View File

@@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebSocket Durability Test</title>
</head>
<body>
<h1>WebSocket Client</h1>
<p>
This client will send a message every 500ms, or you can send message
manually.
</p>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type a message..." />
<button onclick="sendMessage()">Send Message</button>
<script>
const socket = new WebSocket("ws://localhost:8000/ws");
socket.onopen = function (event) {
appendMessage("Connection opened");
setInterval(() => autoSendMessage(), 500)
};
socket.onmessage = function (event) {
appendMessage("Received: " + event.data);
};
socket.onclose = function (event) {
appendMessage("Connection closed");
};
function sendMessage() {
const messageInput = document.getElementById("messageInput");
const message = messageInput.value;
if (message.trim() !== "") {
socket.send(message);
appendMessage("Sent: " + message);
messageInput.value = "";
}
}
function autoSendMessage() {
const message = `[AutoSend] A new message has been sent at ${new Date().toUTCString()}`;
socket.send(message);
}
function appendMessage(message) {
const messagesDiv = document.getElementById("messages");
messagesDiv.innerHTML += `<p>${message}</p>`;
}
</script>
</body>
</html>

View File

@@ -1,117 +0,0 @@
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8001;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root ../data;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}

View File

@@ -1,9 +0,0 @@
id = "example"
[[locations]]
id = "example-location"
host = ["localhost:8000"]
path = ["/"]
[[locations.destinations]]
id = "example-destination"
uri = "files://../data/spa?fallback=index.html"

View File

@@ -1,31 +0,0 @@
[debug]
print_routes = true
[hypertext]
sideload_ports = [":81"]
sideload_secured_ports = []
ports = [":8000"]
secured_ports = []
[hypertext.certificate]
redirect = false
sideload_key = "./cert.key"
sideload_pem = "./cert.pem"
key = "./cert.key"
pem = "./cert.pem"
[hypertext.limitation]
max_body_size = 549_755_813_888 # 512 GiB
max_qps = -1
[paths]
configs = "./config"
[performance]
request_logging = true
network_timeout = 3_000
prefork = false
[security]
sideload_trusted_proxies = ["localhost"]
credential = "e81f43f32d934271af6322e5376f5f59"

View File

@@ -1,15 +0,0 @@
id = "example-region"
[[locations]]
id = "example-warden"
host = ["localhost:8000"]
path = ["/"]
[[locations.destinations]]
id = "example-warden-destination"
uri = "http://localhost:4321"
[[applications]]
id = "example-warden-app"
workdir = "../data/warden"
command = ["node", "dist/server/entry.mjs"]
environment = ["PUBLIC_CMS=https://smartsheep.studio"]

View File

@@ -1,31 +0,0 @@
[debug]
print_routes = true
[hypertext]
sideload_ports = [":81"]
sideload_secured_ports = []
ports = [":8000"]
secured_ports = []
[hypertext.certificate]
redirect = false
sideload_key = "./cert.key"
sideload_pem = "./cert.pem"
key = "./cert.key"
pem = "./cert.pem"
[hypertext.limitation]
max_body_size = 549_755_813_888 # 512 GiB
max_qps = -1
[paths]
configs = "./config"
[performance]
request_logging = true
network_timeout = 3_000
prefork = false
[security]
sideload_trusted_proxies = ["localhost"]
credential = "e81f43f32d934271af6322e5376f5f59"

View File

@@ -1,9 +0,0 @@
id = "example"
[[locations]]
id = "example-location"
host = ["localhost:8000"]
path = ["/"]
[[locations.destinations]]
id = "example-destination"
uri = "files://../data/spa?fallback=index.html"

View File

@@ -1,31 +0,0 @@
[debug]
print_routes = true
[hypertext]
sideload_ports = [":81"]
sideload_secured_ports = []
ports = [":8000"]
secured_ports = []
[hypertext.certificate]
redirect = false
sideload_key = "./cert.key"
sideload_pem = "./cert.pem"
key = "./cert.key"
pem = "./cert.pem"
[hypertext.limitation]
max_body_size = 549_755_813_888 # 512 GiB
max_qps = -1
[paths]
configs = "./config"
[performance]
request_logging = true
network_timeout = 3_000
prefork = true
[security]
sideload_trusted_proxies = ["localhost"]
credential = "e81f43f32d934271af6322e5376f5f59"

View File

@@ -1,8 +0,0 @@
name: Example Site
rules:
- host: ["localhost:8000"]
path: ["/"]
upstreams:
- id: example
name: Benchmarking Data
uri: files://../data

View File

@@ -1,31 +0,0 @@
[debug]
print_routes = true
[hypertext]
sideload_ports = [":81"]
sideload_secured_ports = []
ports = [":8000"]
secured_ports = []
[hypertext.certificate]
redirect = false
sideload_key = "./cert.key"
sideload_pem = "./cert.pem"
key = "./cert.key"
pem = "./cert.pem"
[hypertext.limitation]
max_body_size = 549_755_813_888 # 512 GiB
max_qps = -1
[paths]
configs = "./config"
[performance]
request_logging = true
network_timeout = 3_000
prefork = false
[security]
sideload_trusted_proxies = ["localhost"]
credential = "e81f43f32d934271af6322e5376f5f59"