Merge branch 'master' into switch-authenticated-health-checks-to-api-key

This commit is contained in:
Karol Wypchło 2022-03-22 18:24:30 +01:00 committed by GitHub
commit f18272f1af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
408 changed files with 13263 additions and 8143 deletions

View File

@ -20,18 +20,6 @@ updates:
directory: "/packages/website"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/accounts"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/caddy"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/handshake"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/nginx"
schedule:

View File

@ -0,0 +1,31 @@
name: Build Storybook - packages/dashboard-v2
on:
push:
branches:
- master
paths:
- "packages/dashboard-v2/**"
pull_request:
paths:
- "packages/dashboard-v2/**"
defaults:
run:
working-directory: packages/dashboard-v2
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
- run: yarn install
- run: yarn build-storybook
- name: "Deploy to Skynet"
uses: skynetlabs/deploy-to-skynet-action@v2
with:
upload-dir: packages/dashboard-v2/storybook-build
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -36,12 +36,16 @@ jobs:
working-directory: packages/website
install: false
record: true
start: yarn serve
wait-on: "http://127.0.0.1:9000"
start: yarn develop
wait-on: http://localhost:8000
wait-on-timeout: 120
config: baseUrl=http://localhost:8000
- name: "Deploy to Skynet"
uses: skynetlabs/deploy-to-skynet-action@v2
with:
upload-dir: packages/website/public
portal-url: https://skynetpro.net
skynet-jwt: ${{ secrets.SKYNET_JWT }}
github-token: ${{ secrets.GITHUB_TOKEN }}
registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && secrets.WEBSITE_REGISTRY_SEED || '' }}

View File

@ -0,0 +1,24 @@
name: Lint - packages/dashboard-v2
on:
pull_request:
paths:
- packages/dashboard-v2/**
defaults:
run:
working-directory: packages/dashboard-v2
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16.x
- run: yarn
- run: yarn prettier --check
- run: yarn lint

View File

@ -10,6 +10,46 @@ Version History
Latest:
## Mar 8, 2022:
### v0.1.4
**Key Updates**
- expose generic skylink serving endpoint on domain aliases
- Add abuse scanner service, activated by adding `u` to `PORTAL_MODULES`
- Add malware scanner service, activated by adding `s` to `PORTAL_MODULES`
- Remove ORY Kratos, ORY Oathkeeper, CockroachDB.
- Add `/serverload` endpoint for CPU usage and free disk space
**Bugs Fixed**
- Add missing servers and blocklist command to the manual blocklist script.
- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters
- Fix `blocklist-skylink.sh` script that didn't removed blocked skylink from
nginx cache.
- fixed uploaded directory name (was "undefined" before)
- fixed empty directory upload progress (size was not calculated for directories)
**Other**
- add new critical health check that scans config and makes sure that all relevant configurations are set
- Add abuse report configuration
- Remove hardcoded Airtable default values from blocklist script. Portal
operators need to define their own values in portal common config (LastPass).
- Add health check for the blocker container
- Drop `Skynet-Requested-Skylink` header
- Dump disk space usage when health-checker script disables portal due to
critical free disk space.
- Enable the accounting module for skyd
- Add link to supported setup process in Gitbook.
- Set `min_free` parameter on the `proxy_cache_path` directive to `100g`
- Parameterize MongoDB replicaset in `docker-compose.mongodb.yml` via
`SKYNET_DB_REPLICASET` from `.env` file.
- Hot reload Nginx after pruning cache files.
- Added script to prune nginx cache.
- Remove hardcoded server list from `blocklist-skylink.sh` so it removes server
list duplication and can also be called from Ansible.
- Remove outdated portal setup documentation and point to developer docs.
- Block skylinks in batches to improve performance.
- Add trimming Airtable skylinks from Takedown Request table.
- Update handshake to use v3.0.1
## Oct 18, 2021:
### v0.1.3
**Key Updates**

View File

@ -1,3 +1,43 @@
## Mar 8, 2022:
### v0.1.4
**Key Updates**
- expose generic skylink serving endpoint on domain aliases
- Add abuse scanner service, activated by adding `u` to `PORTAL_MODULES`
- Add malware scanner service, activated by adding `s` to `PORTAL_MODULES`
- Remove ORY Kratos, ORY Oathkeeper, CockroachDB.
- Add `/serverload` endpoint for CPU usage and free disk space
**Bugs Fixed**
- Add missing servers and blocklist command to the manual blocklist script.
- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters
- Fix `blocklist-skylink.sh` script that didn't removed blocked skylink from
nginx cache.
- fixed uploaded directory name (was "undefined" before)
- fixed empty directory upload progress (size was not calculated for directories)
**Other**
- add new critical health check that scans config and makes sure that all relevant configurations are set
- Add abuse report configuration
- Remove hardcoded Airtable default values from blocklist script. Portal
operators need to define their own values in portal common config (LastPass).
- Add health check for the blocker container
- Drop `Skynet-Requested-Skylink` header
- Dump disk space usage when health-checker script disables portal due to
critical free disk space.
- Enable the accounting module for skyd
- Add link to supported setup process in Gitbook.
- Set `min_free` parameter on the `proxy_cache_path` directive to `100g`
- Parameterize MongoDB replicaset in `docker-compose.mongodb.yml` via
`SKYNET_DB_REPLICASET` from `.env` file.
- Hot reload Nginx after pruning cache files.
- Added script to prune nginx cache.
- Remove hardcoded server list from `blocklist-skylink.sh` so it removes server
list duplication and can also be called from Ansible.
- Remove outdated portal setup documentation and point to developer docs.
- Block skylinks in batches to improve performance.
- Add trimming Airtable skylinks from Takedown Request table.
- Update handshake to use v3.0.1
## Oct 18, 2021:
### v0.1.3
**Key Updates**

View File

@ -1 +0,0 @@
- Add missing servers and blocklist command to the manual blocklist script.

View File

@ -1 +0,0 @@
- fixed a bug when accessing file from skylink via subdomain with a filename that had escaped characters

View File

@ -1,2 +0,0 @@
- Fix `blocklist-skylink.sh` script that didn't removed blocked skylink from
nginx cache.

View File

@ -1,2 +0,0 @@
- fixed uploaded directory name (was "undefined" before)
- fixed empty directory upload progress (size was not calculated for directories)

View File

@ -1 +0,0 @@
- expose generic skylink serving endpoint on domain aliases

View File

@ -1 +0,0 @@
- Add abuse scanner service, activated by adding `u` to `PORTAL_MODULES`

View File

@ -1 +0,0 @@
- Add malware scanner service, activated by adding `s` to `PORTAL_MODULES`

View File

@ -1 +0,0 @@
- Remove ORY Kratos, ORY Oathkeeper, CockroachDB.

View File

@ -1 +0,0 @@
- Add `/serverload` endpoint for CPU usage and free disk space

View File

@ -1 +0,0 @@
- add new critical health check that scans config and makes sure that all relevant configurations are set

View File

@ -1 +0,0 @@
- Add abuse report configuration

View File

@ -1,2 +0,0 @@
- Remove hardcoded Airtable default values from blocklist script. Portal
operators need to define their own values in portal common config (LastPass).

View File

@ -1 +0,0 @@
- Add health check for the blocker container

View File

@ -1 +0,0 @@
- Drop `Skynet-Requested-Skylink` header

View File

@ -1,2 +0,0 @@
- Dump disk space usage when health-checker script disables portal due to
critical free disk space.

View File

@ -1 +0,0 @@
- Enable the accounting module for skyd

View File

@ -1 +0,0 @@
- Add link to supported setup process in Gitbook.

View File

@ -1 +0,0 @@
- Set `min_free` parameter on the `proxy_cache_path` directive to `100g`

View File

@ -1,2 +0,0 @@
- Parameterize MongoDB replicaset in `docker-compose.mongodb.yml` via
`SKYNET_DB_REPLICASET` from `.env` file.

View File

@ -1 +0,0 @@
- Hot reload Nginx after pruning cache files.

View File

@ -1 +0,0 @@
- Added script to prune nginx cache.

View File

@ -1,2 +0,0 @@
- Remove hardcoded server list from `blocklist-skylink.sh` so it removes server
list duplication and can also be called from Ansible.

View File

@ -1 +0,0 @@
- Remove outdated portal setup documentation and point to developer docs.

View File

@ -1 +0,0 @@
- Block skylinks in batches to improve performance.

View File

@ -1 +0,0 @@
- Add trimming Airtable skylinks from Takedown Request table.

View File

@ -1 +0,0 @@
- Update handshake to use v3.0.1

4
dc
View File

@ -41,9 +41,9 @@ for i in $(seq 1 ${#PORTAL_MODULES}); do
COMPOSE_FILES+=" -f docker-compose.mongodb.yml"
fi
# abuse module - alias "u"
# abuse-scanner module - alias "u"
if [[ ${PORTAL_MODULES:i-1:1} == "u" ]]; then
COMPOSE_FILES+=" -f docker-compose.mongodb.yml -f docker-compose.blocker.yml -f docker-compose.abuse.yml"
COMPOSE_FILES+=" -f docker-compose.mongodb.yml -f docker-compose.blocker.yml -f docker-compose.abuse-scanner.yml"
fi
done

View File

@ -7,11 +7,9 @@ x-logging: &default-logging
max-file: "3"
services:
abuse:
build:
context: ./docker/abuse
dockerfile: Dockerfile
container_name: abuse
abuse-scanner:
image: skynetlabs/abuse-scanner
container_name: abuse-scanner
restart: unless-stopped
logging: *default-logging
env_file:

View File

@ -13,9 +13,7 @@ services:
- BLOCKER_PORT=4000
blocker:
build:
context: ./docker/blocker
dockerfile: Dockerfile
image: skynetlabs/blocker
container_name: blocker
restart: unless-stopped
logging: *default-logging

View File

@ -21,7 +21,7 @@ services:
- JAEGER_REPORTER_LOG_SPANS=false
jaeger-agent:
image: jaegertracing/jaeger-agent
image: jaegertracing/jaeger-agent:1.32.0
command:
[
"--reporter.grpc.host-port=jaeger-collector:14250",
@ -43,7 +43,7 @@ services:
- jaeger-collector
jaeger-collector:
image: jaegertracing/jaeger-collector
image: jaegertracing/jaeger-collector:1.32.0
entrypoint: /wait_to_start.sh
container_name: jaeger-collector
restart: on-failure
@ -68,7 +68,7 @@ services:
- elasticsearch
jaeger-query:
image: jaegertracing/jaeger-query
image: jaegertracing/jaeger-query:1.32.0
entrypoint: /wait_to_start.sh
container_name: jaeger-query
restart: on-failure

View File

@ -26,19 +26,15 @@ services:
ipv4_address: 10.10.10.100
malware-scanner:
build:
context: ./docker/malware-scanner
dockerfile: Dockerfile
args:
branch: main
image: skynetlabs/malware-scanner
container_name: malware-scanner
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- CLAMAV_IP=${CLAMAV_IP:-10.10.10.100}
- CLAMAV_PORT=${CLAMAV_PORT:-3310}
- CLAMAV_IP=10.10.10.100
- CLAMAV_PORT=3310
- BLOCKER_IP=10.10.10.110
- BLOCKER_PORT=4000
expose:

View File

@ -1,12 +0,0 @@
version: "3.7"
services:
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile.bionic
args:
RESTY_ADD_PACKAGE_BUILDDEPS: git
RESTY_EVAL_PRE_CONFIGURE: git clone https://github.com/fdintino/nginx-upload-module /tmp/nginx-upload-module
RESTY_CONFIG_OPTIONS_MORE: --add-module=/tmp/nginx-upload-module
RESTY_EVAL_POST_MAKE: /usr/local/openresty/luajit/bin/luarocks install luasocket

View File

@ -25,7 +25,7 @@ services:
logging: *default-logging
environment:
- SIA_MODULES=gctwra
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-false}
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-true}
- SKYD_DISK_CACHE_SIZE=${SKYD_DISK_CACHE_SIZE:-53690000000} # 50GB
- SKYD_DISK_CACHE_MIN_HITS=${SKYD_DISK_CACHE_MIN_HITS:-3}
- SKYD_DISK_CACHE_HIT_PERIOD=${SKYD_DISK_CACHE_HIT_PERIOD:-3600} # 1h
@ -39,21 +39,19 @@ services:
expose:
- 9980
caddy:
build:
context: ./docker/caddy
dockerfile: Dockerfile
container_name: caddy
certbot:
image: certbot/dns-route53:v1.25.0
entrypoint: sh /entrypoint.sh
container_name: certbot
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- CERTBOT_ARGS=--dns-route53
volumes:
- ./docker/data/caddy/data:/data
- ./docker/data/caddy/config:/config
networks:
shared:
ipv4_address: 10.10.10.20
- ./docker/certbot/entrypoint.sh:/entrypoint.sh
- ./docker/data/certbot:/etc/letsencrypt
nginx:
build:
@ -64,15 +62,13 @@ services:
logging: *default-logging
env_file:
- .env
environment:
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-false}
volumes:
- ./docker/data/nginx/cache:/data/nginx/cache
- ./docker/data/nginx/blocker:/data/nginx/blocker
- ./docker/data/nginx/logs:/usr/local/openresty/nginx/logs
- ./docker/data/nginx/skynet:/data/nginx/skynet:ro
- ./docker/data/sia/apipassword:/data/sia/apipassword:ro
- ./docker/data/caddy/data:/data/caddy:ro
- ./docker/data/certbot:/etc/letsencrypt
networks:
shared:
ipv4_address: 10.10.10.30
@ -81,7 +77,6 @@ services:
- "80:80"
depends_on:
- sia
- caddy
- handshake-api
- dnslink-api
- website
@ -102,9 +97,7 @@ services:
- 9000
handshake:
build:
context: ./docker/handshake
dockerfile: Dockerfile
image: skynetlabs/hsd:3.0.1
command: --chain-migrate=2 --wallet-migrate=1
container_name: handshake
restart: unless-stopped
@ -178,5 +171,3 @@ services:
- STATE_DIR=/usr/app/state
expose:
- 3100
depends_on:
- caddy

View File

@ -1,16 +0,0 @@
FROM golang:1.16.7
LABEL maintainer="SkynetLabs <devs@siasky.net>"
ENV GOOS linux
ENV GOARCH amd64
ARG branch=main
WORKDIR /root
RUN git clone --single-branch --branch ${branch} https://github.com/SkynetLabs/abuse-scanner.git && \
cd abuse-scanner && \
go mod download && \
make release
ENTRYPOINT ["abuse-scanner"]

View File

@ -1,16 +0,0 @@
FROM golang:1.16.7
LABEL maintainer="SkynetLabs <devs@siasky.net>"
ENV GOOS linux
ENV GOARCH amd64
ARG branch=main
WORKDIR /root
RUN git clone --single-branch --branch ${branch} https://github.com/SkynetLabs/blocker.git && \
cd blocker && \
go mod download && \
make release
ENTRYPOINT ["blocker"]

View File

@ -1,18 +0,0 @@
FROM caddy:2.4.6-builder AS caddy-builder
# available dns resolvers: https://github.com/caddy-dns
RUN xcaddy build --with github.com/caddy-dns/route53
FROM caddy:2.4.6-alpine
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
# bash required for mo to work (mo is mustache templating engine - https://github.com/tests-always-included/mo)
RUN apk add --no-cache bash
COPY caddy.json.template mo /etc/caddy/
CMD [ "sh", "-c", \
"/etc/caddy/mo < /etc/caddy/caddy.json.template > /etc/caddy/caddy.json ; \
caddy run --config /etc/caddy/caddy.json" \
]

View File

@ -1,39 +0,0 @@
{
"apps": {
"tls": {
"certificates": {
"automate": [
{{#PORTAL_DOMAIN}}
"{{PORTAL_DOMAIN}}", "*.{{PORTAL_DOMAIN}}", "*.hns.{{PORTAL_DOMAIN}}"
{{/PORTAL_DOMAIN}}
{{#PORTAL_DOMAIN}}{{#SERVER_DOMAIN}},{{/SERVER_DOMAIN}}{{/PORTAL_DOMAIN}}
{{#SERVER_DOMAIN}}
"{{SERVER_DOMAIN}}", "*.{{SERVER_DOMAIN}}", "*.hns.{{SERVER_DOMAIN}}"
{{/SERVER_DOMAIN}}
]
},
"automation": {
"policies": [
{
"issuers": [
{
"module": "acme",
"email": "{{EMAIL_ADDRESS}}",
"challenges": {
"dns": {
"provider": {
"name": "route53"
},
"ttl": "30m"
}
}
}
]
}
]
}
}
}
}

File diff suppressed because it is too large Load Diff

55
docker/certbot/entrypoint.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# Portal domain requires 3 domain certificates:
# - exact portal domain, ie. example.com
# - wildcard subdomain on portal domain, ie. *.example.com
# used for skylinks served from portal subdomain
# - wildcard subdomain on hns portal domain subdomain, ie. *.hns.example.com
# used for resolving handshake domains
DOMAINS=${PORTAL_DOMAIN},*.${PORTAL_DOMAIN},*.hns.${PORTAL_DOMAIN}
# Add server domain when it is not empty and different from portal domain
if [ ! -z "${SERVER_DOMAIN}" ] && [ "${PORTAL_DOMAIN}" != "${SERVER_DOMAIN}" ]; then
# In case where server domain is not covered by portal domain's
# wildcard certificate, add server domain name to domains list.
# - server-001.example.com is covered by *.example.com
# - server-001.servers.example.com or server-001.example-severs.com
# are not covered by any already requested wildcard certificates
#
# The condition checks whether server domain does not match portal domain
# with exactly one level of subdomain (portal domain wildcard cert):
# (start) [anything but the dot] + [dot] + [portal domain] (end)
if ! printf "${SERVER_DOMAIN}" | grep -q -E "^[^\.]+\.${PORTAL_DOMAIN}$"; then
DOMAINS=${DOMAINS},${SERVER_DOMAIN}
fi
# Server domain requires the same set of domain certificates as portal domain.
# Exact server domain case is handled above.
DOMAINS=${DOMAINS},*.${SERVER_DOMAIN},*.hns.${SERVER_DOMAIN}
fi
# The "wait" will prevent an exit from the script while background tasks are
# still active, so we are adding the line below as a method to prevent orphaning
# the background child processe. The trap fires when docker terminates the container.
trap exit TERM
while :; do
# Execute certbot and generate or maintain certificates for given domain string.
# --non-interactive: we are running this as an automation so we cannot be prompted
# --agree-tos: required flag marking agreement with letsencrypt tos
# --cert-name: output directory name
# --email: required for generating certificates, used for communication with CA
# --domains: comma separated list of domains (will generate one bundled SAN cert)
# Use CERTBOT_ARGS env variable to pass any additional arguments, ie --dns-route53
certbot certonly \
--non-interactive --agree-tos --cert-name skynet-portal \
--email ${EMAIL_ADDRESS} --domains ${DOMAINS} ${CERTBOT_ARGS}
# Run a background sleep process that counts down given time
# Certbot docs advise running maintenance process every 12 hours
sleep 12h &
# Await execution until sleep process is finished (it's a background process)
# Syntax explanation: ${!} expands to a pid of last ran process
wait ${!}
done

View File

@ -1,12 +0,0 @@
FROM node:16.14.0-alpine
WORKDIR /opt/hsd
RUN apk update && apk add bash unbound-dev gmp-dev g++ gcc make python2 git
RUN git clone https://github.com/handshake-org/hsd.git /opt/hsd && \
cd /opt/hsd && git checkout v3.0.1 && cd -
RUN npm install --production
ENV PATH="${PATH}:/opt/hsd/bin:/opt/hsd/node_modules/.bin"
ENTRYPOINT ["hsd"]

View File

@ -1,23 +0,0 @@
FROM golang:1.17.3
LABEL maintainer="SkynetLabs <devs@siasky.net>"
ENV GOOS linux
ENV GOARCH amd64
ARG branch=main
WORKDIR /root
RUN git clone --single-branch --branch ${branch} https://github.com/SkynetLabs/malware-scanner.git && \
cd malware-scanner && \
go mod download && \
make release
ENV SKYNET_DB_HOST="localhost"
ENV SKYNET_DB_PORT="27017"
ENV SKYNET_DB_USER="username"
ENV SKYNET_DB_PASS="password"
ENV CLAMAV_IP=127.0.0.1
ENV CLAMAV_PORT=3310
ENTRYPOINT ["malware-scanner"]

View File

@ -18,5 +18,6 @@ CMD [ "bash", "-c", \
./mo < /etc/nginx/conf.d.templates/server.api.conf > /etc/nginx/conf.d/server.api.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;'" \
]

View File

@ -9,8 +9,14 @@
server {
server_name account.{{PORTAL_DOMAIN}}; # example: account.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.key;
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;
}
@ -28,8 +34,8 @@
server {
server_name account.{{SERVER_DOMAIN}}; # example: account.eu-ger-1.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.key;
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;

View File

@ -8,8 +8,14 @@ server {
server {
server_name {{PORTAL_DOMAIN}}; # example: siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{PORTAL_DOMAIN}}/{{PORTAL_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{PORTAL_DOMAIN}}/{{PORTAL_DOMAIN}}.key;
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;
}
@ -27,8 +33,8 @@ server {
server {
server_name {{SERVER_DOMAIN}}; # example: eu-ger-1.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{SERVER_DOMAIN}}/{{SERVER_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{SERVER_DOMAIN}}/{{SERVER_DOMAIN}}.key;
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;

View File

@ -8,8 +8,14 @@ server {
server {
server_name *.hns.{{PORTAL_DOMAIN}}; # example: *.hns.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{PORTAL_DOMAIN}}/wildcard_.hns.{{PORTAL_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{PORTAL_DOMAIN}}/wildcard_.hns.{{PORTAL_DOMAIN}}.key;
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;
@ -28,8 +34,8 @@ server {
server {
server_name *.hns.{{SERVER_DOMAIN}}; # example: *.hns.eu-ger-1.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{SERVER_DOMAIN}}/wildcard_.hns.{{SERVER_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.hns.{{SERVER_DOMAIN}}/wildcard_.hns.{{SERVER_DOMAIN}}.key;
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;

View File

@ -8,8 +8,14 @@ server {
server {
server_name *.{{PORTAL_DOMAIN}}; # example: *.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{PORTAL_DOMAIN}}/wildcard_.{{PORTAL_DOMAIN}}.key;
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;
}
@ -27,8 +33,8 @@ server {
server {
server_name *.{{SERVER_DOMAIN}}; # example: *.eu-ger-1.siasky.net
ssl_certificate /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.crt;
ssl_certificate_key /data/caddy/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wildcard_.{{SERVER_DOMAIN}}/wildcard_.{{SERVER_DOMAIN}}.key;
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;

View File

@ -1,5 +1,10 @@
more_set_headers 'Access-Control-Allow-Origin: $http_origin';
more_set_headers 'Access-Control-Allow-Credentials: true';
more_set_headers 'Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE';
<<<<<<< switch-authenticated-health-checks-to-api-key
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location,Skynet-Api-Key';
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location';
=======
more_set_headers 'Access-Control-Allow-Headers: DNT,User-Agent,X-Requested-With,If-Modified-Since,If-None-Match,Cache-Control,Content-Type,Range,X-HTTP-Method-Override,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location,Skynet-API-Key';
more_set_headers 'Access-Control-Expose-Headers: Content-Length,Content-Range,ETag,Accept-Ranges,Skynet-File-Metadata,Skynet-Skylink,Skynet-Proof,Skynet-Portal-Api,Skynet-Server-Api,upload-offset,upload-metadata,upload-length,tus-version,tus-resumable,tus-extension,tus-max-size,upload-concat,location';
>>>>>>> master

View File

@ -13,6 +13,3 @@ set $skylink "";
# cached account limits (json string) - applies only if accounts are enabled
set $account_limits "";
# set this internal flag to true if current request should not be limited in any way
set $internal_no_limits "false";

View File

@ -1,4 +1,3 @@
include /etc/nginx/conf.d/include/proxy-buffer;
include /etc/nginx/conf.d/include/proxy-pass-internal;
include /etc/nginx/conf.d/include/portal-access-check;
@ -81,8 +80,8 @@ proxy_pass https://127.0.0.1/$skylink$path$is_args$args;
# in case siad returns location header, we need to replace the skylink with the domain name
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
if ngx.header.location then
-- match location redirect part after the skylink

View File

@ -1,6 +1,4 @@
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/proxy-buffer;
include /etc/nginx/conf.d/include/proxy-cache-downloads;
include /etc/nginx/conf.d/include/track-download;
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
@ -9,59 +7,10 @@ limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
# this is important because we want only one format in cache keys and logs
set_by_lua_block $skylink { return require("skynet.skylink").parse(ngx.var.skylink) }
# $skylink_v1 and $skylink_v2 variables default to the same value but in case the requested skylink was:
# a) skylink v1 - it would not matter, no additional logic is executed
# b) skylink v2 - in a lua block below we will resolve the skylink v2 into skylink v1 and update
# $skylink_v1 variable so then the proxy request to skyd can be cached in nginx (proxy_cache_key
# in proxy-cache-downloads includes $skylink_v1 as a part of the cache key)
set $skylink_v1 $skylink;
set $skylink_v2 $skylink;
# variable for Skynet-Proof header that we need to inject
# into a response if the request was for skylink v2
set $skynet_proof '';
# default download rate to unlimited
set $limit_rate 0;
access_by_lua_block {
local httpc = require("resty.http").new()
-- detect whether requested skylink is v2
local isBase32v2 = string.len(ngx.var.skylink) == 55 and string.sub(ngx.var.skylink, 0, 2) == "04"
local isBase64v2 = string.len(ngx.var.skylink) == 46 and string.sub(ngx.var.skylink, 0, 2) == "AQ"
if isBase32v2 or isBase64v2 then
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/resolve/" .. ngx.var.skylink_v2, {
headers = { ["User-Agent"] = "Sia-Agent" }
})
-- print error and exit with 500 or exit with response if status is not 200
if err or (res and res.status ~= ngx.HTTP_OK) then
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
ngx.header["content-type"] = "text/plain"
ngx.say(err or res.body)
return ngx.exit(ngx.status)
end
local json = require('cjson')
local resolve = json.decode(res.body)
ngx.var.skylink_v1 = resolve.skylink
ngx.var.skynet_proof = res.headers["Skynet-Proof"]
end
-- check if skylink v1 is present on blocklist (compare hashes)
if require("skynet.blocklist").is_blocked(ngx.var.skylink_v1) then
return require("skynet.blocklist").exit_illegal()
end
-- if skylink is found on nocache list then set internal nocache variable
-- to tell nginx that it should not try and cache this file (too large)
if ngx.shared.nocache:get(ngx.var.skylink_v1) then
ngx.var.nocache = "1"
end
if require("skynet.account").accounts_enabled() then
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
@ -81,33 +30,10 @@ access_by_lua_block {
end
}
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
-- not empty skynet_proof means this is a skylink v2 request
-- so we should replace the Skynet-Proof header with the one
-- we got from /skynet/resolve/ endpoint, otherwise we would
-- be serving cached empty v1 skylink Skynet-Proof header
if ngx.var.skynet_proof and ngx.var.skynet_proof ~= "" then
ngx.header["Skynet-Proof"] = ngx.var.skynet_proof
end
-- add skylink to nocache list if it exceeds 1GB (1e+9 bytes) threshold
-- (content length can be nil for already cached files - we can ignore them)
if ngx.header["Content-Length"] and tonumber(ngx.header["Content-Length"]) > 1e+9 then
ngx.shared.nocache:set(ngx.var.skylink_v1, ngx.header["Content-Length"])
end
}
limit_rate_after 512k;
limit_rate $limit_rate;
proxy_read_timeout 600;
proxy_set_header User-Agent: Sia-Agent;
# in case the requested skylink was v2 and we already resolved it to skylink v1, we are going to pass resolved
# skylink v1 to skyd to save that extra skylink v2 lookup in skyd but in turn, in case skyd returns a redirect
# we need to rewrite the skylink v1 to skylink v2 in the location header with proxy_redirect
proxy_redirect $skylink_v1 $skylink_v2;
proxy_pass http://sia:9980/skynet/skylink/$skylink_v1$path$is_args$args;
proxy_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;

View File

@ -1,5 +0,0 @@
# if you are expecting large headers (ie. Skynet-Skyfile-Metadata), tune these values to your needs
# read more: https://www.getpagespeed.com/server-setup/nginx/tuning-proxy_buffer_size-in-nginx
proxy_buffer_size 4096k;
proxy_buffers 64 256k;
proxy_busy_buffers_size 4096k; # at least as high as proxy_buffer_size

View File

@ -1,21 +0,0 @@
proxy_cache skynet; # cache name
proxy_cache_key $skylink_v1$path$arg_format$arg_attachment$arg_start$arg_end$http_range; # unique cache key
proxy_cache_min_uses 3; # cache after 3 uses
proxy_cache_valid 200 206 307 308 48h; # keep 200, 206, 307 and 308 responses valid for up to 2 days
add_header X-Proxy-Cache $upstream_cache_status; # add response header to indicate cache hits and misses
# bypass - this will bypass cache hit on request (status BYPASS)
# but still stores file in cache if cache conditions are met
proxy_cache_bypass $cookie_nocache $arg_nocache;
# no cache - this will ignore cache on request (status MISS)
# and does not store file in cache under no condition
set_if_empty $nocache "0";
# map skyd env variable value to "1" for true and "0" for false (expected by proxy_no_cache)
set_by_lua_block $skyd_disk_cache_enabled {
return os.getenv("SKYD_DISK_CACHE_ENABLED") == "true" and "1" or "0"
}
# disable cache when nocache is set or skyd cache is enabled
proxy_no_cache $nocache $skyd_disk_cache_enabled;

View File

@ -1,5 +1,8 @@
# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&hsts=false&ocsp=false&guideline=5.6
ssl_certificate /etc/letsencrypt/live/skynet-portal/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/skynet-portal/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
@ -11,3 +14,13 @@ ssl_dhparam /etc/nginx/conf.d/dhparam.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate /etc/letsencrypt/live/skynet-portal/chain.pem;

View File

@ -1,8 +1,9 @@
# register the download in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if require("skynet.account").accounts_enabled() then
local function track(premature, skylink, status, body_bytes_sent, jwt)
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()
@ -11,7 +12,7 @@ log_by_lua_block {
-- 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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
headers = auth_headers,
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
@ -19,8 +20,9 @@ log_by_lua_block {
end
end
if ngx.header["Skynet-Skylink"] and ngx.var.skynet_jwt ~= "" and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.status, ngx.var.body_bytes_sent, ngx.var.skynet_jwt)
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

View File

@ -1,8 +1,9 @@
# register the registry access in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if require("skynet.account").accounts_enabled() then
local function track(premature, request_method, jwt)
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()
@ -14,7 +15,7 @@ log_by_lua_block {
-- 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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
headers = auth_headers,
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
@ -22,8 +23,9 @@ log_by_lua_block {
end
end
if ngx.var.skynet_jwt ~= "" and (ngx.status == ngx.HTTP_OK or ngx.status == ngx.HTTP_NOT_FOUND) then
local ok, err = ngx.timer.at(0, track, ngx.req.get_method(), ngx.var.skynet_jwt)
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

View File

@ -1,8 +1,9 @@
# register the upload in accounts service (cookies should contain jwt)
log_by_lua_block {
-- this block runs only when accounts are enabled
if require("skynet.account").accounts_enabled() then
local function track(premature, skylink, jwt)
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,7 +11,7 @@ log_by_lua_block {
-- 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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
headers = auth_headers,
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
@ -19,8 +20,9 @@ log_by_lua_block {
end
-- report all skylinks (header empty if request failed) but only if jwt is preset (user is authenticated)
if ngx.header["Skynet-Skylink"] and ngx.var.skynet_jwt ~= "" then
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.var.skynet_jwt)
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

View File

@ -2,14 +2,12 @@ lua_shared_dict dnslink 10m;
server {
listen 80 default_server;
listen [::]:80 default_server;
include /etc/nginx/conf.d/server/server.dnslink;
}
server {
listen 443 default_server;
listen [::]:443 default_server;
ssl_certificate /etc/ssl/local-certificate.crt;
ssl_certificate_key /etc/ssl/local-certificate.key;

View File

@ -1,10 +0,0 @@
server {
# local server - do not expose this port externally
listen 8000;
listen [::]:8000;
# secure traffic by limiting to only local networks
include /etc/nginx/conf.d/include/local-network-only;
include /etc/nginx/conf.d/server/server.local;
}

View File

@ -1,5 +1,4 @@
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
@ -36,9 +35,23 @@ location /api/register {
proxy_pass http://accounts:3000;
}
location /api/user/pubkey/register {
include /etc/nginx/conf.d/include/cors;
rewrite /api/(.*) /$1 break;
proxy_pass http://accounts:3000;
}
location /api/login {
include /etc/nginx/conf.d/include/cors;
rewrite /api/(.*) /$1 break;
proxy_pass http://accounts:3000;
}
location /api/logout {
include /etc/nginx/conf.d/include/cors;
rewrite /api/(.*) /$1 break;
proxy_pass http://accounts:3000;
}

View File

@ -1,5 +1,4 @@
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
@ -22,40 +21,42 @@ client_max_body_size 128k;
rewrite ^/portals /skynet/portals permanent;
rewrite ^/stats /skynet/stats permanent;
rewrite ^/skynet/blacklist /skynet/blocklist permanent;
rewrite ^/docs(?:/(.*))?$ https://sdk.skynetlabs.com/$1 permanent;
location / {
include /etc/nginx/conf.d/include/cors;
set $skylink "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30";
set $path $uri;
set $internal_no_limits "true";
include /etc/nginx/conf.d/include/location-skylink;
proxy_intercept_errors on;
error_page 400 404 490 500 502 503 504 =200 @fallback;
}
location @fallback {
proxy_pass http://website:9000;
}
location /docs {
proxy_pass https://skynetlabs.github.io/skynet-docs;
}
location /skynet/blocklist {
include /etc/nginx/conf.d/include/cors;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache skynet;
proxy_cache_valid any 1m; # cache blocklist for 1 minute
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980/skynet/blocklist;
}
location /skynet/portal/blocklist {
include /etc/nginx/conf.d/include/cors;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache skynet;
proxy_cache_valid 200 204 15m; # cache portal blocklist for 15 minutes
# 10.10.10.110 points to blocker service
proxy_pass http://10.10.10.110:4000/blocklist;
}
location /skynet/portals {
include /etc/nginx/conf.d/include/cors;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache skynet;
proxy_cache_valid any 1m; # cache portals for 1 minute
proxy_set_header User-Agent: Sia-Agent;
@ -65,6 +66,8 @@ location /skynet/portals {
location /skynet/stats {
include /etc/nginx/conf.d/include/cors;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache skynet;
proxy_cache_valid any 1m; # cache stats for 1 minute
proxy_set_header User-Agent: Sia-Agent;
@ -90,6 +93,8 @@ location /serverload {
location /skynet/health {
include /etc/nginx/conf.d/include/cors;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_cache skynet;
proxy_cache_key $request_uri; # use whole request uri (uri + args) as cache key
proxy_cache_valid any 1m; # cache responses for 1 minute
@ -169,25 +174,26 @@ location /skynet/registry/subscription {
set $notificationdelay "0";
rewrite_by_lua_block {
-- this block runs only when accounts are enabled
if os.getenv("PORTAL_MODULES"):match("a") then
local httpc = require("resty.http").new()
local skynet_account = require("skynet.account")
-- fetch account limits and set download bandwidth and registry delays accordingly
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits", {
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
})
if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end
-- fail gracefully in case /user/limits failed
if err or (res and res.status ~= ngx.HTTP_OK) then
ngx.log(ngx.ERR, "Failed accounts service request /user/limits: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
elseif res and res.status == ngx.HTTP_OK then
local json = require('cjson')
local limits = json.decode(res.body)
-- check if portal is in subscription only mode
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = skynet_account.get_account_limits()
-- apply bandwidth limit and notification delay
ngx.var.bandwidthlimit = limits.download
ngx.var.notificationdelay = limits.registry
end
end
}
proxy_set_header User-Agent: Sia-Agent;
@ -248,26 +254,27 @@ location /skynet/tus {
proxy_set_header X-Forwarded-Proto $scheme;
# rewrite proxy request to use correct host uri from env variable (required to return correct location header)
set_by_lua_block $server_domain { return os.getenv("SERVER_DOMAIN") }
proxy_redirect $scheme://$host $scheme://$server_domain;
proxy_redirect $scheme://$host $scheme://$skynet_server_domain;
# proxy /skynet/tus requests to siad endpoint with all arguments
proxy_pass http://sia:9980;
access_by_lua_block {
if require("skynet.account").accounts_enabled() then
local skynet_account = require("skynet.account")
if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end
-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
local limits = skynet_account.get_account_limits()
-- apply upload size limits
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
@ -276,8 +283,8 @@ location /skynet/tus {
# extract skylink from base64 encoded upload metadata and assign to a proper header
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
if ngx.header["Upload-Metadata"] then
local encodedSkylink = string.match(ngx.header["Upload-Metadata"], "Skylink ([^,?]+)")
@ -311,8 +318,8 @@ location /skynet/metadata {
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
}
proxy_set_header User-Agent: Sia-Agent;
@ -324,8 +331,8 @@ location /skynet/resolve {
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
}
proxy_set_header User-Agent: Sia-Agent;
@ -350,7 +357,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/proxy-buffer;
include /etc/nginx/conf.d/include/track-download;
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
@ -359,19 +365,21 @@ location /skynet/trustless/basesector {
set $limit_rate 0;
access_by_lua_block {
if require("skynet.account").accounts_enabled() then
local skynet_account = require("skynet.account")
if skynet_account.accounts_enabled() then
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("skynet.account").exit_access_unauthorized()
if skynet_account.is_access_unauthorized() then
return skynet_account.exit_access_unauthorized()
end
-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
if skynet_account.is_access_forbidden() then
return skynet_account.exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
local limits = skynet_account.get_account_limits()
-- apply download speed limit
ngx.var.limit_rate = limits.download
@ -394,14 +402,20 @@ location /__internal/do/not/use/accounts {
content_by_lua_block {
local json = require('cjson')
local accounts_enabled = require("skynet.account").accounts_enabled()
local is_auth_required = require("skynet.account").is_auth_required()
local is_authenticated = accounts_enabled and require("skynet.account").is_authenticated()
local skynet_account = require("skynet.account")
local accounts_enabled = skynet_account.accounts_enabled()
local is_auth_required = skynet_account.is_auth_required()
local is_subscription_required = skynet_account.is_subscription_required()
local is_authenticated = skynet_account.is_authenticated()
local has_subscription = skynet_account.has_subscription()
ngx.say(json.encode{
enabled = accounts_enabled,
auth_required = is_auth_required,
subscription_required = is_subscription_required,
authenticated = is_authenticated,
subscription = has_subscription,
})
return ngx.exit(ngx.HTTP_OK)
}

View File

@ -38,8 +38,6 @@ location / {
end
ngx.var.skylink = require("skynet.skylink").parse(ngx.var.skylink)
ngx.var.skylink_v1 = ngx.var.skylink
ngx.var.skylink_v2 = ngx.var.skylink
}
include /etc/nginx/conf.d/include/location-skylink;

View File

@ -1,5 +1,4 @@
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;

View File

@ -1,5 +1,4 @@
listen 80;
listen [::]:80;
include /etc/nginx/conf.d/include/init-optional-variables;

View File

@ -1,37 +0,0 @@
include /etc/nginx/conf.d/include/init-optional-variables;
location /skynet/blocklist {
client_max_body_size 10m; # increase max body size to account for large lists
client_body_buffer_size 10m; # force whole body to memory so we can read it
content_by_lua_block {
local httpc = require("resty.http").new()
ngx.req.read_body() -- ensure the post body data is read before using get_body_data
-- proxy blocklist update request
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/blocklist", {
method = "POST",
body = ngx.req.get_body_data(),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = require("skynet.utils").authorization_header(),
["User-Agent"] = "Sia-Agent",
}
})
-- print error and exit with 500 or exit with response if status is not 204
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
ngx.header["content-type"] = "text/plain"
ngx.say(err or res.body)
return ngx.exit(ngx.status)
end
require("skynet.blocklist").reload()
ngx.status = ngx.HTTP_NO_CONTENT
return ngx.exit(ngx.status)
}
}

View File

@ -1,5 +1,4 @@
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;

View File

@ -1,13 +1,46 @@
local _M = {}
-- constant tier ids
local tier_id_anonymous = 0
local tier_id_free = 1
-- fallback - remember to keep those updated
local anon_limits = { ["tierName"] = "anonymous", ["upload"] = 655360, ["download"] = 655360, ["maxUploadSize"] = 1073741824, ["registry"] = 250 }
local anon_limits = {
["tierID"] = tier_id_anonymous,
["tierName"] = "anonymous",
["upload"] = 655360,
["download"] = 655360,
["maxUploadSize"] = 1073741824,
["registry"] = 250
}
-- no limits applied
local no_limits = { ["tierName"] = "internal", ["upload"] = 0, ["download"] = 0, ["maxUploadSize"] = 0, ["registry"] = 0 }
-- get all non empty authentication headers from request, we want to return
-- all of them and let accounts service deal with validation and prioritisation
function _M.get_auth_headers()
local utils = require("utils")
local request_headers = ngx.req.get_headers()
local headers = {}
-- free tier name
local free_tier = "free"
-- try to extract skynet-jwt cookie from cookie header
local skynet_jwt_cookie = utils.extract_cookie(request_headers["Cookie"], "skynet[-]jwt")
-- if skynet-jwt cookie is present, pass it as is
if skynet_jwt_cookie then
headers["Cookie"] = skynet_jwt_cookie
end
-- if authorization header is set, pass it as is
if request_headers["Authorization"] then
headers["Authorization"] = request_headers["Authorization"]
end
-- if skynet api key header is set, pass it as is
if request_headers["Skynet-Api-Key"] then
headers["Skynet-Api-Key"] = request_headers["Skynet-Api-Key"]
end
return headers
end
-- handle request exit when access to portal should be restricted to authenticated users only
function _M.exit_access_unauthorized(message)
@ -31,12 +64,11 @@ end
function _M.get_account_limits()
local cjson = require('cjson')
local utils = require('utils')
local auth_headers = _M.get_auth_headers()
if ngx.var.internal_no_limits == "true" then
return no_limits
end
if ngx.var.skynet_jwt == "" then
-- simple case of anonymous request - none of available auth headers exist
if utils.is_table_empty(auth_headers) then
return anon_limits
end
@ -44,13 +76,13 @@ function _M.get_account_limits()
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/user/limits", {
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
headers = auth_headers,
})
-- fail gracefully in case /user/limits failed
if err or (res and res.status ~= ngx.HTTP_OK) then
ngx.log(ngx.ERR, "Failed accounts service request /user/limits: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
ngx.log(ngx.ERR, "Failed accounts service request /user/limits?unit=byte: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
ngx.var.account_limits = cjson.encode(anon_limits)
elseif res and res.status == ngx.HTTP_OK then
ngx.var.account_limits = res.body
@ -62,20 +94,24 @@ end
-- detect whether current user is authenticated
function _M.is_authenticated()
if not _M.accounts_enabled() then return false end
local limits = _M.get_account_limits()
return limits.tierName ~= anon_limits.tierName
return limits.tierID > tier_id_anonymous
end
-- detect whether current user has active subscription
function _M.is_subscription_account()
function _M.has_subscription()
local limits = _M.get_account_limits()
return limits.tierName ~= anon_limits.tierName and limits.tierName ~= free_tier
return limits.tierID > tier_id_free
end
function _M.is_auth_required()
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated"
-- authentication is required if mode is set to "authenticated"
-- or "subscription" (require active subscription to a premium plan)
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated" or _M.is_subscription_required()
end
function _M.is_subscription_required()
@ -101,7 +137,7 @@ function _M.is_access_forbidden()
if is_access_always_allowed() then return false end
-- check if active subscription is required and request is from user without it
return _M.is_subscription_required() and not _M.is_subscription_account()
return _M.is_subscription_required() and not _M.has_subscription()
end
return _M

View File

@ -1,66 +0,0 @@
local _M = {}
function _M.reload()
local httpc = require("resty.http").new()
-- fetch blocklist records (all blocked skylink hashes)
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.10:9980/skynet/blocklist", {
headers = {
["User-Agent"] = "Sia-Agent",
}
})
-- fail whole request in case this request failed, we want to make sure
-- the blocklist is pre cached before serving first skylink
if err or (res and res.status ~= ngx.HTTP_OK) then
ngx.log(ngx.ERR, "Failed skyd service request /skynet/blocklist: ", err or ("[HTTP " .. res.status .. "] " .. res.body))
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
ngx.header["content-type"] = "text/plain"
ngx.say(err or res.body)
return ngx.exit(ngx.status)
elseif res and res.status == ngx.HTTP_OK then
local json = require('cjson')
local data = json.decode(res.body)
-- mark all existing entries as expired
ngx.shared.blocklist:flush_all()
-- check if blocklist is table (it is null when empty)
if type(data.blocklist) == "table" then
-- set all cache entries one by one (resets expiration)
for i, hash in ipairs(data.blocklist) do
ngx.shared.blocklist:set(hash, true)
end
end
-- ensure that init flag is persisted
ngx.shared.blocklist:set("__init", true)
-- remove all leftover expired entries
ngx.shared.blocklist:flush_expired()
end
end
function _M.is_blocked(skylink)
-- make sure that blocklist has been preloaded
if not ngx.shared.blocklist:get("__init") then _M.reload() end
-- hash skylink before comparing it with blocklist
local hash = require("skynet.skylink").hash(skylink)
-- we need to use get_stale because we are expiring previous
-- entries when the blocklist is reloading and we still want
-- to block them until the reloading is finished
return ngx.shared.blocklist:get_stale(hash) == true
end
-- exit with 416 illegal content status code
function _M.exit_illegal()
ngx.status = ngx.HTTP_ILLEGAL
ngx.header["content-type"] = "text/plain"
ngx.say("Unavailable For Legal Reasons")
return ngx.exit(ngx.status)
end
return _M

View File

@ -0,0 +1,45 @@
local _M = {}
-- utility function for checking if table is empty
function _M.is_table_empty(check)
-- bind next to local variable to achieve ultimate efficiency
-- https://stackoverflow.com/a/1252776
local next = next
return next(check) == nil
end
-- extract full cookie name and value by its name from cookie string
-- note: name matcher argument is a pattern so you will need to escape
-- any special characters, read more https://www.lua.org/pil/20.2.html
function _M.extract_cookie(cookie_string, name_matcher)
-- nil cookie string safeguard
if cookie_string == nil then
return nil
end
local start, stop = string.find(cookie_string, name_matcher .. "=[^;]+")
if start then
return string.sub(cookie_string, start, stop)
end
return nil
end
-- extract just the cookie value by its name from cookie string
-- note: name matcher argument is a pattern so you will need to escape
-- any special characters, read more https://www.lua.org/pil/20.2.html
function _M.extract_cookie_value(cookie_string, name_matcher)
local cookie = _M.extract_cookie(cookie_string, name_matcher)
if cookie == nil then
return nil
end
local value_start = string.find(cookie, "=") + 1
return string.sub(cookie, value_start)
end
return _M

View File

@ -0,0 +1,79 @@
local utils = require('utils')
describe("is_table_empty", function()
it("should return true for empty table", function()
assert.is_true(utils.is_table_empty({}))
end)
it("should return false for not empty table", function()
assert.is_false(utils.is_table_empty({ ["foo"] = "bar" }))
end)
end)
describe("extract_cookie", function()
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
it("should return nil if cookie string is nil", function()
local cookie = utils.extract_cookie_value(nil, "aaa")
assert.is_nil(cookie)
end)
it("should return nil if cookie name is not found", function()
local cookie = utils.extract_cookie(cookie_string, "foo")
assert.is_nil(cookie)
end)
it("should return cookie if cookie_string starts with that cookie name", function()
local cookie = utils.extract_cookie(cookie_string, "aaa")
assert.are.equals(cookie, "aaa=bbb")
end)
it("should return cookie if cookie_string ends with that cookie name", function()
local cookie = utils.extract_cookie(cookie_string, "xxx")
assert.are.equals(cookie, "xxx=yyy")
end)
it("should return cookie with custom matcher", function()
local cookie = utils.extract_cookie(cookie_string, "skynet[-]jwt")
assert.are.equals(cookie, "skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
end)
end)
describe("extract_cookie_value", function()
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
it("should return nil if cookie string is nil", function()
local value = utils.extract_cookie_value(nil, "aaa")
assert.is_nil(value)
end)
it("should return nil if cookie name is not found", function()
local value = utils.extract_cookie_value(cookie_string, "foo")
assert.is_nil(value)
end)
it("should return value if cookie_string starts with that cookie name", function()
local value = utils.extract_cookie_value(cookie_string, "aaa")
assert.are.equals(value, "bbb")
end)
it("should return cookie if cookie_string ends with that cookie name", function()
local value = utils.extract_cookie_value(cookie_string, "xxx")
assert.are.equals(value, "yyy")
end)
it("should return cookie with custom matcher", function()
local value = utils.extract_cookie_value(cookie_string, "skynet[-]jwt")
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
end)
end)

View File

@ -31,7 +31,6 @@ env SERVER_DOMAIN;
env PORTAL_MODULES;
env ACCOUNTS_LIMIT_ACCESS;
env SIA_API_PASSWORD;
env SKYD_DISK_CACHE_ENABLED;
events {
worker_connections 8192;
@ -50,7 +49,7 @@ http {
'"$upstream_http_content_type" "$upstream_cache_status" '
'"$server_alias" "$sent_http_skynet_skylink" '
'$upstream_connect_time $upstream_header_time '
'$request_time "$hns_domain" "$skylink"';
'$request_time "$hns_domain" "$skylink" $upstream_http_skynet_cache_ratio';
access_log logs/access.log main;
@ -75,28 +74,18 @@ http {
# proxy cache definition
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=50g min_free=100g inactive=48h use_temp_path=off;
# create a shared blocklist dictionary with size of 30 megabytes
# estimated capacity of 1 megabyte dictionary is 3500 blocklist entries
# that gives us capacity of around 100k entries in 30 megabyte dictionary
lua_shared_dict blocklist 30m;
# create a shared dictionary to fill with skylinks that should not
# be cached due to the large size or some other reasons
lua_shared_dict nocache 10m;
# this runs before forking out nginx worker processes
init_by_lua_block {
require "cjson"
require "resty.http"
require "skynet.blocklist"
require "skynet.skylink"
require "skynet.utils"
}
# include skynet-portal-api and skynet-server-api header on every request
header_filter_by_lua_block {
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. os.getenv("PORTAL_DOMAIN")
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. os.getenv("SERVER_DOMAIN")
ngx.header["Skynet-Portal-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
}
# ratelimit specified IPs
@ -128,13 +117,6 @@ http {
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# skynet-jwt contains dash so we cannot use $cookie_skynet-jwt
# https://richardhart.me/2012/03/18/logging-nginx-cookies-with-dashes/
map $http_cookie $skynet_jwt {
default '';
~skynet-jwt=(?<match>[^\;]+) $match;
}
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.extra.d/*.conf;
}

View File

@ -0,0 +1,4 @@
node_modules/
.cache/
public/
storybook-build/

View File

@ -0,0 +1,6 @@
module.exports = {
globals: {
__PATH_PREFIX__: true,
},
extends: ["react-app", "plugin:storybook/recommended"],
};

View File

@ -1,3 +1,4 @@
node_modules/
.cache/
public
public/
storybook-build/

View File

@ -0,0 +1,4 @@
node_modules/
.cache/
public/
storybook-build/

View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@ -0,0 +1,19 @@
module.exports = {
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-gatsby",
{
name: "@storybook/addon-postcss",
options: {
postcssLoaderOptions: {
implementation: require("postcss"),
},
},
},
],
core: {
builder: "webpack5",
},
};

View File

@ -0,0 +1,20 @@
import "tailwindcss/tailwind.css";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
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";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
layout: "fullscreen",
};

View File

@ -2,9 +2,9 @@
Code behind [account.skynetpro.net](https://account.skynetpro.net/)
## Development
This is a Gatsby application. To run it locally, all you need is:
* `yarn install`
* `yarn start`
- `yarn install`
- `yarn start`

View File

@ -1,4 +1,4 @@
import React from "react";
import * as React from "react";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
@ -8,6 +8,6 @@ import "@fontsource/source-sans-pro/600.css"; // semibold
import "./src/styles/global.css";
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment
return <Layout {...props}>{element}</Layout>
const Layout = element.type.Layout ?? React.Fragment;
return <Layout {...props}>{element}</Layout>;
}

View File

@ -1,31 +1,35 @@
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = {
siteMetadata: {
title: `Accounts Dashboard`,
siteUrl: `https://www.yourdomain.tld`
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [
"gatsby-plugin-image",
"gatsby-plugin-provide-react",
"gatsby-plugin-react-helmet",
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
"gatsby-plugin-postcss", {
resolve: 'gatsby-source-filesystem',
"gatsby-plugin-styled-components",
"gatsby-plugin-postcss",
{
resolve: "gatsby-source-filesystem",
options: {
"name": "images",
"path": "./src/images/"
name: "images",
path: "./src/images/",
},
__key: "images"
}, {
resolve: `gatsby-plugin-alias-imports`,
options: {
alias: {
// Allows npm link-ing skynet-storybook during development.
"styled-components": "./node_modules/styled-components",
__key: "images",
},
extensions: [
"js",
],
}
}
]
developMiddleware: (app) => {
app.use(
"/api/",
createProxyMiddleware({
target: "https://account.siasky.net",
secure: false, // Do not reject self-signed certificates.
changeOrigin: true,
})
);
},
};

View File

@ -0,0 +1,13 @@
import * as React from "react";
import "@fontsource/sora/300.css"; // light
import "@fontsource/sora/400.css"; // normal
import "@fontsource/sora/500.css"; // medium
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";
export function wrapPageElement({ element, props }) {
const Layout = element.type.Layout ?? React.Fragment;
return <Layout {...props}>{element}</Layout>;
}

View File

@ -12,29 +12,66 @@
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
"clean": "gatsby clean"
"clean": "gatsby clean",
"lint": "eslint .",
"prettier": "prettier .",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook -o storybook-build"
},
"dependencies": {
"@fontsource/sora": "^4.5.0",
"@fontsource/source-sans-pro": "^4.5.1",
"babel-plugin-styled-components": "^2.0.2",
"@fontsource/sora": "^4.5.3",
"@fontsource/source-sans-pro": "^4.5.3",
"classnames": "^2.3.1",
"copy-text-to-clipboard": "^3.0.1",
"dayjs": "^1.10.8",
"gatsby": "^4.6.2",
"gatsby-plugin-image": "^2.6.0",
"gatsby-plugin-react-helmet": "^5.6.0",
"gatsby-plugin-sharp": "^4.6.0",
"gatsby-plugin-styled-components": "^5.7.0",
"gatsby-source-filesystem": "^4.6.0",
"gatsby-transformer-sharp": "^4.6.0",
"gatsby-plugin-postcss": "^5.7.0",
"http-status-codes": "^2.2.0",
"nanoid": "^3.3.1",
"path-browserify": "^1.0.1",
"postcss": "^8.4.6",
"pretty-bytes": "^6.0.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropzone": "^12.0.4",
"react-helmet": "^6.1.0",
"tailwindcss": "^3.0.22"
"react-use": "^17.3.2",
"skynet-js": "^3.0.2",
"swr": "^1.2.2",
"tailwindcss": "^3.0.23"
},
"devDependencies": {
"@babel/core": "^7.17.4",
"@storybook/addon-actions": "^6.4.19",
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-interactions": "^6.4.19",
"@storybook/addon-links": "^6.4.19",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack5": "^6.4.19",
"@storybook/manager-webpack5": "^6.4.19",
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"autoprefixer": "^10.4.2",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.3",
"babel-plugin-preval": "^5.1.0",
"babel-plugin-styled-components": "^2.0.2",
"eslint": "^8.9.0",
"eslint-config-react-app": "^7.0.0",
"eslint-plugin-storybook": "^0.5.6",
"gatsby-plugin-alias-imports": "^1.0.5",
"gatsby-plugin-postcss": "^5.7.0",
"postcss": "^8.4.6",
"gatsby-plugin-image": "^2.6.0",
"gatsby-plugin-preval": "^1.0.0",
"gatsby-plugin-provide-react": "^1.0.2",
"gatsby-plugin-react-helmet": "^5.6.0",
"gatsby-plugin-sharp": "^4.6.0",
"gatsby-plugin-styled-components": "^5.8.0",
"gatsby-source-filesystem": "^4.6.0",
"gatsby-transformer-sharp": "^4.6.0",
"http-proxy-middleware": "^1.3.1",
"prettier": "2.5.1",
"react-is": "^17.0.2",
"storybook-addon-gatsby": "^0.0.5",
"styled-components": "^5.3.3"
}
}

View File

@ -1,3 +1,3 @@
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer")],
plugins: [require("tailwindcss/nesting"), require("tailwindcss"), require("autoprefixer")],
};

View File

@ -0,0 +1,35 @@
import { useEffect, useState } from "react";
import { useUser } from "../../contexts/user";
import { SimpleUploadIcon } from "../Icons";
const AVATAR_PLACEHOLDER = "/images/avatar-placeholder.svg";
export const AvatarUploader = (props) => {
const { user } = useUser();
const [imageUrl, setImageUrl] = useState(AVATAR_PLACEHOLDER);
useEffect(() => {
setImageUrl(user.avatarUrl ?? AVATAR_PLACEHOLDER);
}, [user]);
return (
<div {...props}>
<div
className={`flex justify-center items-center xl:w-[245px] xl:h-[245px] bg-contain bg-none xl:bg-[url(/images/avatar-bg.svg)]`}
>
<img src={imageUrl} className="w-[160px]" alt="" />
</div>
<div className="flex justify-center">
<button
className="flex items-center gap-4 hover:underline decoration-1 decoration-dashed underline-offset-2 decoration-gray-400"
type="button"
onClick={console.info.bind(console)}
>
<SimpleUploadIcon size={20} className="shrink-0" /> Upload profile picture
</button>
{/* TODO: actual uploading */}
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./AvatarUploader";

View File

@ -0,0 +1,27 @@
import PropTypes from "prop-types";
import styled from "styled-components";
/**
* Primary UI component for user interaction
*/
export const Button = styled.button.attrs(({ disabled, $primary }) => ({
type: "button",
className: `px-6 py-2.5 rounded-full font-sans uppercase text-xs tracking-wide text-palette-600 transition-[filter]
${$primary ? "bg-primary" : "bg-white border-2 border-black"}
${disabled ? "saturate-50 brightness-125 cursor-default text-palette-400" : "hover:brightness-90"}`,
}))``;
Button.propTypes = {
/**
* Is this the principal call to action on the page?
*/
$primary: PropTypes.bool,
/**
* Prevent interaction on the button
*/
disabled: PropTypes.bool,
};
Button.defaultProps = {
$primary: false,
disabled: false,
};

View File

@ -1,18 +1,20 @@
import React from "react";
import { Button } from "./Button";
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: "Example/Button",
title: "SkynetLibrary/Button",
component: Button,
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {
backgroundColor: { control: "color" },
},
};
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template = (args) => <Button {...args} />;
export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Primary.args = {
primary: true,
label: "Button",

View File

@ -0,0 +1 @@
export * from "./Button";

View File

@ -0,0 +1,50 @@
import { useCallback, useRef, useState } from "react";
import copy from "copy-text-to-clipboard";
import styled from "styled-components";
import { useClickAway } from "react-use";
import { CopyIcon } from "./Icons";
const Button = styled.button.attrs({
className: "relative inline-flex items-center hover:text-primary",
})``;
const TooltipContainer = styled.div.attrs(({ $visible }) => ({
className: `absolute left-full top-1/2 z-10
bg-white rounded border border-primary/30 shadow-md
pointer-events-none transition-opacity duration-150 ease-in-out
${$visible ? "opacity-100" : "opacity-0"}`,
}))`
transform: translateY(-50%);
`;
const TooltipContent = styled.div.attrs({
className: "bg-primary-light/10 text-palette-600 py-2 px-4 ",
})``;
export const CopyButton = ({ value, className }) => {
const containerRef = useRef();
const [copied, setCopied] = useState(false);
const [timer, setTimer] = useState(null);
const handleCopy = useCallback(() => {
clearTimeout(timer);
copy(value);
setCopied(true);
setTimer(setTimeout(() => setCopied(false), 1500));
}, [value, timer]);
useClickAway(containerRef, () => setCopied(false));
return (
<div ref={containerRef} className={`inline-flex relative overflow-visible pr-2 ${className ?? ""}`}>
<Button onClick={handleCopy} className={copied ? "text-primary" : ""}>
<CopyIcon size={16} />
</Button>
<TooltipContainer $visible={copied}>
<TooltipContent>Copied to clipboard</TooltipContent>
</TooltipContainer>
</div>
);
};

View File

@ -0,0 +1,48 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useUser } from "../../contexts/user";
import useActivePlan from "../../hooks/useActivePlan";
import { ContainerLoadingIndicator } from "../LoadingIndicator";
import LatestPayment from "./LatestPayment";
import SuggestedPlan from "./SuggestedPlan";
dayjs.extend(relativeTime);
const CurrentPlan = () => {
const { user, error: userError } = useUser();
const { plans, activePlan, error: plansError } = useActivePlan(user);
if (!user || !activePlan) {
return <ContainerLoadingIndicator />;
}
if (userError || plansError) {
return (
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
<p>An error occurred while loading this data.</p>
<p>We'll retry automatically.</p>
</div>
);
}
return (
<div>
<h4>{activePlan.name}</h4>
<div className="text-palette-400">
{activePlan.price === 0 && <p>100GB without paying a dime! 🎉</p>}
{activePlan.price !== 0 &&
(user.subscriptionCancelAtPeriodEnd ? (
<p>Your subscription expires {dayjs(user.subscribedUntil).fromNow()}</p>
) : (
<p className="first-letter:uppercase">{dayjs(user.subscribedUntil).fromNow(true)} until the next payment</p>
))}
<LatestPayment user={user} />
<SuggestedPlan plans={plans} activePlan={activePlan} />
</div>
</div>
);
};
export default CurrentPlan;

View File

@ -0,0 +1,18 @@
import dayjs from "dayjs";
// TODO: this is not an accurate information, we need this data from the backend
const LatestPayment = ({ user }) => (
<div className="flex mt-6 justify-between items-center bg-palette-100/50 py-4 px-6 border-l-2 border-primary">
<div className="flex flex-col lg:flex-row">
<span>Latest payment</span>
<span className="lg:before:content-['-'] lg:before:px-2 text-xs lg:text-base">
{dayjs(user.subscribedUntil).subtract(1, "month").format("MM/DD/YYYY")}
</span>
</div>
<div>
<span className="rounded py-1 px-2 bg-primary/10 font-sans text-primary uppercase text-xs">Success</span>
</div>
</div>
);
export default LatestPayment;

View File

@ -0,0 +1,24 @@
import { Link } from "gatsby";
import { useMemo } from "react";
import { Button } from "../Button";
const SuggestedPlan = ({ plans, activePlan }) => {
const nextPlan = useMemo(() => plans.find(({ tier }) => tier > activePlan.tier), [plans, activePlan]);
if (!nextPlan) {
return null;
}
return (
<div className="mt-7">
<p className="font-sans font-semibold text-xs uppercase text-primary">Discover {nextPlan.name}</p>
<p className="pt-1 text-xs sm:text-base">{nextPlan.description}</p>
<Button $primary as={Link} to={`/upgrade?selectedPlan=${nextPlan.id}`} className="mt-6">
Upgrade
</Button>
</div>
);
};
export default SuggestedPlan;

View File

@ -0,0 +1,3 @@
import CurrentPlan from "./CurrentPlan";
export default CurrentPlan;

View File

@ -0,0 +1,111 @@
import { useEffect, useMemo, useState } from "react";
import fileSize from "pretty-bytes";
import { Link } from "gatsby";
import useSWR from "swr";
import { useUser } from "../../contexts/user";
import useActivePlan from "../../hooks/useActivePlan";
import { ContainerLoadingIndicator } from "../LoadingIndicator";
import { GraphBar } from "./GraphBar";
import { UsageGraph } from "./UsageGraph";
const useUsageData = () => {
const { user } = useUser();
const { activePlan, error } = useActivePlan(user);
const { data: stats, error: statsError } = useSWR("user/stats");
const [loading, setLoading] = useState(true);
const [usage, setUsage] = useState({});
const hasError = error || statsError;
const hasData = activePlan && stats;
useEffect(() => {
if (hasData || hasError) {
setLoading(false);
}
if (hasData && !hasError) {
setUsage({
filesUsed: stats?.numUploads,
filesLimit: activePlan?.limits?.maxNumberUploads,
storageUsed: stats?.totalUploadsSize,
storageLimit: activePlan?.limits?.storageLimit,
});
}
}, [hasData, hasError, stats, activePlan]);
return {
error: error || statsError,
loading,
usage,
};
};
const size = (bytes) => {
const text = fileSize(bytes ?? 0, { maximumFractionDigits: 0 });
const [value, unit] = text.split(" ");
return {
text,
value,
unit,
};
};
const ErrorMessage = () => (
<div className="flex text-palette-300 flex-col space-y-4 h-full justify-center items-center">
<p>We were not able to fetch the current usage data.</p>
<p>We'll try again automatically.</p>
</div>
);
export default function CurrentUsage() {
const { usage, error, loading } = useUsageData();
const storageUsage = size(usage.storageUsed);
const storageLimit = size(usage.storageLimit);
const filesUsedLabel = useMemo(() => ({ value: usage.filesUsed, unit: "files" }), [usage.filesUsed]);
if (loading) {
return <ContainerLoadingIndicator />;
}
if (error) {
return <ErrorMessage />;
}
return (
<>
<h4>
{storageUsage.text} of {storageLimit.text}
</h4>
<p className="text-palette-400">
{usage.filesUsed} of {usage.filesLimit} files
</p>
<div className="relative mt-7 font-sans uppercase text-xs">
<div className="flex place-content-between">
<span>Storage</span>
<span>{storageLimit.text}</span>
</div>
<UsageGraph>
<GraphBar value={usage.storageUsed} limit={usage.storageLimit} label={storageUsage} />
<GraphBar value={usage.filesUsed} limit={usage.filesLimit} label={filesUsedLabel} />
</UsageGraph>
<div className="flex place-content-between">
<span>Files</span>
<span className="inline-flex place-content-between w-[37%]">
<Link
to="/upgrade"
className="text-primary underline-offset-3 decoration-dotted hover:text-primary-light hover:underline"
>
UPGRADE
</Link>{" "}
{/* TODO: proper URL */}
<span>{usage.filesLimit}</span>
</span>
</div>
</div>
</>
);
}

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