Compare commits

..

5 Commits

Author SHA1 Message Date
Ivaylo Novakov b7d9c35c8a
Don't cache in lua.
Remove more unnecessary code.
2022-03-15 10:51:25 +01:00
Ivaylo Novakov fc24b64479
CLeanup. 2022-03-14 19:00:16 +01:00
Ivaylo Novakov 40b5d41f9d
Fix bypass. 2022-03-14 18:50:24 +01:00
Ivaylo Novakov 803638bec5
Fix syntax 2022-03-14 18:47:14 +01:00
Ivaylo Novakov ae7dac59cc
Disable nginx proxy cache and rate limits. 2022-03-14 18:45:04 +01:00
495 changed files with 55454 additions and 316 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @kwypchlo @meeh0w

View File

@ -1,6 +1,54 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/packages/dashboard"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/packages/dnslink-api"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/packages/handshake-api"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/packages/health-check"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/packages/website"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/caddy"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/nginx"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/docker/sia"
schedule:
interval: monthly
interval: weekly
- package-ecosystem: docker
directory: "/packages/dashboard"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/packages/dnslink-api"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/packages/handshake-api"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/packages/health-check"
schedule:
interval: weekly
- package-ecosystem: docker
directory: "/packages/website"
schedule:
interval: weekly

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '32 21 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

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 }}

51
.github/workflows/deploy-website.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Deploy website to Skynet
on:
push:
branches:
- master
paths:
- "packages/website/**"
pull_request:
paths:
- "packages/website/**"
defaults:
run:
working-directory: packages/website
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 build
- name: "Integration tests"
uses: cypress-io/github-action@v2
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
working-directory: packages/website
install: false
record: true
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

@ -0,0 +1,24 @@
name: Lint - packages/dashboard
on:
pull_request:
paths:
- packages/dashboard/**
defaults:
run:
working-directory: packages/dashboard
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 next lint

View File

@ -0,0 +1,23 @@
name: Lint - packages/dnslink-api
on:
pull_request:
paths:
- packages/dnslink-api/**
defaults:
run:
working-directory: packages/dnslink-api
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 .

View File

@ -0,0 +1,23 @@
name: Lint - packages/handshake-api
on:
pull_request:
paths:
- packages/handshake-api/**
defaults:
run:
working-directory: packages/handshake-api
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 .

View File

@ -0,0 +1,23 @@
name: Lint - packages/health-check
on:
pull_request:
paths:
- packages/health-check/**
defaults:
run:
working-directory: packages/health-check
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 .

View File

@ -0,0 +1,23 @@
name: Lint - packages/website
on:
pull_request:
paths:
- packages/website/**
defaults:
run:
working-directory: packages/website
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 .

View File

@ -0,0 +1,33 @@
# Install and run unit tests with busted
# Docs: http://olivinelabs.com/busted/
name: Nginx Lua Unit Tests
on:
pull_request:
paths:
- "docker/nginx/libs/**.lua"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.x"
architecture: "x64"
- name: Install Dependencies
run: |
pip install hererocks
hererocks env --lua=5.1 -rlatest
source env/bin/activate
luarocks install busted
luarocks install hasher
- name: Unit Tests
run: |
source env/bin/activate
busted --verbose --pattern=spec --directory=docker/nginx/libs .

View File

@ -0,0 +1,23 @@
name: Test - packages/health-check
on:
pull_request:
paths:
- packages/health-check/**
defaults:
run:
working-directory: packages/health-check
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 jest

8
.gitignore vendored
View File

@ -67,6 +67,10 @@ yarn-error.log
# Yarn Integrity file
.yarn-integrity
# Cypress
packages/website/cypress/screenshots
packages/website/cypress/videos
# Docker data
docker/data
@ -82,10 +86,6 @@ __pycache__
/.idea/
/venv*
# Luacov file
luacov.stats.out
luacov.report.out
# Setup-script log files
setup-scripts/serverload.log
setup-scripts/serverload.json

View File

@ -3,10 +3,27 @@
## Latest Setup Documentation
Latest Skynet Webportal setup documentation and the setup process Skynet Labs
supports is located at https://portal-docs.skynetlabs.com/.
supports is located at https://docs.siasky.net/webportal-management/overview.
Some scripts and setup documentation contained in this repository
(`skynet-webportal`) may be outdated and generally should not be used.
Some of the scripts and setup documentation contained in this repository
(`skynet-webportal`) can be outdated and generally should not be used.
## Web application
Change current directory with `cd packages/website`.
Use `yarn start` to start the development server.
Use `yarn build` to compile the application to `/public` directory.
You can use the below build parameters to customize your web application.
- development example `GATSBY_API_URL=https://siasky.dev yarn start`
- production example `GATSBY_API_URL=https://siasky.net yarn build`
List of available parameters:
- `GATSBY_API_URL`: override api url (defaults to location origin)
## License
@ -16,3 +33,19 @@ and distribute the software, but you must preserve the payment mechanism in the
For the purposes of complying with our code license, you can use the following Siacoin address:
`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.
## Contributing
### Testing Your Code
Before pushing your code, you should verify that it will pass our online test suite.
**Cypress Tests**
Verify the Cypress test suite by doing the following:
1. In one terminal screen run `GATSBY_API_URL=https://siasky.net website serve`
1. In a second terminal screen run `yarn cypress run`

View File

@ -1,2 +0,0 @@
- Fix `dashboard-v2` Dockerfile context in `docker-compose.accounts.yml` to
avoid Ansible deploy (docker compose build) `permission denied` issues.

View File

@ -1 +0,0 @@
- Fix missing `logs` dir that is required for backup scripts (cron jobs).

View File

@ -1 +0,0 @@
- Add Pinner service to the portal stack. Activate it by selecting the 'p' module.

34
dc
View File

@ -5,59 +5,51 @@
# would use docker-compose with the only difference being that you don't need to specify compose files. For more
# information you can run `./dc` or `./dc help`.
# get current working directory of this script and prefix all files with it to
# be able to call this script from anywhere and not only root directory of
# skynet-webportal project
cwd="$(dirname -- "$0";)";
# get portal modules configuration from .env file (if defined more than once, the last one is used)
if [[ -f "${cwd}/.env" ]]; then
PORTAL_MODULES=$(grep -e "^PORTAL_MODULES=" ${cwd}/.env | tail -1 | sed "s/PORTAL_MODULES=//")
if [ -f .env ]; then
OLD_IFS=$IFS
IFS=$'\n'
for x in $(grep -v '^#.*' .env); do export $x; done
IFS=$OLD_IFS
fi
# include base docker compose file
COMPOSE_FILES="-f ${cwd}/docker-compose.yml"
COMPOSE_FILES="-f docker-compose.yml"
for i in $(seq 1 ${#PORTAL_MODULES}); do
# accounts module - alias "a"
if [[ ${PORTAL_MODULES:i-1:1} == "a" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.accounts.yml"
COMPOSE_FILES+=" -f docker-compose.mongodb.yml -f docker-compose.accounts.yml"
fi
# blocker module - alias "b"
if [[ ${PORTAL_MODULES:i-1:1} == "b" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.blocker.yml"
COMPOSE_FILES+=" -f docker-compose.mongodb.yml -f docker-compose.blocker.yml"
fi
# jaeger module - alias "j"
if [[ ${PORTAL_MODULES:i-1:1} == "j" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.jaeger.yml"
COMPOSE_FILES+=" -f docker-compose.jaeger.yml"
fi
# malware-scanner module - alias "s"
if [[ ${PORTAL_MODULES:i-1:1} == "s" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.blocker.yml -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.malware-scanner.yml"
COMPOSE_FILES+=" -f docker-compose.blocker.yml -f docker-compose.mongodb.yml -f docker-compose.malware-scanner.yml"
fi
# mongodb module - alias "m"
if [[ ${PORTAL_MODULES:i-1:1} == "m" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml"
COMPOSE_FILES+=" -f docker-compose.mongodb.yml"
fi
# abuse-scanner module - alias "u"
if [[ ${PORTAL_MODULES:i-1:1} == "u" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.blocker.yml -f ${cwd}/docker-compose.abuse-scanner.yml"
fi
# pinner module - alias "p"
if [[ ${PORTAL_MODULES:i-1:1} == "p" ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.mongodb.yml -f ${cwd}/docker-compose.pinner.yml"
COMPOSE_FILES+=" -f docker-compose.mongodb.yml -f docker-compose.blocker.yml -f docker-compose.abuse-scanner.yml"
fi
done
# override file if exists
if [[ -f docker-compose.override.yml ]]; then
COMPOSE_FILES+=" -f ${cwd}/docker-compose.override.yml"
COMPOSE_FILES+=" -f docker-compose.override.yml"
fi
docker-compose $COMPOSE_FILES $@

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -8,9 +8,7 @@ x-logging: &default-logging
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:0.4.0
image: skynetlabs/abuse-scanner
container_name: abuse-scanner
restart: unless-stopped
logging: *default-logging
@ -36,6 +34,3 @@ services:
depends_on:
- mongo
- blocker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /tmp:/tmp

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -20,9 +20,11 @@ services:
- ACCOUNTS_LIMIT_ACCESS=${ACCOUNTS_LIMIT_ACCESS:-authenticated} # default to authenticated access only
accounts:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/skynet-accounts.git#main
image: skynetlabs/skynet-accounts:1.3.0
build:
context: ./docker/accounts
dockerfile: Dockerfile
args:
branch: main
container_name: accounts
restart: unless-stopped
logging: *default-logging
@ -55,23 +57,23 @@ services:
- mongo
dashboard:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-accounts-dashboard.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-accounts-dashboard:2.1.1
build:
context: ./packages/dashboard
dockerfile: Dockerfile
container_name: dashboard
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- NEXT_PUBLIC_PORTAL_DOMAIN=${PORTAL_DOMAIN}
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY}
volumes:
- ./docker/data/dashboard/.cache:/usr/app/.cache
- ./docker/data/dashboard/public:/usr/app/public
- ./docker/data/dashboard/.next:/usr/app/.next
networks:
shared:
ipv4_address: 10.10.10.85
expose:
- 9000
- 3000
depends_on:
- mongo

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -13,9 +13,7 @@ services:
- BLOCKER_PORT=4000
blocker:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/blocker.git#main
image: skynetlabs/blocker:0.1.2
image: skynetlabs/blocker
container_name: blocker
restart: unless-stopped
logging: *default-logging

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -21,7 +21,7 @@ services:
- JAEGER_REPORTER_LOG_SPANS=false
jaeger-agent:
image: jaegertracing/jaeger-agent:1.38.1
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:1.38.1
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:1.38.1
image: jaegertracing/jaeger-query:1.32.0
entrypoint: /wait_to_start.sh
container_name: jaeger-query
restart: on-failure
@ -93,7 +93,7 @@ services:
- elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.6
image: docker.elastic.co/elasticsearch/elasticsearch:7.13.2
container_name: elasticsearch
restart: on-failure
logging: *default-logging

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -17,14 +17,16 @@ services:
- ./docker/clamav/clamd.conf:/etc/clamav/clamd.conf:ro
expose:
- 3310 # NEVER expose this outside of the local network!
deploy:
resources:
limits:
cpus: "${CLAMAV_CPU:-0.50}"
networks:
shared:
ipv4_address: 10.10.10.100
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:0.1.0
image: skynetlabs/malware-scanner
container_name: malware-scanner
restart: unless-stopped
logging: *default-logging

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -14,8 +14,8 @@ services:
- MONGODB_PASSWORD=${SKYNET_DB_PASS}
mongo:
image: mongo:4.4.17
command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet} --setParameter ShardingTaskExecutorPoolMinSize=10
image: mongo:4.4.1
command: --keyFile=/data/mgkey --replSet=${SKYNET_DB_REPLICASET:-skynet}
container_name: mongo
restart: unless-stopped
logging: *default-logging

View File

@ -1,30 +0,0 @@
version: "3.8"
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
pinner:
# uncomment "build" and comment out "image" to build from sources
# build: https://github.com/SkynetLabs/pinner.git#main
image: skynetlabs/pinner:0.7.8
container_name: pinner
restart: unless-stopped
logging: *default-logging
env_file:
- .env
volumes:
- ./docker/data/pinner/logs:/logs
environment:
- PINNER_LOG_LEVEL=${PINNER_LOG_LEVEL:-info}
expose:
- 4000
networks:
shared:
ipv4_address: 10.10.10.130
depends_on:
- mongo
- sia

View File

@ -1,4 +1,4 @@
version: "3.8"
version: "3.7"
x-logging: &default-logging
driver: json-file
@ -15,19 +15,16 @@ networks:
services:
sia:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/docker-skyd.git#main
# dockerfile: scratch/Dockerfile
# args:
# branch: master
image: skynetlabs/skyd:1.6.9
command: --disable-api-security --api-addr :9980 --modules gctwra
build:
context: ./docker/sia
dockerfile: Dockerfile
args:
branch: portal-latest
container_name: sia
restart: unless-stopped
stop_grace_period: 5m
logging: *default-logging
environment:
- SIA_MODULES=gctwra
- 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}
@ -42,55 +39,40 @@ services:
expose:
- 9980
certbot:
# replace this image with the image supporting your dns provider from
# https://hub.docker.com/r/certbot/certbot and adjust CERTBOT_ARGS env variable
# note: you will need to authenticate your dns request so consult the plugin docs
# configuration https://eff-certbot.readthedocs.io/en/stable/using.html#dns-plugins
#
# =================================================================================
# example docker-compose.yml changes required for Cloudflare dns provider:
#
# image: certbot/dns-cloudflare
# environment:
# - CERTBOT_ARGS=--dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini
#
# create ./docker/data/certbot/cloudflare.ini file with the following content:
# dns_cloudflare_api_token = <api key generated at https://dash.cloudflare.com/profile/api-tokens>
#
# make sure that the file has 0400 permissions with:
# chmod 0400 ./docker/data/certbot/cloudflare.ini
image: certbot/dns-route53:v1.31.0
entrypoint: sh /entrypoint.sh
container_name: certbot
caddy:
build:
context: ./docker/caddy
dockerfile: Dockerfile
container_name: caddy
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- CERTBOT_ARGS=--dns-route53
volumes:
- ./docker/certbot/entrypoint.sh:/entrypoint.sh
- ./docker/data/certbot:/etc/letsencrypt
- ./docker/data/caddy/data:/data
- ./docker/data/caddy/config:/config
networks:
shared:
ipv4_address: 10.10.10.20
nginx:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-nginx.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-nginx:1.0.0
build:
context: ./docker/nginx
dockerfile: Dockerfile
container_name: nginx
restart: unless-stopped
logging: *default-logging
env_file:
- .env
environment:
- SKYD_DISK_CACHE_ENABLED=${SKYD_DISK_CACHE_ENABLED:-true}
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/certbot:/etc/letsencrypt
- ./docker/data/caddy/data:/data/caddy:ro
networks:
shared:
ipv4_address: 10.10.10.30
@ -99,22 +81,18 @@ services:
- "80:80"
depends_on:
- sia
- caddy
- handshake-api
- dnslink-api
- website
website:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-website.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-website:0.2.3
build:
context: ./packages/website
dockerfile: Dockerfile
container_name: website
restart: unless-stopped
logging: *default-logging
volumes:
- ./docker/data/website/.cache:/usr/app/.cache
- ./docker/data/website/.public:/usr/app/public
env_file:
- .env
networks:
@ -124,11 +102,18 @@ services:
- 9000
handshake:
image: handshakeorg/hsd:4.0.2
command: --chain-migrate=3 --no-wallet --no-auth --compact-tree-on-init --network=main --http-host=0.0.0.0
image: skynetlabs/hsd:3.0.1
command: --chain-migrate=2 --wallet-migrate=1
container_name: handshake
restart: unless-stopped
logging: *default-logging
environment:
- HSD_LOG_CONSOLE=false
- HSD_HTTP_HOST=0.0.0.0
- HSD_NETWORK=main
- HSD_PORT=12037
env_file:
- .env
volumes:
- ./docker/data/handshake/.hsd:/root/.hsd
networks:
@ -138,11 +123,9 @@ services:
- 12037
handshake-api:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-handshake-api.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-handshake-api:0.1.3
build:
context: ./packages/handshake-api
dockerfile: Dockerfile
container_name: handshake-api
restart: unless-stopped
logging: *default-logging
@ -162,11 +145,9 @@ services:
- handshake
dnslink-api:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-dnslink-api.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-dnslink-api:0.2.1
build:
context: ./packages/dnslink-api
dockerfile: Dockerfile
container_name: dnslink-api
restart: unless-stopped
logging: *default-logging
@ -177,11 +158,9 @@ services:
- 3100
health-check:
# uncomment "build" and comment out "image" to build from sources
# build:
# context: https://github.com/SkynetLabs/webportal-health-check.git#main
# dockerfile: Dockerfile
image: skynetlabs/webportal-health-check:1.0.0
build:
context: ./packages/health-check
dockerfile: Dockerfile
container_name: health-check
restart: unless-stopped
logging: *default-logging
@ -197,3 +176,5 @@ services:
- STATE_DIR=/usr/app/state
expose:
- 3100
depends_on:
- caddy

View File

@ -0,0 +1,22 @@
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/skynet-accounts.git && \
cd skynet-accounts && \
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 SKYNET_ACCOUNTS_PORT=3000
ENTRYPOINT ["skynet-accounts"]

18
docker/caddy/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
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

@ -0,0 +1,38 @@
{
"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"
}
}
}
}
]
}
]
}
}
}
}

1106
docker/caddy/mo Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
#!/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

22
docker/nginx/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
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
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
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.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 ; \
/usr/local/openresty/bin/openresty '-g daemon off;'" \
]

View File

@ -0,0 +1,51 @@
{{#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}}"
}
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;
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
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;
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
}
{{/SERVER_DOMAIN}}
{{/ACCOUNTS_ENABLED}}

View File

@ -0,0 +1,49 @@
{{#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}}"
}
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;
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
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;
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
}
{{/SERVER_DOMAIN}}

View File

@ -0,0 +1,51 @@
{{#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}}"
}
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;
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
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;
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
}
{{/SERVER_DOMAIN}}

View File

@ -0,0 +1,49 @@
{{#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}}"
}
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;
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}}" }
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;
include /etc/nginx/conf.d/server/server.skylink;
set_by_lua_block $server_alias { return string.match("{{SERVER_DOMAIN}}", "^([^.]+)") }
}
{{/SERVER_DOMAIN}}

View File

@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
-----END DH PARAMETERS-----

View File

@ -0,0 +1,18 @@
# enables gzip compression
gzip on;
# set the gzip compression level (1-9)
gzip_comp_level 6;
# tells proxies to cache both gzipped and regular versions of a resource
gzip_vary on;
# informs NGINX to not compress anything smaller than the defined size
gzip_min_length 256;
# compress data even for clients that are connecting via proxies if a response header includes
# the "expired", "no-cache", "no-store", "private", and "Authorization" parameters
gzip_proxied expired no-cache no-store private auth;
# enables the types of files that can be compressed
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

View File

@ -0,0 +1,9 @@
if ($request_method = 'OPTIONS') {
include /etc/nginx/conf.d/include/cors-headers;
more_set_headers 'Access-Control-Max-Age: 1728000';
more_set_headers 'Content-Type: text/plain; charset=utf-8';
more_set_headers 'Content-Length: 0';
return 204;
}
include /etc/nginx/conf.d/include/cors-headers;

View File

@ -0,0 +1,5 @@
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';
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';

View File

@ -0,0 +1,11 @@
# Extract 2 sets of 2 characters from $request_id and assign to $dir1, $dir2
# respectfully. The rest of the $request_id is going to be assigned to $dir3.
# We use those variables to automatically generate a unique path for the uploaded file.
# This ensures that not all uploaded files end up in the same directory, which is something
# that causes performance issues in the renter.
# Example path result: /af/24/9bc5ec894920ccc45634dc9a8065
if ($request_id ~* "(\w{2})(\w{2})(\w+)") {
set $dir1 $1;
set $dir2 $2;
set $dir3 $3;
}

View File

@ -0,0 +1,15 @@
# optional variables initialisation - those variables are used in log_format
# but are not set on every route so we need to initialise them with empty value
# because otherwise logger with throw error
# set only on hns routes
set $hns_domain "";
# set only if server has been access through SERVER_DOMAIN
set $server_alias "";
# expose skylink variable so we can use it in access log
set $skylink "";
# cached account limits (json string) - applies only if accounts are enabled
set $account_limits "";

View File

@ -0,0 +1,3 @@
allow 127.0.0.1/32; # localhost
allow 10.10.10.0/24; # docker network
deny all;

View File

@ -0,0 +1,95 @@
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;
# variable definititions - we need to define a variable to be able to access it in lua by ngx.var.something
set $skylink ''; # placeholder for the raw 46 bit skylink
# resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
rewrite_by_lua_block {
local json = require('cjson')
local httpc = require("resty.http").new()
-- make a get request to /hnsres endpoint with the domain name from request_uri
-- 10.10.10.50 points to handshake-api service (alias not available when using resty-http)
local hnsres_res, hnsres_err = httpc:request_uri("http://10.10.10.50:3100/hnsres/" .. ngx.var.hns_domain)
-- print error and exit with 500 or exit with response if status is not 200
if hnsres_err or (hnsres_res and hnsres_res.status ~= ngx.HTTP_OK) then
ngx.status = (hnsres_err and ngx.HTTP_INTERNAL_SERVER_ERROR) or hnsres_res.status
ngx.header["content-type"] = "text/plain"
ngx.say(hnsres_err or hnsres_res.body)
return ngx.exit(ngx.status)
end
-- since /hnsres endpoint response is a json, we need to decode it before we access it
-- example response: '{"skylink":"sia://XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg"}'
local hnsres_json = json.decode(hnsres_res.body)
-- define local variable containing rest of the skylink if provided
local skylink_rest
if hnsres_json.skylink then
-- try to match the skylink with sia:// prefix
skylink, skylink_rest = string.match(hnsres_json.skylink, "sia://([^/?]+)(.*)")
-- in case the skylink did not match, assume that there is no sia:// prefix and try to match again
if skylink == nil then
skylink, skylink_rest = string.match(hnsres_json.skylink, "/?([^/?]+)(.*)")
end
elseif hnsres_json.registry then
local publickey = hnsres_json.registry.publickey
local datakey = hnsres_json.registry.datakey
-- make a get request to /skynet/registry endpoint with the credentials from text record
-- 10.10.10.10 points to sia service (alias not available when using resty-http)
local registry_res, registry_err = httpc:request_uri("http://10.10.10.10:9980/skynet/registry?publickey=" .. publickey .. "&datakey=" .. datakey, {
headers = { ["User-Agent"] = "Sia-Agent" }
})
-- print error and exit with 500 or exit with response if status is not 200
if registry_err or (registry_res and registry_res.status ~= ngx.HTTP_OK) then
ngx.status = (registry_err and ngx.HTTP_INTERNAL_SERVER_ERROR) or registry_res.status
ngx.header["content-type"] = "text/plain"
ngx.say(registry_err or registry_res.body)
return ngx.exit(ngx.status)
end
-- since /skynet/registry endpoint response is a json, we need to decode it before we access it
local registry_json = json.decode(registry_res.body)
-- response will contain a hex encoded skylink, we need to decode it
local data = (registry_json.data:gsub('..', function (cc)
return string.char(tonumber(cc, 16))
end))
skylink = data
end
-- fail with a generic 404 if skylink has not been extracted from a valid /hnsres response for some reason
if not skylink then
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
ngx.var.skylink = skylink
if ngx.var.path == "/" and skylink_rest ~= nil and skylink_rest ~= "" and skylink_rest ~= "/" then
ngx.var.path = skylink_rest
end
}
# we proxy to another nginx location rather than directly to siad because we do not want to deal with caching here
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 .. "://" .. 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
local path = string.match(ngx.header.location, "[^/?]+(.*)");
-- because siad will set the location header to ie. XABvi7JtJbQSMAcDwnUnmp2FKDPjg8_tTTFP4BwMSxVdEg/index.html
-- we need to replace the skylink with the domain_name so we are not redirected to skylink
ngx.header.location = ngx.var.hns_domain .. path
end
}

View File

@ -0,0 +1,48 @@
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
# ensure that skylink that we pass around is base64 encoded (transform base32 encoded ones)
# 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) }
# 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 accounts = require("skynet.account")
if accounts.accounts_enabled() then
-- check if portal is in authenticated only mode
if accounts.is_access_unauthorized() then
return accounts.exit_access_unauthorized()
end
-- check if portal is in subscription only mode
if accounts.is_access_forbidden() then
return accounts.exit_access_forbidden()
end
end
}
header_filter_by_lua_block {
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
}
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_pass http://sia:9980/skynet/skylink/$skylink$path$is_args$args;

View File

@ -0,0 +1,32 @@
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;
proxy_set_header User-Agent: Sia-Agent;
proxy_read_timeout 600; # siad should timeout with 404 after 5 minutes
proxy_pass http://sia:9980/skynet/registry;
access_by_lua_block {
if require("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()
end
-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
-- apply registry rate limits (forced delay)
if limits.registry > 0 then
ngx.sleep(limits.registry / 1000)
end
end
}

View File

@ -0,0 +1,11 @@
access_by_lua_block {
-- check portal access rules and exit if access is restricted
if require("skynet.account").is_access_unauthorized() then
return require("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()
end
}

View File

@ -0,0 +1,5 @@
# 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

@ -0,0 +1,4 @@
add_header X-Proxy-Cache $upstream_cache_status; # add response header to indicate cache hits and misses
# disable caching
proxy_no_cache "1";

View File

@ -0,0 +1,10 @@
# ----------------------------------------------------------------
# this file should be included on all locations that proxy_pass to
# another nginx location - internal nginx traffic
# ----------------------------------------------------------------
# increase the timeout on internal nginx proxy_pass locations to a
# value that is significantly higher than expected and let the end
# location handle correct timeout
proxy_read_timeout 30m;
proxy_send_timeout 30m;

View File

@ -0,0 +1,6 @@
# Add a list of IPs here that should be severely rate limited on upload.
# Note that it is possible to add IP ranges as well as the full IP address.
#
# Examples:
# 192.168.0.0/24 1;
# 79.85.222.247 1;

View File

@ -0,0 +1,4 @@
rewrite_by_lua_block {
-- set basic authorization header with base64 encoded apipassword
ngx.req.set_header("Authorization", require("skynet.utils").authorization_header())
}

View File

@ -0,0 +1,13 @@
# 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_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/nginx/conf.d/dhparam.pem;
# intermediate configuration
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;

View File

@ -0,0 +1,51 @@
# 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)
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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
ngx.log(ngx.ERR, "Failed accounts service request /track/download/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
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 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
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
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
}

View File

@ -0,0 +1,30 @@
# 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)
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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
ngx.log(ngx.ERR, "Failed accounts service request /track/registry/" .. registry_action .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
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 err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
end
end
}

View File

@ -0,0 +1,51 @@
# 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)
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 = { ["Cookie"] = "skynet-jwt=" .. jwt },
})
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
ngx.log(ngx.ERR, "Failed accounts service request /track/upload/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
end
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 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
ngx.log(ngx.ERR, "Failed malware-scanner request /scan/" .. skylink .. ": ", err or ("[HTTP " .. res.status .. "] " .. res.body))
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
}

View File

@ -0,0 +1,7 @@
# Every file from within this directory will be included in the server block
# of the nginx configuration, at the very end. See client.conf.
#
# Example:
# location /blog {
# root /var/www/blog;
# }

View File

@ -0,0 +1,16 @@
lua_shared_dict dnslink 10m;
server {
listen 80 default_server;
include /etc/nginx/conf.d/server/server.dnslink;
}
server {
listen 443 default_server;
ssl_certificate /etc/ssl/local-certificate.crt;
ssl_certificate_key /etc/ssl/local-certificate.key;
include /etc/nginx/conf.d/server/server.dnslink;
}

View File

@ -0,0 +1,9 @@
server {
# local server - do not expose this port externally
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

@ -0,0 +1,57 @@
listen 443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
location / {
proxy_pass http://dashboard:3000;
}
location /health {
proxy_pass http://accounts:3000;
}
location /stripe/webhook {
proxy_pass http://accounts:3000;
}
location /api/stripe/billing {
proxy_pass http://dashboard:3000;
}
location /api/stripe/checkout {
proxy_pass http://dashboard:3000;
}
location /api {
rewrite /api/(.*) /$1 break;
proxy_pass http://accounts:3000;
}
location /api/register {
include /etc/nginx/conf.d/include/cors;
rewrite /api/(.*) /$1 break;
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

@ -0,0 +1,418 @@
listen 443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
# ddos protection: closing slow connections
client_body_timeout 1h;
client_header_timeout 1h;
send_timeout 1h;
proxy_connect_timeout 1h;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
# Increase the body buffer size, to ensure the internal POSTs can always
# parse the full POST contents into memory.
client_body_buffer_size 128k;
client_max_body_size 128k;
# legacy endpoint rewrite
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;
proxy_pass http://website:9000;
}
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
proxy_pass http://blocker: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;
proxy_pass http://sia:9980/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;
proxy_read_timeout 5m; # extend the read timeout
proxy_pass http://sia:9980/skynet/stats;
}
# Define path for server load endpoint
location /serverload {
# Define root directory in the nginx container to load file from
root /usr/local/share;
# including this because of peer pressure from the other routes
include /etc/nginx/conf.d/include/cors;
# tell nginx to expect json
default_type 'application/json';
# Allow for /serverload to load /serverload.json file
try_files $uri $uri.json =404;
}
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
proxy_set_header User-Agent: Sia-Agent;
proxy_read_timeout 5m; # extend the read timeout
proxy_pass http://sia:9980;
}
location /health-check {
include /etc/nginx/conf.d/include/cors;
access_log off; # do not log traffic to health-check endpoint
proxy_pass http://10.10.10.60:3100; # hardcoded ip because health-check waits for nginx
}
location /abuse {
return 308 /0404guluqu38oaqapku91ed11kbhkge55smh9lhjukmlrj37lfpm8no/;
}
location /abuse/report {
include /etc/nginx/conf.d/include/cors;
# 10.10.10.110 points to blocker service
proxy_pass http://10.10.10.110:4000/powblock;
}
location /hns {
include /etc/nginx/conf.d/include/cors;
# match the request_uri and extract the hns domain and anything that is passed in the uri after it
# example: /hns/something/foo/bar matches:
# > hns_domain: something
# > path: /foo/bar/
set_by_lua_block $hns_domain { return string.match(ngx.var.uri, "/hns/([^/?]+)") }
set_by_lua_block $path { return string.match(ngx.var.uri, "/hns/[^/?]+(.*)") }
proxy_set_header Host $host;
include /etc/nginx/conf.d/include/location-hns;
}
location /hnsres {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
proxy_pass http://handshake-api:3100;
}
location /skynet/registry {
include /etc/nginx/conf.d/include/location-skynet-registry;
}
location /skynet/restore {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/sia-auth;
include /etc/nginx/conf.d/include/portal-access-check;
client_max_body_size 5M;
# increase request timeouts
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
proxy_set_header Expect $http_expect;
proxy_set_header User-Agent: Sia-Agent;
# proxy this call to siad endpoint (make sure the ip is correct)
proxy_pass http://sia:9980;
}
location /skynet/registry/subscription {
include /etc/nginx/conf.d/include/cors;
# default to unlimited bandwidth and no delay
set $bandwidthlimit "0";
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()
-- 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 }
})
-- 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)
ngx.var.bandwidthlimit = limits.download
ngx.var.notificationdelay = limits.registry
end
end
}
proxy_set_header User-Agent: Sia-Agent;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://sia:9980/skynet/registry/subscription?bandwidthlimit=$bandwidthlimit&notificationdelay=$notificationdelay;
}
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;
limit_req zone=uploads_by_ip burst=10 nodelay;
limit_req zone=uploads_by_ip_throttled;
limit_conn upload_conn 5;
limit_conn upload_conn_rl 1;
client_max_body_size 5000M; # make sure to limit the size of upload to a sane value
# increase request timeouts
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_request_buffering off; # stream uploaded files through the proxy as it comes in
proxy_set_header Expect $http_expect;
proxy_set_header User-Agent: Sia-Agent;
# 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;
}
# 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;
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;
# Those timeouts need to be elevated since skyd can stall reading
# data for a while when overloaded which would terminate connection
client_body_timeout 1h;
proxy_send_timeout 1h;
# Add X-Forwarded-* headers
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
# rewrite proxy request to use correct host uri from env variable (required to return correct location header)
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
-- check if portal is in authenticated only mode
if require("skynet.account").is_access_unauthorized() then
return require("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()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
-- apply upload size limits
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
end
}
# 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 .. "://" .. 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 ([^,?]+)")
if encodedSkylink then
ngx.header["Skynet-Skylink"] = ngx.decode_base64(encodedSkylink)
end
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;
limit_req zone=uploads_by_ip burst=10 nodelay;
limit_req zone=uploads_by_ip_throttled;
limit_conn upload_conn 5;
limit_conn upload_conn_rl 1;
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980$uri?siapath=$dir1/$dir2/$dir3&$args;
}
location /skynet/metadata {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block {
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;
proxy_pass http://sia:9980;
}
location /skynet/resolve {
include /etc/nginx/conf.d/include/cors;
include /etc/nginx/conf.d/include/portal-access-check;
header_filter_by_lua_block {
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;
proxy_pass http://sia:9980;
}
location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
set $skylink $2;
set $path $3;
include /etc/nginx/conf.d/include/location-skylink;
}
location ~ "^/file/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
set $skylink $2;
set $path $3;
set $args attachment=true&$args;
#set $is_args ?;
include /etc/nginx/conf.d/include/location-skylink;
}
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
# default download rate to unlimited
set $limit_rate 0;
access_by_lua_block {
if require("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()
end
-- check if portal is in subscription only mode
if require("skynet.account").is_access_forbidden() then
return require("skynet.account").exit_access_forbidden()
end
-- get account limits of currently authenticated user
local limits = require("skynet.account").get_account_limits()
-- apply download speed limit
ngx.var.limit_rate = limits.download
end
}
limit_rate_after 512k;
limit_rate $limit_rate;
proxy_set_header User-Agent: Sia-Agent;
proxy_pass http://sia:9980;
}
location /__internal/do/not/use/accounts {
include /etc/nginx/conf.d/include/cors;
charset utf-8;
charset_types application/json;
default_type application/json;
content_by_lua_block {
local json = require('cjson')
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)
}
}
include /etc/nginx/conf.d/server-override/*;

View File

@ -0,0 +1,46 @@
include /etc/nginx/conf.d/include/init-optional-variables;
location / {
set $skylink "";
set $path $uri;
rewrite_by_lua_block {
local cache = ngx.shared.dnslink
local cache_value = cache:get(ngx.var.host)
if cache_value == nil then
local httpc = require("resty.http").new()
-- 10.10.10.55 points to dnslink-api service (alias not available when using resty-http)
local res, err = httpc:request_uri("http://10.10.10.55:3100/dnslink/" .. ngx.var.host)
if err or (res and res.status ~= ngx.HTTP_OK) then
-- check whether we can fallback to regular skylink request
local match_skylink = ngx.re.match(ngx.var.uri, "^/([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?")
if match_skylink then
ngx.var.skylink = match_skylink[1]
ngx.var.path = match_skylink[2] or "/"
else
ngx.status = (err and ngx.HTTP_INTERNAL_SERVER_ERROR) or res.status
ngx.header["content-type"] = "text/plain"
ngx.say(err or res.body)
ngx.exit(ngx.status)
end
else
ngx.var.skylink = res.body
local cache_ttl = 300 -- 5 minutes cache expire time
cache:set(ngx.var.host, ngx.var.skylink, cache_ttl)
end
else
ngx.var.skylink = cache_value
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

@ -0,0 +1,11 @@
listen 443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
location / {
set_by_lua_block $hns_domain { return string.match(ngx.var.host, "[^%.]+") }
set $path $uri;
include /etc/nginx/conf.d/include/location-hns;
}

View File

@ -0,0 +1,7 @@
listen 80;
include /etc/nginx/conf.d/include/init-optional-variables;
location / {
return 301 https://$host$request_uri;
}

View File

@ -0,0 +1,37 @@
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

@ -0,0 +1,16 @@
listen 443 ssl http2;
include /etc/nginx/conf.d/include/ssl-settings;
include /etc/nginx/conf.d/include/init-optional-variables;
location / {
set_by_lua_block $skylink { return string.match(ngx.var.host, "%w+") }
set_by_lua_block $path {
-- strip ngx.var.request_uri from query params - this is basically the same as ngx.var.uri but
-- do not use ngx.var.uri because it will already be unescaped and we need to use escaped path
-- examples: escaped uri "/b%20r56+7" and unescaped uri "/b r56 7"
return string.gsub(ngx.var.request_uri, "?.*", "")
}
include /etc/nginx/conf.d/include/location-skylink;
}

View File

@ -0,0 +1,301 @@
-- source: https://github.com/aiq/basexx
-- license: MIT
-- modified: exposed from_basexx and to_basexx generic functions
--------------------------------------------------------------------------------
-- util functions
--------------------------------------------------------------------------------
local function divide_string( str, max )
local result = {}
local start = 1
for i = 1, #str do
if i % max == 0 then
table.insert( result, str:sub( start, i ) )
start = i + 1
elseif i == #str then
table.insert( result, str:sub( start, i ) )
end
end
return result
end
local function number_to_bit( num, length )
local bits = {}
while num > 0 do
local rest = math.floor( math.fmod( num, 2 ) )
table.insert( bits, rest )
num = ( num - rest ) / 2
end
while #bits < length do
table.insert( bits, "0" )
end
return string.reverse( table.concat( bits ) )
end
local function ignore_set( str, set )
if set then
str = str:gsub( "["..set.."]", "" )
end
return str
end
local function pure_from_bit( str )
return ( str:gsub( '........', function ( cc )
return string.char( tonumber( cc, 2 ) )
end ) )
end
local function unexpected_char_error( str, pos )
local c = string.sub( str, pos, pos )
return string.format( "unexpected character at position %d: '%s'", pos, c )
end
--------------------------------------------------------------------------------
local basexx = {}
--------------------------------------------------------------------------------
-- base2(bitfield) decode and encode function
--------------------------------------------------------------------------------
local bitMap = { o = "0", i = "1", l = "1" }
function basexx.from_bit( str, ignore )
str = ignore_set( str, ignore )
str = string.lower( str )
str = str:gsub( '[ilo]', function( c ) return bitMap[ c ] end )
local pos = string.find( str, "[^01]" )
if pos then return nil, unexpected_char_error( str, pos ) end
return pure_from_bit( str )
end
function basexx.to_bit( str )
return ( str:gsub( '.', function ( c )
local byte = string.byte( c )
local bits = {}
for _ = 1,8 do
table.insert( bits, byte % 2 )
byte = math.floor( byte / 2 )
end
return table.concat( bits ):reverse()
end ) )
end
--------------------------------------------------------------------------------
-- base16(hex) decode and encode function
--------------------------------------------------------------------------------
function basexx.from_hex( str, ignore )
str = ignore_set( str, ignore )
local pos = string.find( str, "[^%x]" )
if pos then return nil, unexpected_char_error( str, pos ) end
return ( str:gsub( '..', function ( cc )
return string.char( tonumber( cc, 16 ) )
end ) )
end
function basexx.to_hex( str )
return ( str:gsub( '.', function ( c )
return string.format('%02X', string.byte( c ) )
end ) )
end
--------------------------------------------------------------------------------
-- generic function to decode and encode base32/base64
--------------------------------------------------------------------------------
function basexx.from_basexx( str, alphabet, bits )
local result = {}
for i = 1, #str do
local c = string.sub( str, i, i )
if c ~= '=' then
local index = string.find( alphabet, c, 1, true )
if not index then
return nil, unexpected_char_error( str, i )
end
table.insert( result, number_to_bit( index - 1, bits ) )
end
end
local value = table.concat( result )
local pad = #value % 8
return pure_from_bit( string.sub( value, 1, #value - pad ) )
end
function basexx.to_basexx( str, alphabet, bits, pad )
local bitString = basexx.to_bit( str )
local chunks = divide_string( bitString, bits )
local result = {}
for _,value in ipairs( chunks ) do
if ( #value < bits ) then
value = value .. string.rep( '0', bits - #value )
end
local pos = tonumber( value, 2 ) + 1
table.insert( result, alphabet:sub( pos, pos ) )
end
table.insert( result, pad )
return table.concat( result )
end
--------------------------------------------------------------------------------
-- rfc 3548: http://www.rfc-editor.org/rfc/rfc3548.txt
--------------------------------------------------------------------------------
local base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
local base32PadMap = { "", "======", "====", "===", "=" }
function basexx.from_base32( str, ignore )
str = ignore_set( str, ignore )
return basexx.from_basexx( string.upper( str ), base32Alphabet, 5 )
end
function basexx.to_base32( str )
return basexx.to_basexx( str, base32Alphabet, 5, base32PadMap[ #str % 5 + 1 ] )
end
--------------------------------------------------------------------------------
-- crockford: http://www.crockford.com/wrmg/base32.html
--------------------------------------------------------------------------------
local crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
local crockfordMap = { O = "0", I = "1", L = "1" }
function basexx.from_crockford( str, ignore )
str = ignore_set( str, ignore )
str = string.upper( str )
str = str:gsub( '[ILOU]', function( c ) return crockfordMap[ c ] end )
return basexx.from_basexx( str, crockfordAlphabet, 5 )
end
function basexx.to_crockford( str )
return basexx.to_basexx( str, crockfordAlphabet, 5, "" )
end
--------------------------------------------------------------------------------
-- base64 decode and encode function
--------------------------------------------------------------------------------
local base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
"abcdefghijklmnopqrstuvwxyz"..
"0123456789+/"
local base64PadMap = { "", "==", "=" }
function basexx.from_base64( str, ignore )
str = ignore_set( str, ignore )
return basexx.from_basexx( str, base64Alphabet, 6 )
end
function basexx.to_base64( str )
return basexx.to_basexx( str, base64Alphabet, 6, base64PadMap[ #str % 3 + 1 ] )
end
--------------------------------------------------------------------------------
-- URL safe base64 decode and encode function
--------------------------------------------------------------------------------
local url64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
"abcdefghijklmnopqrstuvwxyz"..
"0123456789-_"
function basexx.from_url64( str, ignore )
str = ignore_set( str, ignore )
return basexx.from_basexx( str, url64Alphabet, 6 )
end
function basexx.to_url64( str )
return basexx.to_basexx( str, url64Alphabet, 6, "" )
end
--------------------------------------------------------------------------------
--
--------------------------------------------------------------------------------
local function length_error( len, d )
return string.format( "invalid length: %d - must be a multiple of %d", len, d )
end
local z85Decoder = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00,
0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47,
0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00,
0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20,
0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 }
function basexx.from_z85( str, ignore )
str = ignore_set( str, ignore )
if ( #str % 5 ) ~= 0 then
return nil, length_error( #str, 5 )
end
local result = {}
local value = 0
for i = 1, #str do
local index = string.byte( str, i ) - 31
if index < 1 or index >= #z85Decoder then
return nil, unexpected_char_error( str, i )
end
value = ( value * 85 ) + z85Decoder[ index ]
if ( i % 5 ) == 0 then
local divisor = 256 * 256 * 256
while divisor ~= 0 do
local b = math.floor( value / divisor ) % 256
table.insert( result, string.char( b ) )
divisor = math.floor( divisor / 256 )
end
value = 0
end
end
return table.concat( result )
end
local z85Encoder = "0123456789"..
"abcdefghijklmnopqrstuvwxyz"..
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"..
".-:+=^!/*?&<>()[]{}@%$#"
function basexx.to_z85( str )
if ( #str % 4 ) ~= 0 then
return nil, length_error( #str, 4 )
end
local result = {}
local value = 0
for i = 1, #str do
local b = string.byte( str, i )
value = ( value * 256 ) + b
if ( i % 4 ) == 0 then
local divisor = 85 * 85 * 85 * 85
while divisor ~= 0 do
local index = ( math.floor( value / divisor ) % 85 ) + 1
table.insert( result, z85Encoder:sub( index, index ) )
divisor = math.floor( divisor / 85 )
end
value = 0
end
end
return table.concat( result )
end
--------------------------------------------------------------------------------
return basexx

View File

@ -0,0 +1,111 @@
local _M = {}
-- constant tier ids
local tier_id_anonymous = 0
local tier_id_free = 1
-- fallback - remember to keep those updated
local anon_limits = {
["tierID"] = tier_id_anonymous,
["tierName"] = "anonymous",
["upload"] = 655360,
["download"] = 655360,
["maxUploadSize"] = 1073741824,
["registry"] = 250
}
-- handle request exit when access to portal should be restricted to authenticated users only
function _M.exit_access_unauthorized(message)
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.header["content-type"] = "text/plain"
ngx.say(message or "Portal operator restricted access to authenticated users only")
return ngx.exit(ngx.status)
end
-- handle request exit when access to portal should be restricted to subscription users only
function _M.exit_access_forbidden(message)
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header["content-type"] = "text/plain"
ngx.say(message or "Portal operator restricted access to users with active subscription only")
return ngx.exit(ngx.status)
end
function _M.accounts_enabled()
return os.getenv("PORTAL_MODULES"):match("a") ~= nil
end
function _M.get_account_limits()
local cjson = require('cjson')
-- TODO: This needs to accommodate authorization tokens and api keys.
if ngx.var.skynet_jwt == "" then
return anon_limits
end
if ngx.var.account_limits == "" then
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 }
})
-- 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.var.account_limits = cjson.encode(anon_limits)
elseif res and res.status == ngx.HTTP_OK then
ngx.var.account_limits = res.body
end
end
return cjson.decode(ngx.var.account_limits)
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.tierID > tier_id_anonymous
end
-- detect whether current user has active subscription
function _M.has_subscription()
local limits = _M.get_account_limits()
return limits.tierID > tier_id_free
end
function _M.is_auth_required()
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "authenticated" or os.getenv("ACCOUNTS_LIMIT_ACCESS") == "subscription"
end
function _M.is_subscription_required()
return os.getenv("ACCOUNTS_LIMIT_ACCESS") == "subscription"
end
function is_access_always_allowed()
-- options requests do not attach cookies - should always be available
-- requests should not be limited based on accounts if accounts are not enabled
return ngx.req.get_method() == "OPTIONS" or not _M.accounts_enabled()
end
-- check whether access is restricted if portal requires authorization
function _M.is_access_unauthorized()
if is_access_always_allowed() then return false end
-- check if authentication is required and request is not authenticated
return _M.is_auth_required() and not _M.is_authenticated()
end
-- check whether user is authenticated but does not have access to given resources
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.has_subscription()
end
return _M

View File

@ -0,0 +1,66 @@
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,40 @@
local _M = {}
local basexx = require("basexx")
local hasher = require("hasher")
-- parse any skylink and return base64 version
function _M.parse(skylink)
if string.len(skylink) == 55 then
local decoded = basexx.from_basexx(string.upper(skylink), "0123456789ABCDEFGHIJKLMNOPQRSTUV", 5)
return basexx.to_url64(decoded)
end
return skylink
end
-- hash skylink into 32 bytes hash used in blocklist
function _M.hash(skylink)
-- ensure that the skylink is base64 encoded
local base64Skylink = _M.parse(skylink)
-- decode skylink from base64 encoding
local rawSkylink = basexx.from_url64(base64Skylink)
-- drop first two bytes and leave just merkle root
local rawMerkleRoot = string.sub(rawSkylink, 3)
-- parse with blake2b with key length of 32
local blake2bHashed = hasher.blake2b(rawMerkleRoot, 32)
-- hex encode the blake hash
local hexHashed = basexx.to_hex(blake2bHashed)
-- lowercase the hex encoded hash
local lowerHexHashed = string.lower(hexHashed)
return lowerHexHashed
end
return _M

View File

@ -0,0 +1,23 @@
local skynet_skylink = require("skynet.skylink")
describe("parse", function()
local base32 = "0404dsjvti046fsua4ktor9grrpe76erq9jot9cvopbhsvsu76r4r30"
local base64 = "AQBG8n_sgEM_nlEp3G0w3vLjmdvSZ46ln8ZXHn-eObZNjA"
it("should return unchanged base64 skylink", function()
assert.is.same(skynet_skylink.parse(base64), base64)
end)
it("should transform base32 skylink into base64", function()
assert.is.same(skynet_skylink.parse(base32), base64)
end)
end)
describe("hash", function()
local base64 = "EADi4QZWt87sSDCSjVTcmyI5tE_YAsuC90BcCi_jEmG5NA"
local hash = "6cfb9996ad74e5614bbb8e7228e72f1c1bc14dd9ce8a83b3ccabdb6d8d70f330"
it("should hash skylink", function()
assert.is.same(hash, skynet_skylink.hash(base64))
end)
end)

View File

@ -0,0 +1,23 @@
local _M = {}
function _M.authorization_header()
-- read api password from env variable
local apipassword = os.getenv("SIA_API_PASSWORD")
-- if api password is not available as env variable, read it from disk
if apipassword == nil or 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")
-- read apipassword file contents and trim newline (important)
apipassword = apipassword_file:read("*all"):gsub("%s+", "")
-- make sure to close file after reading the password
apipassword_file.close()
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)
-- set authorization header with proper base64 encoded string
return "Basic " .. content
end
return _M

1106
docker/nginx/mo Executable file

File diff suppressed because it is too large Load Diff

140
docker/nginx/nginx.conf Normal file
View File

@ -0,0 +1,140 @@
# nginx.conf -- docker-openresty
#
# This file is installed to:
# `/usr/local/openresty/nginx/conf/nginx.conf`
# and is the file loaded by nginx at startup,
# unless the user specifies otherwise.
#
# It tracks the upstream OpenResty's `nginx.conf`, but removes the `server`
# section and adds this directive:
# `include /etc/nginx/conf.d/*.conf;`
#
# The `docker-openresty` file `nginx.vh.default.conf` is copied to
# `/etc/nginx/conf.d/default.conf`. It contains the `server section
# of the upstream `nginx.conf`.
#
# See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
#
user root;
worker_processes auto;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
# declare env variables to use it in config
env PORTAL_DOMAIN;
env SERVER_DOMAIN;
env PORTAL_MODULES;
env ACCOUNTS_LIMIT_ACCESS;
env SIA_API_PASSWORD;
env SKYD_DISK_CACHE_ENABLED;
events {
worker_connections 8192;
}
http {
include mime.types;
default_type application/octet-stream;
lua_package_path "/etc/nginx/libs/?.lua;;";
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $upstream_response_time '
'$upstream_bytes_sent $upstream_bytes_received '
'"$upstream_http_content_type" "$upstream_cache_status" '
'"$server_alias" "$sent_http_skynet_skylink" '
'$upstream_connect_time $upstream_header_time '
'$request_time "$hns_domain" "$skylink" $upstream_http_skynet_cache_ratio';
access_log logs/access.log main;
# See Move default writable paths to a dedicated directory (#119)
# https://github.com/openresty/docker-openresty/issues/119
client_body_temp_path /var/run/openresty/nginx-client-body;
proxy_temp_path /var/run/openresty/nginx-proxy;
fastcgi_temp_path /var/run/openresty/nginx-fastcgi;
uwsgi_temp_path /var/run/openresty/nginx-uwsgi;
scgi_temp_path /var/run/openresty/nginx-scgi;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
# globally enable http 1.1 on all proxied requests
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_http_version
proxy_http_version 1.1;
# 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 .. "://" .. ngx.var.skynet_portal_domain
ngx.header["Skynet-Server-Api"] = ngx.var.scheme .. "://" .. ngx.var.skynet_server_domain
}
# ratelimit specified IPs
geo $limit {
default 0;
include /etc/nginx/conf.d/include/ratelimited;
}
map $limit $limit_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $binary_remote_addr zone=uploads_by_ip:10m rate=10r/s;
limit_req_zone $limit_key zone=uploads_by_ip_throttled:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=registry_access_by_ip:10m rate=60r/m;
limit_req_zone $limit_key zone=registry_access_by_ip_throttled:10m rate=20r/m;
limit_conn_zone $binary_remote_addr zone=upload_conn:10m;
limit_conn_zone $limit_key zone=upload_conn_rl:10m;
limit_conn_zone $binary_remote_addr zone=downloads_by_ip:10m;
limit_req_status 429;
limit_conn_status 429;
# Add X-Forwarded-* headers
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;
}

16
docker/sia/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM golang:1.16.7 AS sia-builder
ENV GOOS linux
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
FROM nebulouslabs/sia:latest
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

View File

@ -1,2 +0,0 @@
Purpose of this file is that `logs` dir can be commited to git and it will be
present on portal servers. The rest of files in `logs` dir are git ignored.

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"],
};

4
packages/dashboard-v2/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.cache/
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

@ -0,0 +1,10 @@
# Skynet Account Dashboard
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`

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

@ -0,0 +1,35 @@
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = {
siteMetadata: {
title: `Accounts Dashboard`,
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-styled-components",
"gatsby-plugin-postcss",
{
resolve: "gatsby-source-filesystem",
options: {
name: "images",
path: "./src/images/",
},
__key: "images",
},
],
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

@ -0,0 +1,77 @@
{
"name": "accounts-dashboard",
"version": "1.0.0",
"private": true,
"description": "Accounts Dashboard",
"author": "Skynet Labs",
"keywords": [
"gatsby"
],
"scripts": {
"develop": "gatsby develop",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
"clean": "gatsby clean",
"lint": "eslint .",
"prettier": "prettier .",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook -o storybook-build"
},
"dependencies": {
"@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-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",
"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-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

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

View File

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

View File

@ -0,0 +1,38 @@
import { Button } from "./Button";
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
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",
};
export const Secondary = Template.bind({});
Secondary.args = {
label: "Button",
};
export const Large = Template.bind({});
Large.args = {
size: "large",
label: "Button",
};
export const Small = Template.bind({});
Small.args = {
size: "small",
label: "Button",
};

View File

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

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>
</>
);
}

View File

@ -0,0 +1,35 @@
import styled from "styled-components";
const Bar = styled.div.attrs({
className: `relative flex justify-end h-4 bg-primary rounded-l rounded-r-lg`,
})`
min-width: 1rem;
width: ${({ $percentage }) => $percentage}%;
`;
const BarTip = styled.span.attrs({
className: "relative w-4 h-4 border-2 rounded-full bg-white border-primary",
})``;
const BarLabel = styled.span.attrs({
className: "bg-white rounded border-2 border-palette-200 px-3 whitespace-nowrap absolute shadow",
})`
right: max(0%, ${({ $percentage }) => 100 - $percentage}%);
top: -0.5rem;
transform: translateX(50%);
`;
export const GraphBar = ({ value, limit, label }) => {
const percentage = typeof limit !== "number" || limit === 0 ? 0 : (value / limit) * 100;
return (
<div className="relative flex items-center">
<Bar $percentage={percentage}>
<BarTip />
</Bar>
<BarLabel $percentage={percentage}>
<span className="font-sora text-lg">{label.value}</span> <span>{label.unit}</span>
</BarLabel>
</div>
);
};

View File

@ -0,0 +1,9 @@
import styled from "styled-components";
export const UsageGraph = styled.div.attrs({
className: "w-full my-3 grid grid-flow-row grid-rows-2",
})`
height: 146px;
background: url(/images/usage-graph-bg.svg) no-repeat;
background-size: cover;
`;

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