Compare commits

52 Commits

Author SHA1 Message Date
0b38c4a470 Expand config search area 2024-02-14 15:06:51 +08:00
515f086f19 Compress 2024-02-14 14:51:00 +08:00
d75ac2999b 🗑️ Remove test data 2024-02-14 14:31:29 +08:00
7eee10c4ff WebSocket Support 2024-02-14 13:53:43 +08:00
e40fe6049f 🐛 Fix http proxy 2024-02-13 22:56:22 +08:00
ed9434b85a 💚 Fix dockerfile missing build-base 2024-02-13 21:03:33 +08:00
804108a209 💚 Fix dockerfile missing openssl during build 2024-02-13 20:51:39 +08:00
46736c12b9 🔨 Update dockerfile 2024-02-13 20:48:40 +08:00
1a562fbee8 🔨 Add rust version as a pre-release version 2024-02-13 20:44:52 +08:00
7796ee3554 🔀 Merge pull request '♻️ 使用 Actix RS 重构' (#8) from refactor/actix-rs into refactor/rust
Reviewed-on: https://code.smartsheep.studio/Goatworks/RoadSign/pulls/8
2024-02-13 12:39:08 +00:00
12add73ecb Multiple listeners 2024-02-13 20:32:13 +08:00
3fbe1db1ef TLS 2024-02-13 18:27:01 +08:00
e27023c130 More detailed error 2024-02-13 01:42:03 +08:00
2478a05c89 More details trace 2024-02-13 01:20:27 +08:00
cb8eab6c1b 🛂 Secured sideload 2024-02-13 01:04:43 +08:00
ae3894bea6 Sideload 2024-02-13 00:34:54 +08:00
b7d4a54d62 ♻️ Migrated to actix rs 2024-02-13 00:01:39 +08:00
ead748a508 Warden process manager 2024-01-15 23:22:53 +08:00
4c08d78bed Add headers in reserve proxy 2024-01-14 19:12:38 +08:00
a088f6224e Basic Auth 2024-01-14 18:39:22 +08:00
f02977b7d7 Metrics 2024-01-14 17:55:14 +08:00
905b70349b Clean url with suffix 2024-01-14 14:52:19 +08:00
91ecf9d7bb Static files 2024-01-14 13:14:41 +08:00
c991d0b54a Route and forward for http(s) 2024-01-14 02:01:56 +08:00
5de1d13907 Config loader 2024-01-13 14:46:26 +08:00
bf7004c89c 🎉 Start developing! 2024-01-13 12:35:59 +08:00
3f434bfe46 💚 Bug fixes
All checks were successful
release-nightly / build-docker (push) Successful in 3m48s
2024-01-01 20:59:19 +08:00
afd6daae18 💚 Use official docker registery to store
Some checks failed
release-nightly / build-docker (push) Failing after 18s
2024-01-01 20:58:17 +08:00
4a35602388 🔀 Merge pull request ' 现在支持 Web Admin 面板' (#4) from features/web-admin into master
Some checks failed
release-nightly / build-docker (push) Failing after 3m52s
Reviewed-on: https://code.smartsheep.studio/Goatworks/RoadSign/pulls/4
2024-01-01 10:18:27 +00:00
14a7d936d2 RoadSign Sideload now built-in web ui 2024-01-01 18:16:57 +08:00
86b65cd21f 🎉 Basic Web Sideload 2024-01-01 18:07:21 +08:00
9a5c5e9fca Auto Redirect
All checks were successful
release-nightly / build-docker (push) Successful in 3m33s
2024-01-01 01:42:32 +08:00
90ac125886 🐛 Fix json encode and decode issue
All checks were successful
release-nightly / build-docker (push) Successful in 1m14s
2023-12-16 23:08:07 +08:00
0ffd582d80 🐛 Bug fixes
All checks were successful
release-nightly / build-docker (push) Successful in 1m19s
2023-12-16 22:57:32 +08:00
4942a8b7a2 Support set environment for process manager
All checks were successful
release-nightly / build-docker (push) Successful in 1m10s
2023-12-16 21:12:45 +08:00
47bc1c6aa1 🐛 Bug fixes of compress transformer
All checks were successful
release-nightly / build-docker (push) Successful in 1m3s
2023-12-16 12:49:08 +08:00
cea4114019 Replace json encoder and decoder
All checks were successful
release-nightly / build-docker (push) Successful in 1m1s
2023-12-16 12:35:42 +08:00
79e060da5d 💩 Clean up server header
All checks were successful
release-nightly / build-docker (push) Successful in 1m18s
2023-12-16 12:03:25 +08:00
f31d35c86c Compress Transformer
All checks were successful
release-nightly / build-docker (push) Successful in 1m4s
2023-12-16 11:42:14 +08:00
ae165e0f12 ♻️ Move transformer to an isolated module
All checks were successful
release-nightly / build-docker (push) Successful in 1m3s
2023-12-16 10:25:49 +08:00
ed1b20873d Move preheat operation in the goroutine
All checks were successful
release-nightly / build-docker (push) Successful in 1m7s
2023-12-14 21:29:26 +08:00
996e52a5f9 🐛 Fix cannot read upstreams settings from querystring
All checks were successful
release-nightly / build-docker (push) Successful in 1m8s
2023-12-14 21:24:17 +08:00
8e2ec23856 📝 Update README
All checks were successful
release-nightly / build-docker (push) Successful in 1m4s
2023-12-14 21:17:45 +08:00
17196c5835 More cli compatibility check
All checks were successful
release-nightly / build-docker (push) Successful in 1m6s
2023-12-13 20:05:34 +08:00
0d8583e395 Detailed version
All checks were successful
release-nightly / build-docker (push) Successful in 1m2s
2023-12-13 20:00:05 +08:00
8f1ac85148 🚚 Rename management APIs 2023-12-13 19:52:56 +08:00
fb24f44e22 🎨 Improve callback functions
All checks were successful
release-nightly / build-docker (push) Successful in 1m6s
2023-12-13 19:45:26 +08:00
eae2b12764 🐛 Fix preheat didn't run yet
All checks were successful
release-nightly / build-docker (push) Successful in 1m7s
2023-12-13 13:16:00 +08:00
426af568dc Preheat Process
All checks were successful
release-nightly / build-docker (push) Successful in 1m15s
2023-12-12 21:07:05 +08:00
ed2b65355c Fix a test environment
All checks were successful
release-nightly / build-docker (push) Successful in 1m19s
2023-12-12 20:57:11 +08:00
6905c60d82 ♻️ Refactored site config synchronization 2023-12-12 20:52:57 +08:00
4dc2729024 📝 Update README 2023-12-12 20:49:03 +08:00
70 changed files with 3780 additions and 2219 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -2,28 +2,27 @@ name: release-nightly
on: on:
push: push:
branches: [ master ] branches: [ refactor/rust ]
jobs: jobs:
build-docker: build-image:
runs-on: edge runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: code.smartsheep.studio
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@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile
push: true push: true
tags: code.smartsheep.studio/goatworks/roadsign:nightly file: ./Dockerfile
tags: xsheep2010/roadsign:sigma

9
.gitignore vendored
View File

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

11
.idea/RoadSign.iml generated
View File

@ -1,9 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="FacetManager">
<facet type="Python" name="Python facet">
<configuration sdkName="Python 3.9" />
</facet>
</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" />
</component> </component>
</module> </module>

59
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,59 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

2182
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
Cargo.toml Normal file
View File

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

View File

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

View File

@ -10,23 +10,85 @@ A blazing fast reverse proxy with a lot of shining features.
4. Integrate with CI/CD 4. Integrate with CI/CD
5. Webhook integration 5. Webhook integration
6. ~~Web management panel~~ 6. ~~Web management panel~~
7. **Blazing fast ⚡** 7. One-liner CLI
8. **Blazing fast ⚡**
> Deleted item means under construction, check out our roadmap! > Deleted item means under construction, check out our roadmap!
### How fast is it? ### How fast is it?
We use roadsign and nginx to host a same static file, and test them with [go-wrk](https://github.com/tsliwowicz/go-wrk). We use roadsign and nginx to host a same static file, and test them with [go-wrk](https://github.com/tsliwowicz/go-wrk).
Here's the result: Here's the result:
| **Software** | Total Requests | Requests per Seconds | Transfer per Seconds | Avg Time | Fastest Time | Slowest Time | Errors Count | | **Software** | Total Requests | Requests per Seconds | Transfer per Seconds | Avg Time | Fastest Time | Slowest Time | Errors Count |
|:---------------------:|:--------------:|:--------------------:|:--------------------:|:-----------:|:------------:|:------------:|:------------:| |:---------------------:|:--------------:|:--------------------:|:--------------------:|:-----------:|:------------:|:------------:|:------------:|
| _Nginx_ | 515749 | 4299.58 | 2.05MB | 13.954846ms | 0s (Cached) | 410.6972ms | 0 | | _Nginx_ | 515749 | 4299.58 | 2.05MB | 13.954846ms | 0s (Cached) | 410.6972ms | 0 |
| _RoadSign_ | 8905230 | 76626.70 | 30.98MB | 783.016µs | 28.542µs | 46.773083ms | 0 | | _RoadSign_ | 8905230 | 76626.70 | 30.98MB | 783.016µs | 28.542µs | 46.773083ms | 0 |
| _RoadSign w/ Prefork_ | 4784308 | 40170.41 | 16.24MB | 1.493636ms | 34.291µs | 8.727666ms | 0 | | _RoadSign w/ Prefork_ | 4784308 | 40170.41 | 16.24MB | 1.493636ms | 34.291µs | 8.727666ms | 0 |
As result, roadsign undoubtedly is the fastest one. As result, roadsign undoubtedly is the fastest one.
It can be found that the prefork feature makes RoadSign more stable in concurrency. We can see this from the **Slowest Time**. At the same time, the **Fastest Time** is affected because reusing ports requires some extra steps to handle load balancing. Enable this feature at your own discretion depending on your use case. It can be found that the prefork feature makes RoadSign more stable in concurrency. We can see this from the **Slowest
Time**. At the same time, the **Fastest Time** is affected because reusing ports requires some extra steps to handle
load balancing. Enable this feature at your own discretion depending on your use case.
More details can be found at benchmark's [README.md](./test/README.md) More details can be found at benchmark's [README.md](./test/README.md)
## Installation
We strongly recommend you install RoadSign via docker compose.
```yaml
version: "3"
services:
roadsign:
image: code.smartsheep.studio/goatworks/roadsign:nightly
restart: always
volumes:
- "./certs:/certs" # Optional, use for storage certificates
- "./config:/config"
- "./wwwroot:/wwwroot" # Optional, use for storage web apps
- "./settings.yml:/settings.yml"
ports:
- "80:80"
- "443:443"
- "81:81"
```
After that, you can manage your roadsign instance with RoadSign CLI aka. RDS CLI.
To install it, run this command. (Make sure you have golang toolchain on your computer)
```shell
go install -buildvcs code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest
# Tips: Add `buildvsc` flag to provide more detail compatibility check.
```
## Usage
To use roadsign, you need to add a configuration for it. Create a file locally.
Name whatever you like. And follow our [documentation](https://wiki.smartsheep.studio/roadsign/configuration/index.html) to
write it.
After configure, you need sync your config to remote server. Before that, add a connection between roadsign server and
rds cli with this command.
```shell
rds connect <id> <url> <password>
# ID will allow you find this server.py.rs in after commands.
# URL is to your roadsign server.py.rs sideload api.
# Password is your roadsign server.py.rs credential.
# ======================================================================
# !WARNING! All these things will storage in your $HOME/.roadsignrc.yaml
# ======================================================================
```
Then, sync your local config to remote.
```shell
rds sync <server.py.rs id> <site id> <config file>
# Server ID is your server.py.rs added by last command.
# Site ID is your new site id or old site id if you need update it.
# Config File is your local config file path.
```
After a few seconds, your website is ready!

17
Settings.toml Normal file
View File

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

55
go.mod
View File

@ -1,55 +0,0 @@
module code.smartsheep.studio/goatworks/roadsign
go 1.21.4
require (
github.com/gofiber/fiber/v2 v2.51.0
github.com/google/uuid v1.4.0
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.50.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sync v0.5.0 // indirect
)
require (
github.com/andybalholm/brotli v1.0.5 // 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.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.2.0 // 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.10.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.15.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)

569
go.sum
View File

@ -1,569 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/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/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/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.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
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/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
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=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

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

View File

@ -1,85 +0,0 @@
package administration
import (
"context"
"os"
"path/filepath"
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"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 site *sign.SiteConfig
var upstream *sign.UpstreamConfig
var process *sign.ProcessConfig
for _, item := range sign.App.Sites {
if item.ID == c.Params("site") {
site = item
for _, stream := range item.Upstreams {
if stream.ID == c.Params("slug") {
upstream = stream
workdir, _ = stream.GetRawURI()
break
}
}
for _, proc := range item.Processes {
if proc.ID == c.Params("slug") {
process = proc
workdir = proc.Workdir
break
}
}
break
}
}
if upstream == nil && process == nil {
return fiber.ErrNotFound
} else if upstream != nil && upstream.GetType() != sign.UpstreamTypeFile {
return fiber.ErrUnprocessableEntity
}
for _, process := range site.Processes {
process.StopProcess()
}
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
}
}
}
}
return c.SendStatus(fiber.StatusOK)
}

View File

@ -1,51 +0,0 @@
package administration
import (
"fmt"
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 InitAdministration() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign Administration",
ServerHeader: fmt.Sprintf("RoadSign Administration v%s", roadsign.AppVersion),
DisableStartupMessage: true,
EnableIPValidation: true,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
TrustedProxies: viper.GetStringSlice("security.administration_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: "[Administration] [${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")
},
}))
cgi := app.Group("/cgi").Name("CGI")
{
cgi.All("/connectivity", responseConnectivity)
}
webhooks := app.Group("/webhooks").Name("WebHooks")
{
webhooks.Put("/publish/:site/:slug", doPublish)
webhooks.Put("/sync/:slug", doSyncSite)
}
return app
}

View File

@ -1,48 +0,0 @@
package administration
import (
"fmt"
"os"
"path/filepath"
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
func doSyncSite(c *fiber.Ctx) error {
var req sign.SiteConfig
if err := c.BodyParser(&req); err != nil {
return err
}
id := c.Params("slug")
path := filepath.Join(viper.GetString("paths.configs"), fmt.Sprintf("%s.yaml", 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, _ := yaml.Marshal(req)
file.Write(raw)
defer file.Close()
}
pushed := false
sign.App.Sites = lo.Map(sign.App.Sites, func(item *sign.SiteConfig, idx int) *sign.SiteConfig {
if item.ID == id {
pushed = true
return &req
} else {
return item
}
})
if !pushed {
sign.App.Sites = append(sign.App.Sites, &req)
}
return c.SendStatus(fiber.StatusOK)
}

View File

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

View File

@ -1,37 +0,0 @@
package conn
import (
"encoding/json"
"fmt"
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) GetConnectivity() error {
client := fiber.Get(v.Url + "/cgi/connectivity")
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 {
log.Warn().Msg("Server connected successfully, but remote server version mismatch than CLI version, some features may buggy or completely unusable.")
}
}
return nil
}

View File

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

View File

@ -1,94 +0,0 @@
package deploy
import (
"fmt"
"io"
"os"
"strings"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds/conn"
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.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")
}
// 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",
Aliases: []string{"sc"},
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")
}
var site sign.SiteConfig
if file, err := os.Open(ctx.Args().Get(2)); err != nil {
return err
} else {
raw, _ := io.ReadAll(file)
yaml.Unmarshal(raw, &site)
}
url := fmt.Sprintf("/webhooks/sync/%s", ctx.Args().Get(1))
client := fiber.Put(server.Url+url).
JSON(site).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to sync to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site configuration is up-to-date! 🎉")
return nil
},
},
}

View File

@ -1,48 +0,0 @@
package main
import (
"os"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds/conn"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds/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("yaml")
// Load settings
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
viper.SafeWriteConfig()
viper.ReadInConfig()
} else {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
}
// Configure CLI
app := &cli.App{
Name: "RoadSign CLI",
Version: roadsign.AppVersion,
Suggest: true,
Commands: append(append([]*cli.Command{}, conn.CliCommands...), deploy.DeployCommands...),
}
// Run CLI
if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("An error occurred when running cli.")
}
}

View File

@ -1,78 +0,0 @@
package main
import (
"os"
"os/signal"
"strings"
"syscall"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/administration"
"code.smartsheep.studio/goatworks/roadsign/pkg/hypertext"
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"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("yaml")
// 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 sign
if err := sign.ReadInConfig(viper.GetString("paths.configs")); err != nil {
log.Panic().Err(err).Msg("An error occurred when loading configurations.")
} else {
log.Info().Int("count", len(sign.App.Sites)).Msg("All configuration has been loaded.")
}
// 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 administration server
hypertext.RunServer(
administration.InitAdministration(),
viper.GetStringSlice("hypertext.administration_ports"),
viper.GetStringSlice("hypertext.administration_secured_ports"),
viper.GetString("hypertext.certificate.administration_pem"),
viper.GetString("hypertext.certificate.administration_key"),
)
log.Info().Msgf("RoadSign v%s is started...", roadsign.AppVersion)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info().Msgf("RoadSign v%s is quitting...", roadsign.AppVersion)
}

View File

@ -1,107 +0,0 @@
package hypertext
import (
"regexp"
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"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 _, site := range sign.App.Sites {
// Matching rules
for _, rule := range site.Rules {
if !lo.Contains(rule.Host, host) {
continue
}
if !func() bool {
flag := false
for _, pattern := range rule.Path {
if ok, _ := regexp.MatchString(pattern, path); ok {
flag = true
break
}
}
return flag
}() {
continue
}
// Filter query strings
flag := true
for rk, rv := range rule.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 rule.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
}
// Passing all the rules means the site is what we are looking for.
// Let us respond to our client!
return makeResponse(ctx, site)
}
}
// 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, site *sign.SiteConfig) error {
// Modify request
for _, transformer := range site.Transformers {
transformer.TransformRequest(ctx)
}
// Forward
err := sign.App.Forward(ctx, site)
// Modify response
for _, transformer := range site.Transformers {
transformer.TransformResponse(ctx)
}
return err
}

View File

@ -1,62 +0,0 @@
package hypertext
import (
"fmt"
"time"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"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: fmt.Sprintf("RoadSign v%s", roadsign.AppVersion),
DisableStartupMessage: true,
EnableIPValidation: true,
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 err := app.Listen(port); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.")
}
}()
}
for _, port := range securedPorts {
port := port
go func() {
if err := app.ListenTLS(port, pem, key); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.")
}
}()
}
}

View File

@ -1,5 +0,0 @@
package roadsign
const (
AppVersion = "1.2.1"
)

View File

@ -1,60 +0,0 @@
package sign
import (
"io"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
var App *AppConfig
func ReadInConfig(root string) error {
cfg := &AppConfig{
Sites: []*SiteConfig{},
}
if err := filepath.Walk(root, func(fp string, info os.FileInfo, err error) error {
var site SiteConfig
if info.IsDir() {
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 := yaml.Unmarshal(data, &site); err != nil {
return err
} else {
defer file.Close()
// Extract file name as site id
site.ID = strings.SplitN(filepath.Base(fp), ".", 2)[0]
cfg.Sites = append(cfg.Sites, &site)
}
return nil
}); err != nil {
return err
}
App = cfg
return nil
}
func SaveInConfig(root string, cfg *AppConfig) error {
for _, site := range cfg.Sites {
data, _ := yaml.Marshal(site)
fp := filepath.Join(root, site.ID)
if file, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil {
return err
} else if _, err := file.Write(data); err != nil {
return err
}
}
return nil
}

View File

@ -1,10 +0,0 @@
package sign
import "encoding/json"
func DeserializeOptions[T any](data any) T {
var out T
raw, _ := json.Marshal(data)
_ = json.Unmarshal(raw, &out)
return out
}

View File

@ -1,80 +0,0 @@
package sign
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
type ProcessConfig struct {
ID string `json:"id" yaml:"id"`
Workdir string `json:"workdir" yaml:"workdir"`
Command []string `json:"command" yaml:"command"`
Prepares [][]string `json:"prepares" yaml:"prepares"`
Cmd *exec.Cmd `json:"-"`
}
func (v *ProcessConfig) BootProcess() error {
if v.Cmd != nil {
return nil
}
if err := v.PreapreProcess(); err != nil {
return err
}
if v.Cmd == nil {
return v.StartProcess()
}
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
return v.StartProcess()
}
if v.Cmd.ProcessState.Exited() {
return v.StartProcess()
} 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 *ProcessConfig) PreapreProcess() error {
for _, script := range v.Prepares {
if len(script) <= 0 {
continue
}
cmd := exec.Command(script[0], script[1:]...)
cmd.Dir = filepath.Join(v.Workdir)
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
func (v *ProcessConfig) StartProcess() error {
if len(v.Command) <= 0 {
return fmt.Errorf("you need set the command for %s to enable process manager", v.ID)
}
v.Cmd = exec.Command(v.Command[0], v.Command[1:]...)
v.Cmd.Dir = filepath.Join(v.Workdir)
return v.Cmd.Start()
}
func (v *ProcessConfig) StopProcess() 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
}

View File

@ -1,127 +0,0 @@
package sign
import (
"errors"
"fmt"
"io/fs"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/proxy"
"github.com/gofiber/fiber/v2/utils"
"github.com/samber/lo"
"github.com/spf13/viper"
"github.com/valyala/fasthttp"
)
func makeHypertextResponse(c *fiber.Ctx, upstream *UpstreamConfig) error {
timeout := time.Duration(viper.GetInt64("performance.network_timeout")) * time.Millisecond
return proxy.Do(c, upstream.MakeURI(c), &fasthttp.Client{
ReadTimeout: timeout,
WriteTimeout: timeout,
})
}
func makeFileResponse(c *fiber.Ctx, upstream *UpstreamConfig) error {
uri, queries := upstream.GetRawURI()
root := http.Dir(uri)
method := c.Method()
// We only serve static assets for GET and HEAD methods
if method != fiber.MethodGet && method != fiber.MethodHead {
return c.Next()
}
// Strip prefix
prefix := c.Route().Path
path := strings.TrimPrefix(c.Path(), prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Add prefix
if queries.Get("prefix") != "" {
path = queries.Get("prefix") + path
}
if len(path) > 1 {
path = utils.TrimRight(path, '/')
}
file, err := root.Open(path)
if err != nil && errors.Is(err, fs.ErrNotExist) {
if queries.Get("suffix") != "" {
file, err = root.Open(path + queries.Get("suffix"))
}
if err != nil && queries.Get("fallback") != "" {
file, err = root.Open(queries.Get("fallback"))
}
}
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return fiber.ErrNotFound
}
return fmt.Errorf("failed to open: %w", err)
}
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to stat: %w", err)
}
// Serve index if path is directory
if stat.IsDir() {
indexFile := lo.Ternary(len(queries.Get("index")) > 0, queries.Get("index"), "index.html")
indexPath := utils.TrimRight(path, '/') + indexFile
index, err := root.Open(indexPath)
if err == nil {
indexStat, err := index.Stat()
if err == nil {
file = index
stat = indexStat
}
}
}
c.Status(fiber.StatusOK)
modTime := stat.ModTime()
contentLength := int(stat.Size())
// Set Content-Type header
if queries.Get("charset") == "" {
c.Type(filepath.Ext(stat.Name()))
} else {
c.Type(filepath.Ext(stat.Name()), queries.Get("charset"))
}
// Set Last-Modified header
if !modTime.IsZero() {
c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat))
}
if method == fiber.MethodGet {
maxAge, err := strconv.Atoi(queries.Get("maxAge"))
if lo.Ternary(err != nil, maxAge, 0) > 0 {
c.Set(fiber.HeaderCacheControl, "public, max-age="+queries.Get("maxAge"))
}
c.Response().SetBodyStream(file, contentLength)
return nil
}
if method == fiber.MethodHead {
c.Request().ResetBody()
c.Response().SkipBody = true
c.Response().Header.SetContentLength(contentLength)
if err := file.Close(); err != nil {
return fmt.Errorf("failed to close: %w", err)
}
return nil
}
return fiber.ErrNotFound
}

View File

@ -1,55 +0,0 @@
package sign
import (
"errors"
"math/rand"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type AppConfig struct {
Sites []*SiteConfig `json:"sites"`
}
func (v *AppConfig) Forward(ctx *fiber.Ctx, site *SiteConfig) error {
if len(site.Upstreams) == 0 {
return errors.New("invalid configuration")
}
// Boot processes
for _, process := range site.Processes {
if err := process.BootProcess(); err != nil {
log.Warn().Err(err).Msgf("An error occurred when booting process (%s) for %s", process.ID, site.ID)
return fiber.ErrBadGateway
}
}
// Do forward
idx := rand.Intn(len(site.Upstreams))
upstream := site.Upstreams[idx]
switch upstream.GetType() {
case UpstreamTypeHypertext:
return makeHypertextResponse(ctx, upstream)
case UpstreamTypeFile:
return makeFileResponse(ctx, upstream)
default:
return fiber.ErrBadGateway
}
}
type SiteConfig struct {
ID string `json:"id"`
Rules []*RouterRuleConfig `json:"rules" yaml:"rules"`
Transformers []*RequestTransformerConfig `json:"transformers" yaml:"transformers"`
Upstreams []*UpstreamConfig `json:"upstreams" yaml:"upstreams"`
Processes []*ProcessConfig `json:"processes" yaml:"processes"`
}
type RouterRuleConfig struct {
Host []string `json:"host" yaml:"host"`
Path []string `json:"path" yaml:"path"`
Queries map[string]string `json:"queries" yaml:"queries"`
Headers map[string][]string `json:"headers" yaml:"headers"`
}

View File

@ -1,59 +0,0 @@
package sign
import (
"regexp"
"strings"
"github.com/gofiber/fiber/v2"
)
type RequestTransformer struct {
ModifyRequest func(options any, ctx *fiber.Ctx)
ModifyResponse func(options any, ctx *fiber.Ctx)
}
type RequestTransformerConfig struct {
Type string `json:"type" yaml:"type"`
Options any `json:"options" yaml:"options"`
}
func (v *RequestTransformerConfig) TransformRequest(ctx *fiber.Ctx) {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyRequest != nil {
f.ModifyRequest(v.Options, ctx)
}
break
}
}
}
func (v *RequestTransformerConfig) TransformResponse(ctx *fiber.Ctx) {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyResponse != nil {
f.ModifyResponse(v.Options, ctx)
}
break
}
}
}
var Transformers = map[string]RequestTransformer{
"replacePath": {
ModifyRequest: func(options any, ctx *fiber.Ctx) {
opts := DeserializeOptions[struct {
Pattern string `json:"pattern"`
Value string `json:"value"`
Repl string `json:"repl"` // Use when complex mode(regexp) enabled
Complex bool `json:"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))
}
},
},
}

View File

@ -1,57 +0,0 @@
package sign
import (
"fmt"
"net/url"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
const (
UpstreamTypeFile = "file"
UpstreamTypeHypertext = "hypertext"
UpstreamTypeUnknown = "unknown"
)
type UpstreamConfig struct {
ID string `json:"id" yaml:"id"`
URI string `json:"uri" yaml:"uri"`
}
func (v *UpstreamConfig) GetType() string {
protocol := strings.SplitN(v.URI, "://", 2)[0]
switch protocol {
case "file", "files":
return UpstreamTypeFile
case "http", "https":
return UpstreamTypeHypertext
}
return UpstreamTypeUnknown
}
func (v *UpstreamConfig) GetRawURI() (string, url.Values) {
uri := strings.SplitN(v.URI, "://", 2)[1]
data := strings.SplitN(uri, "?", 2)
qs, _ := url.ParseQuery(uri)
return data[0], qs
}
func (v *UpstreamConfig) 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())
return v.URI + path +
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
lo.Ternary(len(hash) > 0, "#"+hash, "")
}

12
regions/index.html Normal file
View File

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

21
regions/index.toml Normal file
View File

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

1
regions/kokodayo.txt Normal file
View File

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

3
regions/script.sh Executable file
View File

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

View File

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

View File

@ -1,27 +0,0 @@
debug:
print_routes: true
hypertext:
administration_ports:
- :81
administration_secured_ports: []
certificate:
administration_key: ./cert.key
administration_pem: ./cert.pem
key: ./cert.key
pem: ./cert.pem
limitation:
max_body_size: 536870912
max_qps: -1
ports:
- :8000
secured_ports: []
paths:
configs: ./config
performance:
request_logging: true
network_timeout: 3000
prefork: false
security:
administration_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

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

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

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

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

69
src/main.rs Normal file
View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7
src/server.rs Normal file
View File

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

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

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

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

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

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

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

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

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

76
src/tls.rs Normal file
View File

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

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

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

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

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

View File

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

View File

@ -1,3 +0,0 @@
/ssr
/spa
/congress

View File

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

View File

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

View File

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

View File

@ -1,26 +0,0 @@
debug:
print_routes: false
hypertext:
administration_ports: [":81"]
administration_secured_ports: []
certificate:
administration_key: ./cert.key
administration_pem: ./cert.pem
key: ./cert.key
pem: ./cert.pem
limitation:
max_body_size: 536870912
max_qps: -1
ports:
- :8000
secured_ports: []
paths:
configs: ./config
performance:
request_logging: false
network_timeout: 3000
prefork: false
security:
administration_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

View File

@ -1,12 +0,0 @@
name: Example Site
rules:
- host: ["localhost:8000"]
path: ["/"]
upstreams:
- id: example
name: Benchmarking Data
uri: http://localhost:3000
processes:
- id: nuxt-ssr
workdir: ../data/ssr
command: ["node", ".output/server/index.mjs"]

View File

@ -1,26 +0,0 @@
debug:
print_routes: false
hypertext:
administration_ports: [":81"]
administration_secured_ports: []
certificate:
administration_key: ./cert.key
administration_pem: ./cert.pem
key: ./cert.key
pem: ./cert.pem
limitation:
max_body_size: 536870912
max_qps: -1
ports:
- :8000
secured_ports: []
paths:
configs: ./config
performance:
request_logging: false
network_timeout: 3000
prefork: false
security:
administration_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

View File

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

View File

@ -1,26 +0,0 @@
debug:
print_routes: false
hypertext:
administration_ports: [":81"]
administration_secured_ports: []
certificate:
administration_key: ./cert.key
administration_pem: ./cert.pem
key: ./cert.key
pem: ./cert.pem
limitation:
max_body_size: 536870912
max_qps: -1
ports:
- :8000
secured_ports: []
paths:
configs: ./config
performance:
request_logging: false
network_timeout: 3000
prefork: true
security:
administration_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

View File

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

View File

@ -1,26 +0,0 @@
debug:
print_routes: false
hypertext:
administration_ports: [":81"]
administration_secured_ports: []
certificate:
administration_key: ./cert.key
administration_pem: ./cert.pem
key: ./cert.key
pem: ./cert.pem
limitation:
max_body_size: 536870912
max_qps: -1
ports:
- :8000
secured_ports: []
paths:
configs: ./config
performance:
request_logging: false
network_timeout: 3000
prefork: false
security:
administration_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

29
test/websocket/server.py Normal file
View File

@ -0,0 +1,29 @@
import asyncio
import websockets
async def handle_websocket(websocket, path):
# This function will be called whenever a new WebSocket connection is established
# Send a welcome message to the client
await websocket.send("Welcome to the WebSocket server!")
try:
# Enter the main loop to handle incoming messages
async for message in websocket:
# Print the received message
print(f"Received message: {message}")
# Send a response back to the client
response = f"Server received: {message}"
await websocket.send(response)
except websockets.exceptions.ConnectionClosedError:
print("Connection closed by the client.")
# Create the WebSocket server
start_server = websockets.serve(handle_websocket, "localhost", 8765)
print("WebSocket server started at ws://localhost:8765")
# Run the server indefinitely
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()