Compare commits

...

10 Commits

Author SHA1 Message Date
LittleSheep
b7d4a54d62 ♻️ Migrated to actix rs 2024-02-13 00:01:39 +08:00
LittleSheep
ead748a508 Warden process manager 2024-01-15 23:22:53 +08:00
LittleSheep
4c08d78bed Add headers in reserve proxy 2024-01-14 19:12:38 +08:00
LittleSheep
a088f6224e Basic Auth 2024-01-14 18:39:22 +08:00
LittleSheep
f02977b7d7 Metrics 2024-01-14 17:55:14 +08:00
LittleSheep
905b70349b Clean url with suffix 2024-01-14 14:52:19 +08:00
LittleSheep
91ecf9d7bb Static files 2024-01-14 13:14:41 +08:00
LittleSheep
c991d0b54a Route and forward for http(s) 2024-01-14 02:01:56 +08:00
LittleSheep
5de1d13907 Config loader 2024-01-13 14:46:26 +08:00
LittleSheep
bf7004c89c 🎉 Start developing! 2024-01-13 12:35:59 +08:00
139 changed files with 10558 additions and 3215 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -5,24 +5,24 @@ on:
branches: [ master ]
jobs:
build-docker:
runs-on: edge
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: xsheep2010/roadsign:nightly
file: ./Dockerfile
tags: xsheep2010/roadsign:sigma

6
.gitignore vendored
View File

@@ -1,2 +1,6 @@
/config
/letsencrypt
/letsencrypt
# Added by cargo
/target

5
.idea/RoadSign.iml generated
View File

@@ -2,7 +2,10 @@
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

3103
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
Cargo.toml Normal file
View File

@@ -0,0 +1,43 @@
[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 = "4.5.1"
awc = "3.4.0"
config = { version = "0.13.4", features = ["toml"] }
futures-util = "0.3.30"
http = "1.0.0"
hyper-util = { version = "0.1.2", features = ["full"] }
lazy_static = "1.4.0"
mime = "0.3.17"
percent-encoding = "2.3.1"
poem = { version = "2.0.0", features = [
"tokio-metrics",
"websocket",
"static-files",
"reqwest",
] }
poem-openapi = { version = "4.0.0" }
queryst = "3.0.0"
rand = "0.8.5"
regex = "1.10.2"
reqwest = { git = "https://github.com/seanmonstar/reqwest.git", branch = "hyper-v1", version = "0.11.23" }
serde = "1.0.195"
serde_json = "1.0.111"
tokio = { version = "1.35.1", features = [
"rt-multi-thread",
"macros",
"time",
"full",
] }
tokio-tungstenite = "0.21.0"
toml = "0.8.8"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
wildmatch = "2.3.0"

7
Settings.toml Normal file
View File

@@ -0,0 +1,7 @@
regions = "./regions"
secret = "aEXcED5xJ3"
[listen]
proxies = "0.0.0.0:80"
proxies_tls = "0.0.0.0:443"
sideload = "0.0.0.0:81"

98
certs/fullchain.pem Normal file
View File

@@ -0,0 +1,98 @@
-----BEGIN CERTIFICATE-----
MIIGITCCBQmgAwIBAgISBPa2ahmRa5l0SrbR75K29TlqMA0GCSqGSIb3DQEBCwUA
MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
EwJSMzAeFw0yNDAxMjgxNjM1MzZaFw0yNDA0MjcxNjM1MzVaMBwxGjAYBgNVBAMT
EXNtYXJ0c2hlZXAuc3R1ZGlvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAumtDoEkBHiqBGNl9JUbt3Nw1a/iLa2JHZvCmUuilvafAj0k1UPEu457iJVjh
nxlTl/tgpCooRNviboB0lMAecwiKb5PIZzInY2/gb+rceL7rycalDAjWnwZZg8Tl
3JWJaU0D3+jeBQ9p7xkRPTcRpkGxFX9hRdInRqFGmCK4/OXrBwI3FkoP9Z8Nvgzc
RyIhmMEafOi2AaLJSXZQE508djHpcxlwoxvVmFIYsjTMJiq+zdqCZDTjDqt7bZC+
mp3n/DFLL0sTzIBLuHnPkrjwlE4w/XTLkMLgrGPvEJVHn4kOvnJSWJb95d3sRZqB
PLrekQGDtcb0dpFx8ctWycVp0qhBTpuLAoN+AK6cJ3IkxOcGm7sKzYwRINjAblMe
n1Y6as6JKL4zSZBt1jxua1NGIRQ1c4ReA78NniTMrFIk1mFL4kNT9ppgDsWGNTJx
FqC76rr3TdKXKzD/c8h57j3CPTF9lGB4Vmlkrq/zA++br0IG7Ki/+1n04Q1UDN0E
8z/vgUsT+pMItrmUhpnhb+4QWLQHTWOg1CyWT9xSm77ArkuNSdzNFGjpSbYZEPlh
pmAYTz+kWvnyg7T3Lt7fmSPCBnnIWhi9CJofvyomICWfcKEfwc0msJquXMyJBayA
K91VMoudbveI+CX8Zuc5u4uCOk/Dr6JJWnYkIDK7cW+tFh0CAwEAAaOCAkUwggJB
MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPabqUNdBFv+3fEpgh2seAHvPQFgwHwYD
VR0jBBgwFoAUFC6zF7dYVsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEG
CCsGAQUFBzABhhVodHRwOi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0
dHA6Ly9yMy5pLmxlbmNyLm9yZy8wTQYDVR0RBEYwRIIaKi5wbGF5bWMuc21hcnRz
aGVlcC5zdHVkaW+CEyouc21hcnRzaGVlcC5zdHVkaW+CEXNtYXJ0c2hlZXAuc3R1
ZGlvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMIIBBQYKKwYBBAHWeQIEAgSB9gSB8wDx
AHcAO1N3dT4tuYBOizBbBv5AO2fYT8P0x70ADS1yb+H61BcAAAGNUSQZ5gAABAMA
SDBGAiEA9i8QVPKh68OY9Ug+KqZ1aWSx5SVUvPKVyp8KkI/RMssCIQD0JsqwhLFx
SQFVROa1yhbZIdIHs9NMuW7lHbOWOU4F+AB2AO7N0GTV2xrOxVy3nbTNE6Iyh0Z8
vOzew1FIWUZxH7WbAAABjVEkGesAAAQDAEcwRQIhAKXGxcDa/aGK50QyhOXAdlKl
VCk0Yo9wYzw2sB48BF1TAiBsW2R+CJXhnCBzcRRh8GQuj+aBcYIzHBaRvsanC9nx
9zANBgkqhkiG9w0BAQsFAAOCAQEAgYR8uizQZkkvlEyI5dsz/tGJ0Vuejnd6yfoE
OT6BaNF45UtdMfSoJRLwgW6SM1hsuwmPjoUVS9VZ83NAhnWQqSeR9P4m7aMfhaeK
qPlDDcGh/SZOvgeja8AsUhCq+9csUzR+FEfk7xJXFflcG1FNGzRglzmqrSvFHKhE
EyJQuk6Oa/UiL0ICdKiLxh75gTJvTB/7wznTP7NGsayzqkBj4nxqHIwCGh7hPe2c
2xMEbcFA9aArInJEg2PzlYRy1C9qtuhU2hJUhLjEix4WVCHUyAV/X6kwjqIaUEOz
NdUrFCahGkWDVZZcPgl/FdFSPfSL/pnT8jOotELnhj0AiX2SNQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
nLRbwHOoq7hHwg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
-----END CERTIFICATE-----

51
certs/privkey.pem Normal file
View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAumtDoEkBHiqBGNl9JUbt3Nw1a/iLa2JHZvCmUuilvafAj0k1
UPEu457iJVjhnxlTl/tgpCooRNviboB0lMAecwiKb5PIZzInY2/gb+rceL7rycal
DAjWnwZZg8Tl3JWJaU0D3+jeBQ9p7xkRPTcRpkGxFX9hRdInRqFGmCK4/OXrBwI3
FkoP9Z8NvgzcRyIhmMEafOi2AaLJSXZQE508djHpcxlwoxvVmFIYsjTMJiq+zdqC
ZDTjDqt7bZC+mp3n/DFLL0sTzIBLuHnPkrjwlE4w/XTLkMLgrGPvEJVHn4kOvnJS
WJb95d3sRZqBPLrekQGDtcb0dpFx8ctWycVp0qhBTpuLAoN+AK6cJ3IkxOcGm7sK
zYwRINjAblMen1Y6as6JKL4zSZBt1jxua1NGIRQ1c4ReA78NniTMrFIk1mFL4kNT
9ppgDsWGNTJxFqC76rr3TdKXKzD/c8h57j3CPTF9lGB4Vmlkrq/zA++br0IG7Ki/
+1n04Q1UDN0E8z/vgUsT+pMItrmUhpnhb+4QWLQHTWOg1CyWT9xSm77ArkuNSdzN
FGjpSbYZEPlhpmAYTz+kWvnyg7T3Lt7fmSPCBnnIWhi9CJofvyomICWfcKEfwc0m
sJquXMyJBayAK91VMoudbveI+CX8Zuc5u4uCOk/Dr6JJWnYkIDK7cW+tFh0CAwEA
AQKCAgAZaOeF4I2TdA22umZxf1KKyUVK20z9rR64bFLveCCnUkdQAJWQ298dTZnb
p1rRmd0oGS6aEdj9Uc9yanX5lJpR6bcc8FLfphlyV1yLRMMafkObdEUo98dxU6c9
68e9InDhdorxqUch/3DcE1mjM29nbwrZOEyk/Lk3ymHZ+NmoNxqrEOU7V4sHWs+/
uPcJhyB3NtMKdZnhbTPsnIDbu5HzNmhwtOYChZe+tGoDWGj/RtJNMtyVFhRg+oTP
cWxZnO5zFLD/2EbxmAc6NPhTMnwwatwIoDPjHn0LYm/CneZuW3s0AFczFhmzjGVZ
M28sPPh7YW53h4/o3XhfkezZgdwji+VzHNSUVuHywRvAuwncFfgy8uxX/BbncllV
7ZPMA4PKrEQya7+eYMEjuai/b8oRdDMYV+b0W2tMkbQYgOh2SvzPEPOSIw2P01sP
Zz4IQeBMjKPDd/QR/5VBwVscdup5T09ERfn9xjT64zshvYLB1DFyYfQBT94453xn
wD6sdmoxd5Hb+O+1Vn8iwxbPUqjceLPQV7nuoeuhuwR41tcM+7fIdX6C/BymRidk
+ZBOD9HCyLRJPeKPXrs/bbXEOGgw2aEHpPXRRY+Tz+VnVqYIEFflVJxm5LFKV3pM
O13NYrlF82ngygEIUb05sPsUudPIOl9Ow7csxxxU1C4FvFQzgQKCAQEA7PBsA/i9
XeCMiygvrfepxYL6rKng62+r3ykouSVXojvsTHZqmxQXliPj9n8MuNFIT1e+BpMH
GERwmmfPlfn5m+wwuXh2vAYE1RICG3/y5KBHlAlDa5OlOK0/DoIqwHQGCfrcika8
bWYYT4YAQSJkHXohY4feRc5Mu4pHCdQn4mtqwrdSHReQWF5sPIfOxZboMGwYwP+L
z12A34Fxw+9cuXbg1r094LzBU9Y/Lk/4EMQhPlx89GyCrbeSfbLzJt7z4XvfbDR+
bYx0r6MzoNC46Ag+rFd0xspUFjXuxwTXjbD53SKuHF/HHqhxhPaKkZ7cB3dkFBpZ
l8VAdU25THs0oQKCAQEAyWprQMaYJ8GSqEvD/NgpJkNhdgaC+4H5rpIxBxTyUpeF
eGP/GyANJElNWngX1PYOyACvPnq1gx3kATCrpOX2m4B5+lmBV0R93MTveivfrNiY
NOCRgNj/HcxV87A9K26Xn2X1KEptIRWu0NI0slQOrSAFbym/mcRC0zn9ZwYhn2j7
K9hjB1xZ0IiD7vj70WDKQQ4B5UBhYvl8Hoqc1Mq7fxgYksW+cQdo7Ff+cXoQM4kt
59v45c4SL5wOaP4NhgCyigfWAbDpc06dvpiccoq4km8Dh3goKm6X6/t2ViJwo3ms
hnpnVSIwk57xkQKE44VBKWO7uKf7gF60Klxw6a4z/QKCAQAGfy0nCrn+ifqwkiCE
j6brEIVZGKtfKUe4LcO9F+YIP8Zh7llL+UgQSNmmV6N8qdhmvIwsV/m405+WzEPn
49vRR6+qVkBDNZMKWJ0QLfj2BeMQjxR3Law5Gp8BAda0QjosKeHENN5TzZnbDFyT
bKL8H+4scXxJw7dc6RS7k0KotbNOtYId5BiEI8qp+jtUNagWM9CV8iveOr0e48i/
y59qQnU1ziiVMffHGbfPyEeQBiC3Ogf/F76ZjtAIfFCofzKGu0PMDsBsto44hqwS
u8ZmLluxlBBSpxeZKE9sy5gxfWnMg3fstT+bepjqQWxZQ9baqY5n8rUEMVdsH/51
t2chAoIBAQDAGPAr+nzZxgQC4SgOOXedl7wXXgSEraUduy5pa6/l17+jX/PJwhrm
lcbh1xIbuYXFeOmqtEvzvolcHThHcFBwZOtxOvZh7eAAtA8WvUt6RaV9IQWFFvDS
UbVUUq00hCi2DP7xq3JDkLOHVESQJB1PPvAP4ohrtkRpwrANF0rwB6cuXRWtyoWa
nuj9/ZfmTOL5gsuKA4oeZIXddyQ2yxhiEfA148VLdd67Mv1o6xsSm+4daa4MNysf
Hz6bIlpxiK6/eNPCDyHIKJ3ITzM/B1Eb0CdUh+P+/DWHCvEFG5nEQGKsa4esKYyc
7oXEYNuvR8MrH4TFi+kRuMEqebnzFYNdAoIBAQDHBExE+HRcpIrjIqMSngQ2VgxZ
u21sMg10S+c3BxYQYldGzRVg5rzTU+5EKWtsEKjfJ0+zVCMVFlQ/m3LclLTZbXem
ZFUW6fnPqEyZfabrnevwzomgxj56Jzj8mszfWhaCxDUflNgNoB7pVPLiBpnJKqCL
mFiuQ6mhGguqlq3usRi/cNE5ClOiYJlKlzc5W0kEI9Z0LdUdyf6EudpSsXaHVU6d
b15c855UU/LnLNtASTbeWqStJSXDJVSqqj0FR2Zl4KOoXhrRql3xn9N2X2Kti6LD
iaqoobkYpjnhBsB1HyhLaosX78qNdG0+UOfL8i27AkIEX/YzlNiEt7BGgE++
-----END RSA PRIVATE KEY-----

58
go.mod
View File

@@ -1,58 +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/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea // indirect
github.com/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
)

577
go.sum
View File

@@ -1,577 +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/saracen/fastzip v0.1.11 h1:NnExbTEJbya7148cov09BCxwfur9tQ5BQ1QyQH6XleA=
github.com/saracen/fastzip v0.1.11/go.mod h1:/lN5BiU451/OZMS+hfhVsSDj/RNrxYmO9EYxCtMrFrY=
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea h1:8czYLkvzZRE+AElIQeDffQdgR+CC3wKEFILYU/1PeX4=
github.com/saracen/zipextra v0.0.0-20220303013732-0187cb0159ea/go.mod h1:hnzuad9d2wdd3z8fC6UouHQK5qZxqv3F/E6MMzXc7q0=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

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

View File

@@ -1,42 +0,0 @@
package conn
import (
"encoding/json"
"fmt"
"strings"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type CliConnection struct {
ID string `json:"id"`
Url string `json:"url"`
Credential string `json:"credential"`
}
func (v CliConnection) CheckConnectivity() error {
client := fiber.Get(v.Url + "/cgi/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 {
if strings.Contains(roadsign.AppVersion, "#") {
return fmt.Errorf("remote server version mismatch client version, update or downgrade client required")
} else {
log.Warn().Msg("RoadSign CLI didn't complied with vcs information, compatibility was disabled. To enable it, reinstall cli with -buildvcs flag.")
}
}
}
return nil
}

View File

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

View File

@@ -1,101 +0,0 @@
package deploy
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"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")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
// Send request
log.Info().Msg("Now publishing to remote server...")
url := fmt.Sprintf("/webhooks/publish/%s/%s?mimetype=%s", ctx.Args().Get(1), ctx.Args().Get(2), "application/zip")
client := fiber.Put(server.Url+url).
SendFile(ctx.Args().Get(3), "attachments").
MultipartForm(nil).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to publish to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site is successfully published! 🎉")
return nil
},
},
{
Name: "sync",
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")
} else if err := server.CheckConnectivity(); err != nil {
return fmt.Errorf("couldn't connect server: %s", err.Error())
}
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).
JSONEncoder(jsoniter.ConfigCompatibleWithStandardLibrary.Marshal).
JSONDecoder(jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal).
JSON(site).
BasicAuth("RoadSign CLI", server.Credential)
if status, data, err := client.Bytes(); len(err) > 0 {
return fmt.Errorf("failed to sync to remote: %q", err)
} else if status != 200 {
return fmt.Errorf("server rejected request, status code %d, response %s", status, string(data))
}
log.Info().Msg("Well done! Your site configuration is up-to-date! 🎉")
return nil
},
},
}

View File

@@ -1,48 +0,0 @@
package main
import (
"os"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds/conn"
"code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds/deploy"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"github.com/urfave/cli/v2"
)
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
}
func main() {
// Configure settings
viper.AddConfigPath("$HOME")
viper.SetConfigName(".roadsignrc")
viper.SetConfigType("yaml")
// Load settings
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
viper.SafeWriteConfig()
viper.ReadInConfig()
} else {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
}
// Configure CLI
app := &cli.App{
Name: "RoadSign CLI",
Version: roadsign.AppVersion,
Suggest: true,
Commands: append(append([]*cli.Command{}, conn.CliCommands...), deploy.DeployCommands...),
}
// Run CLI
if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("An error occurred when running cli.")
}
}

View File

@@ -1,86 +0,0 @@
package main
import (
"os"
"os/signal"
"strings"
"syscall"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"code.smartsheep.studio/goatworks/roadsign/pkg/hypertext"
"code.smartsheep.studio/goatworks/roadsign/pkg/sideload"
"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.")
}
// Preheat processes
go func() {
log.Info().Msg("Preheating processes...")
sign.App.PreheatProcesses(func(total int, success int) {
log.Info().Int("requested", total).Int("succeed", success).Msgf("Preheat processes completed!")
})
}()
// Init hypertext server
hypertext.RunServer(
hypertext.InitServer(),
viper.GetStringSlice("hypertext.ports"),
viper.GetStringSlice("hypertext.secured_ports"),
viper.GetString("hypertext.certificate.pem"),
viper.GetString("hypertext.certificate.key"),
)
// Init sideload server
hypertext.RunServer(
sideload.InitSideload(),
viper.GetStringSlice("hypertext.sideload_ports"),
viper.GetStringSlice("hypertext.sideload_secured_ports"),
viper.GetString("hypertext.certificate.sideload_pem"),
viper.GetString("hypertext.certificate.sideload_key"),
)
log.Info().Msgf("RoadSign v%s is started...", roadsign.AppVersion)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info().Msgf("RoadSign v%s is quitting...", roadsign.AppVersion)
}

View File

@@ -1,111 +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 {
if err := transformer.TransformRequest(ctx); err != nil {
return err
}
}
// Forward
err := sign.App.Forward(ctx, site)
// Modify response
for _, transformer := range site.Transformers {
if err := transformer.TransformResponse(ctx); err != nil {
return err
}
}
return err
}

View File

@@ -1,79 +0,0 @@
package hypertext
import (
jsoniter "github.com/json-iterator/go"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func InitServer() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign",
ServerHeader: "RoadSign",
DisableStartupMessage: true,
EnableIPValidation: true,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
Prefork: viper.GetBool("performance.prefork"),
BodyLimit: viper.GetInt("hypertext.limitation.max_body_size"),
})
if viper.GetBool("performance.request_logging") {
app.Use(logger.New(logger.Config{
Output: log.Logger,
Format: "[Proxies] [${time}] ${status} - ${latency} ${method} ${path}\n",
}))
}
if viper.GetInt("hypertext.limitation.max_qps") > 0 {
app.Use(limiter.New(limiter.Config{
Max: viper.GetInt("hypertext.limitation.max_qps"),
Expiration: 1 * time.Second,
}))
}
UseProxies(app)
return app
}
func RunServer(app *fiber.App, ports []string, securedPorts []string, pem string, key string) {
for _, port := range ports {
port := port
go func() {
if viper.GetBool("hypertext.certificate.redirect") {
redirector := fiber.New(fiber.Config{
AppName: "RoadSign",
ServerHeader: "RoadSign",
DisableStartupMessage: true,
EnableIPValidation: true,
})
redirector.All("/", func(c *fiber.Ctx) error {
return c.Redirect(strings.ReplaceAll(string(c.Request().URI().FullURI()), "http", "https"))
})
if err := redirector.Listen(port); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext common ports.")
}
} else {
if err := app.Listen(port); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext common ports.")
}
}
}()
}
for _, port := range securedPorts {
port := port
go func() {
if err := app.ListenTLS(port, pem, key); err != nil {
log.Panic().Err(err).Msg("An error occurred when listening hypertext tls ports.")
}
}()
}
}

View File

@@ -1,17 +0,0 @@
package roadsign
import (
"runtime/debug"
)
func init() {
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
AppVersion += "#" + setting.Value
}
}
}
}
var AppVersion = "1.2.1"

View File

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

View File

@@ -1,29 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getProcesses(c *fiber.Ctx) error {
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
return c.JSON(processes)
}
func getProcessLog(c *fiber.Ctx) error {
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
if target, ok := lo.Find(processes, func(item *sign.ProcessInstance) bool {
return item.ID == c.Params("id")
}); !ok {
return fiber.NewError(fiber.StatusNotFound)
} else {
return c.SendString(target.GetLogs())
}
}

View File

@@ -1,85 +0,0 @@
package sideload
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.UpstreamInstance
var process *sign.ProcessInstance
for _, item := range sign.App.Sites {
if item.ID == c.Params("site") {
site = item
for _, stream := range item.Upstreams {
if stream.ID == c.Params("slug") {
upstream = stream
workdir, _ = stream.GetRawURI()
break
}
}
for _, proc := range item.Processes {
if proc.ID == c.Params("slug") {
process = proc
workdir = proc.Workdir
break
}
}
break
}
}
if upstream == nil && process == nil {
return fiber.ErrNotFound
} else if upstream != nil && upstream.GetType() != sign.UpstreamTypeFile {
return fiber.ErrUnprocessableEntity
}
for _, process := range site.Processes {
process.StopProcess()
}
if c.Query("overwrite", "yes") == "yes" {
files, _ := filepath.Glob(filepath.Join(workdir, "*"))
for _, file := range files {
_ = os.Remove(file)
}
}
if form, err := c.MultipartForm(); err == nil {
files := form.File["attachments"]
for _, file := range files {
mimetype := lo.Ternary(len(c.Query("mimetype")) > 0, c.Query("mimetype"), file.Header["Content-Type"][0])
switch mimetype {
case "application/zip":
dst := filepath.Join(os.TempDir(), uuid.NewString()+".zip")
if err := c.SaveFile(file, dst); err != nil {
return err
} else {
if ex, err := fastzip.NewExtractor(dst, workdir); err != nil {
return err
} else if err = ex.Extract(context.Background()); err != nil {
defer ex.Close()
return err
}
}
default:
dst := filepath.Join(workdir, file.Filename)
if err := c.SaveFile(file, dst); err != nil {
return err
}
}
}
}
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,69 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sideload/view"
"fmt"
"github.com/gofiber/fiber/v2/middleware/filesystem"
jsoniter "github.com/json-iterator/go"
"net/http"
roadsign "code.smartsheep.studio/goatworks/roadsign/pkg"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func InitSideload() *fiber.App {
app := fiber.New(fiber.Config{
AppName: "RoadSign Sideload",
ServerHeader: "RoadSign Sideload",
DisableStartupMessage: true,
EnableIPValidation: true,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
TrustedProxies: viper.GetStringSlice("security.sideload_trusted_proxies"),
BodyLimit: viper.GetInt("hypertext.limitation.max_body_size"),
})
if viper.GetBool("performance.request_logging") {
app.Use(logger.New(logger.Config{
Output: log.Logger,
Format: "[Sideload] [${time}] ${status} - ${latency} ${method} ${path}\n",
}))
}
app.Use(basicauth.New(basicauth.Config{
Realm: fmt.Sprintf("RoadSign v%s", roadsign.AppVersion),
Authorizer: func(_, password string) bool {
return password == viper.GetString("security.credential")
},
}))
app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(view.FS),
PathPrefix: "dist",
Index: "index.html",
NotFoundFile: "index.html",
}))
cgi := app.Group("/cgi").Name("CGI")
{
cgi.All("/connectivity", responseConnectivity)
cgi.Get("/statistics", getStatistics)
cgi.Get("/sites", getSites)
cgi.Get("/sites/cfg/:id", getSiteConfig)
cgi.Get("/processes", getProcesses)
cgi.Get("/processes/logs/:id", getProcessLog)
}
webhooks := app.Group("/webhooks").Name("WebHooks")
{
webhooks.Put("/publish/:site/:slug", doPublish)
webhooks.Put("/sync/:slug", doSyncSite)
}
return app
}

View File

@@ -1,63 +0,0 @@
package sideload
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 getSites(c *fiber.Ctx) error {
return c.JSON(sign.App.Sites)
}
func getSiteConfig(c *fiber.Ctx) error {
fp := filepath.Join(viper.GetString("paths.configs"), c.Params("id"))
var err error
var data []byte
if data, err = os.ReadFile(fp + ".yml"); err != nil {
if data, err = os.ReadFile(fp + ".yaml"); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
}
return c.Type("yaml").SendString(string(data))
}
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()
}
if site, ok := lo.Find(sign.App.Sites, func(item *sign.SiteConfig) bool {
return item.ID == id
}); ok {
for _, process := range site.Processes {
process.StopProcess()
}
}
// Reload
sign.ReadInConfig(viper.GetString("paths.configs"))
sign.App.PreheatProcesses()
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -1,28 +0,0 @@
package sideload
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sign"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func getStatistics(c *fiber.Ctx) error {
upstreams := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.UpstreamInstance {
return item.Upstreams
})
processes := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return item.Processes
})
unhealthy := lo.FlatMap(sign.App.Sites, func(item *sign.SiteConfig, idx int) []*sign.ProcessInstance {
return lo.Filter(item.Processes, func(item *sign.ProcessInstance, idx int) bool {
return item.Status != sign.ProcessStarted
})
})
return c.JSON(fiber.Map{
"sites": len(sign.App.Sites),
"upstreams": len(upstreams),
"processes": len(processes),
"status": len(unhealthy) == 0,
})
}

View File

@@ -1,3 +0,0 @@
/dist
/node_modules
/*.lock

View File

@@ -1,15 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

View File

@@ -1,30 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": false,
"printWidth": 120,
"trailingComma": "none"
}

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,46 +0,0 @@
# @roadsign/sideload-ui
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
yarn
```
### Compile and Hot-Reload for Development
```sh
yarn dev
```
### Type-Check, Compile and Minify for Production
```sh
yarn build
```
### Lint with [ESLint](https://eslint.org/)
```sh
yarn lint
```

View File

@@ -1,6 +0,0 @@
package view
import "embed"
//go:embed all:dist
var FS embed.FS

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RoadSign</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,44 +0,0 @@
{
"name": "@roadsign/sideload-ui",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.4.1",
"highlight.js": "^11.9.0",
"js-yaml": "^4.1.0",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^18.19.3",
"@vicons/carbon": "^0.12.0",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"naive-ui": "^2.36.0",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3",
"typescript": "~5.3.0",
"unocss": "^0.58.2",
"vfonts": "^0.0.3",
"vite": "^5.0.10",
"vue-tsc": "^1.8.25"
}
}

View File

@@ -1,6 +0,0 @@
@import "vfonts/IBMPlexSans.css";
@import "vfonts/IBMPlexMono.css";
a {
color: #3f7ee8;
}

View File

@@ -1,135 +0,0 @@
<template>
<div class="flex gap-[4px]">
<n-button size="small" @click="publishing = true">
<template #icon>
<n-icon :component="CloudUpload" />
</template>
</n-button>
<n-button size="small" @click="editConfig()">
<template #icon>
<n-icon :component="Edit" />
</template>
</n-button>
<n-modal
v-model:show="publishing"
class="w-[720px]"
preset="card"
title="Publish Artifacts"
segmented
closable
>
We are sorry about this tool isn't completed yet. <br>
For now, you can use our <b>Wonderful Command Line Tool RDS</b> <br>
Learn more on our <a href="https://wiki.smartsheep.studio/roadsign/index.html" target="_blank">official wiki</a>.
<br>
<br>
Install it by this command below
<n-code code="go install code.smartsheep.studio/goatworks/roadsign/pkg/cmd/rds@latest" />
<br>
Then connect your rds client to this server
<n-code :code="`rds connect <name> ${host} <credentials>`" />
<br>
After that you can publish your stuff (You need to compress them to zip archive before publish)
<n-code :code="`rds deploy <name> ${props.id} <upstream id or process id>`" />
</n-modal>
<n-modal
v-model:show="editing"
class="w-[720px]"
content-style="padding: 0"
preset="card"
title="Edit Configuration"
segmented
closable
>
<div class="relative h-[540px]">
<vue-monaco-editor
v-model:value="config"
:options="{ automaticLayout: true, minimap: { enabled: false } }"
language="yaml"
/>
<div class="fab">
<n-tooltip placement="left">
<template #trigger>
<n-button
circle
type="primary"
size="large"
class="shadow-lg"
:loading="submitting"
@click="syncConfig()"
>
<template #icon>
<n-icon :component="Save" />
</template>
</n-button>
</template>
This operation will restart all processes related. Service may interrupted for some while.
</n-tooltip>
</div>
</div>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { NButton, NCode, NIcon, NModal, NTooltip, useMessage } from "naive-ui"
import { CloudUpload, Edit, Save } from "@vicons/carbon"
import { ref } from "vue"
import { VueMonacoEditor } from "@guolao/vue-monaco-editor"
import * as yaml from "js-yaml"
const message = useMessage()
const props = defineProps<{ id: string, rules: any[], upstreams: any[], processes: any[] }>()
const emits = defineEmits(["reload"])
const host = location.protocol + "//" + location.host
const submitting = ref(false)
const publishing = ref(false)
const editing = ref(false)
const config = ref<string | undefined>(undefined)
async function editConfig() {
const resp = await fetch(`/cgi/sites/cfg/${props.id}`)
config.value = await resp.text()
editing.value = true
}
async function syncConfig() {
if (config.value == null) return
let content
try {
content = yaml.load(config.value)
} catch (e: any) {
message.warning(`Your configuration has some issue: ${e.message}`)
return
}
submitting.value = true
const resp = await fetch(`/webhooks/sync/${props.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(content)
})
if (resp.status != 200) {
message.error(`Something went wrong... ${await resp.text()}`)
} else {
emits("reload")
}
submitting.value = false
}
</script>
<style scoped>
.fab {
position: absolute;
bottom: 16px;
right: 24px;
}
</style>

View File

@@ -1,110 +0,0 @@
<template>
<div>
<n-button circle size="small" type="primary" @click="creating = true">
<template #icon>
<n-icon :component="Add" />
</template>
</n-button>
<n-modal
v-model:show="creating"
class="w-[720px]"
content-style="padding: 0"
preset="card"
title="Create Site"
segmented
closable
>
<div class="py-4 px-5 border border-solid border-b border-[#eee]">
<n-input
v-model:value="data.id"
placeholder="Will be the file name of this file"
/>
</div>
<div class="relative mt-[4px] h-[540px]">
<vue-monaco-editor
v-model:value="data.content"
:options="{ automaticLayout: true, minimap: { enabled: false } }"
language="yaml"
/>
<div class="fab">
<n-tooltip placement="left">
<template #trigger>
<n-button
circle
type="primary"
size="large"
class="shadow-lg"
:loading="submitting"
@click="submit()"
>
<template #icon>
<n-icon :component="Checkmark" />
</template>
</n-button>
</template>
This operation will publish this site right away.
</n-tooltip>
</div>
</div>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { NButton, NIcon, NInput, NModal, NTooltip, useMessage } from "naive-ui"
import { Add, Checkmark } from "@vicons/carbon"
import { VueMonacoEditor } from "@guolao/vue-monaco-editor"
import { ref } from "vue"
import * as yaml from "js-yaml"
const message = useMessage()
const emits = defineEmits(["reload"])
const submitting = ref(false)
const creating = ref(false)
const data = ref<any>({})
async function submit() {
let content
try {
content = yaml.load(data.value.content)
} catch (e: any) {
message.warning(`Your configuration has some issue: ${e.message}`)
return
}
submitting.value = true
const resp = await fetch(`/webhooks/sync/${data.value.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(content)
})
if (resp.status != 200) {
message.error(`Something went wrong... ${await resp.text()}`)
} else {
reset()
emits("reload")
message.success("Your site has been created! 🎉")
creating.value = false
}
submitting.value = false
}
function reset() {
data.value.id = ""
data.value.content = ""
}
</script>
<style scoped>
.fab {
position: absolute;
bottom: 16px;
right: 24px;
}
</style>

View File

@@ -1,36 +0,0 @@
<template>
<div class="flex flex-col gap-1">
<div>
<div class="font-bold">Rules</div>
<n-code :hljs="hljs" :code="parseData(props.rules)" language="json" />
</div>
<div>
<div class="font-bold">Upstreams</div>
<n-code :hljs="hljs" :code="parseData(props.upstreams)" language="json" />
</div>
<div>
<div class="font-bold">Processes</div>
<n-code :hljs="hljs" :code="parseData(props.processes)" language="json" />
</div>
</div>
</template>
<script setup lang="ts">
import { NCode } from "naive-ui"
import hljs from "highlight.js/lib/core"
import json from "highlight.js/lib/languages/json"
hljs.registerLanguage("json", json)
const props = defineProps<{ rules: any[], upstreams: any[], processes: any[] }>()
function parseData(data: any): string {
return JSON.stringify(data, null, 1)
.replace(/ +/g, " ")
.replace(/\n/g, "")
}
</script>

View File

@@ -1,76 +0,0 @@
<template>
<div>
<n-card title="Sites">
<template #header-extra>
<sites-table-add @reload="readSites()" />
</template>
<n-data-table
:columns="columns"
:data="data"
:row-key="(row: any) => row.id"
/>
</n-card>
</div>
</template>
<script setup lang="ts">
import { NCard, NDataTable, NTag } from "naive-ui"
import { h, ref } from "vue"
import SitesTableExpand from "@/components/data/sites-table-expand.vue"
import SitesTableAction from "@/components/data/sites-table-action.vue"
import SitesTableAdd from "@/components/data/sites-table-add.vue"
const columns: any[] = [
{
type: "expand",
renderExpand(row: any) {
return h(SitesTableExpand, { ...row, class: "pl-[38px]" })
}
},
{
title: "ID",
key: "id",
render(row: any) {
return h(NTag, { type: "info", bordered: false, size: "small" }, row?.id)
}
},
{
title: "Rules",
key: "rules",
render(row: any) {
return row?.rules?.length ?? 0
}
},
{
title: "Upstreams",
key: "upstreams",
render(row: any) {
return row?.upstreams?.length ?? 0
}
},
{
title: "Processes",
key: "processes",
render(row: any) {
return row?.processes?.length ?? 0
}
},
{
title: "Actions",
key: "actions",
render(row: any) {
return h(SitesTableAction, { ...row, onReload: () => readSites() })
}
}
]
const data = ref<any[]>([])
async function readSites() {
const resp = await fetch("/cgi/sites")
data.value = await resp.json()
}
readSites()
</script>

View File

@@ -1,60 +0,0 @@
<template>
<n-layout>
<n-layout-header class="header py-[8px] px-[36px]" bordered>
<div class="flex items-center gap-2">
<router-link class="link" to="/">
RoadSign<i>!</i>
</router-link>
</div>
<div class="nav-menu">
<div class="h-full flex items-center header-nav">
<n-menu v-model:value="key" :options="options" mode="horizontal" />
</div>
</div>
</n-layout-header>
<n-layout-content class="h-[calc(100vh-70px)] container mx-auto" content-style="padding: 24px">
<router-view />
</n-layout-content>
</n-layout>
</template>
<script setup lang="ts">
import { type MenuOption, NIcon, NLayout, NLayoutContent, NLayoutHeader, NMenu } from "naive-ui"
import { type Component, h, ref } from "vue"
import { Dashboard } from "@vicons/carbon"
import { RouterLink, useRoute, useRouter } from "vue-router"
const route = useRoute()
const router = useRouter()
const key = ref(route.name?.toString())
router.afterEach((to) => {
key.value = to.name?.toString() ?? "index"
})
const options: MenuOption[] = [
{
label: () => h(RouterLink, { to: { name: "dashboard" } }, "Dashboard"),
icon: renderIcon(Dashboard),
key: "dashboard"
}
]
function renderIcon(icon: Component) {
return () => h(NIcon, null, { default: () => h(icon) })
}
</script>
<style scoped>
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 40px;
}
.link {
all: unset;
cursor: pointer;
}
</style>

View File

@@ -1,16 +0,0 @@
import "./assets/main.css"
import "virtual:uno.css"
import { createApp } from "vue"
import { createPinia } from "pinia"
import root from "./root.vue"
import router from "./router"
const app = createApp(root)
app.use(createPinia())
app.use(router)
app.mount("#app")

View File

@@ -1,9 +0,0 @@
<template>
<n-message-provider>
<router-view />
</n-message-provider>
</template>
<script setup lang="ts">
import { NMessageProvider } from "naive-ui"
</script>

View File

@@ -1,21 +0,0 @@
import { createRouter, createWebHistory } from "vue-router"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "layouts.main",
component: () => import("@/layouts/main.vue"),
children: [
{
path: "/",
name: "dashboard",
component: () => import("@/views/dashboard.vue")
},
]
},
]
})
export default router

View File

@@ -1,35 +0,0 @@
<template>
<div class="flex flex-col gap-2">
<div class="grid gap-2 grid-cols-2 lg:grid-cols-4">
<n-card embedded>
<n-statistic label="Status">{{ data?.status ? "Operational" : "Incident" }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Sites">{{ data?.sites }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Upstreams">{{ data?.upstreams }}</n-statistic>
</n-card>
<n-card embedded>
<n-statistic label="Processes">{{ data?.processes }}</n-statistic>
</n-card>
</div>
<sites-table />
</div>
</template>
<script setup lang="ts">
import { NCard, NStatistic } from "naive-ui"
import { ref } from "vue"
import SitesTable from "@/components/data/sites-table.vue"
const data = ref<any>({})
async function readStatistics() {
const resp = await fetch("/cgi/statistics")
data.value = await resp.json()
}
readStatistics()
</script>

View File

@@ -1,13 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@@ -1,11 +0,0 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -1,17 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -1,5 +0,0 @@
import { defineConfig, presetUno } from "unocss"
export default defineConfig({
presets: [presetUno({ preflight: false })]
})

View File

@@ -1,24 +0,0 @@
import { fileURLToPath, URL } from "node:url"
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import unocss from "unocss/vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
unocss()
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
},
server: {
proxy: {
"/webhooks": "http://127.0.0.1:81",
"/cgi": "http://127.0.0.1:81"
}
}
})

View File

@@ -1,45 +0,0 @@
package sign
import (
"io"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
var App *RoadApp
func ReadInConfig(root string) error {
instance := &RoadApp{
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]
instance.Sites = append(instance.Sites, &site)
}
return nil
}); err != nil {
return err
}
App = instance
return nil
}

View File

@@ -1,144 +0,0 @@
package sign
import (
"fmt"
"github.com/samber/lo"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
type ProcessStatus = int8
const (
ProcessCreated = ProcessStatus(iota)
ProcessStarting
ProcessStarted
ProcessExited
ProcessFailure
)
type ProcessInstance struct {
ID string `json:"id" yaml:"id"`
Workdir string `json:"workdir" yaml:"workdir"`
Command []string `json:"command" yaml:"command"`
Environment []string `json:"environment" yaml:"environment"`
Prepares [][]string `json:"prepares" yaml:"prepares"`
Preheat bool `json:"preheat" yaml:"preheat"`
Cmd *exec.Cmd `json:"-"`
Logger strings.Builder `json:"-"`
Status ProcessStatus `json:"status"`
}
func (v *ProcessInstance) BootProcess() error {
if v.Cmd != nil {
return nil
}
if err := v.PrepareProcess(); 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 *ProcessInstance) PrepareProcess() 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 *ProcessInstance) 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)
v.Cmd.Env = append(v.Cmd.Env, v.Environment...)
v.Cmd.Stdout = &v.Logger
v.Cmd.Stderr = &v.Logger
// Monitor
go func() {
for {
if v.Cmd.Process == nil || v.Cmd.ProcessState == nil {
v.Status = ProcessStarting
} else if !v.Cmd.ProcessState.Exited() {
v.Status = ProcessStarted
} else {
v.Status = lo.Ternary(v.Cmd.ProcessState.Success(), ProcessExited, ProcessFailure)
return
}
time.Sleep(100 * time.Millisecond)
}
}()
return v.Cmd.Start()
}
func (v *ProcessInstance) 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
}
func (v *ProcessInstance) GetLogs() string {
return v.Logger.String()
}
func (v *RoadApp) PreheatProcesses(callbacks ...func(total int, success int)) {
var processes []*ProcessInstance
for _, site := range v.Sites {
for _, process := range site.Processes {
if process.Preheat {
processes = append(processes, process)
}
}
}
success := 0
for _, process := range processes {
if process.BootProcess() == nil {
success++
}
}
if len(callbacks) > 0 {
for _, callback := range callbacks {
callback(len(processes), success)
}
}
}

View File

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

View File

@@ -1,58 +0,0 @@
package sign
import (
"code.smartsheep.studio/goatworks/roadsign/pkg/sign/transformers"
"errors"
"math/rand"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
)
type RoadApp struct {
Sites []*SiteConfig `json:"sites"`
}
func (v *RoadApp) 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 RequestTransformerConfig = transformers.RequestTransformerConfig
type SiteConfig struct {
ID string `json:"id"`
Rules []*RouterRule `json:"rules" yaml:"rules"`
Transformers []*RequestTransformerConfig `json:"transformers" yaml:"transformers"`
Upstreams []*UpstreamInstance `json:"upstreams" yaml:"upstreams"`
Processes []*ProcessInstance `json:"processes" yaml:"processes"`
}
type RouterRule struct {
Host []string `json:"host" yaml:"host"`
Path []string `json:"path" yaml:"path"`
Queries map[string]string `json:"queries" yaml:"queries"`
Headers map[string][]string `json:"headers" yaml:"headers"`
}

View File

@@ -1,41 +0,0 @@
package transformers
import (
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
)
var CompressResponse = RequestTransformer{
ModifyResponse: func(options any, ctx *fiber.Ctx) error {
opts := DeserializeOptions[struct {
Level int `json:"level" yaml:"level"`
}](options)
var fctx = func(c *fasthttp.RequestCtx) {}
var compressor fasthttp.RequestHandler
switch opts.Level {
// Best Speed Mode
case 1:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliBestSpeed,
fasthttp.CompressBestSpeed,
)
// Best Compression Mode
case 2:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliBestCompression,
fasthttp.CompressBestCompression,
)
// Default Mode
default:
compressor = fasthttp.CompressHandlerBrotliLevel(fctx,
fasthttp.CompressBrotliDefaultCompression,
fasthttp.CompressDefaultCompression,
)
}
compressor(ctx.Context())
return nil
},
}

View File

@@ -1,61 +0,0 @@
package transformers
import (
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
)
// Definitions
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type RequestTransformer struct {
ModifyRequest func(options any, ctx *fiber.Ctx) error
ModifyResponse func(options any, ctx *fiber.Ctx) error
}
type RequestTransformerConfig struct {
Type string `json:"type" yaml:"type"`
Options any `json:"options" yaml:"options"`
}
func (v *RequestTransformerConfig) TransformRequest(ctx *fiber.Ctx) error {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyRequest != nil {
return f.ModifyRequest(v.Options, ctx)
}
break
}
}
return nil
}
func (v *RequestTransformerConfig) TransformResponse(ctx *fiber.Ctx) error {
for k, f := range Transformers {
if k == v.Type {
if f.ModifyResponse != nil {
return f.ModifyResponse(v.Options, ctx)
}
break
}
}
return nil
}
// Helpers
func DeserializeOptions[T any](data any) T {
var out T
raw, _ := json.Marshal(data)
_ = json.Unmarshal(raw, &out)
return out
}
// Map of Transformers
// Every transformer need to be mapped here so that they can get work.
var Transformers = map[string]RequestTransformer{
"replacePath": ReplacePath,
"compressResponse": CompressResponse,
}

View File

@@ -1,25 +0,0 @@
package transformers
import (
"github.com/gofiber/fiber/v2"
"regexp"
"strings"
)
var ReplacePath = RequestTransformer{
ModifyRequest: func(options any, ctx *fiber.Ctx) error {
opts := DeserializeOptions[struct {
Pattern string `json:"pattern" yaml:"pattern"`
Value string `json:"value" yaml:"value"`
Repl string `json:"repl" yaml:"repl"` // Use when complex mode(regexp) enabled
Complex bool `json:"complex" yaml:"complex"`
}](options)
path := string(ctx.Request().URI().Path())
if !opts.Complex {
ctx.Path(strings.ReplaceAll(path, opts.Pattern, opts.Value))
} else if ex := regexp.MustCompile(opts.Pattern); ex != nil {
ctx.Path(ex.ReplaceAllString(path, opts.Repl))
}
return nil
},
}

View File

@@ -1,58 +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 UpstreamInstance struct {
ID string `json:"id" yaml:"id"`
URI string `json:"uri" yaml:"uri"`
}
func (v *UpstreamInstance) 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 *UpstreamInstance) GetRawURI() (string, url.Values) {
uri := strings.SplitN(v.URI, "://", 2)[1]
data := strings.SplitN(uri, "?", 2)
data = append(data, " ") // Make data array least have two element
qs, _ := url.ParseQuery(data[0])
return data[0], qs
}
func (v *UpstreamInstance) MakeURI(ctx *fiber.Ctx) string {
var queries []string
for k, v := range ctx.Queries() {
parsed, _ := url.QueryUnescape(v)
value := url.QueryEscape(parsed)
queries = append(queries, fmt.Sprintf("%s=%s", k, value))
}
path := string(ctx.Request().URI().Path())
hash := string(ctx.Request().URI().Hash())
return v.URI + path +
lo.Ternary(len(queries) > 0, "?"+strings.Join(queries, "&"), "") +
lo.Ternary(len(hash) > 0, "#"+hash, "")
}

12
regions/index.html Normal file
View File

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

14
regions/index.toml Normal file
View File

@@ -0,0 +1,14 @@
id = "index"
[[locations]]
id = "root"
hosts = ["localhost"]
paths = ["/"]
[[locations.destinations]]
id = "static"
uri = "files://regions?index=index.html"
[[applications]]
id = "script"
exe = "./script.sh"
workdir = "regions"

1
regions/kokodayo.txt Normal file
View File

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

3
regions/script.sh Executable file
View File

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

View File

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

View File

@@ -1,28 +0,0 @@
debug:
print_routes: true
hypertext:
sideload_ports:
- :81
sideload_secured_ports: [ ]
certificate:
redirect: false
sideload_key: ./cert.key
sideload_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:
sideload_trusted_proxies:
- localhost
credential: e81f43f32d934271af6322e5376f5f59

50
src/auth.rs Normal file
View File

@@ -0,0 +1,50 @@
use http::StatusCode;
use poem::{
web::headers::{self, authorization::Basic, HeaderMapExt},
Endpoint, Error, Middleware, Request, Response, Result,
};
pub struct BasicAuth {
pub username: String,
pub password: String,
}
impl<E: Endpoint> Middleware<E> for BasicAuth {
type Output = BasicAuthEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
BasicAuthEndpoint {
ep,
username: self.username.clone(),
password: self.password.clone(),
}
}
}
pub struct BasicAuthEndpoint<E> {
ep: E,
username: String,
password: String,
}
#[poem::async_trait]
impl<E: Endpoint> Endpoint for BasicAuthEndpoint<E> {
type Output = E::Output;
async fn call(&self, req: Request) -> Result<Self::Output> {
if let Some(auth) = req.headers().typed_get::<headers::Authorization<Basic>>() {
if auth.0.username() == self.username && auth.0.password() == self.password {
return self.ep.call(req).await;
}
}
Err(Error::from_response(
Response::builder()
.header(
"WWW-Authenticate",
"Basic realm=\"RoadSig\", charset=\"UTF-8\"",
)
.status(StatusCode::UNAUTHORIZED)
.finish(),
))
}
}

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

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

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

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

67
src/main.rs Normal file
View File

@@ -0,0 +1,67 @@
pub mod auth;
mod config;
mod proxies;
pub mod warden;
use actix_web::{App, HttpServer, web};
use awc::Client;
use lazy_static::lazy_static;
use proxies::RoadInstance;
use tokio::sync::Mutex;
use tracing::{error, info, Level};
use crate::proxies::route;
lazy_static! {
static ref ROAD: Mutex<RoadInstance> = Mutex::new(RoadInstance::new());
}
#[tokio::main]
async fn main() -> Result<(), std::io::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::C
.read()
.await
.get_string("regions")
.unwrap_or("./regions".to_string()),
) {
Err(_) => error!("Loading proxy regions... failed"),
Ok((regions, count)) => {
ROAD.lock().await.regions = regions;
info!(count, "Loading proxy regions... done")
}
};
// Proxies
let proxies_server = HttpServer::new(|| {
App::new()
.app_data(web::Data::new(Client::default()))
.route("/", web::to(route::handle))
}).bind(
config::C
.read()
.await
.get_string("listen.proxies")
.unwrap_or("0.0.0.0:80".to_string())
)?.run();
// Process manager
{
let mut app = ROAD.lock().await;
{
let reg = app.regions.clone();
app.warden.scan(reg);
}
app.warden.start().await;
}
tokio::try_join!(proxies_server)?;
Ok(())
}

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

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

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

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

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

@@ -0,0 +1,94 @@
use std::collections::VecDeque;
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use super::config::{Destination, Location, Region};
#[derive(Debug, Object, Clone, Serialize, Deserialize, PartialEq)]
pub struct RoadTrace {
pub region: String,
pub location: String,
pub destination: String,
pub error: Option<String>,
}
impl RoadTrace {
pub fn from_structs(reg: Region, loc: Location, end: Destination) -> RoadTrace {
RoadTrace {
region: reg.id,
location: loc.id,
destination: end.id,
error: None,
}
}
pub fn from_structs_with_error(
reg: Region,
loc: Location,
end: Destination,
err: String,
) -> RoadTrace {
RoadTrace {
region: reg.id,
location: loc.id,
destination: end.id,
error: Some(err),
}
}
}
#[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 = 10;
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, reg: Region, loc: Location, end: Destination) {
self.requests_count += 1;
self.recent_successes
.push_back(RoadTrace::from_structs(reg, loc, end));
if self.recent_successes.len() > MAX_TRACE_COUNT {
self.recent_successes.pop_front();
}
}
pub fn add_faliure_request(
&mut self,
reg: Region,
loc: Location,
end: Destination,
err: String, // For some reason error is rarely clonable, so we use preformatted message
) {
self.requests_count += 1;
self.failures_count += 1;
self.recent_errors
.push_back(RoadTrace::from_structs_with_error(reg, loc, end, err));
if self.recent_errors.len() > MAX_TRACE_COUNT {
self.recent_errors.pop_front();
}
}
}

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

@@ -0,0 +1,89 @@
use actix_web::http::header::HeaderMap;
use actix_web::http::{Method, Uri};
use regex::Regex;
use wildmatch::WildMatch;
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;
#[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))
})
}
}

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

@@ -0,0 +1,135 @@
use futures_util::{SinkExt};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use actix_files::{NamedFile};
use actix_proxy::IntoHttpResponse;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::http::Method;
use awc::Client;
pub async fn respond_hypertext(
uri: String,
req: HttpRequest,
client: web::Data<Client>,
) -> Result<HttpResponse, HttpResponse> {
let ip = req.peer_addr().unwrap().ip().to_string();
let proto = req.uri().scheme_str().unwrap();
let host = req.uri().host().unwrap();
let mut headers = req.headers().clone();
headers.insert("Server".parse().unwrap(), "RoadSign".parse().unwrap());
headers.insert("X-Forward-For".parse().unwrap(), ip.parse().unwrap());
headers.insert("X-Forwarded-Proto".parse().unwrap(), proto.parse().unwrap());
headers.insert("X-Forwarded-Host".parse().unwrap(), host.parse().unwrap());
headers.insert("X-Real-IP".parse().unwrap(), ip.parse().unwrap());
headers.insert(
"Forwarded".parse().unwrap(),
format!("by={};for={};host={};proto={}", ip, ip, host, proto)
.parse()
.unwrap(),
);
let res = client.get(uri).send().await;
return match res {
Ok(result) => {
let mut res = result.into_http_response();
res.headers_mut().insert("Server".parse().unwrap(), "RoadSign".parse().unwrap());
Ok(res)
}
Err(error) => {
Err(HttpResponse::BadGateway()
.body(format!("Something went wrong... {:}", error)))
}
};
}
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, HttpResponse> {
if req.method() != Method::GET {
return Err(HttpResponse::MethodNotAllowed()
.body("This destination only support GET request."));
}
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(HttpResponse::NotFound().body("Not found."));
}
};
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(HttpResponse::Forbidden()
.body("Unexpected path."));
}
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(HttpResponse::NotFound().body("Not found."));
}
return 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(HttpResponse::NotFound().body("Not found."))
};
}

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

@@ -0,0 +1,80 @@
use actix_web::{HttpRequest, HttpResponse, web};
use awc::Client;
use rand::seq::SliceRandom;
use crate::{
proxies::{
config::{Destination, DestinationType},
responder,
},
ROAD,
};
pub async fn handle(req: HttpRequest, 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 HttpResponse::NotFound()
.body("There are no region be able to respone this request.");
}
};
let destination = location
.destinations
.choose_weighted(&mut rand::thread_rng(), |item| item.weight.unwrap_or(1))
.unwrap();
async fn forward(
end: &Destination,
req: HttpRequest,
client: web::Data<Client>,
) -> Result<HttpResponse, HttpResponse> {
// Handle normal web request
match end.get_type() {
DestinationType::Hypertext => {
let Ok(uri) = end.get_hypertext_uri() else {
return Err(HttpResponse::NotImplemented()
.body("This destination was not support web requests."));
};
responder::respond_hypertext(uri, req, client).await
}
DestinationType::StaticFiles => {
let Ok(cfg) = end.get_static_config() else {
return Err(HttpResponse::NotImplemented()
.body("This destination was not support static files."));
};
responder::respond_static(cfg, req).await
}
_ => {
return Err(HttpResponse::NotImplemented()
.body("Unsupported destination protocol."));
}
}
}
let reg = region.clone();
let loc = location.clone();
let end = destination.clone();
return match forward(&end, req, client).await {
Ok(resp) => {
tokio::spawn(async move {
let writable_app = &mut ROAD.lock().await;
writable_app.metrics.add_success_request(reg, loc, end);
});
resp
}
Err(resp) => {
tokio::spawn(async move {
let writable_app = &mut ROAD.lock().await;
writable_app
.metrics
.add_faliure_request(reg, loc, end, "TODO".to_owned());
});
resp
}
}
}

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

@@ -0,0 +1,73 @@
pub mod runner;
use std::collections::HashMap;
use futures_util::lock::Mutex;
use lazy_static::lazy_static;
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
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())
.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, Object, Clone, Serialize, Deserialize)]
pub struct Application {
pub id: String,
pub exe: String,
pub args: Option<Vec<String>>,
pub env: Option<HashMap<String, String>>,
pub workdir: String,
}

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

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
:root{--bs-body-font-family: "IBM Plex Serif", "Noto Serif SC", sans-serif !important}html,body{font-family:var(--bs-body-font-family)}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:100;src:url(/_astro/ibm-plex-serif-v19-latin-100.6qNbweSL.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:100;src:url(/_astro/ibm-plex-serif-v19-latin-100italic.E22nrI7z.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:200;src:url(/_astro/ibm-plex-serif-v19-latin-200.GFXE_YJc.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:200;src:url(/_astro/ibm-plex-serif-v19-latin-200italic.pJK4yaaG.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:300;src:url(/_astro/ibm-plex-serif-v19-latin-300.RVbRgkxX.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:300;src:url(/_astro/ibm-plex-serif-v19-latin-300italic.ZdSVgmcR.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:400;src:url(/_astro/ibm-plex-serif-v19-latin-regular.HRmMD3sQ.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:400;src:url(/_astro/ibm-plex-serif-v19-latin-italic.MiJiQVsi.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:500;src:url(/_astro/ibm-plex-serif-v19-latin-500.xAA_w-Ac.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:500;src:url(/_astro/ibm-plex-serif-v19-latin-500italic.Unq84pJ7.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:600;src:url(/_astro/ibm-plex-serif-v19-latin-600.cuuqzllG.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:600;src:url(/_astro/ibm-plex-serif-v19-latin-600italic.vDhUog1q.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:normal;font-weight:700;src:url(/_astro/ibm-plex-serif-v19-latin-700.yX9JjmCp.woff2) format("woff2")}@font-face{font-display:swap;font-family:"IBM Plex Serif";font-style:italic;font-weight:700;src:url(/_astro/ibm-plex-serif-v19-latin-700italic.QM1RA0vx.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:200;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-200.g4OBZhIi.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:300;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-300.yFtdUYoh.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:400;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-regular.9muiKgFz.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:500;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-500.exkAspFQ.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:600;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-600.4n6uFOXj.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:700;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-700.HyiB9Pzv.woff2) format("woff2")}@font-face{font-display:swap;font-family:"Noto Serif SC";font-style:normal;font-weight:900;src:url(/_astro/noto-serif-sc-v22-chinese-simplified-900.ERSRy_0V.woff2) format("woff2")}.astro-route-announcer{position:absolute;left:0;top:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(50%);clip-path:inset(50%);overflow:hidden;white-space:nowrap;width:1px;height:1px}.h-fullpage{height:calc(100vh - 64px)}.max-h-fullpage{max-height:calc(100vh - 64px)}.mt-header{margin-top:64px}.top-header{top:64px}html{overflow-x:hidden!important;overflow-y:auto!important}@keyframes astroFadeInOut{0%{opacity:1}to{opacity:0}}@keyframes astroFadeIn{0%{opacity:0}}@keyframes astroFadeOut{to{opacity:0}}@keyframes astroSlideFromRight{0%{transform:translate(100%)}}@keyframes astroSlideFromLeft{0%{transform:translate(-100%)}}@keyframes astroSlideToRight{to{transform:translate(100%)}}@keyframes astroSlideToLeft{to{transform:translate(-100%)}}@media (prefers-reduced-motion){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}[data-astro-transition-scope]{animation:none!important}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More