Compare commits
11 Commits
refactor/r
...
440851a305
Author | SHA1 | Date | |
---|---|---|---|
|
440851a305 | ||
|
61d30315ec | ||
|
450250c419 | ||
|
97df54a315 | ||
|
e8c39f38cc | ||
|
1ab4c5984e | ||
|
da90d12a3a | ||
|
bfce13fc74 | ||
|
4bdddf72e9 | ||
|
b906edc022 | ||
|
7ad17d9417 |
@@ -3,9 +3,8 @@
|
|||||||
{
|
{
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"name": "Run RoadSign",
|
"name": "Run RoadSign",
|
||||||
"goExecPath": "C:\\Tools\\Scoop\\shims\\go.exe",
|
"goExecPath": "/opt/homebrew/bin/go",
|
||||||
"buildParams": ["code.smartsheep.studio/goatworks/roadsign/pkg/cmd"],
|
"buildParams": ["code.smartsheep.studio/goatworks/roadsign/pkg/cmd/server"],
|
||||||
},
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
@@ -2,27 +2,27 @@ name: release-nightly
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ refactor/rust ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: edge
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
tags: xsheep2010/roadsign:sigma
|
push: true
|
||||||
|
tags: xsheep2010/roadsign:nightly
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,3 @@
|
|||||||
/config
|
|
||||||
/certs
|
|
||||||
/test/data
|
|
||||||
/letsencrypt
|
/letsencrypt
|
||||||
|
|
||||||
# Added by cargo
|
.DS_Store
|
||||||
|
|
||||||
/target
|
|
5
.idea/RoadSign.iml
generated
5
.idea/RoadSign.iml
generated
@@ -7,10 +7,7 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="Go" enabled="true" />
|
<component name="Go" enabled="true" />
|
||||||
<component name="NewModuleRootManager">
|
<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="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
|
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
|
||||||
|
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?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
2182
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@@ -1,38 +0,0 @@
|
|||||||
[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,13 +1,21 @@
|
|||||||
# Building Backend
|
# Building Backend
|
||||||
FROM rust:alpine as roadsign-server
|
FROM golang:alpine as roadsign-server
|
||||||
|
|
||||||
RUN apk add libressl-dev build-base
|
RUN apk add nodejs npm
|
||||||
|
|
||||||
WORKDIR /source
|
WORKDIR /source
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV RUSTFLAGS="-C target-feature=-crt-static"
|
WORKDIR /source/pkg/sideload/view
|
||||||
RUN cargo build --release
|
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
|
||||||
|
|
||||||
EXPOSE 81
|
EXPOSE 81
|
||||||
|
|
||||||
CMD ["/source/target/release/roadsign"]
|
CMD ["/roadsign/server"]
|
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)
|
To install it, run this command. (Make sure you have golang toolchain on your computer)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go install -buildvcs code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest
|
go install -buildvcs code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rdc@latest
|
||||||
# Tips: Add `buildvsc` flag to provide more detail compatibility check.
|
# Tips: Add `buildvsc` flag to provide more detail compatibility check.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ rds cli with this command.
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
rds connect <id> <url> <password>
|
rds connect <id> <url> <password>
|
||||||
# ID will allow you find this server.py.rs in after commands.
|
# ID will allow you find this server in after commands.
|
||||||
# URL is to your roadsign server.py.rs sideload api.
|
# URL is to your roadsign server sideload api.
|
||||||
# Password is your roadsign server.py.rs credential.
|
# Password is your roadsign server credential.
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
# !WARNING! All these things will storage in your $HOME/.roadsignrc.yaml
|
# !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.
|
Then, sync your local config to remote.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
rds sync <server.py.rs id> <site id> <config file>
|
rds sync <server id> <site id> <config file>
|
||||||
# Server ID is your server.py.rs added by last command.
|
# Server ID is your server added by last command.
|
||||||
# Site ID is your new site id or old site id if you need update it.
|
# Site ID is your new site id or old site id if you need update it.
|
||||||
# Config File is your local config file path.
|
# Config File is your local config file path.
|
||||||
```
|
```
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
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"
|
|
31
config/example.toml
Normal file
31
config/example.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
id = "example-region"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-websocket"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/ws"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-websocket-destination"
|
||||||
|
uri = "http://localhost:8765"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-warden"
|
||||||
|
host = ["localhost:4321"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-warden-destination"
|
||||||
|
uri = "http://localhost:4321"
|
||||||
|
|
||||||
|
[[applications]]
|
||||||
|
id = "example-warden-app"
|
||||||
|
workdir = "test/data/warden"
|
||||||
|
command = ["node", "dist/server/entry.mjs"]
|
||||||
|
environment = ["PUBLIC_CMS=https://smartsheep.studio"]
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-static"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/roadsign"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-static-destination"
|
||||||
|
uri = "files://test/data"
|
61
go.mod
Normal file
61
go.mod
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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
Normal file
170
go.sum
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
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=
|
89
pkg/cmd/rdc/conn/commands.go
Normal file
89
pkg/cmd/rdc/conn/commands.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
42
pkg/cmd/rdc/conn/connect.go
Normal file
42
pkg/cmd/rdc/conn/connect.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
}
|
17
pkg/cmd/rdc/conn/directory.go
Normal file
17
pkg/cmd/rdc/conn/directory.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
98
pkg/cmd/rdc/deploy/commands.go
Normal file
98
pkg/cmd/rdc/deploy/commands.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
48
pkg/cmd/rdc/main.go
Normal file
48
pkg/cmd/rdc/main.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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.")
|
||||||
|
}
|
||||||
|
}
|
81
pkg/cmd/server/main.go
Normal file
81
pkg/cmd/server/main.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
}
|
139
pkg/hypertext/proxies.go
Normal file
139
pkg/hypertext/proxies.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
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
|
||||||
|
}
|
79
pkg/hypertext/server.go
Normal file
79
pkg/hypertext/server.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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
Normal file
17
pkg/meta.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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"
|
52
pkg/navi/config.go
Normal file
52
pkg/navi/config.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
|
}
|
25
pkg/navi/metrics.go
Normal file
25
pkg/navi/metrics.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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:]
|
||||||
|
}
|
||||||
|
}
|
204
pkg/navi/responder.go
Normal file
204
pkg/navi/responder.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
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
|
||||||
|
}
|
25
pkg/navi/route.go
Normal file
25
pkg/navi/route.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
87
pkg/navi/struct.go
Normal file
87
pkg/navi/struct.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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)
|
||||||
|
}
|
41
pkg/navi/transformers/compress.go
Normal file
41
pkg/navi/transformers/compress.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
61
pkg/navi/transformers/module.go
Normal file
61
pkg/navi/transformers/module.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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,
|
||||||
|
}
|
26
pkg/navi/transformers/replace_path.go
Normal file
26
pkg/navi/transformers/replace_path.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
17
pkg/navi/warden.go
Normal file
17
pkg/navi/warden.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
26
pkg/sideload/applications.go
Normal file
26
pkg/sideload/applications.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
13
pkg/sideload/metadata.go
Normal file
13
pkg/sideload/metadata.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
10
pkg/sideload/metrics.go
Normal file
10
pkg/sideload/metrics.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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)
|
||||||
|
}
|
91
pkg/sideload/publish.go
Normal file
91
pkg/sideload/publish.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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)
|
||||||
|
}
|
67
pkg/sideload/regions.go
Normal file
67
pkg/sideload/regions.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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)
|
||||||
|
}
|
71
pkg/sideload/server.go
Normal file
71
pkg/sideload/server.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
|
}
|
27
pkg/sideload/statistics.go
Normal file
27
pkg/sideload/statistics.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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
Normal file
24
pkg/sideload/view/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 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?
|
6
pkg/sideload/view/embed.go
Normal file
6
pkg/sideload/view/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:dist
|
||||||
|
var FS embed.FS
|
13
pkg/sideload/view/index.html
Normal file
13
pkg/sideload/view/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
24
pkg/sideload/view/package.json
Normal file
24
pkg/sideload/view/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
6
pkg/sideload/view/postcss.config.js
Normal file
6
pkg/sideload/view/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
3
pkg/sideload/view/src/index.css
Normal file
3
pkg/sideload/view/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
17
pkg/sideload/view/src/index.tsx
Normal file
17
pkg/sideload/view/src/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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!);
|
11
pkg/sideload/view/src/layouts/RootLayout.tsx
Normal file
11
pkg/sideload/view/src/layouts/RootLayout.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Navbar from "./shared/Navbar";
|
||||||
|
|
||||||
|
export default function RootLayout(props: any) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main class="h-[calc(100vh-64px)]">{props.children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
62
pkg/sideload/view/src/layouts/shared/Navbar.tsx
Normal file
62
pkg/sideload/view/src/layouts/shared/Navbar.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
10
pkg/sideload/view/src/pages/dashboard.tsx
Normal file
10
pkg/sideload/view/src/pages/dashboard.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
Normal file
1
pkg/sideload/view/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
44
pkg/sideload/view/tailwind.config.js
Normal file
44
pkg/sideload/view/tailwind.config.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/** @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")],
|
||||||
|
};
|
26
pkg/sideload/view/tsconfig.json
Normal file
26
pkg/sideload/view/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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" }]
|
||||||
|
}
|
10
pkg/sideload/view/tsconfig.node.json
Normal file
10
pkg/sideload/view/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
6
pkg/sideload/view/vite.config.ts
Normal file
6
pkg/sideload/view/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
|
})
|
1499
pkg/sideload/view/yarn.lock
Normal file
1499
pkg/sideload/view/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
120
pkg/warden/executor.go
Normal file
120
pkg/warden/executor.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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()
|
||||||
|
}
|
8
pkg/warden/manifest.go
Normal file
8
pkg/warden/manifest.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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"`
|
||||||
|
}
|
@@ -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 benchmarking test data!</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,21 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
Ko Ko Da Yo~
|
|
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "Good morning!" > ./kokodayo.txt
|
|
@@ -1,15 +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 benchmarking test data! And you are in the subfolder
|
|
||||||
now!
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
35
settings.toml
Normal file
35
settings.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[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"
|
@@ -1,10 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
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
69
src/main.rs
@@ -1,69 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
@@ -1,117 +0,0 @@
|
|||||||
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(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@@ -1,111 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,136 +0,0 @@
|
|||||||
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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,288 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,87 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct ServerBindConfig {
|
|
||||||
pub addr: String,
|
|
||||||
pub tls: bool,
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
@@ -1,52 +0,0 @@
|
|||||||
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<_>>(),
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
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
76
src/tls.rs
@@ -1,76 +0,0 @@
|
|||||||
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))
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,72 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
@@ -1,104 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
85
test/README.md
Normal file
85
test/README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 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
|
||||||
|
```
|
1
test/data/.gitignore
vendored
Normal file
1
test/data/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/warden
|
12
test/data/index.html
Normal file
12
test/data/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 vs. nginx benchmarking test data!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
test/data/sse/index.html
Normal file
27
test/data/sse/index.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!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>
|
28
test/data/sse/server.py
Normal file
28
test/data/sse/server.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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)
|
57
test/data/websocket/index.html
Normal file
57
test/data/websocket/index.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<!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>
|
117
test/nginx/nginx.conf
Normal file
117
test/nginx/nginx.conf
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
#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;
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
|
||||||
|
}
|
9
test/roadsign-spa/config/example.toml
Normal file
9
test/roadsign-spa/config/example.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id = "example"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-location"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-destination"
|
||||||
|
uri = "files://../data/spa?fallback=index.html"
|
31
test/roadsign-spa/settings.toml
Normal file
31
test/roadsign-spa/settings.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[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"
|
15
test/roadsign-ssr/config/example.toml
Normal file
15
test/roadsign-ssr/config/example.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
id = "example-region"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-warden"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-warden-destination"
|
||||||
|
uri = "http://localhost:4321"
|
||||||
|
|
||||||
|
[[applications]]
|
||||||
|
id = "example-warden-app"
|
||||||
|
workdir = "../data/warden"
|
||||||
|
command = ["node", "dist/server/entry.mjs"]
|
||||||
|
environment = ["PUBLIC_CMS=https://smartsheep.studio"]
|
31
test/roadsign-ssr/settings.toml
Normal file
31
test/roadsign-ssr/settings.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[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"
|
9
test/roadsign-with-prefork/config/example.toml
Normal file
9
test/roadsign-with-prefork/config/example.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
id = "example"
|
||||||
|
|
||||||
|
[[locations]]
|
||||||
|
id = "example-location"
|
||||||
|
host = ["localhost:8000"]
|
||||||
|
path = ["/"]
|
||||||
|
[[locations.destinations]]
|
||||||
|
id = "example-destination"
|
||||||
|
uri = "files://../data/spa?fallback=index.html"
|
31
test/roadsign-with-prefork/settings.toml
Normal file
31
test/roadsign-with-prefork/settings.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[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"
|
8
test/roadsign/config/example.yaml
Normal file
8
test/roadsign/config/example.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name: Example Site
|
||||||
|
rules:
|
||||||
|
- host: ["localhost:8000"]
|
||||||
|
path: ["/"]
|
||||||
|
upstreams:
|
||||||
|
- id: example
|
||||||
|
name: Benchmarking Data
|
||||||
|
uri: files://../data
|
31
test/roadsign/settings.toml
Normal file
31
test/roadsign/settings.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[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