diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 05669038..beb0a9e2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,48 +3,48 @@ updates:
- package-ecosystem: npm
directory: "/packages/dashboard"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: npm
directory: "/packages/dnslink-api"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: npm
directory: "/packages/handshake-api"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: npm
directory: "/packages/health-check"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: npm
directory: "/packages/website"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/docker/nginx"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/docker/sia"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/packages/dashboard"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/packages/dnslink-api"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/packages/handshake-api"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/packages/health-check"
schedule:
- interval: weekly
+ interval: monthly
- package-ecosystem: docker
directory: "/packages/website"
schedule:
- interval: weekly
+ interval: monthly
diff --git a/.github/workflows/lint-dockerfiles.yml b/.github/workflows/lint-dockerfiles.yml
new file mode 100644
index 00000000..afdd6558
--- /dev/null
+++ b/.github/workflows/lint-dockerfiles.yml
@@ -0,0 +1,29 @@
+name: Dockerfile Lint
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+jobs:
+ hadolint:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ dockerfile:
+ - docker/nginx/Dockerfile
+ - docker/nginx/testing/Dockerfile
+ - docker/sia/Dockerfile
+ - packages/dashboard/Dockerfile
+ - packages/dashboard-v2/Dockerfile
+ - packages/dnslink-api/Dockerfile
+ - packages/handshake-api/Dockerfile
+ - packages/health-check/Dockerfile
+ - packages/website/Dockerfile
+ steps:
+ - uses: actions/checkout@v3
+ - uses: hadolint/hadolint-action@v2.0.0
+ with:
+ dockerfile: ${{ matrix.dockerfile }}
diff --git a/.github/workflows/nginx-lua-unit-tests.yml b/.github/workflows/nginx-lua-unit-tests.yml
index b86a4e04..5af9c101 100644
--- a/.github/workflows/nginx-lua-unit-tests.yml
+++ b/.github/workflows/nginx-lua-unit-tests.yml
@@ -6,48 +6,41 @@ name: Nginx Lua Unit Tests
on:
push:
branches:
- - "master"
+ - master
paths:
- - ".github/workflows/nginx-lua-unit-tests.yml"
- - "docker/nginx/libs/**.lua"
+ - docker/nginx/libs/**
pull_request:
paths:
- - ".github/workflows/nginx-lua-unit-tests.yml"
- - "docker/nginx/libs/**.lua"
+ - docker/nginx/libs/**
jobs:
- build:
+ test:
runs-on: ubuntu-latest
-
+ container: openresty/openresty:1.19.9.1-focal
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
- with:
- python-version: "3.x"
- architecture: "x64"
+ - uses: actions/checkout@v3
- name: Install Dependencies
run: |
- pip install hererocks
- hererocks env --lua=5.1 -rlatest
- source env/bin/activate
+ luarocks install lua-resty-http
+ luarocks install hasher
luarocks install busted
luarocks install luacov
- luarocks install hasher
luarocks install luacheck
- - name: Lint code
- run: |
- source env/bin/activate
- luacheck docker/nginx/libs --std ngx_lua+busted
+ - name: Lint Code With Luacheck
+ run: luacheck docker/nginx/libs --std ngx_lua+busted
- - name: Unit Tests
- run: |
- source env/bin/activate
- busted --verbose --coverage --pattern=spec --directory=docker/nginx/libs .
- cd docker/nginx/libs && luacov
+ - name: Run Tests With Busted
+ # ran from root repo directory; produces luacov.stats.out file
+ run: docker/nginx/testing/rbusted --lpath='docker/nginx/libs/?.lua;docker/nginx/libs/?/?.lua' --verbose --coverage --pattern=spec docker/nginx/libs
- - uses: codecov/codecov-action@v2
+ - name: Generate Code Coverage Report With Luacov
+ # requires config file in cwd; produces luacov.report.out file
+ run: cp docker/nginx/testing/.luacov . && luacov && rm .luacov
+
+ - uses: codecov/codecov-action@v3
with:
- directory: docker/nginx/libs
+ root_dir: ${GITHUB_WORKSPACE}
+ files: ./luacov.report.out
flags: nginx-lua
diff --git a/.gitignore b/.gitignore
index 4b85194e..8a98ee28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,6 +86,10 @@ __pycache__
/.idea/
/venv*
+# Luacov file
+luacov.stats.out
+luacov.report.out
+
# Setup-script log files
setup-scripts/serverload.log
setup-scripts/serverload.json
diff --git a/README.md b/README.md
index 4c27d268..e2c1714f 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,10 @@
## Latest Setup Documentation
Latest Skynet Webportal setup documentation and the setup process Skynet Labs
-supports is located at https://docs.siasky.net/webportal-management/overview.
+supports is located at https://portal-docs.skynetlabs.com/.
-Some of the scripts and setup documentation contained in this repository
-(`skynet-webportal`) can be outdated and generally should not be used.
+Some scripts and setup documentation contained in this repository
+(`skynet-webportal`) may be outdated and generally should not be used.
## Web application
@@ -35,7 +35,7 @@ For the purposes of complying with our code license, you can use the following S
`fb6c9320bc7e01fbb9cd8d8c3caaa371386928793c736837832e634aaaa484650a3177d6714a`
## Running a Portal
-For those interested in running a Webportal, head over to our developer docs [here](https://docs.siasky.net/webportal-management/overview.) to learn more.
+For those interested in running a Webportal, head over to our developer docs [here](https://portal-docs.skynetlabs.com/) to learn more.
## Contributing
diff --git a/changelog/items/bugs-fixed/fix-context.md b/changelog/items/bugs-fixed/fix-context.md
new file mode 100644
index 00000000..e94c8e6e
--- /dev/null
+++ b/changelog/items/bugs-fixed/fix-context.md
@@ -0,0 +1,2 @@
+- Fix `dashboard-v2` Dockerfile context in `docker-compose.accounts.yml` to
+ avoid Ansible deploy (docker compose build) `permission denied` issues.
\ No newline at end of file
diff --git a/docker-compose.abuse-scanner.yml b/docker-compose.abuse-scanner.yml
index 4edb6556..2eae443b 100644
--- a/docker-compose.abuse-scanner.yml
+++ b/docker-compose.abuse-scanner.yml
@@ -10,7 +10,7 @@ services:
abuse-scanner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/abuse-scanner.git#main
- image: skynetlabs/abuse-scanner
+ image: skynetlabs/abuse-scanner:0.1.1
container_name: abuse-scanner
restart: unless-stopped
logging: *default-logging
diff --git a/docker-compose.accounts.yml b/docker-compose.accounts.yml
index 5d9d345c..99b10ff4 100644
--- a/docker-compose.accounts.yml
+++ b/docker-compose.accounts.yml
@@ -55,9 +55,11 @@ services:
- mongo
dashboard:
- build:
- context: ./packages/dashboard
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./packages/dashboard/Dockerfile
+ image: skynetlabs/dashboard
container_name: dashboard
restart: unless-stopped
logging: *default-logging
@@ -75,3 +77,26 @@ services:
- 3000
depends_on:
- mongo
+
+ dashboard-v2:
+ build:
+ context: ./packages/dashboard-v2
+ dockerfile: Dockerfile
+ container_name: dashboard-v2
+ restart: unless-stopped
+ logging: *default-logging
+ env_file:
+ - .env
+ environment:
+ - GATSBY_PORTAL_DOMAIN=${PORTAL_DOMAIN}
+ - GATSBY_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
+ volumes:
+ - ./docker/data/dashboard-v2/.cache:/usr/app/.cache
+ - ./docker/data/dashboard-v2/public:/usr/app/public
+ networks:
+ shared:
+ ipv4_address: 10.10.10.86
+ expose:
+ - 9000
+ depends_on:
+ - mongo
diff --git a/docker-compose.blocker.yml b/docker-compose.blocker.yml
index edcb45c0..db398805 100644
--- a/docker-compose.blocker.yml
+++ b/docker-compose.blocker.yml
@@ -15,7 +15,7 @@ services:
blocker:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/blocker.git#main
- image: skynetlabs/blocker
+ image: skynetlabs/blocker:0.1.1
container_name: blocker
restart: unless-stopped
logging: *default-logging
diff --git a/docker-compose.malware-scanner.yml b/docker-compose.malware-scanner.yml
index fba60f98..ee3b6700 100644
--- a/docker-compose.malware-scanner.yml
+++ b/docker-compose.malware-scanner.yml
@@ -28,7 +28,7 @@ services:
malware-scanner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/malware-scanner.git#main
- image: skynetlabs/malware-scanner
+ image: skynetlabs/malware-scanner:0.1.0
container_name: malware-scanner
restart: unless-stopped
logging: *default-logging
diff --git a/docker-compose.mongodb.yml b/docker-compose.mongodb.yml
index 610d5308..e8eb9aca 100644
--- a/docker-compose.mongodb.yml
+++ b/docker-compose.mongodb.yml
@@ -15,7 +15,7 @@ services:
mongo:
image: mongo:4.4.1
- command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet}
+ command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet} --setParameter ShardingTaskExecutorPoolMinSize=10
container_name: mongo
restart: unless-stopped
logging: *default-logging
diff --git a/docker-compose.yml b/docker-compose.yml
index 2879b8b6..6450a10f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -54,9 +54,11 @@ services:
- ./docker/data/certbot:/etc/letsencrypt
nginx:
- build:
- context: ./docker/nginx
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./docker/nginx/Dockerfile
+ image: skynetlabs/nginx
container_name: nginx
restart: unless-stopped
logging: *default-logging
@@ -69,6 +71,10 @@ services:
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
- ./docker/data/certbot:/etc/letsencrypt
+ - ./docker/nginx/libs:/etc/nginx/libs
+ - ./docker/nginx/conf.d:/etc/nginx/conf.d
+ - ./docker/nginx/conf.d.templates:/etc/nginx/templates
+ - ./docker/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
networks:
shared:
ipv4_address: 10.10.10.30
@@ -82,9 +88,11 @@ services:
- website
website:
- build:
- context: ./packages/website
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./packages/website/Dockerfile
+ image: skynetlabs/website
container_name: website
restart: unless-stopped
logging: *default-logging
@@ -118,9 +126,11 @@ services:
- 12037
handshake-api:
- build:
- context: ./packages/handshake-api
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./packages/handshake-api/Dockerfile
+ image: skynetlabs/handshake-api
container_name: handshake-api
restart: unless-stopped
logging: *default-logging
@@ -140,9 +150,11 @@ services:
- handshake
dnslink-api:
- build:
- context: ./packages/dnslink-api
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./packages/dnslink-api/Dockerfile
+ image: skynetlabs/dnslink-api
container_name: dnslink-api
restart: unless-stopped
logging: *default-logging
@@ -153,9 +165,11 @@ services:
- 3100
health-check:
- build:
- context: ./packages/health-check
- dockerfile: Dockerfile
+ # uncomment "build" and comment out "image" to build from sources
+ # build:
+ # context: https://github.com/SkynetLabs/skynet-webportal.git#master
+ # dockerfile: ./packages/health-check/Dockerfile
+ image: skynetlabs/health-check
container_name: health-check
restart: unless-stopped
logging: *default-logging
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index 2093872c..f35c0aff 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -1,24 +1,21 @@
FROM openresty/openresty:1.19.9.1-focal
-RUN luarocks install lua-resty-http && \
- luarocks install hasher && \
- openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
- -subj '/CN=local-certificate' \
- -keyout /etc/ssl/local-certificate.key \
- -out /etc/ssl/local-certificate.crt
+WORKDIR /
-COPY mo ./
-COPY libs /etc/nginx/libs
-COPY conf.d /etc/nginx/conf.d
-COPY conf.d.templates /etc/nginx/conf.d.templates
-COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
+RUN apt-get update && apt-get --no-install-recommends -y install bc=1.07.1-2build1 && \
+ apt-get clean && rm -rf /var/lib/apt/lists/* && \
+ luarocks install lua-resty-http && \
+ luarocks install hasher
-CMD [ "bash", "-c", \
- "./mo < /etc/nginx/conf.d.templates/server.account.conf > /etc/nginx/conf.d/server.account.conf ; \
- ./mo < /etc/nginx/conf.d.templates/server.api.conf > /etc/nginx/conf.d/server.api.conf; \
- ./mo < /etc/nginx/conf.d.templates/server.dnslink.conf > /etc/nginx/conf.d/server.dnslink.conf; \
- ./mo < /etc/nginx/conf.d.templates/server.hns.conf > /etc/nginx/conf.d/server.hns.conf; \
- ./mo < /etc/nginx/conf.d.templates/server.skylink.conf > /etc/nginx/conf.d/server.skylink.conf ; \
- while :; do sleep 6h & wait ${!}; /usr/local/openresty/bin/openresty -s reload; done & \
- /usr/local/openresty/bin/openresty '-g daemon off;'" \
- ]
+# reload nginx every 6 hours (for reloading certificates)
+ENV NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS 6
+
+# copy entrypoint and entrypoint scripts
+COPY docker/nginx/docker-entrypoint.sh /
+COPY docker/nginx/docker-entrypoint.d /docker-entrypoint.d
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+
+STOPSIGNAL SIGQUIT
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/docker/nginx/conf.d.templates/server.account.conf b/docker/nginx/conf.d.templates/server.account.conf
deleted file mode 100644
index af3b7c4d..00000000
--- a/docker/nginx/conf.d.templates/server.account.conf
+++ /dev/null
@@ -1,45 +0,0 @@
-{{#ACCOUNTS_ENABLED}}
- {{#PORTAL_DOMAIN}}
- server {
- server_name account.{{PORTAL_DOMAIN}}; # example: account.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
- }
-
- server {
- server_name account.{{PORTAL_DOMAIN}}; # example: account.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain {
- -- fall back to portal domain if server domain is not defined
- if "{{SERVER_DOMAIN}}" == "" then
- return "{{PORTAL_DOMAIN}}"
- end
- return "{{SERVER_DOMAIN}}"
- }
-
- include /etc/nginx/conf.d/server/server.account;
- }
- {{/PORTAL_DOMAIN}}
-
- {{#SERVER_DOMAIN}}
- server {
- server_name account.{{SERVER_DOMAIN}}; # example: account.eu-ger-1.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
- }
-
- server {
- server_name account.{{SERVER_DOMAIN}}; # example: account.eu-ger-1.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
-
- include /etc/nginx/conf.d/server/server.account;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
- }
- {{/SERVER_DOMAIN}}
-{{/ACCOUNTS_ENABLED}}
diff --git a/docker/nginx/conf.d.templates/server.account.conf.template b/docker/nginx/conf.d.templates/server.account.conf.template
new file mode 100644
index 00000000..8507c407
--- /dev/null
+++ b/docker/nginx/conf.d.templates/server.account.conf.template
@@ -0,0 +1,44 @@
+server {
+ server_name account.${PORTAL_DOMAIN}; # example: account.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+}
+
+server {
+ server_name account.${PORTAL_DOMAIN}; # example: account.siasky.net
+
+ set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
+ set_by_lua_block $skynet_server_domain {
+ -- fall back to portal domain if server domain is not defined
+ if "${SERVER_DOMAIN}" == "" then
+ return "${PORTAL_DOMAIN}"
+ end
+ return "${SERVER_DOMAIN}"
+ }
+
+ include /etc/nginx/conf.d/server/server.account;
+}
+
+server {
+ server_name account.${SERVER_DOMAIN}; # example: account.eu-ger-1.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
+
+server {
+ server_name account.${SERVER_DOMAIN}; # example: account.eu-ger-1.siasky.net
+
+ set_by_lua_block $skynet_portal_domain {
+ -- when accessing portal directly through server domain, portal domain should be set to server domain
+ -- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
+ -- portal address and it should be server domain when accessing specific server by its domain address
+ return "${SERVER_DOMAIN}"
+ }
+ set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
+
+ include /etc/nginx/conf.d/server/server.account;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
diff --git a/docker/nginx/conf.d.templates/server.api.conf b/docker/nginx/conf.d.templates/server.api.conf
deleted file mode 100644
index 591212ba..00000000
--- a/docker/nginx/conf.d.templates/server.api.conf
+++ /dev/null
@@ -1,43 +0,0 @@
-{{#PORTAL_DOMAIN}}
-server {
- server_name {{PORTAL_DOMAIN}}; # example: siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-}
-
-server {
- server_name {{PORTAL_DOMAIN}}; # example: siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain {
- -- fall back to portal domain if server domain is not defined
- if "{{SERVER_DOMAIN}}" == "" then
- return "{{PORTAL_DOMAIN}}"
- end
- return "{{SERVER_DOMAIN}}"
- }
-
- include /etc/nginx/conf.d/server/server.api;
-}
-{{/PORTAL_DOMAIN}}
-
-{{#SERVER_DOMAIN}}
-server {
- server_name {{SERVER_DOMAIN}}; # example: eu-ger-1.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-
-server {
- server_name {{SERVER_DOMAIN}}; # example: eu-ger-1.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
-
- include /etc/nginx/conf.d/server/server.api;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-{{/SERVER_DOMAIN}}
diff --git a/docker/nginx/conf.d.templates/server.api.conf.template b/docker/nginx/conf.d.templates/server.api.conf.template
new file mode 100644
index 00000000..5f742127
--- /dev/null
+++ b/docker/nginx/conf.d.templates/server.api.conf.template
@@ -0,0 +1,44 @@
+server {
+ server_name ${PORTAL_DOMAIN}; # example: siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+}
+
+server {
+ server_name ${PORTAL_DOMAIN}; # example: siasky.net
+
+ set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
+ set_by_lua_block $skynet_server_domain {
+ -- fall back to portal domain if server domain is not defined
+ if "${SERVER_DOMAIN}" == "" then
+ return "${PORTAL_DOMAIN}"
+ end
+ return "${SERVER_DOMAIN}"
+ }
+
+ include /etc/nginx/conf.d/server/server.api;
+}
+
+server {
+ server_name ${SERVER_DOMAIN}; # example: eu-ger-1.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
+
+server {
+ server_name ${SERVER_DOMAIN}; # example: eu-ger-1.siasky.net
+
+ set_by_lua_block $skynet_portal_domain {
+ -- when accessing portal directly through server domain, portal domain should be set to server domain
+ -- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
+ -- portal address and it should be server domain when accessing specific server by its domain address
+ return "${SERVER_DOMAIN}"
+ }
+ set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
+
+ include /etc/nginx/conf.d/server/server.api;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
diff --git a/docker/nginx/conf.d.templates/server.dnslink.conf b/docker/nginx/conf.d.templates/server.dnslink.conf.template
similarity index 71%
rename from docker/nginx/conf.d.templates/server.dnslink.conf
rename to docker/nginx/conf.d.templates/server.dnslink.conf.template
index d42ee245..95c623b6 100644
--- a/docker/nginx/conf.d.templates/server.dnslink.conf
+++ b/docker/nginx/conf.d.templates/server.dnslink.conf.template
@@ -12,13 +12,13 @@ server {
ssl_certificate /etc/ssl/local-certificate.crt;
ssl_certificate_key /etc/ssl/local-certificate.key;
- set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
+ set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
set_by_lua_block $skynet_server_domain {
-- fall back to portal domain if server domain is not defined
- if "{{SERVER_DOMAIN}}" == "" then
- return "{{PORTAL_DOMAIN}}"
+ if "${SERVER_DOMAIN}" == "" then
+ return "${PORTAL_DOMAIN}"
end
- return "{{SERVER_DOMAIN}}"
+ return "${SERVER_DOMAIN}"
}
include /etc/nginx/conf.d/server/server.dnslink;
diff --git a/docker/nginx/conf.d.templates/server.hns.conf b/docker/nginx/conf.d.templates/server.hns.conf
deleted file mode 100644
index 0e4f21f3..00000000
--- a/docker/nginx/conf.d.templates/server.hns.conf
+++ /dev/null
@@ -1,45 +0,0 @@
-{{#PORTAL_DOMAIN}}
-server {
- server_name *.hns.{{PORTAL_DOMAIN}}; # example: *.hns.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-}
-
-server {
- server_name *.hns.{{PORTAL_DOMAIN}}; # example: *.hns.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain {
- -- fall back to portal domain if server domain is not defined
- if "{{SERVER_DOMAIN}}" == "" then
- return "{{PORTAL_DOMAIN}}"
- end
- return "{{SERVER_DOMAIN}}"
- }
-
- proxy_set_header Host {{PORTAL_DOMAIN}};
- include /etc/nginx/conf.d/server/server.hns;
-}
-{{/PORTAL_DOMAIN}}
-
-{{#SERVER_DOMAIN}}
-server {
- server_name *.hns.{{SERVER_DOMAIN}}; # example: *.hns.eu-ger-1.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-
-server {
- server_name *.hns.{{SERVER_DOMAIN}}; # example: *.hns.eu-ger-1.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
-
- proxy_set_header Host {{SERVER_DOMAIN}};
- include /etc/nginx/conf.d/server/server.hns;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-{{/SERVER_DOMAIN}}
diff --git a/docker/nginx/conf.d.templates/server.hns.conf.template b/docker/nginx/conf.d.templates/server.hns.conf.template
new file mode 100644
index 00000000..f7bb43fb
--- /dev/null
+++ b/docker/nginx/conf.d.templates/server.hns.conf.template
@@ -0,0 +1,46 @@
+server {
+ server_name *.hns.${PORTAL_DOMAIN}; # example: *.hns.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+}
+
+server {
+ server_name *.hns.${PORTAL_DOMAIN}; # example: *.hns.siasky.net
+
+ set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
+ set_by_lua_block $skynet_server_domain {
+ -- fall back to portal domain if server domain is not defined
+ if "${SERVER_DOMAIN}" == "" then
+ return "${PORTAL_DOMAIN}"
+ end
+ return "${SERVER_DOMAIN}"
+ }
+
+ proxy_set_header Host ${PORTAL_DOMAIN};
+ include /etc/nginx/conf.d/server/server.hns;
+}
+
+server {
+ server_name *.hns.${SERVER_DOMAIN}; # example: *.hns.eu-ger-1.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
+
+server {
+ server_name *.hns.${SERVER_DOMAIN}; # example: *.hns.eu-ger-1.siasky.net
+
+ set_by_lua_block $skynet_portal_domain {
+ -- when accessing portal directly through server domain, portal domain should be set to server domain
+ -- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
+ -- portal address and it should be server domain when accessing specific server by its domain address
+ return "${SERVER_DOMAIN}"
+ }
+ set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
+
+ proxy_set_header Host ${SERVER_DOMAIN};
+ include /etc/nginx/conf.d/server/server.hns;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
diff --git a/docker/nginx/conf.d.templates/server.skylink.conf b/docker/nginx/conf.d.templates/server.skylink.conf
deleted file mode 100644
index a97e240c..00000000
--- a/docker/nginx/conf.d.templates/server.skylink.conf
+++ /dev/null
@@ -1,43 +0,0 @@
-{{#PORTAL_DOMAIN}}
-server {
- server_name *.{{PORTAL_DOMAIN}}; # example: *.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-}
-
-server {
- server_name *.{{PORTAL_DOMAIN}}; # example: *.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{PORTAL_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain {
- -- fall back to portal domain if server domain is not defined
- if "{{SERVER_DOMAIN}}" == "" then
- return "{{PORTAL_DOMAIN}}"
- end
- return "{{SERVER_DOMAIN}}"
- }
-
- include /etc/nginx/conf.d/server/server.skylink;
-}
-{{/PORTAL_DOMAIN}}
-
-{{#SERVER_DOMAIN}}
-server {
- server_name *.{{SERVER_DOMAIN}}; # example: *.eu-ger-1.siasky.net
-
- include /etc/nginx/conf.d/server/server.http;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-
-server {
- server_name *.{{SERVER_DOMAIN}}; # example: *.eu-ger-1.siasky.net
-
- set_by_lua_block $skynet_portal_domain { return "{{SERVER_DOMAIN}}" }
- set_by_lua_block $skynet_server_domain { return "{{SERVER_DOMAIN}}" }
-
- include /etc/nginx/conf.d/server/server.skylink;
-
- set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
-}
-{{/SERVER_DOMAIN}}
diff --git a/docker/nginx/conf.d.templates/server.skylink.conf.template b/docker/nginx/conf.d.templates/server.skylink.conf.template
new file mode 100644
index 00000000..0d337abb
--- /dev/null
+++ b/docker/nginx/conf.d.templates/server.skylink.conf.template
@@ -0,0 +1,44 @@
+server {
+ server_name *.${PORTAL_DOMAIN}; # example: *.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+}
+
+server {
+ server_name *.${PORTAL_DOMAIN}; # example: *.siasky.net
+
+ set_by_lua_block $skynet_portal_domain { return "${PORTAL_DOMAIN}" }
+ set_by_lua_block $skynet_server_domain {
+ -- fall back to portal domain if server domain is not defined
+ if "${SERVER_DOMAIN}" == "" then
+ return "${PORTAL_DOMAIN}"
+ end
+ return "${SERVER_DOMAIN}"
+ }
+
+ include /etc/nginx/conf.d/server/server.skylink;
+}
+
+server {
+ server_name *.${SERVER_DOMAIN}; # example: *.eu-ger-1.siasky.net
+
+ include /etc/nginx/conf.d/server/server.http;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
+
+server {
+ server_name *.${SERVER_DOMAIN}; # example: *.eu-ger-1.siasky.net
+
+ set_by_lua_block $skynet_portal_domain {
+ -- when accessing portal directly through server domain, portal domain should be set to server domain
+ -- motivation: skynet-js uses Skynet-Portal-Api header (that is set to $skynet_portal_domain) to detect current
+ -- portal address and it should be server domain when accessing specific server by its domain address
+ return "${SERVER_DOMAIN}"
+ }
+ set_by_lua_block $skynet_server_domain { return "${SERVER_DOMAIN}" }
+
+ include /etc/nginx/conf.d/server/server.skylink;
+
+ set_by_lua_block $server_alias { return string.match("${SERVER_DOMAIN}", "^([^.]+)") }
+}
diff --git a/docker/nginx/conf.d/include/location-skylink b/docker/nginx/conf.d/include/location-skylink
index 995a6e2d..b214e3a9 100644
--- a/docker/nginx/conf.d/include/location-skylink
+++ b/docker/nginx/conf.d/include/location-skylink
@@ -1,5 +1,4 @@
include /etc/nginx/conf.d/include/cors;
-include /etc/nginx/conf.d/include/track-download;
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
@@ -37,3 +36,18 @@ proxy_read_timeout 600;
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;
+
+log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_modules = require("skynet.modules")
+ local skynet_scanner = require("skynet.scanner")
+ local skynet_tracker = require("skynet.tracker")
+
+ if skynet_modules.is_enabled("a") then
+ skynet_tracker.track_download(ngx.header["Skynet-Skylink"], ngx.status, skynet_account.get_auth_headers(), ngx.var.body_bytes_sent)
+ end
+
+ if skynet_modules.is_enabled("s") then
+ skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
+ end
+}
diff --git a/docker/nginx/conf.d/include/location-skynet-registry b/docker/nginx/conf.d/include/location-skynet-registry
index 33838f70..cd450be9 100644
--- a/docker/nginx/conf.d/include/location-skynet-registry
+++ b/docker/nginx/conf.d/include/location-skynet-registry
@@ -1,6 +1,5 @@
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
-include /etc/nginx/conf.d/include/track-registry;
limit_req zone=registry_access_by_ip burst=600 nodelay;
limit_req zone=registry_access_by_ip_throttled burst=200 nodelay;
@@ -30,3 +29,10 @@ access_by_lua_block {
end
end
}
+
+log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_tracker = require("skynet.tracker")
+
+ skynet_tracker.track_registry(ngx.status, skynet_account.get_auth_headers(), ngx.req.get_method())
+}
diff --git a/docker/nginx/conf.d/include/track-download b/docker/nginx/conf.d/include/track-download
deleted file mode 100644
index 4e12fd41..00000000
--- a/docker/nginx/conf.d/include/track-download
+++ /dev/null
@@ -1,55 +0,0 @@
-log_by_lua_block {
- local skynet_account = require("skynet.account")
-
- -- tracking runs only when request comes from authenticated user
- if skynet_account.is_authenticated() then
- local function track(premature, skylink, status, body_bytes_sent, auth_headers)
- if premature then return end
-
- local httpc = require("resty.http").new()
- local query = table.concat({ "status=" .. status, "bytes=" .. body_bytes_sent }, "&")
-
- -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.70:3000/track/download/" .. skylink .. "?" .. query, {
- method = "POST",
- headers = auth_headers,
- })
-
- if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
- local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
- ngx.log(ngx.ERR, "Failed accounts service request /track/download/" .. skylink .. ": ", error_response)
- end
- end
-
- if ngx.header["Skynet-Skylink"] and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
- local auth_headers = skynet_account.get_auth_headers()
- local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.status, ngx.var.body_bytes_sent, auth_headers)
- if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
- end
- end
-
- -- this block runs only when scanner module is enabled
- if os.getenv("PORTAL_MODULES"):match("s") then
- local function scan(premature, skylink)
- if premature then return end
-
- local httpc = require("resty.http").new()
-
- -- 10.10.10.101 points to malware-scanner service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.101:4000/scan/" .. skylink, {
- method = "POST",
- })
-
- if err or (res and res.status ~= ngx.HTTP_OK) then
- local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
- ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
- end
- end
-
- -- scan all skylinks but make sure to only run if skylink is present (empty if request failed)
- if ngx.header["Skynet-Skylink"] then
- local ok, err = ngx.timer.at(0, scan, ngx.header["Skynet-Skylink"])
- if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
- end
- end
-}
diff --git a/docker/nginx/conf.d/include/track-registry b/docker/nginx/conf.d/include/track-registry
deleted file mode 100644
index 2c840491..00000000
--- a/docker/nginx/conf.d/include/track-registry
+++ /dev/null
@@ -1,33 +0,0 @@
-log_by_lua_block {
- local skynet_account = require("skynet.account")
-
- -- tracking runs only when request comes from authenticated user
- if skynet_account.is_authenticated() then
- local function track(premature, request_method, auth_headers)
- if premature then return end
-
- local httpc = require("resty.http").new()
-
- -- based on request method we assign a registry action string used
- -- in track endpoint namely "read" for GET and "write" for POST
- local registry_action = request_method == "GET" and "read" or "write"
-
- -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.70:3000/track/registry/" .. registry_action, {
- method = "POST",
- headers = auth_headers,
- })
-
- if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
- local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
- ngx.log(ngx.ERR, "Failed accounts service request /track/registry/" .. registry_action .. ": ", error_response)
- end
- end
-
- if ngx.status == ngx.HTTP_OK or ngx.status == ngx.HTTP_NOT_FOUND then
- local auth_headers = skynet_account.get_auth_headers()
- local ok, err = ngx.timer.at(0, track, ngx.req.get_method(), auth_headers)
- if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
- end
- end
-}
diff --git a/docker/nginx/conf.d/include/track-upload b/docker/nginx/conf.d/include/track-upload
deleted file mode 100644
index 36b12b9e..00000000
--- a/docker/nginx/conf.d/include/track-upload
+++ /dev/null
@@ -1,55 +0,0 @@
-log_by_lua_block {
- local skynet_account = require("skynet.account")
-
- -- tracking runs only when request comes from authenticated user
- if skynet_account.is_authenticated() then
- local function track(premature, skylink, auth_headers)
- if premature then return end
-
- local httpc = require("resty.http").new()
-
- -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.70:3000/track/upload/" .. skylink, {
- method = "POST",
- headers = auth_headers,
- })
-
- if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
- local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
- ngx.log(ngx.ERR, "Failed accounts service request /track/upload/" .. skylink .. ": ", error_response)
- end
- end
-
- -- report all skylinks (header empty if request failed) but only if jwt is preset (user is authenticated)
- if ngx.header["Skynet-Skylink"] then
- local auth_headers = skynet_account.get_auth_headers()
- local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], auth_headers)
- if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
- end
- end
-
- -- this block runs only when scanner module is enabled
- if os.getenv("PORTAL_MODULES"):match("s") then
- local function scan(premature, skylink)
- if premature then return end
-
- local httpc = require("resty.http").new()
-
- -- 10.10.10.101 points to malware-scanner service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.101:4000/scan/" .. skylink, {
- method = "POST",
- })
-
- if err or (res and res.status ~= ngx.HTTP_OK) then
- local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
- ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
- end
- end
-
- -- scan all skylinks but make sure to only run if skylink is present (empty if request failed)
- if ngx.header["Skynet-Skylink"] then
- local ok, err = ngx.timer.at(0, scan, ngx.header["Skynet-Skylink"])
- if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
- end
- end
-}
diff --git a/docker/nginx/conf.d/server/server.account b/docker/nginx/conf.d/server/server.account
index 127ba4bf..9d444296 100644
--- a/docker/nginx/conf.d/server/server.account
+++ b/docker/nginx/conf.d/server/server.account
@@ -3,6 +3,11 @@ listen 443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
+# Uncomment to launch new Dashboard under /v2 path
+# location /v2 {
+# proxy_pass http://dashboard-v2:9000;
+# }
+
location / {
proxy_pass http://dashboard:3000;
}
diff --git a/docker/nginx/conf.d/server/server.api b/docker/nginx/conf.d/server/server.api
index f681cca8..48e7a638 100644
--- a/docker/nginx/conf.d/server/server.api
+++ b/docker/nginx/conf.d/server/server.api
@@ -206,7 +206,6 @@ location /skynet/registry/subscription {
location /skynet/skyfile {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
- include /etc/nginx/conf.d/include/track-upload;
include /etc/nginx/conf.d/include/generate-siapath;
include /etc/nginx/conf.d/include/portal-access-check;
@@ -228,12 +227,31 @@ location /skynet/skyfile {
# proxy this call to siad endpoint (make sure the ip is correct)
proxy_pass http://sia:9980/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
+
+ log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_modules = require("skynet.modules")
+ local skynet_scanner = require("skynet.scanner")
+ local skynet_tracker = require("skynet.tracker")
+
+ if skynet_modules.is_enabled("a") then
+ skynet_tracker.track_upload(
+ ngx.header["Skynet-Skylink"],
+ ngx.status,
+ skynet_account.get_auth_headers(),
+ ngx.var.remote_addr
+ )
+ end
+
+ if skynet_modules.is_enabled("s") then
+ skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
+ end
+ }
}
# endpoint implementing resumable file uploads open protocol https://tus.io
location /skynet/tus {
include /etc/nginx/conf.d/include/cors-headers; # include cors headers but do not overwrite OPTIONS response
- include /etc/nginx/conf.d/include/track-upload;
limit_req zone=uploads_by_ip burst=10 nodelay;
limit_req zone=uploads_by_ip_throttled;
@@ -241,8 +259,8 @@ location /skynet/tus {
limit_conn upload_conn 5;
limit_conn upload_conn_rl 1;
- # TUS chunks size is 40M + leaving 10M of breathing room
- client_max_body_size 50M;
+ # Do not limit body size in nginx, skyd will reject early on too large upload
+ client_max_body_size 0;
# Those timeouts need to be elevated since skyd can stall reading
# data for a while when overloaded which would terminate connection
@@ -294,12 +312,31 @@ location /skynet/tus {
end
end
}
+
+ log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_modules = require("skynet.modules")
+ local skynet_scanner = require("skynet.scanner")
+ local skynet_tracker = require("skynet.tracker")
+
+ if skynet_modules.is_enabled("a") then
+ skynet_tracker.track_upload(
+ ngx.header["Skynet-Skylink"],
+ ngx.status,
+ skynet_account.get_auth_headers(),
+ ngx.var.remote_addr
+ )
+ end
+
+ if skynet_modules.is_enabled("s") then
+ skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
+ end
+ }
}
location /skynet/pin {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
- include /etc/nginx/conf.d/include/track-upload;
include /etc/nginx/conf.d/include/generate-siapath;
include /etc/nginx/conf.d/include/portal-access-check;
@@ -311,6 +348,26 @@ location /skynet/pin {
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980$uri?siapath=$dir1/$dir2/$dir3&$args;
+
+ log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_modules = require("skynet.modules")
+ local skynet_scanner = require("skynet.scanner")
+ local skynet_tracker = require("skynet.tracker")
+
+ if skynet_modules.is_enabled("a") then
+ skynet_tracker.track_upload(
+ ngx.header["Skynet-Skylink"],
+ ngx.status,
+ skynet_account.get_auth_headers(),
+ ngx.var.remote_addr
+ )
+ end
+
+ if skynet_modules.is_enabled("s") then
+ skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
+ end
+ }
}
location /skynet/metadata {
@@ -357,7 +414,6 @@ location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
location /skynet/trustless/basesector {
include /etc/nginx/conf.d/include/cors;
- include /etc/nginx/conf.d/include/track-download;
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
@@ -391,6 +447,21 @@ location /skynet/trustless/basesector {
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980;
+
+ log_by_lua_block {
+ local skynet_account = require("skynet.account")
+ local skynet_modules = require("skynet.modules")
+ local skynet_scanner = require("skynet.scanner")
+ local skynet_tracker = require("skynet.tracker")
+
+ if skynet_modules.is_enabled("a") then
+ skynet_tracker.track_download(ngx.header["Skynet-Skylink"], ngx.status, skynet_account.get_auth_headers(), ngx.var.body_bytes_sent)
+ end
+
+ if skynet_modules.is_enabled("s") then
+ skynet_scanner.scan_skylink(ngx.header["Skynet-Skylink"])
+ end
+ }
}
location /__internal/do/not/use/accounts {
diff --git a/docker/nginx/conf.d/server/server.dnslink b/docker/nginx/conf.d/server/server.dnslink
index cf385a1d..139196ef 100644
--- a/docker/nginx/conf.d/server/server.dnslink
+++ b/docker/nginx/conf.d/server/server.dnslink
@@ -5,6 +5,7 @@ location / {
set $path $uri;
rewrite_by_lua_block {
+ local cjson = require("cjson")
local cache = ngx.shared.dnslink
local cache_value = cache:get(ngx.var.host)
@@ -28,13 +29,23 @@ location / {
ngx.exit(ngx.status)
end
else
- ngx.var.skylink = res.body
+ local resolved = cjson.decode(res.body)
+
+ ngx.var.skylink = resolved.skylink
+ if resolved.sponsor then
+ ngx.req.set_header("Skynet-Api-Key", resolved.sponsor)
+ end
local cache_ttl = 300 -- 5 minutes cache expire time
- cache:set(ngx.var.host, ngx.var.skylink, cache_ttl)
+ cache:set(ngx.var.host, res.body, cache_ttl)
end
else
- ngx.var.skylink = cache_value
+ local resolved = cjson.decode(cache_value)
+
+ ngx.var.skylink = resolved.skylink
+ if resolved.sponsor then
+ ngx.req.set_header("Skynet-Api-Key", resolved.sponsor)
+ end
end
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
diff --git a/docker/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh b/docker/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh
new file mode 100755
index 00000000..be9c5f8e
--- /dev/null
+++ b/docker/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+
+# https://github.com/nginxinc/docker-nginx/blob/master/entrypoint/20-envsubst-on-templates.sh
+# https://github.com/nginxinc/docker-nginx/blob/master/LICENSE
+
+# Copyright (C) 2011-2016 Nginx, Inc.
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+set -e
+
+ME=$(basename $0)
+
+auto_envsubst() {
+ local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}"
+ local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}"
+ local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}"
+
+ local template defined_envs relative_path output_path subdir
+ defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
+ [ -d "$template_dir" ] || return 0
+ if [ ! -w "$output_dir" ]; then
+ echo >&3 "$ME: ERROR: $template_dir exists, but $output_dir is not writable"
+ return 0
+ fi
+ find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do
+ relative_path="${template#$template_dir/}"
+ output_path="$output_dir/${relative_path%$suffix}"
+ subdir=$(dirname "$relative_path")
+ # create a subdirectory where the template file exists
+ mkdir -p "$output_dir/$subdir"
+ echo >&3 "$ME: Running envsubst on $template to $output_path"
+ envsubst "$defined_envs" < "$template" > "$output_path"
+ done
+}
+
+auto_envsubst
+
+exit 0
diff --git a/docker/nginx/docker-entrypoint.d/40-reload-every-x-hours.sh b/docker/nginx/docker-entrypoint.d/40-reload-every-x-hours.sh
new file mode 100755
index 00000000..cdd7d17e
--- /dev/null
+++ b/docker/nginx/docker-entrypoint.d/40-reload-every-x-hours.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# source: https://github.com/nginxinc/docker-nginx/pull/509
+
+set -e
+
+ME=$(basename $0)
+
+[ "${NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS:-}" ] || exit 0
+if [ $(echo "$NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS > 0" | bc) = 0 ]; then
+ echo >&3 "$ME: Error. Provide integer or floating point number greater that 0. See 'man sleep'."
+ exit 1
+fi
+
+start_background_reload() {
+ echo >&3 "$ME: Reloading Nginx every $NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS hour(s)"
+ while :; do sleep ${NGINX_ENTRYPOINT_RELOAD_EVERY_X_HOURS}h; echo >&3 "$ME: Reloading Nginx ..." && nginx -s reload; done &
+}
+
+start_background_reload
diff --git a/docker/nginx/docker-entrypoint.d/50-generate-local-certificate.sh b/docker/nginx/docker-entrypoint.d/50-generate-local-certificate.sh
new file mode 100755
index 00000000..f5ecada1
--- /dev/null
+++ b/docker/nginx/docker-entrypoint.d/50-generate-local-certificate.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Generate locally signed ssl certificate to be used on routes
+# that do not require certificate issued by trusted CA
+
+set -e
+
+ME=$(basename $0)
+
+generate_local_certificate() {
+ echo >&3 "$ME: Generating locally signed ssl certificate"
+ openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
+ -subj '/CN=local-certificate' \
+ -keyout /etc/ssl/local-certificate.key \
+ -out /etc/ssl/local-certificate.crt
+}
+
+generate_local_certificate
diff --git a/docker/nginx/docker-entrypoint.sh b/docker/nginx/docker-entrypoint.sh
new file mode 100755
index 00000000..d45a6d9a
--- /dev/null
+++ b/docker/nginx/docker-entrypoint.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+# vim:sw=4:ts=4:et
+
+# https://github.com/nginxinc/docker-nginx/blob/master/entrypoint/docker-entrypoint.sh
+# https://github.com/nginxinc/docker-nginx/blob/master/LICENSE
+
+# Copyright (C) 2011-2016 Nginx, Inc.
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+set -e
+
+if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
+ exec 3>&1
+else
+ exec 3>/dev/null
+fi
+
+if [ "$1" = "nginx" -o "$1" = "nginx-debug" ]; then
+ if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then
+ echo >&3 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration"
+
+ echo >&3 "$0: Looking for shell scripts in /docker-entrypoint.d/"
+ find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do
+ case "$f" in
+ *.sh)
+ if [ -x "$f" ]; then
+ echo >&3 "$0: Launching $f";
+ "$f"
+ else
+ # warn on shell scripts without exec bit
+ echo >&3 "$0: Ignoring $f, not executable";
+ fi
+ ;;
+ *) echo >&3 "$0: Ignoring $f";;
+ esac
+ done
+
+ echo >&3 "$0: Configuration complete; ready for start up"
+ else
+ echo >&3 "$0: No files found in /docker-entrypoint.d/, skipping configuration"
+ fi
+fi
+
+exec "$@"
diff --git a/docker/nginx/libs/skynet/account.lua b/docker/nginx/libs/skynet/account.lua
index 6fa2c4d2..709d8130 100644
--- a/docker/nginx/libs/skynet/account.lua
+++ b/docker/nginx/libs/skynet/account.lua
@@ -59,7 +59,9 @@ function _M.exit_access_forbidden(message)
end
function _M.accounts_enabled()
- return os.getenv("PORTAL_MODULES"):match("a") ~= nil
+ local skynet_modules = require("skynet.modules")
+
+ return skynet_modules.is_enabled("a")
end
function _M.get_account_limits()
@@ -74,9 +76,16 @@ function _M.get_account_limits()
if ngx.var.account_limits == "" then
local httpc = require("resty.http").new()
+ local uri = "http://10.10.10.70:3000/user/limits"
+
+ -- include skylink if it is available in the context of request
+ -- todo: this should not rely on skylink variable to be defined
+ if ngx.var.skylink ~= nil and ngx.var.skylink ~= "" then
+ uri = uri .. "/" .. ngx.var.skylink
+ end
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
- local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
+ local res, err = httpc:request_uri(uri .. "?unit=byte", {
headers = auth_headers,
})
diff --git a/docker/nginx/libs/skynet/modules.lua b/docker/nginx/libs/skynet/modules.lua
new file mode 100644
index 00000000..607e6d8e
--- /dev/null
+++ b/docker/nginx/libs/skynet/modules.lua
@@ -0,0 +1,23 @@
+local _M = {}
+
+local utils = require("utils")
+
+function _M.is_enabled(module_abbr)
+ if type(module_abbr) ~= "string" or module_abbr:len() ~= 1 then
+ error("Module abbreviation '" .. tostring(module_abbr) .. "' should be exactly one character long string")
+ end
+
+ local enabled_modules = utils.getenv("PORTAL_MODULES")
+
+ if not enabled_modules then
+ return false
+ end
+
+ return enabled_modules:find(module_abbr) ~= nil
+end
+
+function _M.is_disabled(module_abbr)
+ return not _M.is_enabled(module_abbr)
+end
+
+return _M
diff --git a/docker/nginx/libs/skynet/modules.spec.lua b/docker/nginx/libs/skynet/modules.spec.lua
new file mode 100644
index 00000000..0eaaf081
--- /dev/null
+++ b/docker/nginx/libs/skynet/modules.spec.lua
@@ -0,0 +1,95 @@
+-- luacheck: ignore os
+
+local skynet_modules = require("skynet.modules")
+
+describe("is_enabled", function()
+ before_each(function()
+ stub(os, "getenv")
+ end)
+
+ after_each(function()
+ os.getenv:revert()
+ end)
+
+ it("should return false if PORTAL_MODULES are not defined", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns(nil)
+
+ assert.is_false(skynet_modules.is_enabled("a"))
+ end)
+
+ it("should return false if PORTAL_MODULES are empty", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("")
+
+ assert.is_false(skynet_modules.is_enabled("a"))
+ end)
+
+ it("should return false if module is not enabled", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("qwerty")
+
+ assert.is_false(skynet_modules.is_enabled("a"))
+ end)
+
+ it("should return true if module is enabled", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("asdfg")
+
+ assert.is_true(skynet_modules.is_enabled("a"))
+ end)
+
+ it("should throw an error for empty module", function()
+ assert.has_error(function()
+ skynet_modules.is_enabled()
+ end, "Module abbreviation 'nil' should be exactly one character long string")
+ end)
+
+ it("should throw an error for too long module", function()
+ assert.has_error(function()
+ skynet_modules.is_enabled("gandalf")
+ end, "Module abbreviation 'gandalf' should be exactly one character long string")
+ end)
+end)
+
+describe("is_disabled", function()
+ before_each(function()
+ stub(os, "getenv")
+ end)
+
+ after_each(function()
+ os.getenv:revert()
+ end)
+
+ it("should return true if PORTAL_MODULES are not defined", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns(nil)
+
+ assert.is_true(skynet_modules.is_disabled("a"))
+ end)
+
+ it("should return true if PORTAL_MODULES are empty", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("")
+
+ assert.is_true(skynet_modules.is_disabled("a"))
+ end)
+
+ it("should return true if module is not enabled", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("qwerty")
+
+ assert.is_true(skynet_modules.is_disabled("a"))
+ end)
+
+ it("should return false if module is enabled", function()
+ os.getenv.on_call_with("PORTAL_MODULES").returns("asdfg")
+
+ assert.is_false(skynet_modules.is_disabled("a"))
+ end)
+
+ it("should throw an error for empty module", function()
+ assert.has_error(function()
+ skynet_modules.is_disabled()
+ end, "Module abbreviation 'nil' should be exactly one character long string")
+ end)
+
+ it("should throw an error for too long module", function()
+ assert.has_error(function()
+ skynet_modules.is_disabled("gandalf")
+ end, "Module abbreviation 'gandalf' should be exactly one character long string")
+ end)
+end)
diff --git a/docker/nginx/libs/skynet/scanner.lua b/docker/nginx/libs/skynet/scanner.lua
new file mode 100644
index 00000000..445f1ae9
--- /dev/null
+++ b/docker/nginx/libs/skynet/scanner.lua
@@ -0,0 +1,26 @@
+local _M = {}
+
+function _M.scan_skylink_timer(premature, skylink)
+ if premature then return end
+
+ local httpc = require("resty.http").new()
+
+ -- 10.10.10.101 points to malware-scanner service (alias not available when using resty-http)
+ local res, err = httpc:request_uri("http://10.10.10.101:4000/scan/" .. skylink, {
+ method = "POST",
+ })
+
+ if err or (res and res.status ~= ngx.HTTP_OK) then
+ local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
+ ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", error_response)
+ end
+end
+
+function _M.scan_skylink(skylink)
+ if not skylink then return end
+
+ local ok, err = ngx.timer.at(0, _M.scan_skylink_timer, skylink)
+ if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
+end
+
+return _M
diff --git a/docker/nginx/libs/skynet/scanner.spec.lua b/docker/nginx/libs/skynet/scanner.spec.lua
new file mode 100644
index 00000000..533ef44c
--- /dev/null
+++ b/docker/nginx/libs/skynet/scanner.spec.lua
@@ -0,0 +1,119 @@
+-- luacheck: ignore ngx
+
+local skynet_scanner = require("skynet.scanner")
+local skylink = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
+
+describe("scan_skylink", function()
+ before_each(function()
+ stub(ngx.timer, "at")
+ end)
+
+ after_each(function()
+ ngx.timer.at:revert()
+ end)
+
+ it("should schedule a timer when skylink is provided", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_scanner.scan_skylink(skylink)
+
+ assert.stub(ngx.timer.at).was_called_with(0, skynet_scanner.scan_skylink_timer, skylink)
+ end)
+
+ it("should log an error if timer failed to create", function()
+ stub(ngx, "log")
+
+ ngx.timer.at.invokes(function() return false, "such a failure" end)
+
+ skynet_scanner.scan_skylink(skylink)
+
+ assert.stub(ngx.timer.at).was_called_with(0, skynet_scanner.scan_skylink_timer, skylink)
+ assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
+
+ ngx.log:revert()
+ end)
+
+ it("should not schedule a timer if skylink is not provided", function()
+ skynet_scanner.scan_skylink()
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+end)
+
+describe("scan_skylink_timer", function()
+ before_each(function()
+ stub(ngx, "log")
+ end)
+
+ after_each(function()
+ local resty_http = require("resty.http")
+
+ ngx.log:revert()
+ resty_http.new:revert()
+ end)
+
+ it("should exit early on premature", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new()
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_scanner.scan_skylink_timer(true, skylink)
+
+ assert.stub(request_uri).was_not_called()
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should make a post request with skylink to scanner service", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 200 } -- return 200 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_scanner.scan_skylink_timer(false, skylink)
+
+ local uri = "http://10.10.10.101:4000/scan/" .. skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should log message on scanner request failure with response code", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 404, body = "baz" } -- return 404 failure
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_scanner.scan_skylink_timer(false, skylink)
+
+ local uri = "http://10.10.10.101:4000/scan/" .. skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed malware-scanner request /scan/AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA: ",
+ "[HTTP 404] baz"
+ )
+ end)
+
+ it("should log message on scanner request error", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return nil, "foo != bar" -- return error
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_scanner.scan_skylink_timer(false, skylink)
+
+ local uri = "http://10.10.10.101:4000/scan/" .. skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST" })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed malware-scanner request /scan/AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA: ",
+ "foo != bar"
+ )
+ end)
+end)
diff --git a/docker/nginx/libs/skynet/tracker.lua b/docker/nginx/libs/skynet/tracker.lua
new file mode 100644
index 00000000..37413215
--- /dev/null
+++ b/docker/nginx/libs/skynet/tracker.lua
@@ -0,0 +1,99 @@
+local _M = {}
+
+local utils = require("utils")
+
+function _M.track_download_timer(premature, skylink, status, auth_headers, body_bytes_sent)
+ if premature then return end
+
+ local httpc = require("resty.http").new()
+ local query = table.concat({ "status=" .. status, "bytes=" .. body_bytes_sent }, "&")
+
+ -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
+ local res, err = httpc:request_uri("http://10.10.10.70:3000/track/download/" .. skylink .. "?" .. query, {
+ method = "POST",
+ headers = auth_headers,
+ })
+
+ if err or (res and res.status ~= 204) then
+ local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
+ ngx.log(ngx.ERR, "Failed accounts service request /track/download/" .. skylink .. ": ", error_response)
+ end
+end
+
+function _M.track_download(skylink, status_code, auth_headers, body_bytes_sent)
+ local has_auth_headers = not utils.is_table_empty(auth_headers)
+ local status_success = status_code >= 200 and status_code <= 299
+
+ if skylink and status_success and has_auth_headers then
+ local ok, err = ngx.timer.at(0, _M.track_download_timer, skylink, status_code, auth_headers, body_bytes_sent)
+ if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
+ end
+end
+
+function _M.track_upload_timer(premature, skylink, auth_headers, uploader_ip)
+ if premature then return end
+
+ local httpc = require("resty.http").new()
+
+ -- set correct content type header and include auth headers
+ local headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded",
+ }
+ for key, value in pairs(auth_headers) do
+ headers[key] = value
+ end
+
+ -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
+ local res, err = httpc:request_uri("http://10.10.10.70:3000/track/upload/" .. skylink, {
+ method = "POST",
+ headers = headers,
+ body = "ip=" .. uploader_ip,
+ })
+
+ if err or (res and res.status ~= 204) then
+ local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
+ ngx.log(ngx.ERR, "Failed accounts service request /track/upload/" .. skylink .. ": ", error_response)
+ end
+end
+
+function _M.track_upload(skylink, status_code, auth_headers, uploader_ip)
+ local status_success = status_code >= 200 and status_code <= 299
+
+ if skylink and status_success then
+ local ok, err = ngx.timer.at(0, _M.track_upload_timer, skylink, auth_headers, uploader_ip)
+ if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
+ end
+end
+
+function _M.track_registry_timer(premature, auth_headers, request_method)
+ if premature then return end
+
+ local httpc = require("resty.http").new()
+
+ -- based on request method we assign a registry action string used
+ -- in track endpoint namely "read" for GET and "write" for POST
+ local registry_action = request_method == "GET" and "read" or "write"
+
+ -- 10.10.10.70 points to accounts service (alias not available when using resty-http)
+ local res, err = httpc:request_uri("http://10.10.10.70:3000/track/registry/" .. registry_action, {
+ method = "POST",
+ headers = auth_headers,
+ })
+
+ if err or (res and res.status ~= 204) then
+ local error_response = err or ("[HTTP " .. res.status .. "] " .. res.body)
+ ngx.log(ngx.ERR, "Failed accounts service request /track/registry/" .. registry_action .. ": ", error_response)
+ end
+end
+
+function _M.track_registry(status_code, auth_headers, request_method)
+ local has_auth_headers = not utils.is_table_empty(auth_headers)
+ local tracked_status = status_code == 200 or status_code == 404
+
+ if tracked_status and has_auth_headers then
+ local ok, err = ngx.timer.at(0, _M.track_registry_timer, auth_headers, request_method)
+ if not ok then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
+ end
+end
+
+return _M
diff --git a/docker/nginx/libs/skynet/tracker.spec.lua b/docker/nginx/libs/skynet/tracker.spec.lua
new file mode 100644
index 00000000..98d587d8
--- /dev/null
+++ b/docker/nginx/libs/skynet/tracker.spec.lua
@@ -0,0 +1,584 @@
+-- luacheck: ignore ngx
+
+local skynet_tracker = require("skynet.tracker")
+
+local valid_skylink = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
+local valid_status_code = 200
+local valid_auth_headers = { ["Skynet-Api-Key"] = "foo" }
+local valid_ip = "12.34.56.78"
+
+describe("track_download", function()
+ local valid_body_bytes_sent = 12345
+
+ before_each(function()
+ stub(ngx.timer, "at")
+ end)
+
+ after_each(function()
+ ngx.timer.at:revert()
+ end)
+
+ it("should schedule a timer when conditions are met", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_download(valid_skylink, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_download_timer,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+ end)
+
+ it("should not schedule a timer if skylink is empty", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_download(nil, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should not schedule a timer if status code is not in 2XX range", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ -- couple of example of 4XX and 5XX codes
+ skynet_tracker.track_download(valid_skylink, 401, valid_auth_headers, valid_body_bytes_sent)
+ skynet_tracker.track_download(valid_skylink, 403, valid_auth_headers, valid_body_bytes_sent)
+ skynet_tracker.track_download(valid_skylink, 490, valid_auth_headers, valid_body_bytes_sent)
+ skynet_tracker.track_download(valid_skylink, 500, valid_auth_headers, valid_body_bytes_sent)
+ skynet_tracker.track_download(valid_skylink, 502, valid_auth_headers, valid_body_bytes_sent)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should not schedule a timer if auth headers are empty", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_download(valid_skylink, valid_status_code, {}, valid_body_bytes_sent)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should log an error if timer failed to create", function()
+ stub(ngx, "log")
+ ngx.timer.at.invokes(function() return false, "such a failure" end)
+
+ skynet_tracker.track_download(valid_skylink, valid_status_code, valid_auth_headers, valid_body_bytes_sent)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_download_timer,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+ assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
+
+ ngx.log:revert()
+ end)
+
+ describe("track_download_timer", function()
+ before_each(function()
+ stub(ngx, "log")
+ end)
+
+ after_each(function()
+ local resty_http = require("resty.http")
+
+ ngx.log:revert()
+ resty_http.new:revert()
+ end)
+
+ it("should exit early on premature", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 200 } -- return 200 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_download_timer(
+ true,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+
+ assert.stub(request_uri).was_not_called()
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should make a post request to tracker service", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 204 } -- return 204 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_download_timer(
+ false,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+
+ local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
+ local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should log message on tracker request failure with response code", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 404, body = "baz" } -- return 404 failure
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_download_timer(
+ false,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+
+ local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
+ local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/download/" .. valid_skylink .. ": ",
+ "[HTTP 404] baz"
+ )
+ end)
+
+ it("should log message on tracker request error", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return nil, "foo != bar" -- return error
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_download_timer(
+ false,
+ valid_skylink,
+ valid_status_code,
+ valid_auth_headers,
+ valid_body_bytes_sent
+ )
+
+ local uri_params = "status=" .. valid_status_code .. "&bytes=" .. valid_body_bytes_sent
+ local uri = "http://10.10.10.70:3000/track/download/" .. valid_skylink .. "?" .. uri_params
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/download/" .. valid_skylink .. ": ",
+ "foo != bar"
+ )
+ end)
+ end)
+end)
+
+describe("track_upload", function()
+ before_each(function()
+ stub(ngx.timer, "at")
+ end)
+
+ after_each(function()
+ ngx.timer.at:revert()
+ end)
+
+ it("should schedule a timer when conditions are met", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_upload(valid_skylink, valid_status_code, valid_auth_headers, valid_ip)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_upload_timer,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+ end)
+
+ it("should not schedule a timer if skylink is empty", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_upload(nil, valid_status_code, valid_auth_headers, valid_ip)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should not schedule a timer if status code is not in 2XX range", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ -- couple of example of 4XX and 5XX codes
+ skynet_tracker.track_upload(valid_skylink, 401, valid_auth_headers, valid_ip)
+ skynet_tracker.track_upload(valid_skylink, 403, valid_auth_headers, valid_ip)
+ skynet_tracker.track_upload(valid_skylink, 490, valid_auth_headers, valid_ip)
+ skynet_tracker.track_upload(valid_skylink, 500, valid_auth_headers, valid_ip)
+ skynet_tracker.track_upload(valid_skylink, 502, valid_auth_headers, valid_ip)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should schedule a timer if auth headers are empty", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_upload(valid_skylink, valid_status_code, {}, valid_ip)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_upload_timer,
+ valid_skylink,
+ {},
+ valid_ip
+ )
+ end)
+
+ it("should log an error if timer failed to create", function()
+ stub(ngx, "log")
+ ngx.timer.at.invokes(function() return false, "such a failure" end)
+
+ skynet_tracker.track_upload(valid_skylink, valid_status_code, valid_auth_headers, valid_ip)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_upload_timer,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+ assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
+
+ ngx.log:revert()
+ end)
+
+ describe("track_upload_timer", function()
+ before_each(function()
+ stub(ngx, "log")
+ end)
+
+ after_each(function()
+ local resty_http = require("resty.http")
+
+ ngx.log:revert()
+ resty_http.new:revert()
+ end)
+
+ it("should exit early on premature", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 200 } -- return 200 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_upload_timer(
+ true,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+
+ assert.stub(request_uri).was_not_called()
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should make a post request to tracker service", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 204 } -- return 204 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_upload_timer(
+ false,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+
+ local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, {
+ method = "POST",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded",
+ ["Skynet-Api-Key"] = "foo",
+ },
+ body = "ip=" .. valid_ip
+ })
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should log message on tracker request failure with response code", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 404, body = "baz" } -- return 404 failure
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_upload_timer(
+ false,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+
+ local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, {
+ method = "POST",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded",
+ ["Skynet-Api-Key"] = "foo",
+ },
+ body = "ip=" .. valid_ip
+ })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/upload/" .. valid_skylink .. ": ",
+ "[HTTP 404] baz"
+ )
+ end)
+
+ it("should log message on tracker request error", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return nil, "foo != bar" -- return error
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_upload_timer(
+ false,
+ valid_skylink,
+ valid_auth_headers,
+ valid_ip
+ )
+
+ local uri = "http://10.10.10.70:3000/track/upload/" .. valid_skylink
+ assert.stub(request_uri).was_called_with(httpc, uri, {
+ method = "POST",
+ headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded",
+ ["Skynet-Api-Key"] = "foo",
+ },
+ body = "ip=" .. valid_ip
+ })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/upload/" .. valid_skylink .. ": ",
+ "foo != bar"
+ )
+ end)
+ end)
+end)
+
+describe("track_registry", function()
+ local status_code_ok = 200
+ local status_code_not_found = 404
+ local request_method_write = "POST"
+ local request_method_read = "GET"
+
+ before_each(function()
+ stub(ngx.timer, "at")
+ end)
+
+ after_each(function()
+ ngx.timer.at:revert()
+ end)
+
+ it("should schedule a timer when status code was 200", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_registry(status_code_ok, valid_auth_headers, request_method_write)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_registry_timer,
+ valid_auth_headers,
+ request_method_write
+ )
+ end)
+
+ it("should schedule a timer when status code was 404", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_registry(status_code_not_found, valid_auth_headers, request_method_write)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_registry_timer,
+ valid_auth_headers,
+ request_method_write
+ )
+ end)
+
+ it("should not schedule a timer if status code is not in 200 or 404", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ -- couple of example of invalid 2XX, 4XX and 5XX codes
+ skynet_tracker.track_registry(204, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(206, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(401, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(403, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(490, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(500, valid_auth_headers, request_method_write)
+ skynet_tracker.track_registry(502, valid_auth_headers, request_method_write)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should not schedule a timer if auth headers are empty", function()
+ ngx.timer.at.invokes(function() return true, nil end)
+
+ skynet_tracker.track_registry(status_code_ok, {}, request_method_write)
+
+ assert.stub(ngx.timer.at).was_not_called()
+ end)
+
+ it("should log an error if timer failed to create", function()
+ stub(ngx, "log")
+ ngx.timer.at.invokes(function() return false, "such a failure" end)
+
+ skynet_tracker.track_registry(status_code_ok, valid_auth_headers, request_method_write)
+
+ assert.stub(ngx.timer.at).was_called_with(
+ 0,
+ skynet_tracker.track_registry_timer,
+ valid_auth_headers,
+ request_method_write
+ )
+ assert.stub(ngx.log).was_called_with(ngx.ERR, "Failed to create timer: ", "such a failure")
+
+ ngx.log:revert()
+ end)
+
+ describe("track_registry_timer", function()
+ before_each(function()
+ stub(ngx, "log")
+ end)
+
+ after_each(function()
+ local resty_http = require("resty.http")
+
+ ngx.log:revert()
+ resty_http.new:revert()
+ end)
+
+ it("should exit early on premature", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 200 } -- return 200 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_registry_timer(
+ true,
+ valid_auth_headers,
+ request_method_write
+ )
+
+ assert.stub(request_uri).was_not_called()
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should make a post request to registry write tracker service", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 204 } -- return 204 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_registry_timer(
+ false,
+ valid_auth_headers,
+ request_method_write
+ )
+
+ local uri = "http://10.10.10.70:3000/track/registry/write"
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should make a post request to registry read tracker service", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 204 } -- return 204 success
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_registry_timer(
+ false,
+ valid_auth_headers,
+ request_method_read
+ )
+
+ local uri = "http://10.10.10.70:3000/track/registry/read"
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_not_called()
+ end)
+
+ it("should log message on tracker request failure with response code", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return { status = 404, body = "baz" } -- return 404 failure
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_registry_timer(
+ false,
+ valid_auth_headers,
+ request_method_write
+ )
+
+ local uri = "http://10.10.10.70:3000/track/registry/write"
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/registry/write: ",
+ "[HTTP 404] baz"
+ )
+ end)
+
+ it("should log message on tracker request error", function()
+ local resty_http = require("resty.http")
+ local request_uri = spy.new(function()
+ return nil, "foo != bar" -- return error
+ end)
+ local httpc = mock({ request_uri = request_uri })
+ stub(resty_http, "new").returns(httpc)
+
+ skynet_tracker.track_registry_timer(
+ false,
+ valid_auth_headers,
+ request_method_write
+ )
+
+ local uri = "http://10.10.10.70:3000/track/registry/write"
+ assert.stub(request_uri).was_called_with(httpc, uri, { method = "POST", headers = valid_auth_headers })
+ assert.stub(ngx.log).was_called_with(
+ ngx.ERR,
+ "Failed accounts service request /track/registry/write: ",
+ "foo != bar"
+ )
+ end)
+ end)
+end)
\ No newline at end of file
diff --git a/docker/nginx/libs/skynet/utils.lua b/docker/nginx/libs/skynet/utils.lua
index adee23b2..05755f7b 100644
--- a/docker/nginx/libs/skynet/utils.lua
+++ b/docker/nginx/libs/skynet/utils.lua
@@ -1,13 +1,20 @@
local _M = {}
+local ngx_base64 = require("ngx.base64")
+local utils = require("utils")
+
function _M.authorization_header()
-- read api password from env variable
- local apipassword = os.getenv("SIA_API_PASSWORD")
+ local apipassword = utils.getenv("SIA_API_PASSWORD")
-- if api password is not available as env variable, read it from disk
- if apipassword == nil or apipassword == "" then
+ if not apipassword then
-- open apipassword file for reading (b flag is required for some reason)
-- (file /etc/.sia/apipassword has to be mounted from the host system)
local apipassword_file = io.open("/data/sia/apipassword", "rb")
+ -- make sure to throw a meaningful error if apipassword file does not exist
+ if not apipassword_file then
+ error("Error reading /data/sia/apipassword file")
+ end
-- read apipassword file contents and trim newline (important)
apipassword = apipassword_file:read("*all"):gsub("%s+", "")
-- make sure to close file after reading the password
@@ -15,7 +22,7 @@ function _M.authorization_header()
end
-- encode the user:password authorization string
-- (in our case user is empty so it is just :password)
- local content = require("ngx.base64").encode_base64url(":" .. apipassword)
+ local content = ngx_base64.encode_base64url(":" .. apipassword)
-- set authorization header with proper base64 encoded string
return "Basic " .. content
end
diff --git a/docker/nginx/libs/skynet/utils.spec.lua b/docker/nginx/libs/skynet/utils.spec.lua
new file mode 100644
index 00000000..171be4fa
--- /dev/null
+++ b/docker/nginx/libs/skynet/utils.spec.lua
@@ -0,0 +1,65 @@
+-- luacheck: ignore io
+
+local utils = require('utils')
+local skynet_utils = require('skynet.utils')
+
+describe("authorization_header", function()
+ local apipassword = "ddd0c1430fbf2708"
+ local expected_header = "Basic OmRkZDBjMTQzMGZiZjI3MDg"
+
+ it("reads SIA_API_PASSWORD from env variable and returns a header", function()
+ -- stub getenv on SIA_API_PASSWORD
+ stub(utils, "getenv")
+ utils.getenv.on_call_with("SIA_API_PASSWORD").returns(apipassword)
+
+ local header = skynet_utils.authorization_header()
+
+ assert.is_equal(header, expected_header)
+
+ -- revert stub to original function
+ utils.getenv:revert()
+ end)
+
+ it("uses /data/sia/apipassword file if SIA_API_PASSWORD env var is missing", function()
+ -- stub /data/sia/apipassword file
+ stub(io, "open")
+ io.open.on_call_with("/data/sia/apipassword", "rb").returns(mock({
+ read = spy.new(function() return apipassword end),
+ close = spy.new()
+ }))
+
+ local header = skynet_utils.authorization_header()
+
+ assert.is_equal(header, expected_header)
+
+ -- revert stub to original function
+ io.open:revert()
+ end)
+
+ it("should choose env variable over file if both are available", function()
+ -- stub getenv on SIA_API_PASSWORD
+ stub(utils, "getenv")
+ utils.getenv.on_call_with("SIA_API_PASSWORD").returns(apipassword)
+
+ -- stub /data/sia/apipassword file
+ stub(io, "open")
+ io.open.on_call_with("/data/sia/apipassword", "rb").returns(mock({
+ read = spy.new(function() return "foooooooooooooo" end),
+ close = spy.new()
+ }))
+
+ local header = skynet_utils.authorization_header()
+
+ assert.is_equal(header, "Basic OmRkZDBjMTQzMGZiZjI3MDg")
+
+ -- revert stubs to original function
+ utils.getenv:revert()
+ io.open:revert()
+ end)
+
+ it("should error out if neither env variable is available nor file exists", function()
+ assert.has_error(function()
+ skynet_utils.authorization_header()
+ end, "Error reading /data/sia/apipassword file")
+ end)
+end)
diff --git a/docker/nginx/libs/utils.lua b/docker/nginx/libs/utils.lua
index 4330c94c..8b77d802 100644
--- a/docker/nginx/libs/utils.lua
+++ b/docker/nginx/libs/utils.lua
@@ -42,4 +42,42 @@ function _M.extract_cookie_value(cookie_string, name_matcher)
return string.sub(cookie, value_start)
end
+-- utility function that builds on os.getenv to get environment variable value
+-- * will always return nil for both unset and empty env vars
+-- * parse "boolean": "true" and "1" as true, "false" and "0" as false, throws for others
+-- * parse "integer": any numerical string gets converted, otherwise returns nil
+function _M.getenv(name, parse)
+ local value = os.getenv(name)
+
+ -- treat empty string value as nil to simplify comparisons
+ if value == nil or value == "" then
+ return nil
+ end
+
+ -- do not parse the value
+ if parse == nil then
+ return value
+ end
+
+ -- try to parse as boolean
+ if parse == "boolean" then
+ if string.lower(value) == "true" or value == "1" then
+ return true
+ end
+
+ if string.lower(value) == "false" or value == "0" then
+ return false
+ end
+
+ error("utils.getenv: Parsing value '" .. tostring(value) .. "' to boolean is not supported")
+ end
+
+ -- try to parse as integer
+ if parse == "integer" then
+ return tonumber(value)
+ end
+
+ error("utils.getenv: Parsing to '" .. parse .. "' is not supported")
+end
+
return _M
diff --git a/docker/nginx/libs/utils.spec.lua b/docker/nginx/libs/utils.spec.lua
index c853c8cd..71ef086c 100644
--- a/docker/nginx/libs/utils.spec.lua
+++ b/docker/nginx/libs/utils.spec.lua
@@ -1,3 +1,5 @@
+-- luacheck: ignore os
+
local utils = require('utils')
describe("is_table_empty", function()
@@ -77,3 +79,137 @@ describe("extract_cookie_value", function()
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
end)
end)
+
+describe("getenv", function()
+ before_each(function()
+ stub(os, "getenv")
+ end)
+
+ after_each(function()
+ os.getenv:revert()
+ end)
+
+ it("should return nil for not existing env var", function()
+ os.getenv.on_call_with("foo").returns(nil)
+
+ assert.is_nil(utils.getenv("foo"))
+ end)
+
+ it("should return nil for env var that is an empty string", function()
+ os.getenv.on_call_with("foo").returns("")
+
+ assert.is_nil(utils.getenv("foo"))
+ end)
+
+ it("should return the value as is when it is non empty string", function()
+ os.getenv.on_call_with("foo").returns("bar")
+
+ assert.is_equal(utils.getenv("foo"), "bar")
+ end)
+
+ describe("parse", function()
+ it("should throw on not supported parser", function()
+ os.getenv.on_call_with("foo").returns("test")
+
+ assert.has_error(function()
+ utils.getenv("foo", "starwars")
+ end, "utils.getenv: Parsing to 'starwars' is not supported")
+ end)
+
+ describe("as boolean", function()
+ it("should return nil for not existing env var", function()
+ os.getenv.on_call_with("foo").returns(nil)
+
+ assert.is_nil(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should return nil for env var that is an empty string", function()
+ os.getenv.on_call_with("foo").returns("")
+
+ assert.is_nil(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse 'true' string as true", function()
+ os.getenv.on_call_with("foo").returns("true")
+
+ assert.is_true(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse 'True' string as true", function()
+ os.getenv.on_call_with("foo").returns("True")
+
+ assert.is_true(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse '1' string as true", function()
+ os.getenv.on_call_with("foo").returns("1")
+
+ assert.is_true(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse 'false' string as false", function()
+ os.getenv.on_call_with("foo").returns("false")
+
+ assert.is_false(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse 'False' string as false", function()
+ os.getenv.on_call_with("foo").returns("False")
+
+ assert.is_false(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should parse '0' string as false", function()
+ os.getenv.on_call_with("foo").returns("0")
+
+ assert.is_false(utils.getenv("foo", "boolean"))
+ end)
+
+ it("should throw an error for not supported string", function()
+ os.getenv.on_call_with("foo").returns("mandalorian")
+
+ assert.has_error(function()
+ utils.getenv("foo", "boolean")
+ end, "utils.getenv: Parsing value 'mandalorian' to boolean is not supported")
+ end)
+ end)
+
+ describe("as integer", function()
+ it("should return nil for not existing env var", function()
+ os.getenv.on_call_with("foo").returns(nil)
+
+ assert.is_nil(utils.getenv("foo", "integer"))
+ end)
+
+ it("should return nil for env var that is an empty string", function()
+ os.getenv.on_call_with("foo").returns("")
+
+ assert.is_nil(utils.getenv("foo", "integer"))
+ end)
+
+ it("should parse '0' string as 0", function()
+ os.getenv.on_call_with("foo").returns("0")
+
+ assert.equals(utils.getenv("foo", "integer"), 0)
+ end)
+
+ it("should parse '1' string as 1", function()
+ os.getenv.on_call_with("foo").returns("1")
+
+ assert.equals(utils.getenv("foo", "integer"), 1)
+ end)
+
+ it("should parse '-1' string as -1", function()
+ os.getenv.on_call_with("foo").returns("-1")
+
+ assert.equals(utils.getenv("foo", "integer"), -1)
+ end)
+
+ it("should return nil for non numerical string", function()
+ os.getenv.on_call_with("foo").returns("test")
+
+ assert.is_nil(utils.getenv("foo", "integer"))
+ end)
+ end)
+ end)
+end)
diff --git a/docker/nginx/mo b/docker/nginx/mo
deleted file mode 100755
index ba8e48d1..00000000
--- a/docker/nginx/mo
+++ /dev/null
@@ -1,1106 +0,0 @@
-#!/usr/bin/env bash
-#
-#/ Mo is a mustache template rendering software written in bash. It inserts
-#/ environment variables into templates.
-#/
-#/ Simply put, mo will change {{VARIABLE}} into the value of that
-#/ environment variable. You can use {{#VARIABLE}}content{{/VARIABLE}} to
-#/ conditionally display content or iterate over the values of an array.
-#/
-#/ Learn more about mustache templates at https://mustache.github.io/
-#/
-#/ Simple usage:
-#/
-#/ mo [OPTIONS] filenames...
-#/
-#/ Options:
-#/
-#/ -u, --fail-not-set
-#/ Fail upon expansion of an unset variable.
-#/ -x, --fail-on-function
-#/ Fail when a function returns a non-zero status code.
-#/ -e, --false
-#/ Treat the string "false" as empty for conditionals.
-#/ -h, --help
-#/ This message.
-#/ -s=FILE, --source=FILE
-#/ Load FILE into the environment before processing templates.
-#/ Can be used multiple times.
-#
-# Mo is under a MIT style licence with an additional non-advertising clause.
-# See LICENSE.md for the full text.
-#
-# This is open source! Please feel free to contribute.
-#
-# https://github.com/tests-always-included/mo
-
-
-# Public: Template parser function. Writes templates to stdout.
-#
-# $0 - Name of the mo file, used for getting the help message.
-# $@ - Filenames to parse.
-#
-# Options:
-#
-# --allow-function-arguments
-#
-# Permit functions in templates to be called with additional arguments. This
-# puts template data directly in to the path of an eval statement. Use with
-# caution. Not listed in the help because it only makes sense when mo is
-# sourced.
-#
-# -u, --fail-not-set
-#
-# Fail upon expansion of an unset variable. Default behavior is to silently
-# ignore and expand into empty string.
-#
-# -x, --fail-on-function
-#
-# Fail when a function used by a template returns an error status code.
-# Alternately, ou may set the MO_FAIL_ON_FUNCTION environment variable to a
-# non-empty value to enable this behavior.
-#
-# -e, --false
-#
-# Treat "false" as an empty value. You may set the MO_FALSE_IS_EMPTY
-# environment variable instead to a non-empty value to enable this behavior.
-#
-# -h, --help
-#
-# Display a help message.
-#
-# -s=FILE, --source=FILE
-#
-# Source a file into the environment before processing template files.
-# This can be used multiple times.
-#
-# --
-#
-# Used to indicate the end of options. You may optionally use this when
-# filenames may start with two hyphens.
-#
-# Mo uses the following environment variables:
-#
-# MO_ALLOW_FUNCTION_ARGUMENTS - When set to a non-empty value, this allows
-# functions referenced in templates to receive additional
-# options and arguments. This puts the content from the
-# template directly into an eval statement. Use with extreme
-# care.
-# MO_FUNCTION_ARGS - Arguments passed to the function
-# MO_FAIL_ON_FUNCTION - If a function returns a non-zero status code, abort
-# with an error.
-# MO_FAIL_ON_UNSET - When set to a non-empty value, expansion of an unset env
-# variable will be aborted with an error.
-# MO_FALSE_IS_EMPTY - When set to a non-empty value, the string "false" will be
-# treated as an empty value for the purposes of conditionals.
-# MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a
-# help message.
-#
-# Returns nothing.
-mo() (
- # This function executes in a subshell so IFS is reset.
- # Namespace this variable so we don't conflict with desired values.
- local moContent f2source files doubleHyphens
-
- IFS=$' \n\t'
- files=()
- doubleHyphens=false
-
- if [[ $# -gt 0 ]]; then
- for arg in "$@"; do
- if $doubleHyphens; then
- #: After we encounter two hyphens together, all the rest
- #: of the arguments are files.
- files=("${files[@]}" "$arg")
- else
- case "$arg" in
- -h|--h|--he|--hel|--help|-\?)
- moUsage "$0"
- exit 0
- ;;
-
- --allow-function-arguments)
- # shellcheck disable=SC2030
- MO_ALLOW_FUNCTION_ARGUMENTS=true
- ;;
-
- -u | --fail-not-set)
- # shellcheck disable=SC2030
- MO_FAIL_ON_UNSET=true
- ;;
-
- -x | --fail-on-function)
- # shellcheck disable=SC2030
- MO_FAIL_ON_FUNCTION=true
- ;;
-
- -e | --false)
- # shellcheck disable=SC2030
- MO_FALSE_IS_EMPTY=true
- ;;
-
- -s=* | --source=*)
- if [[ "$arg" == --source=* ]]; then
- f2source="${arg#--source=}"
- else
- f2source="${arg#-s=}"
- fi
-
- if [[ -f "$f2source" ]]; then
- # shellcheck disable=SC1090
- . "$f2source"
- else
- echo "No such file: $f2source" >&2
- exit 1
- fi
- ;;
-
- --)
- #: Set a flag indicating we've encountered double hyphens
- doubleHyphens=true
- ;;
-
- *)
- #: Every arg that is not a flag or a option should be a file
- files=(${files[@]+"${files[@]}"} "$arg")
- ;;
- esac
- fi
- done
- fi
-
- moGetContent moContent "${files[@]}" || return 1
- moParse "$moContent" "" true
-)
-
-
-# Internal: Call a function.
-#
-# $1 - Variable for output
-# $2 - Function to call
-# $3 - Content to pass
-# $4 - Additional arguments as a single string
-#
-# This can be dangerous, especially if you are using tags like
-# {{someFunction ; rm -rf / }}
-#
-# Returns nothing.
-moCallFunction() {
- local moArgs moContent moFunctionArgs moFunctionResult
-
- moArgs=()
- moTrimWhitespace moFunctionArgs "$4"
-
- # shellcheck disable=SC2031
- if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then
- # Intentionally bad behavior
- # shellcheck disable=SC2206
- moArgs=($4)
- fi
-
- moContent=$(echo -n "$3" | MO_FUNCTION_ARGS="$moFunctionArgs" eval "$2" "${moArgs[@]}") || {
- moFunctionResult=$?
- # shellcheck disable=SC2031
- if [[ -n "${MO_FAIL_ON_FUNCTION-}" && "$moFunctionResult" != 0 ]]; then
- echo "Function '$2' with args (${moArgs[*]+"${moArgs[@]}"}) failed with status code $moFunctionResult"
- exit "$moFunctionResult"
- fi
- }
-
- # shellcheck disable=SC2031
- local "$1" && moIndirect "$1" "$moContent"
-}
-
-
-# Internal: Scan content until the right end tag is found. Creates an array
-# with the following members:
-#
-# [0] = Content before end tag
-# [1] = End tag (complete tag)
-# [2] = Content after end tag
-#
-# Everything using this function uses the "standalone tags" logic.
-#
-# $1 - Name of variable for the array
-# $2 - Content
-# $3 - Name of end tag
-# $4 - If -z, do standalone tag processing before finishing
-#
-# Returns nothing.
-moFindEndTag() {
- local content remaining scanned standaloneBytes tag
-
- #: Find open tags
- scanned=""
- moSplit content "$2" '{{' '}}'
-
- while [[ "${#content[@]}" -gt 1 ]]; do
- moTrimWhitespace tag "${content[1]}"
-
- #: Restore content[1] before we start using it
- content[1]='{{'"${content[1]}"'}}'
-
- case $tag in
- '#'* | '^'*)
- #: Start another block
- scanned="${scanned}${content[0]}${content[1]}"
- moTrimWhitespace tag "${tag:1}"
- moFindEndTag content "${content[2]}" "$tag" "loop"
- scanned="${scanned}${content[0]}${content[1]}"
- remaining=${content[2]}
- ;;
-
- '/'*)
- #: End a block - could be ours
- moTrimWhitespace tag "${tag:1}"
- scanned="$scanned${content[0]}"
-
- if [[ "$tag" == "$3" ]]; then
- #: Found our end tag
- if [[ -z "${4-}" ]] && moIsStandalone standaloneBytes "$scanned" "${content[2]}" true; then
- #: This is also a standalone tag - clean up whitespace
- #: and move those whitespace bytes to the "tag" element
- # shellcheck disable=SC2206
- standaloneBytes=( $standaloneBytes )
- content[1]="${scanned:${standaloneBytes[0]}}${content[1]}${content[2]:0:${standaloneBytes[1]}}"
- scanned="${scanned:0:${standaloneBytes[0]}}"
- content[2]="${content[2]:${standaloneBytes[1]}}"
- fi
-
- local "$1" && moIndirectArray "$1" "$scanned" "${content[1]}" "${content[2]}"
- return 0
- fi
-
- scanned="$scanned${content[1]}"
- remaining=${content[2]}
- ;;
-
- *)
- #: Ignore all other tags
- scanned="${scanned}${content[0]}${content[1]}"
- remaining=${content[2]}
- ;;
- esac
-
- moSplit content "$remaining" '{{' '}}'
- done
-
- #: Did not find our closing tag
- scanned="$scanned${content[0]}"
- local "$1" && moIndirectArray "$1" "${scanned}" "" ""
-}
-
-
-# Internal: Find the first index of a substring. If not found, sets the
-# index to -1.
-#
-# $1 - Destination variable for the index
-# $2 - Haystack
-# $3 - Needle
-#
-# Returns nothing.
-moFindString() {
- local pos string
-
- string=${2%%$3*}
- [[ "$string" == "$2" ]] && pos=-1 || pos=${#string}
- local "$1" && moIndirect "$1" "$pos"
-}
-
-
-# Internal: Generate a dotted name based on current context and target name.
-#
-# $1 - Target variable to store results
-# $2 - Context name
-# $3 - Desired variable name
-#
-# Returns nothing.
-moFullTagName() {
- if [[ -z "${2-}" ]] || [[ "$2" == *.* ]]; then
- local "$1" && moIndirect "$1" "$3"
- else
- local "$1" && moIndirect "$1" "${2}.${3}"
- fi
-}
-
-
-# Internal: Fetches the content to parse into a variable. Can be a list of
-# partials for files or the content from stdin.
-#
-# $1 - Variable name to assign this content back as
-# $2-@ - File names (optional)
-#
-# Returns nothing.
-moGetContent() {
- local moContent moFilename moTarget
-
- moTarget=$1
- shift
- if [[ "${#@}" -gt 0 ]]; then
- moContent=""
-
- for moFilename in "$@"; do
- #: This is so relative paths work from inside template files
- moContent="$moContent"'{{>'"$moFilename"'}}'
- done
- else
- moLoadFile moContent || return 1
- fi
-
- local "$moTarget" && moIndirect "$moTarget" "$moContent"
-}
-
-
-# Internal: Indent a string, placing the indent at the beginning of every
-# line that has any content.
-#
-# $1 - Name of destination variable to get an array of lines
-# $2 - The indent string
-# $3 - The string to reindent
-#
-# Returns nothing.
-moIndentLines() {
- local content fragment len posN posR result trimmed
-
- result=""
-
- #: Remove the period from the end of the string.
- len=$((${#3} - 1))
- content=${3:0:$len}
-
- if [[ -z "${2-}" ]]; then
- local "$1" && moIndirect "$1" "$content"
-
- return 0
- fi
-
- moFindString posN "$content" $'\n'
- moFindString posR "$content" $'\r'
-
- while [[ "$posN" -gt -1 ]] || [[ "$posR" -gt -1 ]]; do
- if [[ "$posN" -gt -1 ]]; then
- fragment="${content:0:$posN + 1}"
- content=${content:$posN + 1}
- else
- fragment="${content:0:$posR + 1}"
- content=${content:$posR + 1}
- fi
-
- moTrimChars trimmed "$fragment" false true " " $'\t' $'\n' $'\r'
-
- if [[ -n "$trimmed" ]]; then
- fragment="$2$fragment"
- fi
-
- result="$result$fragment"
-
- moFindString posN "$content" $'\n'
- moFindString posR "$content" $'\r'
-
- # If the content ends in a newline, do not indent.
- if [[ "$posN" -eq ${#content} ]]; then
- # Special clause for \r\n
- if [[ "$posR" -eq "$((posN - 1))" ]]; then
- posR=-1
- fi
-
- posN=-1
- fi
-
- if [[ "$posR" -eq ${#content} ]]; then
- posR=-1
- fi
- done
-
- moTrimChars trimmed "$content" false true " " $'\t'
-
- if [[ -n "$trimmed" ]]; then
- content="$2$content"
- fi
-
- result="$result$content"
-
- local "$1" && moIndirect "$1" "$result"
-}
-
-
-# Internal: Send a variable up to the parent of the caller of this function.
-#
-# $1 - Variable name
-# $2 - Value
-#
-# Examples
-#
-# callFunc () {
-# local "$1" && moIndirect "$1" "the value"
-# }
-# callFunc dest
-# echo "$dest" # writes "the value"
-#
-# Returns nothing.
-moIndirect() {
- unset -v "$1"
- printf -v "$1" '%s' "$2"
-}
-
-
-# Internal: Send an array as a variable up to caller of a function
-#
-# $1 - Variable name
-# $2-@ - Array elements
-#
-# Examples
-#
-# callFunc () {
-# local myArray=(one two three)
-# local "$1" && moIndirectArray "$1" "${myArray[@]}"
-# }
-# callFunc dest
-# echo "${dest[@]}" # writes "one two three"
-#
-# Returns nothing.
-moIndirectArray() {
- unset -v "$1"
-
- # IFS must be set to a string containing space or unset in order for
- # the array slicing to work regardless of the current IFS setting on
- # bash 3. This is detailed further at
- # https://github.com/fidian/gg-core/pull/7
- eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")"
-}
-
-
-# Internal: Determine if a given environment variable exists and if it is
-# an array.
-#
-# $1 - Name of environment variable
-#
-# Be extremely careful. Even if strict mode is enabled, it is not honored
-# in newer versions of Bash. Any errors that crop up here will not be
-# caught automatically.
-#
-# Examples
-#
-# var=(abc)
-# if moIsArray var; then
-# echo "This is an array"
-# echo "Make sure you don't accidentally use \$var"
-# fi
-#
-# Returns 0 if the name is not empty, 1 otherwise.
-moIsArray() {
- # Namespace this variable so we don't conflict with what we're testing.
- local moTestResult
-
- moTestResult=$(declare -p "$1" 2>/dev/null) || return 1
- [[ "${moTestResult:0:10}" == "declare -a" ]] && return 0
- [[ "${moTestResult:0:10}" == "declare -A" ]] && return 0
-
- return 1
-}
-
-
-# Internal: Determine if the given name is a defined function.
-#
-# $1 - Function name to check
-#
-# Be extremely careful. Even if strict mode is enabled, it is not honored
-# in newer versions of Bash. Any errors that crop up here will not be
-# caught automatically.
-#
-# Examples
-#
-# moo () {
-# echo "This is a function"
-# }
-# if moIsFunction moo; then
-# echo "moo is a defined function"
-# fi
-#
-# Returns 0 if the name is a function, 1 otherwise.
-moIsFunction() {
- local functionList functionName
-
- functionList=$(declare -F)
- # shellcheck disable=SC2206
- functionList=( ${functionList//declare -f /} )
-
- for functionName in "${functionList[@]}"; do
- if [[ "$functionName" == "$1" ]]; then
- return 0
- fi
- done
-
- return 1
-}
-
-
-# Internal: Determine if the tag is a standalone tag based on whitespace
-# before and after the tag.
-#
-# Passes back a string containing two numbers in the format "BEFORE AFTER"
-# like "27 10". It indicates the number of bytes remaining in the "before"
-# string (27) and the number of bytes to trim in the "after" string (10).
-# Useful for string manipulation:
-#
-# $1 - Variable to set for passing data back
-# $2 - Content before the tag
-# $3 - Content after the tag
-# $4 - true/false: is this the beginning of the content?
-#
-# Examples
-#
-# moIsStandalone RESULT "$before" "$after" false || return 0
-# RESULT_ARRAY=( $RESULT )
-# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}"
-#
-# Returns nothing.
-moIsStandalone() {
- local afterTrimmed beforeTrimmed char
-
- moTrimChars beforeTrimmed "$2" false true " " $'\t'
- moTrimChars afterTrimmed "$3" true false " " $'\t'
- char=$((${#beforeTrimmed} - 1))
- char=${beforeTrimmed:$char}
-
- # If the content before didn't end in a newline
- if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then
- # and there was content or this didn't start the file
- if [[ -n "$char" ]] || ! $4; then
- # then this is not a standalone tag.
- return 1
- fi
- fi
-
- char=${afterTrimmed:0:1}
-
- # If the content after doesn't start with a newline and it is something
- if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then
- # then this is not a standalone tag.
- return 2
- fi
-
- if [[ "$char" == $'\r' ]] && [[ "${afterTrimmed:1:1}" == $'\n' ]]; then
- char="$char"$'\n'
- fi
-
- local "$1" && moIndirect "$1" "$((${#beforeTrimmed})) $((${#3} + ${#char} - ${#afterTrimmed}))"
-}
-
-
-# Internal: Join / implode an array
-#
-# $1 - Variable name to receive the joined content
-# $2 - Joiner
-# $3-$* - Elements to join
-#
-# Returns nothing.
-moJoin() {
- local joiner part result target
-
- target=$1
- joiner=$2
- result=$3
- shift 3
-
- for part in "$@"; do
- result="$result$joiner$part"
- done
-
- local "$target" && moIndirect "$target" "$result"
-}
-
-
-# Internal: Read a file into a variable.
-#
-# $1 - Variable name to receive the file's content
-# $2 - Filename to load - if empty, defaults to /dev/stdin
-#
-# Returns nothing.
-moLoadFile() {
- local content len
-
- # The subshell removes any trailing newlines. We forcibly add
- # a dot to the content to preserve all newlines.
- # As a future optimization, it would be worth considering removing
- # cat and replacing this with a read loop.
-
- content=$(cat -- "${2:-/dev/stdin}" && echo '.') || return 1
- len=$((${#content} - 1))
- content=${content:0:$len} # Remove last dot
-
- local "$1" && moIndirect "$1" "$content"
-}
-
-
-# Internal: Process a chunk of content some number of times. Writes output
-# to stdout.
-#
-# $1 - Content to parse repeatedly
-# $2 - Tag prefix (context name)
-# $3-@ - Names to insert into the parsed content
-#
-# Returns nothing.
-moLoop() {
- local content context contextBase
-
- content=$1
- contextBase=$2
- shift 2
-
- while [[ "${#@}" -gt 0 ]]; do
- moFullTagName context "$contextBase" "$1"
- moParse "$content" "$context" false
- shift
- done
-}
-
-
-# Internal: Parse a block of text, writing the result to stdout.
-#
-# $1 - Block of text to change
-# $2 - Current name (the variable NAME for what {{.}} means)
-# $3 - true when no content before this, false otherwise
-#
-# Returns nothing.
-moParse() {
- # Keep naming variables mo* here to not overwrite needed variables
- # used in the string replacements
- local moArgs moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag
-
- moCurrent=$2
- moIsBeginning=$3
-
- # Find open tags
- moSplit moContent "$1" '{{' '}}'
-
- while [[ "${#moContent[@]}" -gt 1 ]]; do
- moTrimWhitespace moTag "${moContent[1]}"
- moNextIsBeginning=false
-
- case $moTag in
- '#'*)
- # Loop, if/then, or pass content through function
- # Sets context
- moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
- moTrimWhitespace moTag "${moTag:1}"
-
- # Split arguments from the tag name. Arguments are passed to
- # functions.
- moArgs=$moTag
- moTag=${moTag%% *}
- moTag=${moTag%%$'\t'*}
- moArgs=${moArgs:${#moTag}}
- moFindEndTag moBlock "$moContent" "$moTag"
- moFullTagName moTag "$moCurrent" "$moTag"
-
- if moTest "$moTag"; then
- # Show / loop / pass through function
- if moIsFunction "$moTag"; then
- moCallFunction moContent "$moTag" "${moBlock[0]}" "$moArgs"
- moParse "$moContent" "$moCurrent" false
- moContent="${moBlock[2]}"
- elif moIsArray "$moTag"; then
- eval "moLoop \"\${moBlock[0]}\" \"$moTag\" \"\${!${moTag}[@]}\""
- else
- moParse "${moBlock[0]}" "$moCurrent" true
- fi
- fi
-
- moContent="${moBlock[2]}"
- ;;
-
- '>'*)
- # Load partial - get name of file relative to cwd
- moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent"
- moNextIsBeginning=${moContent[1]}
- moContent=${moContent[0]}
- ;;
-
- '/'*)
- # Closing tag - If hit in this loop, we simply ignore
- # Matching tags are found in moFindEndTag
- moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
- ;;
-
- '^'*)
- # Display section if named thing does not exist
- moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
- moTrimWhitespace moTag "${moTag:1}"
- moFindEndTag moBlock "$moContent" "$moTag"
- moFullTagName moTag "$moCurrent" "$moTag"
-
- if ! moTest "$moTag"; then
- moParse "${moBlock[0]}" "$moCurrent" false "$moCurrent"
- fi
-
- moContent="${moBlock[2]}"
- ;;
-
- '!'*)
- # Comment - ignore the tag content entirely
- # Trim spaces/tabs before the comment
- moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
- ;;
-
- .)
- # Current content (environment variable or function)
- moStandaloneDenied moContent "${moContent[@]}"
- moShow "$moCurrent" "$moCurrent"
- ;;
-
- '=')
- # Change delimiters
- # Any two non-whitespace sequences separated by whitespace.
- # This tag is ignored.
- moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
- ;;
-
- '{'*)
- # Unescaped - split on }}} not }}
- moStandaloneDenied moContent "${moContent[@]}"
- moContent="${moTag:1}"'}}'"$moContent"
- moSplit moContent "$moContent" '}}}'
- moTrimWhitespace moTag "${moContent[0]}"
- moArgs=$moTag
- moTag=${moTag%% *}
- moTag=${moTag%%$'\t'*}
- moArgs=${moArgs:${#moTag}}
- moFullTagName moTag "$moCurrent" "$moTag"
- moContent=${moContent[1]}
-
- # Now show the value
- # Quote moArgs here, do not quote it later.
- moShow "$moTag" "$moCurrent" "$moArgs"
- ;;
-
- '&'*)
- # Unescaped
- moStandaloneDenied moContent "${moContent[@]}"
- moTrimWhitespace moTag "${moTag:1}"
- moFullTagName moTag "$moCurrent" "$moTag"
- moShow "$moTag" "$moCurrent"
- ;;
-
- *)
- # Normal environment variable or function call
- moStandaloneDenied moContent "${moContent[@]}"
- moArgs=$moTag
- moTag=${moTag%% *}
- moTag=${moTag%%$'\t'*}
- moArgs=${moArgs:${#moTag}}
- moFullTagName moTag "$moCurrent" "$moTag"
-
- # Quote moArgs here, do not quote it later.
- moShow "$moTag" "$moCurrent" "$moArgs"
- ;;
- esac
-
- moIsBeginning=$moNextIsBeginning
- moSplit moContent "$moContent" '{{' '}}'
- done
-
- echo -n "${moContent[0]}"
-}
-
-
-# Internal: Process a partial.
-#
-# Indentation should be applied to the entire partial.
-#
-# This sends back the "is beginning" flag because the newline after a
-# standalone partial is consumed. That newline is very important in the middle
-# of content. We send back this flag to reset the processing loop's
-# `moIsBeginning` variable, so the software thinks we are back at the
-# beginning of a file and standalone processing continues to work.
-#
-# Prefix all variables.
-#
-# $1 - Name of destination variable. Element [0] is the content, [1] is the
-# true/false flag indicating if we are at the beginning of content.
-# $2 - Content before the tag that was not yet written
-# $3 - Tag content
-# $4 - Content after the tag
-# $5 - true/false: is this the beginning of the content?
-# $6 - Current context name
-#
-# Returns nothing.
-moPartial() {
- # Namespace variables here to prevent conflicts.
- local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented
-
- if moIsStandalone moStandalone "$2" "$4" "$5"; then
- # shellcheck disable=SC2206
- moStandalone=( $moStandalone )
- echo -n "${2:0:${moStandalone[0]}}"
- moIndent=${2:${moStandalone[0]}}
- moContent=${4:${moStandalone[1]}}
- moIsBeginning=true
- else
- moIndent=""
- echo -n "$2"
- moContent=$4
- moIsBeginning=$5
- fi
-
- moTrimWhitespace moFilename "${3:1}"
-
- # Execute in subshell to preserve current cwd and environment
- (
- # It would be nice to remove `dirname` and use a function instead,
- # but that's difficult when you're only given filenames.
- cd "$(dirname -- "$moFilename")" || exit 1
- moUnindented="$(
- moLoadFile moPartial "${moFilename##*/}" || exit 1
- moParse "${moPartial}" "$6" true
-
- # Fix bash handling of subshells and keep trailing whitespace.
- # This is removed in moIndentLines.
- echo -n "."
- )" || exit 1
- moIndentLines moPartial "$moIndent" "$moUnindented"
- echo -n "$moPartial"
- ) || exit 1
-
- # If this is a standalone tag, the trailing newline after the tag is
- # removed and the contents of the partial are added, which typically
- # contain a newline. We need to send a signal back to the processing
- # loop that the moIsBeginning flag needs to be turned on again.
- #
- # [0] is the content, [1] is that flag.
- local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning"
-}
-
-
-# Internal: Show an environment variable or the output of a function to
-# stdout.
-#
-# Limit/prefix any variables used.
-#
-# $1 - Name of environment variable or function
-# $2 - Current context
-# $3 - Arguments string if $1 is a function
-#
-# Returns nothing.
-moShow() {
- # Namespace these variables
- local moJoined moNameParts moContent
-
- if moIsFunction "$1"; then
- moCallFunction moContent "$1" "" "$3"
- moParse "$moContent" "$2" false
- return 0
- fi
-
- moSplit moNameParts "$1" "."
-
- if [[ -z "${moNameParts[1]-}" ]]; then
- if moIsArray "$1"; then
- eval moJoin moJoined "," "\${$1[@]}"
- echo -n "$moJoined"
- else
- # shellcheck disable=SC2031
- if moTestVarSet "$1"; then
- echo -n "${!1}"
- elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then
- echo "Env variable not set: $1" >&2
- exit 1
- fi
- fi
- else
- # Further subindexes are disallowed
- eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\""
- fi
-}
-
-
-# Internal: Split a larger string into an array.
-#
-# $1 - Destination variable
-# $2 - String to split
-# $3 - Starting delimiter
-# $4 - Ending delimiter (optional)
-#
-# Returns nothing.
-moSplit() {
- local pos result
-
- result=( "$2" )
- moFindString pos "${result[0]}" "$3"
-
- if [[ "$pos" -ne -1 ]]; then
- # The first delimiter was found
- result[1]=${result[0]:$pos + ${#3}}
- result[0]=${result[0]:0:$pos}
-
- if [[ -n "${4-}" ]]; then
- moFindString pos "${result[1]}" "$4"
-
- if [[ "$pos" -ne -1 ]]; then
- # The second delimiter was found
- result[2]="${result[1]:$pos + ${#4}}"
- result[1]="${result[1]:0:$pos}"
- fi
- fi
- fi
-
- local "$1" && moIndirectArray "$1" "${result[@]}"
-}
-
-
-# Internal: Handle the content for a standalone tag. This means removing
-# whitespace (not newlines) before a tag and whitespace and a newline after
-# a tag. That is, assuming, that the line is otherwise empty.
-#
-# $1 - Name of destination "content" variable.
-# $2 - Content before the tag that was not yet written
-# $3 - Tag content (not used)
-# $4 - Content after the tag
-# $5 - true/false: is this the beginning of the content?
-#
-# Returns nothing.
-moStandaloneAllowed() {
- local bytes
-
- if moIsStandalone bytes "$2" "$4" "$5"; then
- # shellcheck disable=SC2206
- bytes=( $bytes )
- echo -n "${2:0:${bytes[0]}}"
- local "$1" && moIndirect "$1" "${4:${bytes[1]}}"
- else
- echo -n "$2"
- local "$1" && moIndirect "$1" "$4"
- fi
-}
-
-
-# Internal: Handle the content for a tag that is never "standalone". No
-# adjustments are made for newlines and whitespace.
-#
-# $1 - Name of destination "content" variable.
-# $2 - Content before the tag that was not yet written
-# $3 - Tag content (not used)
-# $4 - Content after the tag
-#
-# Returns nothing.
-moStandaloneDenied() {
- echo -n "$2"
- local "$1" && moIndirect "$1" "$4"
-}
-
-
-# Internal: Determines if the named thing is a function or if it is a
-# non-empty environment variable. When MO_FALSE_IS_EMPTY is set to a
-# non-empty value, then "false" is also treated is an empty value.
-#
-# Do not use variables without prefixes here if possible as this needs to
-# check if any name exists in the environment
-#
-# $1 - Name of environment variable or function
-# $2 - Current value (our context)
-# MO_FALSE_IS_EMPTY - When set to a non-empty value, this will say the
-# string value "false" is empty.
-#
-# Returns 0 if the name is not empty, 1 otherwise. When MO_FALSE_IS_EMPTY
-# is set, this returns 1 if the name is "false".
-moTest() {
- # Test for functions
- moIsFunction "$1" && return 0
-
- if moIsArray "$1"; then
- # Arrays must have at least 1 element
- eval "[[ \"\${#${1}[@]}\" -gt 0 ]]" && return 0
- else
- # If MO_FALSE_IS_EMPTY is set, then return 1 if the value of
- # the variable is "false".
- # shellcheck disable=SC2031
- [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${!1-}" == "false" ]] && return 1
-
- # Environment variables must not be empty
- [[ -n "${!1-}" ]] && return 0
- fi
-
- return 1
-}
-
-# Internal: Determine if a variable is assigned, even if it is assigned an empty
-# value.
-#
-# $1 - Variable name to check.
-#
-# Returns true (0) if the variable is set, 1 if the variable is unset.
-moTestVarSet() {
- [[ "${!1-a}" == "${!1-b}" ]]
-}
-
-
-# Internal: Trim the leading whitespace only.
-#
-# $1 - Name of destination variable
-# $2 - The string
-# $3 - true/false - trim front?
-# $4 - true/false - trim end?
-# $5-@ - Characters to trim
-#
-# Returns nothing.
-moTrimChars() {
- local back current front last target varName
-
- target=$1
- current=$2
- front=$3
- back=$4
- last=""
- shift 4 # Remove target, string, trim front flag, trim end flag
-
- while [[ "$current" != "$last" ]]; do
- last=$current
-
- for varName in "$@"; do
- $front && current="${current/#$varName}"
- $back && current="${current/%$varName}"
- done
- done
-
- local "$target" && moIndirect "$target" "$current"
-}
-
-
-# Internal: Trim leading and trailing whitespace from a string.
-#
-# $1 - Name of variable to store trimmed string
-# $2 - The string
-#
-# Returns nothing.
-moTrimWhitespace() {
- local result
-
- moTrimChars result "$2" true true $'\r' $'\n' $'\t' " "
- local "$1" && moIndirect "$1" "$result"
-}
-
-
-# Internal: Displays the usage for mo. Pulls this from the file that
-# contained the `mo` function. Can only work when the right filename
-# comes is the one argument, and that only happens when `mo` is called
-# with `$0` set to this file.
-#
-# $1 - Filename that has the help message
-#
-# Returns nothing.
-moUsage() {
- grep '^#/' "${MO_ORIGINAL_COMMAND}" | cut -c 4-
- echo ""
- echo "MO_VERSION=$MO_VERSION"
-}
-
-
-# Save the original command's path for usage later
-MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}"
-MO_VERSION="2.2.0"
-
-# If sourced, load all functions.
-# If executed, perform the actions as expected.
-if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then
- mo "$@"
-fi
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index 3517a6bc..de55d276 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -19,6 +19,9 @@
user root;
worker_processes auto;
+# Enables the use of JIT for regular expressions to speed-up their processing.
+pcre_jit on;
+
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
diff --git a/docker/nginx/testing/.luacov b/docker/nginx/testing/.luacov
new file mode 100644
index 00000000..2c55f3da
--- /dev/null
+++ b/docker/nginx/testing/.luacov
@@ -0,0 +1,6 @@
+exclude = {
+ "/usr/local/openresty", -- internal openresty libraries
+ "rbusted", -- busted executable
+ "basexx", -- external library https://github.com/aiq/basexx
+}
+includeuntestedfiles = true
diff --git a/docker/nginx/testing/Dockerfile b/docker/nginx/testing/Dockerfile
new file mode 100644
index 00000000..ed7740b6
--- /dev/null
+++ b/docker/nginx/testing/Dockerfile
@@ -0,0 +1,11 @@
+FROM openresty/openresty:1.19.9.1-focal
+
+WORKDIR /etc/nginx
+
+RUN luarocks install lua-resty-http && \
+ luarocks install hasher && \
+ luarocks install busted
+
+COPY rbusted /etc/nginx/
+
+CMD ["/etc/nginx/rbusted", "--verbose", "--pattern=spec", "/usr/local/openresty/site/lualib"]
diff --git a/docker/nginx/testing/README.md b/docker/nginx/testing/README.md
new file mode 100644
index 00000000..f40e8d95
--- /dev/null
+++ b/docker/nginx/testing/README.md
@@ -0,0 +1,3 @@
+# Running tests locally
+
+`docker run -v $(pwd)/docker/nginx/libs:/usr/local/openresty/site/lualib --rm -it $(docker build -q docker/nginx/testing)`
diff --git a/docker/nginx/testing/rbusted b/docker/nginx/testing/rbusted
new file mode 100755
index 00000000..94149350
--- /dev/null
+++ b/docker/nginx/testing/rbusted
@@ -0,0 +1,8 @@
+#!/usr/bin/env resty
+
+setmetatable(_G, nil)
+
+pcall(require, "luarocks.loader")
+
+-- Busted command-line runner
+require "busted.runner"({ standalone = false })
diff --git a/docker/sia/Dockerfile b/docker/sia/Dockerfile
index 887b92e9..953dd27c 100644
--- a/docker/sia/Dockerfile
+++ b/docker/sia/Dockerfile
@@ -5,12 +5,12 @@ ENV GOARCH amd64
ARG branch=portal-latest
-RUN git clone https://gitlab.com/SkynetLabs/skyd.git Sia --single-branch --branch ${branch}
-RUN make release --directory Sia
+RUN git clone https://gitlab.com/SkynetLabs/skyd.git Sia --single-branch --branch ${branch} && \
+ make release --directory Sia
-FROM nebulouslabs/sia:latest
+FROM nebulouslabs/sia:1.5.6
COPY --from=sia-builder /go/bin/ /usr/bin/
-RUN mv /usr/bin/skyd /usr/bin/siad || true && \
- mv /usr/bin/skyc /usr/bin/siac || true
+RUN if [ -f "/usr/bin/skyd" ]; then mv /usr/bin/skyd /usr/bin/siad; fi && \
+ if [ -f "/usr/bin/skyc" ]; then mv /usr/bin/skyc /usr/bin/siac; fi
diff --git a/packages/dashboard-v2/Dockerfile b/packages/dashboard-v2/Dockerfile
new file mode 100644
index 00000000..86fe89bc
--- /dev/null
+++ b/packages/dashboard-v2/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:16.14.2-alpine
+
+WORKDIR /usr/app
+
+COPY package.json \
+ yarn.lock \
+ ./
+
+RUN yarn --frozen-lockfile
+
+COPY static ./static
+COPY src ./src
+COPY gatsby*.js \
+ postcss.config.js \
+ tailwind.config.js \
+ ./
+
+CMD ["sh", "-c", "yarn build && yarn serve --host 0.0.0.0 -p 9000"]
diff --git a/packages/dashboard-v2/README.md b/packages/dashboard-v2/README.md
index ab0421f8..e8924bc3 100644
--- a/packages/dashboard-v2/README.md
+++ b/packages/dashboard-v2/README.md
@@ -11,15 +11,20 @@ This is a Gatsby application. To run it locally, all you need is:
## Accessing remote APIs
-To be able to log in on a local environment with your production credentials, you'll need to make the browser believe you're actually on the same domain, otherwise the browser will block the session cookie.
+To have a fully functioning local environment, you'll need to make the browser believe you're actually on the same domain as a working API (i.e. a remote dev or production server) -- otherwise the browser will block the session cookie.
+To do the trick, configure proper environment variables in the `.env.development` file.
+This file allows to easily control which domain name you want to use locally and which API you'd like to access.
-To do the trick, edit your `/etc/hosts` file and add a record like this:
+Example:
-```
-127.0.0.1 local.skynetpro.net
+```env
+GATSBY_PORTAL_DOMAIN=skynetfree.net # Use skynetfree.net APIs
+GATSBY_HOST=local.skynetfree.net # Address of your local build
```
-then run `yarn develop:secure` -- it will run `gatsby develop` with `--https --host=local.skynetpro.net -p=443` options.
-If you're on macOS, you may need to `sudo` the command to successfully bind to port `443`.
+> It's recommended to keep the 2LD the same, so any cookies dispatched by the API work without issues.
-> **NOTE:** This should become easier once we have a docker image for the new dashboard.
+With the file configured, run `yarn develop:secure` -- it will run `gatsby develop` with `--https -p=443` options.
+If you're on macOS, you may need to `sudo` the command to successfully bind to port `443` (https).
+
+Gatsby will automatically add a proper entry to your `/etc/hosts` file and clean it up when process exits.
diff --git a/packages/dashboard-v2/gatsby-browser.js b/packages/dashboard-v2/gatsby-browser.js
index 79b58e24..927fd206 100644
--- a/packages/dashboard-v2/gatsby-browser.js
+++ b/packages/dashboard-v2/gatsby-browser.js
@@ -1,4 +1,7 @@
import * as React from "react";
+import { SWRConfig } from "swr";
+import { Elements } from "@stripe/react-stripe-js";
+import { loadStripe } from "@stripe/stripe-js";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
@@ -6,17 +9,24 @@ import "@fontsource/sora/600.css"; // semibold
import "@fontsource/source-sans-pro/400.css"; // normal
import "@fontsource/source-sans-pro/600.css"; // semibold
import "./src/styles/global.css";
+import swrConfig from "./src/lib/swrConfig";
import { MODAL_ROOT_ID } from "./src/components/Modal";
import { PortalSettingsProvider } from "./src/contexts/portal-settings";
+const stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY);
+
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment;
return (
{skylink}
- 100GB without paying a dime! 🎉
} +{humanBytes(activePlan.limits.storageLimit)} without paying a dime! 🎉
+ )} {activePlan.price !== 0 && (user.subscriptionCancelAtPeriodEnd ? (Your subscription expires {dayjs(user.subscribedUntil).fromNow()}
) : ({dayjs(user.subscribedUntil).fromNow(true)} until the next payment
))} -Discover {nextPlan.name}
{nextPlan.description}
-An error occurred while loading this data.
} {data &&No {type} found.
} @@ -32,42 +43,14 @@ const FileList = ({ type }) => { } return ( -© Skynet Labs Inc. All rights reserved.
+
+ Made by