Compare commits
	
		
			52 Commits
		
	
	
		
			85b0cc0c91
			...
			refactor/r
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0b38c4a470 | |||
| 515f086f19 | |||
| d75ac2999b | |||
| 7eee10c4ff | |||
| e40fe6049f | |||
| ed9434b85a | |||
| 804108a209 | |||
| 46736c12b9 | |||
| 1a562fbee8 | |||
| 7796ee3554 | |||
| 12add73ecb | |||
| 3fbe1db1ef | |||
| e27023c130 | |||
| 2478a05c89 | |||
| cb8eab6c1b | |||
| ae3894bea6 | |||
| b7d4a54d62 | |||
| ead748a508 | |||
| 4c08d78bed | |||
| a088f6224e | |||
| f02977b7d7 | |||
| 905b70349b | |||
| 91ecf9d7bb | |||
| c991d0b54a | |||
| 5de1d13907 | |||
| bf7004c89c | |||
| 3f434bfe46 | |||
| afd6daae18 | |||
| 4a35602388 | |||
| 14a7d936d2 | |||
| 86b65cd21f | |||
| 9a5c5e9fca | |||
| 90ac125886 | |||
| 0ffd582d80 | |||
| 4942a8b7a2 | |||
| 47bc1c6aa1 | |||
| cea4114019 | |||
| 79e060da5d | |||
| f31d35c86c | |||
| ae165e0f12 | |||
| ed1b20873d | |||
| 996e52a5f9 | |||
| 8e2ec23856 | |||
| 17196c5835 | |||
| 0d8583e395 | |||
| 8f1ac85148 | |||
| fb24f44e22 | |||
| eae2b12764 | |||
| 426af568dc | |||
| ed2b65355c | |||
| 6905c60d82 | |||
| 4dc2729024 | 
| @@ -2,28 +2,27 @@ name: release-nightly | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|     branches: [ refactor/rust ] | ||||
| 
 | ||||
| jobs: | ||||
|   build-docker: | ||||
|     runs-on: edge | ||||
|   build-image: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         uses: actions/checkout@v4 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|         uses: docker/setup-qemu-action@v3 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v2 | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           registry: code.smartsheep.studio | ||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v4 | ||||
|         uses: docker/build-push-action@v5 | ||||
|         with: | ||||
|           context: . | ||||
|           file: ./Dockerfile | ||||
|           push: true | ||||
|           tags: code.smartsheep.studio/goatworks/roadsign:nightly | ||||
|           file: ./Dockerfile | ||||
|           tags: xsheep2010/roadsign:sigma | ||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,8 @@ | ||||
| /config | ||||
| /config | ||||
| /certs | ||||
| /test/data | ||||
| /letsencrypt | ||||
|  | ||||
| # Added by cargo | ||||
|  | ||||
| /target | ||||
|   | ||||
							
								
								
									
										11
									
								
								.idea/RoadSign.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								.idea/RoadSign.iml
									
									
									
										generated
									
									
									
								
							| @@ -1,9 +1,18 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <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="NewModuleRootManager"> | ||||
|     <content url="file://$MODULE_DIR$" /> | ||||
|     <content url="file://$MODULE_DIR$"> | ||||
|       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/target" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|     <orderEntry type="library" name="Python 3.9 interpreter library" level="application" /> | ||||
|   </component> | ||||
| </module> | ||||
							
								
								
									
										59
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										2182
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										38
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| [package] | ||||
| name = "roadsign" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| actix-files = "0.6.5" | ||||
| actix-proxy = "0.2.0" | ||||
| actix-web = { version = "4.5.1", features = ["rustls-0_22"] } | ||||
| actix-web-httpauth = "0.8.1" | ||||
| awc = { version = "3.4.0", features = ["tls-rustls-0_22"] } | ||||
| config = { version = "0.14.0", features = ["toml"] } | ||||
| lazy_static = "1.4.0" | ||||
| mime = "0.3.17" | ||||
| percent-encoding = "2.3.1" | ||||
| queryst = "3.0.0" | ||||
| rand = "0.8.5" | ||||
| regex = "1.10.2" | ||||
| serde = "1.0.195" | ||||
| serde_json = "1.0.111" | ||||
| tokio = { version = "1.35.1", features = [ | ||||
|   "rt-multi-thread", | ||||
|   "macros", | ||||
|   "time", | ||||
|   "full", | ||||
| ] } | ||||
| toml = "0.8.8" | ||||
| tracing = "0.1.40" | ||||
| tracing-subscriber = "0.3.18" | ||||
| wildmatch = "2.3.0" | ||||
| derive_more = "0.99.17" | ||||
| rustls = "0.22.2" | ||||
| rustls-pemfile = "2.0.0" | ||||
| futures = "0.3.30" | ||||
| actix-web-actors = "4.3.0" | ||||
| actix = "0.13.3" | ||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,15 +1,13 @@ | ||||
| # Building Backend | ||||
| FROM golang:alpine as roadsign-server | ||||
| FROM rust:alpine as roadsign-server | ||||
|  | ||||
| RUN apk add libressl-dev build-base | ||||
|  | ||||
| WORKDIR /source | ||||
| COPY . . | ||||
| RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dist ./pkg/cmd/server/main.go | ||||
|  | ||||
| # Runtime | ||||
| FROM golang:alpine | ||||
|  | ||||
| COPY --from=roadsign-server /dist /roadsign/server | ||||
| ENV RUSTFLAGS="-C target-feature=-crt-static" | ||||
| RUN cargo build --release | ||||
|  | ||||
| EXPOSE 81 | ||||
|  | ||||
| CMD ["/roadsign/server"] | ||||
| CMD ["/source/target/release/roadsign"] | ||||
							
								
								
									
										78
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										78
									
								
								README.md
									
									
									
									
									
								
							| @@ -10,23 +10,85 @@ A blazing fast reverse proxy with a lot of shining features. | ||||
| 4. Integrate with CI/CD | ||||
| 5. Webhook integration | ||||
| 6. ~~Web management panel~~ | ||||
| 7. **Blazing fast ⚡** | ||||
| 7. One-liner CLI | ||||
| 8. **Blazing fast ⚡** | ||||
|  | ||||
| > Deleted item means under construction, check out our roadmap! | ||||
|  | ||||
| ### 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: | ||||
|  | ||||
| |      **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      | | ||||
| |       _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      | | ||||
| |        _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 w/ Prefork_ |    4784308     |       40170.41       |       16.24MB        | 1.493636ms  |   34.291µs   |  8.727666ms  |      0       | | ||||
|  | ||||
| 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
									
								
							
							
						
						
									
										17
									
								
								Settings.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| regions = "./regions" | ||||
| secret = "aEXcED5xJ3" | ||||
|  | ||||
| [sideload] | ||||
| bind_addr = "0.0.0.0:81" | ||||
|  | ||||
| [[proxies.bind]] | ||||
| addr = "0.0.0.0:80" | ||||
| tls = false | ||||
| [[proxies.bind]] | ||||
| addr = "0.0.0.0:443" | ||||
| tls = false | ||||
|  | ||||
| [[certificates]] | ||||
| domain = "localhost" | ||||
| certs = "certs/fullchain.pem" | ||||
| key = "certs/privkey.pem" | ||||
							
								
								
									
										55
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								go.mod
									
									
									
									
									
								
							| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										569
									
								
								go.sum
									
									
									
									
									
								
							| @@ -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= | ||||
| @@ -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, | ||||
| 	}) | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package conn | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
|  | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
|  | ||||
| func GetConnection(id string) (CliConnection, bool) { | ||||
| 	var servers []CliConnection | ||||
| 	raw, _ := json.Marshal(viper.Get("servers")) | ||||
| 	_ = json.Unmarshal(raw, &servers) | ||||
| 	return lo.Find(servers, func(item CliConnection) bool { | ||||
| 		return item.ID == id | ||||
| 	}) | ||||
| } | ||||
| @@ -1,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 | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| @@ -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.") | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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.") | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| package roadsign | ||||
|  | ||||
| const ( | ||||
| 	AppVersion = "1.2.1" | ||||
| ) | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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"` | ||||
| } | ||||
| @@ -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)) | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										12
									
								
								regions/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <title>Hello, World!</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <p>Hello, there!</p> | ||||
|     <p>Here's the roadsign benchmarking test data!</p> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										21
									
								
								regions/index.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								regions/index.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| id = "index" | ||||
|  | ||||
| [[locations]] | ||||
| id = "root" | ||||
| hosts = ["localhost"] | ||||
| paths = ["/"] | ||||
| [[locations.destinations]] | ||||
| id = "websocket" | ||||
| uri = "http://localhost:8765" | ||||
| # [[locations.destinations]] | ||||
| # id = "hypertext" | ||||
| # uri = "https://example.com" | ||||
| # [[locations.destinations]] | ||||
| # id = "static" | ||||
| # uri = "files://regions?index=index.html" | ||||
|  | ||||
|  | ||||
| # [[applications]] | ||||
| # id = "script" | ||||
| # exe = "./script.sh" | ||||
| # workdir = "regions" | ||||
							
								
								
									
										1
									
								
								regions/kokodayo.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								regions/kokodayo.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| Ko Ko Da Yo~ | ||||
							
								
								
									
										3
									
								
								regions/script.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								regions/script.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| echo "Good morning!" > ./kokodayo.txt | ||||
							
								
								
									
										15
									
								
								regions/subfolder/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								regions/subfolder/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <title>Hello, World!</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <p>Hello, there!</p> | ||||
|     <p> | ||||
|       Here's the roadsign benchmarking test data! And you are in the subfolder | ||||
|       now! | ||||
|     </p> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										27
									
								
								settings.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								settings.yml
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										10
									
								
								src/config/loader.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| use config::Config; | ||||
|  | ||||
| pub fn load_settings() -> Config { | ||||
|     Config::builder() | ||||
|         .add_source(config::File::with_name("Settings")) | ||||
|         .add_source(config::File::with_name("/Settings")) | ||||
|         .add_source(config::Environment::with_prefix("ROADSIGN")) | ||||
|         .build() | ||||
|         .unwrap() | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| use config::Config; | ||||
| use lazy_static::lazy_static; | ||||
| use tokio::sync::RwLock; | ||||
|  | ||||
| use crate::config::loader::load_settings; | ||||
|  | ||||
| pub mod loader; | ||||
|  | ||||
| lazy_static! { | ||||
|     pub static ref CFG: RwLock<Config> = RwLock::new(load_settings()); | ||||
| } | ||||
							
								
								
									
										69
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| extern crate core; | ||||
|  | ||||
| mod config; | ||||
| mod proxies; | ||||
| mod sideload; | ||||
| mod warden; | ||||
| mod server; | ||||
| pub mod tls; | ||||
|  | ||||
| use std::error; | ||||
| use lazy_static::lazy_static; | ||||
| use proxies::RoadInstance; | ||||
| use tokio::sync::Mutex; | ||||
| use tokio::task::JoinSet; | ||||
| use tracing::{error, info, Level}; | ||||
| use crate::proxies::server::build_proxies; | ||||
| use crate::sideload::server::build_sideload; | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref ROAD: Mutex<RoadInstance> = Mutex::new(RoadInstance::new()); | ||||
| } | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> Result<(), Box<dyn error::Error>> { | ||||
|     // Setting up logging | ||||
|     tracing_subscriber::fmt() | ||||
|         .with_max_level(Level::DEBUG) | ||||
|         .init(); | ||||
|  | ||||
|     // Prepare all the stuff | ||||
|     info!("Loading proxy regions..."); | ||||
|     match proxies::loader::scan_regions( | ||||
|         config::CFG | ||||
|             .read() | ||||
|             .await | ||||
|             .get_string("regions")? | ||||
|     ) { | ||||
|         Err(_) => error!("Loading proxy regions... failed"), | ||||
|         Ok((regions, count)) => { | ||||
|             ROAD.lock().await.regions = regions; | ||||
|             info!(count, "Loading proxy regions... done") | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let mut server_set = JoinSet::new(); | ||||
|  | ||||
|     // Proxies | ||||
|     for server in build_proxies().await? { | ||||
|         server_set.spawn(server); | ||||
|     } | ||||
|  | ||||
|     // Sideload | ||||
|     server_set.spawn(build_sideload().await?); | ||||
|  | ||||
|     // Process manager | ||||
|     { | ||||
|         let mut app = ROAD.lock().await; | ||||
|         { | ||||
|             let reg = app.regions.clone(); | ||||
|             app.warden.scan(reg); | ||||
|         } | ||||
|         app.warden.start().await; | ||||
|     } | ||||
|  | ||||
|     // Wait for web servers | ||||
|     server_set.join_next().await; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										117
									
								
								src/proxies/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/proxies/config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use queryst::parse; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::json; | ||||
|  | ||||
| use crate::warden::Application; | ||||
|  | ||||
| use super::responder::StaticResponderConfig; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct Region { | ||||
|     pub id: String, | ||||
|     pub locations: Vec<Location>, | ||||
|     pub applications: Option<Vec<Application>>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct Location { | ||||
|     pub id: String, | ||||
|     pub hosts: Vec<String>, | ||||
|     pub paths: Vec<String>, | ||||
|     pub headers: Option<HashMap<String, String>>, | ||||
|     pub queries: Option<Vec<String>>, | ||||
|     pub methods: Option<Vec<String>>, | ||||
|     pub destinations: Vec<Destination>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct Destination { | ||||
|     pub id: String, | ||||
|     pub uri: String, | ||||
|     pub timeout: Option<u32>, | ||||
|     pub weight: Option<u32>, | ||||
| } | ||||
|  | ||||
| pub enum DestinationType { | ||||
|     Hypertext, | ||||
|     StaticFiles, | ||||
|     Unknown, | ||||
| } | ||||
|  | ||||
| impl Destination { | ||||
|     pub fn get_type(&self) -> DestinationType { | ||||
|         match self.get_protocol() { | ||||
|             "http" | "https" => DestinationType::Hypertext, | ||||
|             "file" | "files" => DestinationType::StaticFiles, | ||||
|             _ => DestinationType::Unknown, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_protocol(&self) -> &str { | ||||
|         self.uri.as_str().splitn(2, "://").collect::<Vec<_>>()[0] | ||||
|     } | ||||
|  | ||||
|     pub fn get_queries(&self) -> &str { | ||||
|         self.uri | ||||
|             .as_str() | ||||
|             .splitn(2, '?') | ||||
|             .collect::<Vec<_>>() | ||||
|             .get(1) | ||||
|             .unwrap_or(&"") | ||||
|     } | ||||
|  | ||||
|     pub fn get_host(&self) -> &str { | ||||
|         self | ||||
|             .uri | ||||
|             .as_str() | ||||
|             .splitn(2, "://") | ||||
|             .collect::<Vec<_>>() | ||||
|             .get(1) | ||||
|             .unwrap_or(&"") | ||||
|         .splitn(2, '?') | ||||
|         .collect::<Vec<_>>()[0] | ||||
|     } | ||||
|  | ||||
|     pub fn get_hypertext_uri(&self) -> Result<String, ()> { | ||||
|         match self.get_protocol() { | ||||
|             "http" => Ok("http://".to_string() + self.get_host()), | ||||
|             "https" => Ok("https://".to_string() + self.get_host()), | ||||
|             _ => Err(()), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_static_config(&self) -> Result<StaticResponderConfig, ()> { | ||||
|         match self.get_protocol() { | ||||
|             "file" | "files" => { | ||||
|                 let queries = parse(self.get_queries()).unwrap_or(json!({})); | ||||
|                 Ok(StaticResponderConfig { | ||||
|                     uri: self.get_host().to_string(), | ||||
|                     utf8: queries | ||||
|                         .get("utf8") | ||||
|                         .and_then(|val| val.as_bool()) | ||||
|                         .unwrap_or(false), | ||||
|                     browse: queries | ||||
|                         .get("browse") | ||||
|                         .and_then(|val| val.as_bool()) | ||||
|                         .unwrap_or(false), | ||||
|                     with_slash: queries | ||||
|                         .get("slash") | ||||
|                         .and_then(|val| val.as_bool()) | ||||
|                         .unwrap_or(false), | ||||
|                     index: queries | ||||
|                         .get("index") | ||||
|                         .and_then(|val| val.as_str().map(str::to_string)), | ||||
|                     fallback: queries | ||||
|                         .get("fallback") | ||||
|                         .and_then(|val| val.as_str().map(str::to_string)), | ||||
|                     suffix: queries | ||||
|                         .get("suffix") | ||||
|                         .and_then(|val| val.as_str().map(str::to_string)), | ||||
|                 }) | ||||
|             } | ||||
|             _ => Err(()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								src/proxies/loader.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/proxies/loader.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| use std::ffi::OsStr; | ||||
| use std::fs::{self, DirEntry}; | ||||
| use std::io; | ||||
|  | ||||
| use tracing::warn; | ||||
|  | ||||
| use crate::proxies::config; | ||||
|  | ||||
| pub fn scan_regions(basepath: String) -> io::Result<(Vec<config::Region>, u32)> { | ||||
|     let mut count: u32 = 0; | ||||
|     let mut result = vec![]; | ||||
|     for entry in fs::read_dir(basepath)? { | ||||
|         if let Ok(val) = load_region(entry.unwrap()) { | ||||
|             result.push(val); | ||||
|             count += 1; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     Ok((result, count)) | ||||
| } | ||||
|  | ||||
| pub fn load_region(file: DirEntry) -> Result<config::Region, String> { | ||||
|     if file.metadata().map(|val| val.is_dir()).unwrap() | ||||
|         || file.path().extension().and_then(OsStr::to_str).unwrap() != "toml" | ||||
|     { | ||||
|         return Err("File entry wasn't toml file".to_string()); | ||||
|     } | ||||
|  | ||||
|     let fp = file.path(); | ||||
|     let content = match fs::read_to_string(fp.clone()) { | ||||
|         Ok(val) => val, | ||||
|         Err(err) => { | ||||
|             warn!( | ||||
|                 err = format!("{:?}", err), | ||||
|                 filepath = fp.clone().to_str(), | ||||
|                 "An error occurred when loading region, skipped." | ||||
|             ); | ||||
|             return Err("Failed to load file".to_string()); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let data: config::Region = match toml::from_str(&content) { | ||||
|         Ok(val) => val, | ||||
|         Err(err) => { | ||||
|             warn!( | ||||
|                 err = format!("{:?}", err), | ||||
|                 filepath = fp.clone().to_str(), | ||||
|                 "An error occurred when parsing region, skipped." | ||||
|             ); | ||||
|             return Err("Failed to parse file".to_string()); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Ok(data) | ||||
| } | ||||
							
								
								
									
										111
									
								
								src/proxies/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/proxies/metrics.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| use std::collections::VecDeque; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use super::config::{Destination, Location, Region}; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct RoadTrace { | ||||
|     pub region: String, | ||||
|     pub location: String, | ||||
|     pub destination: String, | ||||
|     pub ip_address: String, | ||||
|     pub user_agent: String, | ||||
|     pub error: Option<String>, | ||||
| } | ||||
|  | ||||
| impl RoadTrace { | ||||
|     pub fn from_structs( | ||||
|         ip: String, | ||||
|         ua: String, | ||||
|         reg: Region, | ||||
|         loc: Location, | ||||
|         end: Destination, | ||||
|     ) -> RoadTrace { | ||||
|         RoadTrace { | ||||
|             ip_address: ip, | ||||
|             user_agent: ua, | ||||
|             region: reg.id, | ||||
|             location: loc.id, | ||||
|             destination: end.id, | ||||
|             error: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn from_structs_with_error( | ||||
|         ip: String, | ||||
|         ua: String, | ||||
|         reg: Region, | ||||
|         loc: Location, | ||||
|         end: Destination, | ||||
|         err: String, | ||||
|     ) -> RoadTrace { | ||||
|         let mut trace = Self::from_structs(ip, ua, reg, loc, end); | ||||
|         trace.error = Some(err); | ||||
|         trace | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct RoadMetrics { | ||||
|     pub requests_count: u64, | ||||
|     pub failures_count: u64, | ||||
|  | ||||
|     pub recent_successes: VecDeque<RoadTrace>, | ||||
|     pub recent_errors: VecDeque<RoadTrace>, | ||||
| } | ||||
|  | ||||
| const MAX_TRACE_COUNT: usize = 32; | ||||
|  | ||||
| impl RoadMetrics { | ||||
|     pub fn new() -> RoadMetrics { | ||||
|         RoadMetrics { | ||||
|             requests_count: 0, | ||||
|             failures_count: 0, | ||||
|             recent_successes: VecDeque::new(), | ||||
|             recent_errors: VecDeque::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_success_rate(&self) -> f64 { | ||||
|         if self.requests_count > 0 { | ||||
|             (self.requests_count - self.failures_count) as f64 / self.requests_count as f64 | ||||
|         } else { | ||||
|             0.0 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn add_success_request( | ||||
|         &mut self, | ||||
|         ip: String, | ||||
|         ua: String, | ||||
|         reg: Region, | ||||
|         loc: Location, | ||||
|         end: Destination, | ||||
|     ) { | ||||
|         self.requests_count += 1; | ||||
|         self.recent_successes | ||||
|             .push_back(RoadTrace::from_structs(ip, ua, reg, loc, end)); | ||||
|         if self.recent_successes.len() > MAX_TRACE_COUNT { | ||||
|             self.recent_successes.pop_front(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn add_failure_request( | ||||
|         &mut self, | ||||
|         ip: String, | ||||
|         ua: String, | ||||
|         reg: Region, | ||||
|         loc: Location, | ||||
|         end: Destination, | ||||
|         err: String, // For some reason error is rarely cloneable, so we use preformatted message | ||||
|     ) { | ||||
|         self.requests_count += 1; | ||||
|         self.failures_count += 1; | ||||
|         self.recent_errors | ||||
|             .push_back(RoadTrace::from_structs_with_error(ip, ua, reg, loc, end, err)); | ||||
|         if self.recent_errors.len() > MAX_TRACE_COUNT { | ||||
|             self.recent_errors.pop_front(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										136
									
								
								src/proxies/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/proxies/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| use actix_web::http::header::{ContentType, HeaderMap}; | ||||
| use actix_web::http::{Method, StatusCode, Uri}; | ||||
| use regex::Regex; | ||||
| use wildmatch::WildMatch; | ||||
| use actix_web::{error, HttpResponse}; | ||||
| use derive_more::{Display}; | ||||
|  | ||||
| use crate::warden::WardenInstance; | ||||
|  | ||||
| use self::{ | ||||
|     config::{Location, Region}, | ||||
|     metrics::RoadMetrics, | ||||
| }; | ||||
|  | ||||
| pub mod config; | ||||
| pub mod loader; | ||||
| pub mod metrics; | ||||
| pub mod responder; | ||||
| pub mod route; | ||||
| pub mod server; | ||||
|  | ||||
| #[derive(Debug, Display)] | ||||
| pub enum ProxyError { | ||||
|     #[display(fmt = "Upgrade required for this connection")] | ||||
|     UpgradeRequired, | ||||
|  | ||||
|     #[display(fmt = "Remote gateway issue")] | ||||
|     BadGateway, | ||||
|  | ||||
|     #[display(fmt = "No configured able to process this request")] | ||||
|     NoGateway, | ||||
|  | ||||
|     #[display(fmt = "Not found")] | ||||
|     NotFound, | ||||
|  | ||||
|     #[display(fmt = "Only accepts method GET")] | ||||
|     MethodGetOnly, | ||||
|  | ||||
|     #[display(fmt = "Invalid request path")] | ||||
|     InvalidRequestPath, | ||||
|  | ||||
|     #[display(fmt = "Upstream does not support protocol you used")] | ||||
|     NotImplemented, | ||||
| } | ||||
|  | ||||
| impl error::ResponseError for ProxyError { | ||||
|     fn status_code(&self) -> StatusCode { | ||||
|         match *self { | ||||
|             ProxyError::UpgradeRequired => StatusCode::UPGRADE_REQUIRED, | ||||
|             ProxyError::BadGateway => StatusCode::BAD_GATEWAY, | ||||
|             ProxyError::NoGateway => StatusCode::NOT_FOUND, | ||||
|             ProxyError::NotFound => StatusCode::NOT_FOUND, | ||||
|             ProxyError::MethodGetOnly => StatusCode::METHOD_NOT_ALLOWED, | ||||
|             ProxyError::InvalidRequestPath => StatusCode::BAD_REQUEST, | ||||
|             ProxyError::NotImplemented => StatusCode::NOT_IMPLEMENTED, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn error_response(&self) -> HttpResponse { | ||||
|         HttpResponse::build(self.status_code()) | ||||
|             .insert_header(ContentType::html()) | ||||
|             .body(self.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct RoadInstance { | ||||
|     pub regions: Vec<Region>, | ||||
|     pub metrics: RoadMetrics, | ||||
|     pub warden: WardenInstance, | ||||
| } | ||||
|  | ||||
| impl RoadInstance { | ||||
|     pub fn new() -> RoadInstance { | ||||
|         RoadInstance { | ||||
|             regions: vec![], | ||||
|             warden: WardenInstance { | ||||
|                 applications: vec![], | ||||
|             }, | ||||
|             metrics: RoadMetrics::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn filter( | ||||
|         &self, | ||||
|         uri: &Uri, | ||||
|         method: &Method, | ||||
|         headers: &HeaderMap, | ||||
|     ) -> Option<(&Region, &Location)> { | ||||
|         self.regions.iter().find_map(|region| { | ||||
|             let location = region.locations.iter().find(|location| { | ||||
|                 let mut hosts = location.hosts.iter(); | ||||
|                 if !hosts.any(|item| { | ||||
|                     WildMatch::new(item.as_str()).matches(uri.host().unwrap_or("localhost")) | ||||
|                 }) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 let mut paths = location.paths.iter(); | ||||
|                 if !paths.any(|item| { | ||||
|                     uri.path().starts_with(item) | ||||
|                         || Regex::new(item.as_str()).unwrap().is_match(uri.path()) | ||||
|                 }) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 if let Some(val) = location.methods.clone() { | ||||
|                     if !val.iter().any(|item| *item == method.to_string()) { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if let Some(val) = location.headers.clone() { | ||||
|                     match !val.keys().all(|item| { | ||||
|                         headers.get(item).unwrap() | ||||
|                             == location.headers.clone().unwrap().get(item).unwrap() | ||||
|                     }) { | ||||
|                         true => return false, | ||||
|                         false => (), | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 if let Some(val) = location.queries.clone() { | ||||
|                     let queries: Vec<&str> = uri.query().unwrap_or("").split('&').collect(); | ||||
|                     if !val.iter().all(|item| queries.contains(&item.as_str())) { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 true | ||||
|             }); | ||||
|  | ||||
|             location.map(|location| (region, location)) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										288
									
								
								src/proxies/responder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								src/proxies/responder.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | ||||
| use crate::proxies::ProxyError; | ||||
| use crate::proxies::ProxyError::{BadGateway, UpgradeRequired}; | ||||
| use actix_files::NamedFile; | ||||
| use actix_web::http::{header, Method}; | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use awc::error::HeaderValue; | ||||
| use awc::http::Uri; | ||||
| use awc::Client; | ||||
| use futures::Sink; | ||||
| use futures::stream::StreamExt; | ||||
| use std::str::FromStr; | ||||
| use std::time::Duration; | ||||
| use std::{ | ||||
|     ffi::OsStr, | ||||
|     path::{Path, PathBuf}, | ||||
| }; | ||||
| use actix::io::{SinkWrite, WriteHandler}; | ||||
| use actix::{Actor, ActorContext, AsyncContext, StreamHandler}; | ||||
| use actix_web_actors::ws; | ||||
| use actix_web_actors::ws::{CloseReason, handshake, ProtocolError, WebsocketContext}; | ||||
| use tracing::log::warn; | ||||
|  | ||||
| pub async fn respond_hypertext( | ||||
|     uri: String, | ||||
|     req: HttpRequest, | ||||
|     payload: web::Payload, | ||||
|     client: web::Data<Client>, | ||||
| ) -> Result<HttpResponse, ProxyError> { | ||||
|     let mut append_part = req.uri().to_string(); | ||||
|     if let Some(stripped_uri) = append_part.strip_prefix('/') { | ||||
|         append_part = stripped_uri.to_string(); | ||||
|     } | ||||
|  | ||||
|     let uri = Uri::from_str(uri.as_str()).expect("Invalid upstream"); | ||||
|     let target_url = format!("{}{}", uri, append_part); | ||||
|  | ||||
|     let forwarded_req = client | ||||
|         .request_from(target_url.as_str(), req.head()) | ||||
|         .insert_header((header::HOST, uri.host().expect("Invalid upstream"))); | ||||
|  | ||||
|     let forwarded_req = match req.connection_info().realip_remote_addr() { | ||||
|         Some(addr) => forwarded_req | ||||
|             .insert_header((header::X_FORWARDED_FOR, addr)) | ||||
|             .insert_header((header::X_FORWARDED_PROTO, req.connection_info().scheme())) | ||||
|             .insert_header((header::X_FORWARDED_HOST, req.connection_info().host())) | ||||
|             .insert_header(( | ||||
|                 header::FORWARDED, | ||||
|                 format!( | ||||
|                     "by={};for={};host={};proto={}", | ||||
|                     addr, | ||||
|                     addr, | ||||
|                     req.connection_info().host(), | ||||
|                     req.connection_info().scheme() | ||||
|                 ), | ||||
|             )), | ||||
|         None => forwarded_req, | ||||
|     }; | ||||
|  | ||||
|     if req | ||||
|         .headers() | ||||
|         .get(header::UPGRADE) | ||||
|         .unwrap_or(&HeaderValue::from_static("")) | ||||
|         .to_str() | ||||
|         .unwrap_or("") | ||||
|         .to_lowercase() | ||||
|         == "websocket" | ||||
|     { | ||||
|         let uri = uri.to_string().replacen("http", "ws", 1); | ||||
|         return respond_websocket(uri, req, payload).await; | ||||
|     } | ||||
|  | ||||
|     let res = forwarded_req | ||||
|         .timeout(Duration::from_secs(1800)) | ||||
|         .send_stream(payload) | ||||
|         .await | ||||
|         .map_err(|err| { | ||||
|             warn!("Remote gateway issue... {}", err); | ||||
|             BadGateway | ||||
|         })?; | ||||
|  | ||||
|     let mut client_resp = HttpResponse::build(res.status()); | ||||
|     for (header_name, header_value) in res | ||||
|         .headers() | ||||
|         .iter() | ||||
|         .filter(|(h, _)| *h != header::CONNECTION && *h != header::CONTENT_ENCODING) | ||||
|     { | ||||
|         client_resp.insert_header((header_name.clone(), header_value.clone())); | ||||
|     } | ||||
|  | ||||
|     Ok(client_resp.streaming(res)) | ||||
| } | ||||
|  | ||||
| pub struct WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + Sink<ws::Message>, | ||||
| { | ||||
|     send: SinkWrite<ws::Message, S>, | ||||
| } | ||||
|  | ||||
| impl<S> WriteHandler<ProtocolError> for WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + 'static + Sink<ws::Message>, | ||||
| { | ||||
|     fn error(&mut self, err: ProtocolError, ctx: &mut Self::Context) -> actix::Running { | ||||
|         self.error(err, ctx); | ||||
|         actix::Running::Stop | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<S> Actor for WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + 'static + Sink<ws::Message>, | ||||
| { | ||||
|     type Context = WebsocketContext<Self>; | ||||
| } | ||||
|  | ||||
| impl<S> StreamHandler<Result<ws::Frame, ProtocolError>> for WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + Sink<ws::Message> + 'static, | ||||
| { | ||||
|     fn handle(&mut self, item: Result<ws::Frame, ProtocolError>, ctx: &mut Self::Context) { | ||||
|         let frame = match item { | ||||
|             Ok(frame) => frame, | ||||
|             Err(err) => return self.error(err, ctx), | ||||
|         }; | ||||
|         let msg = match frame { | ||||
|             ws::Frame::Text(t) => match t.try_into() { | ||||
|                 Ok(t) => ws::Message::Text(t), | ||||
|                 Err(e) => { | ||||
|                     self.error(e, ctx); | ||||
|                     return; | ||||
|                 } | ||||
|             }, | ||||
|             ws::Frame::Binary(b) => ws::Message::Binary(b), | ||||
|             ws::Frame::Continuation(c) => ws::Message::Continuation(c), | ||||
|             ws::Frame::Ping(p) => ws::Message::Ping(p), | ||||
|             ws::Frame::Pong(p) => ws::Message::Pong(p), | ||||
|             ws::Frame::Close(r) => ws::Message::Close(r), | ||||
|         }; | ||||
|  | ||||
|         ctx.write_raw(msg) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<S> StreamHandler<Result<ws::Message, ProtocolError>> for WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + Sink<ws::Message> + 'static, | ||||
| { | ||||
|     fn handle(&mut self, item: Result<ws::Message, ProtocolError>, ctx: &mut Self::Context) { | ||||
|         let msg = match item { | ||||
|             Ok(msg) => msg, | ||||
|             Err(err) => return self.error(err, ctx), | ||||
|         }; | ||||
|  | ||||
|         let _ = self.send.write(msg); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<S> WebsocketProxy<S> | ||||
|     where | ||||
|         S: Unpin + Sink<ws::Message> + 'static, | ||||
| { | ||||
|     fn error<E>(&mut self, err: E, ctx: &mut <Self as Actor>::Context) | ||||
|         where | ||||
|             E: std::error::Error, | ||||
|     { | ||||
|         let reason = Some(CloseReason { | ||||
|             code: ws::CloseCode::Error, | ||||
|             description: Some(err.to_string()), | ||||
|         }); | ||||
|  | ||||
|         ctx.close(reason.clone()); | ||||
|         let _ = self.send.write(ws::Message::Close(reason)); | ||||
|         self.send.close(); | ||||
|  | ||||
|         ctx.stop(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn respond_websocket( | ||||
|     uri: String, | ||||
|     req: HttpRequest, | ||||
|     payload: web::Payload, | ||||
| ) -> Result<HttpResponse, ProxyError> { | ||||
|     let mut res = handshake(&req).map_err(|_| UpgradeRequired)?; | ||||
|  | ||||
|     let (_, conn) = awc::Client::new() | ||||
|         .ws(uri) | ||||
|         .connect() | ||||
|         .await | ||||
|         .map_err(|_| BadGateway)?; | ||||
|  | ||||
|     let (send, recv) = conn.split(); | ||||
|  | ||||
|     let out = WebsocketContext::with_factory(payload, |ctx| { | ||||
|         ctx.add_stream(recv); | ||||
|         WebsocketProxy { | ||||
|             send: SinkWrite::new(send, ctx), | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     Ok(res.streaming(out)) | ||||
| } | ||||
|  | ||||
| pub struct StaticResponderConfig { | ||||
|     pub uri: String, | ||||
|     pub utf8: bool, | ||||
|     pub browse: bool, | ||||
|     pub with_slash: bool, | ||||
|     pub index: Option<String>, | ||||
|     pub fallback: Option<String>, | ||||
|     pub suffix: Option<String>, | ||||
| } | ||||
|  | ||||
| pub async fn respond_static( | ||||
|     cfg: StaticResponderConfig, | ||||
|     req: HttpRequest, | ||||
| ) -> Result<HttpResponse, ProxyError> { | ||||
|     if req.method() != Method::GET { | ||||
|         return Err(ProxyError::MethodGetOnly); | ||||
|     } | ||||
|  | ||||
|     let path = req | ||||
|         .uri() | ||||
|         .path() | ||||
|         .trim_start_matches('/') | ||||
|         .trim_end_matches('/'); | ||||
|  | ||||
|     let path = match percent_encoding::percent_decode_str(path).decode_utf8() { | ||||
|         Ok(val) => val, | ||||
|         Err(_) => { | ||||
|             return Err(ProxyError::NotFound); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let base_path = cfg.uri.parse::<PathBuf>().unwrap(); | ||||
|     let mut file_path = base_path.clone(); | ||||
|     for p in Path::new(&*path) { | ||||
|         if p == OsStr::new(".") { | ||||
|             continue; | ||||
|         } else if p == OsStr::new("..") { | ||||
|             file_path.pop(); | ||||
|         } else { | ||||
|             file_path.push(p); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if !file_path.starts_with(cfg.uri) { | ||||
|         return Err(ProxyError::InvalidRequestPath); | ||||
|     } | ||||
|  | ||||
|     if !file_path.exists() { | ||||
|         if let Some(suffix) = cfg.suffix { | ||||
|             let file_name = file_path | ||||
|                 .file_name() | ||||
|                 .and_then(OsStr::to_str) | ||||
|                 .unwrap() | ||||
|                 .to_string(); | ||||
|             file_path.pop(); | ||||
|             file_path.push((file_name + &suffix).as_str()); | ||||
|             if file_path.is_file() { | ||||
|                 return Ok(NamedFile::open(file_path).unwrap().into_response(&req)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if let Some(file) = cfg.fallback { | ||||
|             let fallback_path = base_path.join(file); | ||||
|             if fallback_path.is_file() { | ||||
|                 return Ok(NamedFile::open(fallback_path).unwrap().into_response(&req)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return Err(ProxyError::NotFound); | ||||
|     } | ||||
|  | ||||
|     if file_path.is_file() { | ||||
|         Ok(NamedFile::open(file_path).unwrap().into_response(&req)) | ||||
|     } else { | ||||
|         if let Some(index_file) = &cfg.index { | ||||
|             let index_path = file_path.join(index_file); | ||||
|             if index_path.is_file() { | ||||
|                 return Ok(NamedFile::open(index_path).unwrap().into_response(&req)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Err(ProxyError::NotFound) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										87
									
								
								src/proxies/route.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/proxies/route.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| use actix_web::{HttpRequest, HttpResponse, ResponseError, web}; | ||||
| use actix_web::http::header; | ||||
| use awc::Client; | ||||
| use rand::seq::SliceRandom; | ||||
|  | ||||
| use crate::{ | ||||
|     proxies::{ | ||||
|         config::{Destination, DestinationType}, | ||||
|         responder, | ||||
|     }, | ||||
|     ROAD, | ||||
| }; | ||||
| use crate::proxies::ProxyError; | ||||
|  | ||||
| pub async fn handle(req: HttpRequest, payload: web::Payload, client: web::Data<Client>) -> HttpResponse { | ||||
|     let readable_app = ROAD.lock().await; | ||||
|     let (region, location) = match readable_app.filter(req.uri(), req.method(), req.headers()) { | ||||
|         Some(val) => val, | ||||
|         None => { | ||||
|             return ProxyError::NoGateway.error_response(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let destination = location | ||||
|         .destinations | ||||
|         .choose_weighted(&mut rand::thread_rng(), |item| item.weight.unwrap_or(1)) | ||||
|         .unwrap(); | ||||
|  | ||||
|     async fn forward( | ||||
|         end: &Destination, | ||||
|         req: HttpRequest, | ||||
|         payload: web::Payload, | ||||
|         client: web::Data<Client>, | ||||
|     ) -> Result<HttpResponse, ProxyError> { | ||||
|         // Handle normal web request | ||||
|         match end.get_type() { | ||||
|             DestinationType::Hypertext => { | ||||
|                 let Ok(uri) = end.get_hypertext_uri() else { | ||||
|                     return Err(ProxyError::NotImplemented); | ||||
|                 }; | ||||
|  | ||||
|                 responder::respond_hypertext(uri, req, payload, client).await | ||||
|             } | ||||
|             DestinationType::StaticFiles => { | ||||
|                 let Ok(cfg) = end.get_static_config() else { | ||||
|                     return Err(ProxyError::NotImplemented); | ||||
|                 }; | ||||
|  | ||||
|                 responder::respond_static(cfg, req).await | ||||
|             } | ||||
|             _ => Err(ProxyError::NotImplemented) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let reg = region.clone(); | ||||
|     let loc = location.clone(); | ||||
|     let end = destination.clone(); | ||||
|  | ||||
|     let ip = match req.connection_info().realip_remote_addr() { | ||||
|         None => "unknown".to_string(), | ||||
|         Some(val) => val.to_string(), | ||||
|     }; | ||||
|     let ua = match req.headers().get(header::USER_AGENT) { | ||||
|         None => "unknown".to_string(), | ||||
|         Some(val) => val.to_str().unwrap().to_string(), | ||||
|     }; | ||||
|  | ||||
|     match forward(&end, req, payload, client).await { | ||||
|         Ok(resp) => { | ||||
|             tokio::spawn(async move { | ||||
|                 let writable_app = &mut ROAD.lock().await; | ||||
|                 writable_app.metrics.add_success_request(ip, ua, reg, loc, end); | ||||
|             }); | ||||
|             resp | ||||
|         } | ||||
|         Err(resp) => { | ||||
|             let message = resp.to_string(); | ||||
|             tokio::spawn(async move { | ||||
|                 let writable_app = &mut ROAD.lock().await; | ||||
|                 writable_app | ||||
|                     .metrics | ||||
|                     .add_failure_request(ip, ua, reg, loc, end, message); | ||||
|             }); | ||||
|             resp.error_response() | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/proxies/server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/proxies/server.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| use std::error; | ||||
| use actix_web::{App, HttpServer, web}; | ||||
| use actix_web::dev::Server; | ||||
| use actix_web::middleware::{Compress, Logger}; | ||||
| use awc::Client; | ||||
| use crate::config::CFG; | ||||
| use crate::proxies::route; | ||||
| use crate::server::ServerBindConfig; | ||||
| use crate::tls::{load_certificates, use_rustls}; | ||||
|  | ||||
| pub async fn build_proxies() -> Result<Vec<Server>, Box<dyn error::Error>> { | ||||
|     load_certificates().await?; | ||||
|  | ||||
|     let cfg = CFG | ||||
|         .read() | ||||
|         .await | ||||
|         .get::<Vec<ServerBindConfig>>("proxies.bind")?; | ||||
|  | ||||
|     let mut tasks = Vec::new(); | ||||
|     for item in cfg { | ||||
|         tasks.push(build_single_proxy(item)?); | ||||
|     } | ||||
|  | ||||
|     Ok(tasks) | ||||
| } | ||||
|  | ||||
| pub fn build_single_proxy(cfg: ServerBindConfig) -> Result<Server, Box<dyn error::Error>> { | ||||
|     let server = HttpServer::new(|| { | ||||
|         App::new() | ||||
|             .wrap(Logger::default()) | ||||
|             .wrap(Compress::default()) | ||||
|             .app_data(web::Data::new(Client::default())) | ||||
|             .default_service(web::to(route::handle)) | ||||
|     }); | ||||
|     if cfg.tls { | ||||
|         Ok(server.bind_rustls_0_22(cfg.addr, use_rustls()?)?.run()) | ||||
|     } else { | ||||
|         Ok(server.bind(cfg.addr)?.run()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/server.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| pub struct ServerBindConfig { | ||||
|     pub addr: String, | ||||
|     pub tls: bool, | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/sideload/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/sideload/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| use actix_web::{Scope, web}; | ||||
| use crate::sideload::overview::get_overview; | ||||
| use crate::sideload::regions::list_region; | ||||
|  | ||||
| mod overview; | ||||
| mod regions; | ||||
| pub mod server; | ||||
|  | ||||
| static ROOT: &str = ""; | ||||
|  | ||||
| pub fn service() -> Scope { | ||||
|     web::scope("/cgi") | ||||
|         .route(ROOT, web::get().to(get_overview)) | ||||
|         .route("/regions", web::get().to(list_region)) | ||||
| } | ||||
							
								
								
									
										52
									
								
								src/sideload/overview.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/sideload/overview.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| use actix_web::web; | ||||
| use serde::Serialize; | ||||
| use crate::proxies::config::{Destination, Location}; | ||||
| use crate::proxies::metrics::RoadTrace; | ||||
| use crate::ROAD; | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq, Serialize)] | ||||
| pub struct OverviewData { | ||||
|     regions: usize, | ||||
|     locations: usize, | ||||
|     destinations: usize, | ||||
|     requests_count: u64, | ||||
|     failures_count: u64, | ||||
|     successes_count: u64, | ||||
|     success_rate: f64, | ||||
|     recent_successes: Vec<RoadTrace>, | ||||
|     recent_errors: Vec<RoadTrace>, | ||||
| } | ||||
|  | ||||
| pub async fn get_overview() -> web::Json<OverviewData> { | ||||
|     let locked_app = ROAD.lock().await; | ||||
|     let regions = locked_app.regions.clone(); | ||||
|     let locations = regions | ||||
|         .iter() | ||||
|         .flat_map(|item| item.locations.clone()) | ||||
|         .collect::<Vec<Location>>(); | ||||
|     let destinations = locations | ||||
|         .iter() | ||||
|         .flat_map(|item| item.destinations.clone()) | ||||
|         .collect::<Vec<Destination>>(); | ||||
|     web::Json(OverviewData { | ||||
|         regions: regions.len(), | ||||
|         locations: locations.len(), | ||||
|         destinations: destinations.len(), | ||||
|         requests_count: locked_app.metrics.requests_count, | ||||
|         successes_count: locked_app.metrics.requests_count - locked_app.metrics.failures_count, | ||||
|         failures_count: locked_app.metrics.failures_count, | ||||
|         success_rate: locked_app.metrics.get_success_rate(), | ||||
|         recent_successes: locked_app | ||||
|             .metrics | ||||
|             .recent_successes | ||||
|             .clone() | ||||
|             .into_iter() | ||||
|             .collect::<Vec<_>>(), | ||||
|         recent_errors: locked_app | ||||
|             .metrics | ||||
|             .recent_errors | ||||
|             .clone() | ||||
|             .into_iter() | ||||
|             .collect::<Vec<_>>(), | ||||
|     }) | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/sideload/regions.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/sideload/regions.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| use actix_web::web; | ||||
| use crate::proxies::config::Region; | ||||
| use crate::ROAD; | ||||
|  | ||||
| pub async fn list_region() -> web::Json<Vec<Region>> { | ||||
|     let locked_app = ROAD.lock().await; | ||||
|  | ||||
|     web::Json(locked_app.regions.clone()) | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/sideload/server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/sideload/server.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| use std::error; | ||||
| use actix_web::dev::Server; | ||||
| use actix_web::{App, HttpServer}; | ||||
| use actix_web_httpauth::extractors::AuthenticationError; | ||||
| use actix_web_httpauth::headers::www_authenticate::basic::Basic; | ||||
| use actix_web_httpauth::middleware::HttpAuthentication; | ||||
| use crate::sideload; | ||||
|  | ||||
| pub async fn build_sideload() -> Result<Server, Box<dyn error::Error>> { | ||||
|     Ok( | ||||
|         HttpServer::new(|| { | ||||
|             App::new() | ||||
|                 .wrap(HttpAuthentication::basic(|req, credentials| async move { | ||||
|                     let password = match crate::config::CFG | ||||
|                         .read() | ||||
|                         .await | ||||
|                         .get_string("secret") { | ||||
|                         Ok(val) => val, | ||||
|                         Err(_) => return Err((AuthenticationError::new(Basic::new()).into(), req)) | ||||
|                     }; | ||||
|                     if credentials.password().unwrap_or("") != password { | ||||
|                         Err((AuthenticationError::new(Basic::new()).into(), req)) | ||||
|                     } else { | ||||
|                         Ok(req) | ||||
|                     } | ||||
|                 })) | ||||
|                 .service(sideload::service()) | ||||
|         }).bind( | ||||
|             crate::config::CFG | ||||
|                 .read() | ||||
|                 .await | ||||
|                 .get_string("sideload.bind_addr")? | ||||
|         )?.workers(1).run() | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										76
									
								
								src/tls.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/tls.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| use std::fs::File; | ||||
| use std::{error}; | ||||
| use std::io::BufReader; | ||||
| use std::sync::Arc; | ||||
| use config::ConfigError; | ||||
| use lazy_static::lazy_static; | ||||
| use rustls::crypto::ring::sign::RsaSigningKey; | ||||
| use rustls::server::{ClientHello, ResolvesServerCert}; | ||||
| use rustls::sign::CertifiedKey; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::sync::Mutex; | ||||
| use wildmatch::WildMatch; | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref CERTS: Mutex<Vec<CertificateConfig>> = Mutex::new(Vec::new()); | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct ProxyCertResolver; | ||||
|  | ||||
| impl ResolvesServerCert for ProxyCertResolver { | ||||
|     fn resolve(&self, handshake: ClientHello) -> Option<Arc<CertifiedKey>> { | ||||
|         let domain = handshake.server_name()?; | ||||
|  | ||||
|         let certs = CERTS.lock().unwrap(); | ||||
|         for cert in certs.iter() { | ||||
|             if WildMatch::new(cert.domain.as_str()).matches(domain) { | ||||
|                 return match cert.clone().load() { | ||||
|                     Ok(val) => Some(val), | ||||
|                     Err(_) => None | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Serialize, Deserialize)] | ||||
| struct CertificateConfig { | ||||
|     pub domain: String, | ||||
|     pub certs: String, | ||||
|     pub key: String, | ||||
| } | ||||
|  | ||||
| impl CertificateConfig { | ||||
|     pub fn load(self) -> Result<Arc<CertifiedKey>, Box<dyn error::Error>> { | ||||
|         let certs = | ||||
|             rustls_pemfile::certs(&mut BufReader::new(&mut File::open(self.certs)?)) | ||||
|                 .collect::<Result<Vec<_>, _>>()?; | ||||
|         let key = | ||||
|             rustls_pemfile::private_key(&mut BufReader::new(&mut File::open(self.key)?))? | ||||
|                 .unwrap(); | ||||
|         let sign = RsaSigningKey::new(&key)?; | ||||
|  | ||||
|         Ok(Arc::new(CertifiedKey::new(certs, Arc::new(sign)))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn load_certificates() -> Result<(), ConfigError> { | ||||
|     let certs = crate::config::CFG | ||||
|         .read() | ||||
|         .await | ||||
|         .get::<Vec<CertificateConfig>>("certificates")?; | ||||
|  | ||||
|     CERTS.lock().unwrap().clone_from(&certs); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn use_rustls() -> Result<rustls::ServerConfig, ConfigError> { | ||||
|     Ok( | ||||
|         rustls::ServerConfig::builder() | ||||
|             .with_no_client_auth() | ||||
|             .with_cert_resolver(Arc::new(ProxyCertResolver)) | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/warden/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/warden/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| pub mod runner; | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use lazy_static::lazy_static; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use tokio::sync::Mutex; | ||||
| use tracing::{debug, warn}; | ||||
|  | ||||
| use crate::proxies::config::Region; | ||||
|  | ||||
| use self::runner::AppInstance; | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref INSTANCES: Mutex<HashMap<String, AppInstance>> = Mutex::new(HashMap::new()); | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct WardenInstance { | ||||
|     pub applications: Vec<Application>, | ||||
| } | ||||
|  | ||||
| impl WardenInstance { | ||||
|     pub fn new() -> WardenInstance { | ||||
|         WardenInstance { | ||||
|             applications: vec![], | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn scan(&mut self, regions: Vec<Region>) { | ||||
|         self.applications = regions | ||||
|             .iter() | ||||
|             .flat_map(|item| item.applications.clone().unwrap_or_default()) | ||||
|             .collect::<Vec<Application>>(); | ||||
|         debug!( | ||||
|             applications = format!("{:?}", self.applications), | ||||
|             "Warden scan accomplished." | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     pub async fn start(&self) { | ||||
|         for item in self.applications.iter() { | ||||
|             let mut instance = AppInstance::new(); | ||||
|             match instance.start(item.clone()).await { | ||||
|                 Ok(_) => { | ||||
|                     debug!(id = item.id, "Warden successfully created instance for"); | ||||
|                     INSTANCES.lock().await.insert(item.clone().id, instance); | ||||
|                 } | ||||
|                 Err(err) => warn!( | ||||
|                     id = item.id, | ||||
|                     err = format!("{:?}", err), | ||||
|                     "Warden failed to create an instance for" | ||||
|                 ), | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for WardenInstance { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct Application { | ||||
|     pub id: String, | ||||
|     pub exe: String, | ||||
|     pub args: Option<Vec<String>>, | ||||
|     pub env: Option<HashMap<String, String>>, | ||||
|     pub workdir: String, | ||||
| } | ||||
							
								
								
									
										104
									
								
								src/warden/runner.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/warden/runner.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| use std::{borrow::BorrowMut, collections::HashMap, io}; | ||||
|  | ||||
| use super::Application; | ||||
| use lazy_static::lazy_static; | ||||
| use tokio::{ | ||||
|     io::{AsyncBufReadExt, BufReader}, | ||||
|     process::{Child, Command}, | ||||
| }; | ||||
| use tokio::sync::Mutex; | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref STDOUT: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new()); | ||||
|     static ref STDERR: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new()); | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct AppInstance { | ||||
|     pub app: Option<Application>, | ||||
|     pub program: Option<Child>, | ||||
| } | ||||
|  | ||||
| impl AppInstance { | ||||
|     pub fn new() -> AppInstance { | ||||
|         AppInstance { | ||||
|             app: None, | ||||
|             program: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn start(&mut self, app: Application) -> io::Result<()> { | ||||
|         return match Command::new(app.exe.clone()) | ||||
|             .args(app.args.clone().unwrap_or_default()) | ||||
|             .envs(app.env.clone().unwrap_or_default()) | ||||
|             .current_dir(app.workdir.clone()) | ||||
|             .stdout(std::process::Stdio::piped()) | ||||
|             .stderr(std::process::Stdio::piped()) | ||||
|             .spawn() | ||||
|         { | ||||
|             Ok(mut child) => { | ||||
|                 let stderr_reader = BufReader::new(child.stderr.take().unwrap()); | ||||
|                 let stdout_reader = BufReader::new(child.stdout.take().unwrap()); | ||||
|  | ||||
|                 tokio::spawn(read_stream_and_capture(stderr_reader, app.id.clone(), true)); | ||||
|                 tokio::spawn(read_stream_and_capture( | ||||
|                     stdout_reader, | ||||
|                     app.id.clone(), | ||||
|                     false, | ||||
|                 )); | ||||
|  | ||||
|                 self.app = Some(app.clone()); | ||||
|                 self.program = Some(child); | ||||
|  | ||||
|                 Ok(()) | ||||
|             } | ||||
|             Err(err) => Err(err), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub async fn stop(&mut self) -> Result<(), io::Error> { | ||||
|         if let Some(child) = self.program.borrow_mut() { | ||||
|             return child.kill().await; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub async fn get_stdout(&self) -> Option<String> { | ||||
|         if let Some(app) = self.app.clone() { | ||||
|             STDOUT.lock().await.get(&app.id).cloned() | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn get_stderr(&self) -> Option<String> { | ||||
|         if let Some(app) = self.app.clone() { | ||||
|             STDERR.lock().await.get(&app.id).cloned() | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for AppInstance { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn read_stream_and_capture<R>(reader: R, id: String, is_err: bool) -> io::Result<()> | ||||
| where | ||||
|     R: tokio::io::AsyncBufRead + Unpin, | ||||
| { | ||||
|     let mut lines = reader.lines(); | ||||
|     while let Some(line) = lines.next_line().await? { | ||||
|         if !is_err { | ||||
|             if let Some(out) = STDOUT.lock().await.get_mut(&id) { | ||||
|                 out.push_str(&line); | ||||
|             } | ||||
|         } else if let Some(out) = STDERR.lock().await.get_mut(&id) { | ||||
|             out.push_str(&line); | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| # Benchmark | ||||
|  | ||||
| This result is design for test the performance of the roadsign. | ||||
| Welcome to contribute more tests of others reverse proxy software! | ||||
|  | ||||
| ## Platform | ||||
|  | ||||
| All tests are running on my workstation: | ||||
|  | ||||
| ```text | ||||
|                      ..'          littlesheep@LittleSheepdeMacBook-Pro | ||||
|                  ,xNMM.           ------------------------------------ | ||||
|                .OMMMMo            OS: macOS Sonoma 14.1 23B2073 arm64 | ||||
|                lMM"               Host: MacBook Pro (14-inch, Nov 2023, Three Thunderbolt 4 ports) | ||||
|      .;loddo:.  .olloddol;.       Kernel: 23.1.0 | ||||
|    cKMMMMMMMMMMNWMMMMMMMMMM0:     Uptime: 2 days, 1 hour, 16 mins | ||||
|  .KMMMMMMMMMMMMMMMMMMMMMMMWd.     Packages: 63 (brew), 4 (brew-cask) | ||||
|  XMMMMMMMMMMMMMMMMMMMMMMMX.       Shell: zsh 5.9 | ||||
| ;MMMMMMMMMMMMMMMMMMMMMMMM:        Display (Color LCD): 3024x1964 @ 120Hz (as 1512x982) [Built-in] | ||||
| :MMMMMMMMMMMMMMMMMMMMMMMM:        DE: Aqua | ||||
| .MMMMMMMMMMMMMMMMMMMMMMMMX.       WM: Quartz Compositor | ||||
|  kMMMMMMMMMMMMMMMMMMMMMMMMWd.     WM Theme: Multicolor (Dark) | ||||
|  'XMMMMMMMMMMMMMMMMMMMMMMMMMMk    Font: .AppleSystemUIFont [System], Helvetica [User] | ||||
|   'XMMMMMMMMMMMMMMMMMMMMMMMMK.    Cursor: Fill - Black, Outline - White (32px) | ||||
|     kMMMMMMMMMMMMMMMMMMMMMMd      Terminal: iTerm 3.4.22 | ||||
|      ;KMMMMMMMWXXWMMMMMMMk.       Terminal Font: MesloLGMNFM-Regular (12pt) | ||||
|        "cooc*"    "*coo'"         CPU: Apple M3 Max (14) @ 4.06 GHz | ||||
|                                   GPU: Apple M3 Max (30) [Integrated] | ||||
|                                   Memory: 18.45 GiB / 36.00 GiB (51%) | ||||
|                                   Swap: Disabled | ||||
|                                   Disk (/): 72.52 GiB / 926.35 GiB (8%) - apfs [Read-only] | ||||
|                                   Local IP (en0): 192.168.50.0/24 * | ||||
|                                   Battery: 100% [AC connected] | ||||
|                                   Power Adapter: 96W USB-C Power Adapter | ||||
|                                   Locale: zh_CN.UTF-8 | ||||
| ``` | ||||
|  | ||||
| ## Results | ||||
|  | ||||
| The tests are run in the order `nginx -> roadsign without prefork -> roadsign with prefork`. There is no reason why nginx performance should be affected by hardware temperature. | ||||
|  | ||||
| ### Nginx | ||||
|  | ||||
| ```shell | ||||
| go-wrk -c 60 -d 120 http://localhost:8001 | ||||
| # => Running 120s test @ http://localhost:8001 | ||||
| # =>   60 goroutine(s) running concurrently | ||||
| # => 515749 requests in 1m59.953302003s, 245.92MB read | ||||
| # => Requests/sec:           4299.58 | ||||
| # => Transfer/sec:           2.05MB | ||||
| # => Avg Req Time:           13.954846ms | ||||
| # => Fastest Request:        0s | ||||
| # => Slowest Request:        410.6972ms | ||||
| # => Number of Errors:       0 | ||||
| ``` | ||||
|  | ||||
| ### RoadSign | ||||
|  | ||||
| ```shell | ||||
| go-wrk -c 60 -d 120 http://localhost:8000 | ||||
| # => Running 120s test @ http://localhost:8000 | ||||
| # =>   60 goroutine(s) running concurrently | ||||
| # => 8905230 requests in 1m56.215762709s, 3.52GB read | ||||
| # => Requests/sec:		76626.70 | ||||
| # => Transfer/sec:		30.98MB | ||||
| # => Avg Req Time:		783.016µs | ||||
| # => Fastest Request:	28.542µs | ||||
| # => Slowest Request:	46.773083ms | ||||
| # => Number of Errors:	0 | ||||
| ``` | ||||
|  | ||||
| ### RoadSign w/ Prefork | ||||
|  | ||||
| ```shell | ||||
| go-wrk -c 60 -d 120 http://localhost:8000 | ||||
| # => Running 120s test @ http://localhost:8000 | ||||
| # =>  60 goroutine(s) running concurrently | ||||
| # => 4784308 requests in 1m59.100307178s, 1.89GB read | ||||
| # => Requests/sec:		40170.41 | ||||
| # => Transfer/sec:		16.24MB | ||||
| # => Avg Req Time:		1.493636ms | ||||
| # => Fastest Request:	34.291µs | ||||
| # => Slowest Request:	8.727666ms | ||||
| # => Number of Errors:	0 | ||||
| ``` | ||||
							
								
								
									
										3
									
								
								test/data/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								test/data/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | ||||
| /ssr | ||||
| /spa | ||||
| /congress | ||||
| @@ -1,12 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
| 	<meta charset="utf-8"> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 	<title>Hello, World!</title> | ||||
| </head> | ||||
| <body> | ||||
| 	<p>Hello, there!</p> | ||||
| 	<p>Here's the roadsign vs. nginx benchmarking test data!</p> | ||||
| </body> | ||||
| </html> | ||||
| @@ -1,117 +0,0 @@ | ||||
|  | ||||
| #user  nobody; | ||||
| worker_processes  1; | ||||
|  | ||||
| #error_log  logs/error.log; | ||||
| #error_log  logs/error.log  notice; | ||||
| #error_log  logs/error.log  info; | ||||
|  | ||||
| #pid        logs/nginx.pid; | ||||
|  | ||||
|  | ||||
| events { | ||||
|     worker_connections  1024; | ||||
| } | ||||
|  | ||||
|  | ||||
| http { | ||||
|     include       mime.types; | ||||
|     default_type  application/octet-stream; | ||||
|  | ||||
|     #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" ' | ||||
|     #                  '$status $body_bytes_sent "$http_referer" ' | ||||
|     #                  '"$http_user_agent" "$http_x_forwarded_for"'; | ||||
|  | ||||
|     #access_log  logs/access.log  main; | ||||
|  | ||||
|     sendfile        on; | ||||
|     #tcp_nopush     on; | ||||
|  | ||||
|     #keepalive_timeout  0; | ||||
|     keepalive_timeout  65; | ||||
|  | ||||
|     #gzip  on; | ||||
|  | ||||
|     server { | ||||
|         listen       8001; | ||||
|         server_name  localhost; | ||||
|  | ||||
|         #charset koi8-r; | ||||
|  | ||||
|         #access_log  logs/host.access.log  main; | ||||
|  | ||||
|         location / { | ||||
|             root   ../data; | ||||
|             index  index.html index.htm; | ||||
|         } | ||||
|  | ||||
|         #error_page  404              /404.html; | ||||
|  | ||||
|         # redirect server error pages to the static page /50x.html | ||||
|         # | ||||
|         error_page   500 502 503 504  /50x.html; | ||||
|         location = /50x.html { | ||||
|             root   html; | ||||
|         } | ||||
|  | ||||
|         # proxy the PHP scripts to Apache listening on 127.0.0.1:80 | ||||
|         # | ||||
|         #location ~ \.php$ { | ||||
|         #    proxy_pass   http://127.0.0.1; | ||||
|         #} | ||||
|  | ||||
|         # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 | ||||
|         # | ||||
|         #location ~ \.php$ { | ||||
|         #    root           html; | ||||
|         #    fastcgi_pass   127.0.0.1:9000; | ||||
|         #    fastcgi_index  index.php; | ||||
|         #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name; | ||||
|         #    include        fastcgi_params; | ||||
|         #} | ||||
|  | ||||
|         # deny access to .htaccess files, if Apache's document root | ||||
|         # concurs with nginx's one | ||||
|         # | ||||
|         #location ~ /\.ht { | ||||
|         #    deny  all; | ||||
|         #} | ||||
|     } | ||||
|  | ||||
|  | ||||
|     # another virtual host using mix of IP-, name-, and port-based configuration | ||||
|     # | ||||
|     #server { | ||||
|     #    listen       8000; | ||||
|     #    listen       somename:8080; | ||||
|     #    server_name  somename  alias  another.alias; | ||||
|  | ||||
|     #    location / { | ||||
|     #        root   html; | ||||
|     #        index  index.html index.htm; | ||||
|     #    } | ||||
|     #} | ||||
|  | ||||
|  | ||||
|     # HTTPS server | ||||
|     # | ||||
|     #server { | ||||
|     #    listen       443 ssl; | ||||
|     #    server_name  localhost; | ||||
|  | ||||
|     #    ssl_certificate      cert.pem; | ||||
|     #    ssl_certificate_key  cert.key; | ||||
|  | ||||
|     #    ssl_session_cache    shared:SSL:1m; | ||||
|     #    ssl_session_timeout  5m; | ||||
|  | ||||
|     #    ssl_ciphers  HIGH:!aNULL:!MD5; | ||||
|     #    ssl_prefer_server_ciphers  on; | ||||
|  | ||||
|     #    location / { | ||||
|     #        root   html; | ||||
|     #        index  index.html index.htm; | ||||
|     #    } | ||||
|     #} | ||||
|  | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| name: Example Site | ||||
| rules: | ||||
|   - host: ["localhost:8000"] | ||||
|     path: ["/"] | ||||
| upstreams: | ||||
|   - id: example | ||||
|     name: Benchmarking Data | ||||
|     uri: files://../data/.spa | ||||
| @@ -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 | ||||
| @@ -1,12 +0,0 @@ | ||||
| name: Example Site | ||||
| rules: | ||||
|   - host: ["localhost:8000"] | ||||
|     path: ["/"] | ||||
| upstreams: | ||||
|   - id: example | ||||
|     name: Benchmarking Data | ||||
|     uri: http://localhost:3000 | ||||
| processes: | ||||
|   - id: nuxt-ssr | ||||
|     workdir: ../data/ssr | ||||
|     command: ["node", ".output/server/index.mjs"] | ||||
| @@ -1,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 | ||||
| @@ -1,8 +0,0 @@ | ||||
| name: Example Site | ||||
| rules: | ||||
|   - host: ["localhost:8000"] | ||||
|     path: ["/"] | ||||
| upstreams: | ||||
|   - id: example | ||||
|     name: Benchmarking Data | ||||
|     uri: files://../data | ||||
| @@ -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 | ||||
| @@ -1,8 +0,0 @@ | ||||
| name: Example Site | ||||
| rules: | ||||
|   - host: ["localhost:8000"] | ||||
|     path: ["/"] | ||||
| upstreams: | ||||
|   - id: example | ||||
|     name: Benchmarking Data | ||||
|     uri: files://../data | ||||
| @@ -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
									
								
							
							
						
						
									
										29
									
								
								test/websocket/server.py
									
									
									
									
									
										Normal 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() | ||||
		Reference in New Issue
	
	Block a user