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: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ master ] |     branches: [ refactor/rust ] | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   build-docker: |   build-image: | ||||||
|     runs-on: edge |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v4 | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v2 |         uses: docker/setup-qemu-action@v3 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v2 |         uses: docker/setup-buildx-action@v3 | ||||||
|       - name: Login to Docker Hub |       - name: Login to Docker Hub | ||||||
|         uses: docker/login-action@v2 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
|           registry: code.smartsheep.studio |  | ||||||
|           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} |           username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} | ||||||
|           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} |           password: ${{ secrets.DOCKER_REGISTRY_TOKEN }} | ||||||
|       - name: Build and push |       - name: Build and push | ||||||
|         uses: docker/build-push-action@v4 |         uses: docker/build-push-action@v5 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           file: ./Dockerfile |  | ||||||
|           push: true |           push: true | ||||||
|           tags: code.smartsheep.studio/goatworks/roadsign:nightly |           file: ./Dockerfile | ||||||
|  |           tags: xsheep2010/roadsign:sigma | ||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.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"?> | <?xml version="1.0" encoding="UTF-8"?> | ||||||
| <module type="WEB_MODULE" version="4"> | <module type="WEB_MODULE" version="4"> | ||||||
|  |   <component name="FacetManager"> | ||||||
|  |     <facet type="Python" name="Python facet"> | ||||||
|  |       <configuration sdkName="Python 3.9" /> | ||||||
|  |     </facet> | ||||||
|  |   </component> | ||||||
|   <component name="Go" enabled="true" /> |   <component name="Go" enabled="true" /> | ||||||
|   <component name="NewModuleRootManager"> |   <component name="NewModuleRootManager"> | ||||||
|     <content url="file://$MODULE_DIR$" /> |     <content url="file://$MODULE_DIR$"> | ||||||
|  |       <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/target" /> | ||||||
|  |     </content> | ||||||
|     <orderEntry type="inheritedJdk" /> |     <orderEntry type="inheritedJdk" /> | ||||||
|     <orderEntry type="sourceFolder" forTests="false" /> |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |     <orderEntry type="library" name="Python 3.9 interpreter library" level="application" /> | ||||||
|   </component> |   </component> | ||||||
| </module> | </module> | ||||||
							
								
								
									
										59
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 | # Building Backend | ||||||
| FROM golang:alpine as roadsign-server | FROM rust:alpine as roadsign-server | ||||||
|  |  | ||||||
|  | RUN apk add libressl-dev build-base | ||||||
|  |  | ||||||
| WORKDIR /source | WORKDIR /source | ||||||
| COPY . . | COPY . . | ||||||
| RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dist ./pkg/cmd/server/main.go | ENV RUSTFLAGS="-C target-feature=-crt-static" | ||||||
|  | RUN cargo build --release | ||||||
| # Runtime |  | ||||||
| FROM golang:alpine |  | ||||||
|  |  | ||||||
| COPY --from=roadsign-server /dist /roadsign/server |  | ||||||
|  |  | ||||||
| EXPOSE 81 | EXPOSE 81 | ||||||
|  |  | ||||||
| CMD ["/roadsign/server"] | CMD ["/source/target/release/roadsign"] | ||||||
							
								
								
									
										66
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								README.md
									
									
									
									
									
								
							| @@ -10,7 +10,8 @@ A blazing fast reverse proxy with a lot of shining features. | |||||||
| 4. Integrate with CI/CD | 4. Integrate with CI/CD | ||||||
| 5. Webhook integration | 5. Webhook integration | ||||||
| 6. ~~Web management panel~~ | 6. ~~Web management panel~~ | ||||||
| 7. **Blazing fast ⚡** | 7. One-liner CLI | ||||||
|  | 8. **Blazing fast ⚡** | ||||||
|  |  | ||||||
| > Deleted item means under construction, check out our roadmap! | > Deleted item means under construction, check out our roadmap! | ||||||
|  |  | ||||||
| @@ -27,6 +28,67 @@ Here's the result: | |||||||
|  |  | ||||||
| As result, roadsign undoubtedly is the fastest one. | As result, roadsign undoubtedly is the fastest one. | ||||||
|  |  | ||||||
| It can be found that the prefork feature makes RoadSign more stable in concurrency. We can see this from the **Slowest Time**. At the same time, the **Fastest Time** is affected because reusing ports requires some extra steps to handle load balancing. Enable this feature at your own discretion depending on your use case. | It can be found that the prefork feature makes RoadSign more stable in concurrency. We can see this from the **Slowest | ||||||
|  | Time**. At the same time, the **Fastest Time** is affected because reusing ports requires some extra steps to handle | ||||||
|  | load balancing. Enable this feature at your own discretion depending on your use case. | ||||||
|  |  | ||||||
| More details can be found at benchmark's [README.md](./test/README.md) | More details can be found at benchmark's [README.md](./test/README.md) | ||||||
|  |  | ||||||
|  | ## Installation | ||||||
|  |  | ||||||
|  | We strongly recommend you install RoadSign via docker compose. | ||||||
|  |  | ||||||
|  | ```yaml | ||||||
|  | version: "3" | ||||||
|  | services: | ||||||
|  |   roadsign: | ||||||
|  |     image: code.smartsheep.studio/goatworks/roadsign:nightly | ||||||
|  |     restart: always | ||||||
|  |     volumes: | ||||||
|  |       - "./certs:/certs" # Optional, use for storage certificates | ||||||
|  |       - "./config:/config" | ||||||
|  |       - "./wwwroot:/wwwroot" # Optional, use for storage web apps | ||||||
|  |       - "./settings.yml:/settings.yml" | ||||||
|  |     ports: | ||||||
|  |       - "80:80" | ||||||
|  |       - "443:443" | ||||||
|  |       - "81:81" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | After that, you can manage your roadsign instance with RoadSign CLI aka. RDS CLI. | ||||||
|  | To install it, run this command. (Make sure you have golang toolchain on your computer) | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | go install -buildvcs code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest | ||||||
|  | # Tips: Add `buildvsc` flag to provide more detail compatibility check. | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | To use roadsign, you need to add a configuration for it. Create a file locally. | ||||||
|  | Name whatever you like. And follow our [documentation](https://wiki.smartsheep.studio/roadsign/configuration/index.html) to | ||||||
|  | write it. | ||||||
|  |  | ||||||
|  | After configure, you need sync your config to remote server. Before that, add a connection between roadsign server and | ||||||
|  | rds cli with this command. | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | rds connect <id> <url> <password> | ||||||
|  | # ID will allow you find this server.py.rs in after commands. | ||||||
|  | # URL is to your roadsign server.py.rs sideload api. | ||||||
|  | # Password is your roadsign server.py.rs credential. | ||||||
|  | # ====================================================================== | ||||||
|  | # !WARNING! All these things will storage in your $HOME/.roadsignrc.yaml | ||||||
|  | # ====================================================================== | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Then, sync your local config to remote. | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | rds sync <server.py.rs id> <site id> <config file> | ||||||
|  | # Server ID is your server.py.rs added by last command. | ||||||
|  | # Site ID is your new site id or old site id if you need update it. | ||||||
|  | # Config File is your local config file path. | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | After a few seconds, your website is ready! | ||||||
							
								
								
									
										17
									
								
								Settings.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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