Compare commits
26 Commits
450250c419
...
refactor/r
Author | SHA1 | Date | |
---|---|---|---|
0b38c4a470 | |||
515f086f19 | |||
d75ac2999b | |||
7eee10c4ff | |||
e40fe6049f | |||
ed9434b85a | |||
804108a209 | |||
46736c12b9 | |||
1a562fbee8 | |||
7796ee3554 | |||
12add73ecb | |||
3fbe1db1ef | |||
e27023c130 | |||
2478a05c89 | |||
cb8eab6c1b | |||
ae3894bea6 | |||
b7d4a54d62 | |||
ead748a508 | |||
4c08d78bed | |||
a088f6224e | |||
f02977b7d7 | |||
905b70349b | |||
91ecf9d7bb | |||
c991d0b54a | |||
5de1d13907 | |||
bf7004c89c |
@ -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"],
|
||||
},
|
||||
|
||||
]
|
||||
}
|
@ -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
7
.gitignore
vendored
@ -1,3 +1,8 @@
|
||||
/config
|
||||
/certs
|
||||
/test/data
|
||||
/letsencrypt
|
||||
|
||||
.DS_Store
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
5
.idea/RoadSign.iml
generated
5
.idea/RoadSign.iml
generated
@ -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
6
.idea/misc.xml
generated
@ -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
2182
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
Normal file
38
Cargo.toml
Normal 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"
|
18
Dockerfile
18
Dockerfile
@ -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"]
|
12
README.md
12
README.md
@ -59,7 +59,7 @@ 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 -buildvcs 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.
|
||||
```
|
||||
|
||||
@ -74,9 +74,9 @@ rds cli with this command.
|
||||
|
||||
```shell
|
||||
rds 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.
|
||||
# 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
|
||||
# ======================================================================
|
||||
@ -85,8 +85,8 @@ rds connect <id> <url> <password>
|
||||
Then, sync your local config to remote.
|
||||
|
||||
```shell
|
||||
rds sync <server id> <site 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
17
Settings.toml
Normal 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"
|
@ -1,28 +0,0 @@
|
||||
id = "example-region"
|
||||
|
||||
[[locations]]
|
||||
id = "example-sse"
|
||||
host = ["localhost:8000"]
|
||||
path = ["/sse"]
|
||||
[[locations.destinations]]
|
||||
id = "example-sse-destination"
|
||||
uri = "http://localhost:5000?sse=enable"
|
||||
[[locations.transformers]]
|
||||
type = "replacePath"
|
||||
options = { pattern = "/sse", value = "" }
|
||||
|
||||
[[locations]]
|
||||
id = "example-websocket"
|
||||
host = ["localhost:8000"]
|
||||
path = ["/ws"]
|
||||
[[locations.destinations]]
|
||||
id = "example-websocket-destination"
|
||||
uri = "http://localhost:8765"
|
||||
|
||||
[[locations]]
|
||||
id = "example-location"
|
||||
host = ["localhost:8000"]
|
||||
path = ["/"]
|
||||
[[locations.destinations]]
|
||||
id = "example-destination"
|
||||
uri = "files://test/data"
|
61
go.mod
61
go.mod
@ -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
170
go.sum
@ -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=
|
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
@ -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.")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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.")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
17
pkg/meta.go
17
pkg/meta.go
@ -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"
|
@ -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, ®ion); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer file.Close()
|
||||
|
||||
if region.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
instance.Regions = append(instance.Regions, ®ion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
R = instance
|
||||
|
||||
return nil
|
||||
}
|
@ -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:]
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
@ -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
|
||||
},
|
||||
}
|
@ -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,
|
||||
}
|
@ -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
|
||||
},
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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),
|
||||
})
|
||||
}
|
24
pkg/sideload/view/.gitignore
vendored
24
pkg/sideload/view/.gitignore
vendored
@ -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?
|
@ -1,6 +0,0 @@
|
||||
package view
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed all:dist
|
||||
var FS embed.FS
|
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -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!);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
1
pkg/sideload/view/src/vite-env.d.ts
vendored
1
pkg/sideload/view/src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
@ -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")],
|
||||
};
|
@ -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" }]
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -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
@ -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()
|
||||
}
|
@ -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
12
regions/index.html
Normal 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
21
regions/index.toml
Normal 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
1
regions/kokodayo.txt
Normal file
@ -0,0 +1 @@
|
||||
Ko Ko Da Yo~
|
3
regions/script.sh
Executable file
3
regions/script.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Good morning!" > ./kokodayo.txt
|
15
regions/subfolder/index.html
Normal file
15
regions/subfolder/index.html
Normal 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>
|
@ -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
10
src/config/loader.rs
Normal 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
11
src/config/mod.rs
Normal 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
69
src/main.rs
Normal 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
117
src/proxies/config.rs
Normal 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
55
src/proxies/loader.rs
Normal 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
111
src/proxies/metrics.rs
Normal 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
136
src/proxies/mod.rs
Normal 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
288
src/proxies/responder.rs
Normal 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
87
src/proxies/route.rs
Normal 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
40
src/proxies/server.rs
Normal 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
7
src/server.rs
Normal 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
15
src/sideload/mod.rs
Normal 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
52
src/sideload/overview.rs
Normal 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
9
src/sideload/regions.rs
Normal 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
35
src/sideload/server.rs
Normal 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
76
src/tls.rs
Normal 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
72
src/warden/mod.rs
Normal 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
104
src/warden/runner.rs
Normal 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(())
|
||||
}
|
@ -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
|
||||
```
|
3
test/data/.gitignore
vendored
3
test/data/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/ssr
|
||||
/spa
|
||||
/congress
|
@ -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>
|
@ -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>
|
@ -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)
|
@ -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>
|
@ -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;
|
||||
# }
|
||||
#}
|
||||
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
name: Example Site
|
||||
rules:
|
||||
- host: ["localhost:8000"]
|
||||
path: ["/"]
|
||||
upstreams:
|
||||
- id: example
|
||||
name: Benchmarking Data
|
||||
uri: files://../data/spa?fallback=index.html
|
@ -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"
|
@ -1,12 +0,0 @@
|
||||
name: Example Site
|
||||
rules:
|
||||
- host: ["localhost:8000"]
|
||||
path: ["/"]
|
||||
upstreams:
|
||||
- id: example
|
||||
name: Benchmarking Data
|
||||
uri: http://localhost:3000
|
||||
processes:
|
||||
- id: nuxt-ssr
|
||||
workdir: ../data/ssr
|
||||
command: ["node", ".output/server/index.mjs"]
|
@ -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"
|
@ -1,8 +0,0 @@
|
||||
name: Example Site
|
||||
rules:
|
||||
- host: ["localhost:8000"]
|
||||
path: ["/"]
|
||||
upstreams:
|
||||
- id: example
|
||||
name: Benchmarking Data
|
||||
uri: files://../data
|
@ -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"
|
@ -1,8 +0,0 @@
|
||||
name: Example Site
|
||||
rules:
|
||||
- host: ["localhost:8000"]
|
||||
path: ["/"]
|
||||
upstreams:
|
||||
- id: example
|
||||
name: Benchmarking Data
|
||||
uri: files://../data
|
@ -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"
|
Reference in New Issue
Block a user