503 lines
17 KiB
Plaintext
503 lines
17 KiB
Plaintext
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=skynet:10m max_size=10g inactive=24h use_temp_path=off;
|
|
|
|
# this runs before forking out nginx worker processes
|
|
init_by_lua_block {
|
|
require "cjson"
|
|
require "socket.http"
|
|
}
|
|
|
|
# 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;
|
|
|
|
# since we are proxying request to nginx from caddy, access logs will contain caddy's ip address
|
|
# as the request address so we need to use real_ip_header module to use ip address from
|
|
# X-Forwarded-For header as a real ip address of the request
|
|
set_real_ip_from 10.0.0.0/8;
|
|
set_real_ip_from 127.0.0.1/32;
|
|
set_real_ip_from 172.16.0.0/12;
|
|
set_real_ip_from 192.168.0.0/16;
|
|
real_ip_header X-Forwarded-For;
|
|
|
|
# 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;
|
|
}
|
|
|
|
upstream siad {
|
|
server sia:9980;
|
|
}
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
listen [::]:80 default_server;
|
|
|
|
# understand the regex https://regex101.com/r/BGQvi6/6
|
|
server_name "~^(((?<base32_subdomain>([a-z0-9]{55}))|(?<hns_domain>[^\.]+)\.hns)\.)?((?<portal_domain>[^.]+)\.)?(?<domain>[^.]+)\.(?<tld>[^.]+)$";
|
|
|
|
# ddos protection: closing slow connections
|
|
client_body_timeout 5s;
|
|
client_header_timeout 5s;
|
|
|
|
# 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 ^/account/(.*) https://account.$domain.$tld/$1 permanent;
|
|
|
|
# This is only safe workaround to reroute based on some conditions
|
|
# See https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
|
|
recursive_error_pages on;
|
|
|
|
# redirect links with base32 encoded skylink in subdomain
|
|
error_page 460 = @base32_subdomain;
|
|
if ($base32_subdomain != "") {
|
|
return 460;
|
|
}
|
|
|
|
# redirect links with handshake domain on hns subdomain
|
|
error_page 461 = @hns_domain;
|
|
if ($hns_domain != "") {
|
|
return 461;
|
|
}
|
|
|
|
location / {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_pass http://website:9000;
|
|
}
|
|
|
|
location /docs {
|
|
proxy_pass https://skynetlabs.github.io/skynet-docs;
|
|
}
|
|
|
|
location /skynet/blocklist {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_cache skynet;
|
|
proxy_cache_valid any 1m; # cache blocklist for 1 minute
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
proxy_pass http://siad/skynet/blocklist;
|
|
}
|
|
|
|
location /skynet/portals {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_cache skynet;
|
|
proxy_cache_valid any 1m; # cache portals for 1 minute
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
proxy_pass http://siad/skynet/portals;
|
|
}
|
|
|
|
location /skynet/stats {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
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://siad/skynet/stats;
|
|
}
|
|
|
|
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 /hns {
|
|
include /etc/nginx/conf.d/include/proxy-buffer;
|
|
|
|
# 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
|
|
set $rest ''; # placeholder for the rest of the url that gets appended to skylink (path and args)
|
|
|
|
# resolve handshake domain by requesting to /hnsres endpoint and assign correct values to $skylink and $rest
|
|
access_by_lua_block {
|
|
local json = require('cjson')
|
|
|
|
-- match the request_uri and extract the hns domain and anything that is passed in the uri after it
|
|
-- example: /hns/something/foo/bar?baz=1 matches:
|
|
-- > hns_domain_name: something
|
|
-- > request_uri_rest: /foo/bar/?baz=1
|
|
local hns_domain_name, request_uri_rest = string.match(ngx.var.request_uri, "/hns/([^/?]+)(.*)")
|
|
|
|
-- make a get request to /hnsres endpoint with the domain name from request_uri
|
|
local hnsres_res = ngx.location.capture("/hnsres/" .. hns_domain_name)
|
|
|
|
-- we want to fail with a generic 404 when /hnsres returns anything but 200 OK with a skylink
|
|
if hnsres_res.status ~= ngx.HTTP_OK then
|
|
ngx.exit(ngx.HTTP_NOT_FOUND)
|
|
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
|
|
local registry_res = ngx.location.capture("/skynet/registry/cached?publickey=" .. publickey .. "&datakey=" .. datakey)
|
|
|
|
-- we want to fail with a generic 404 when /skynet/registry returns anything but 200 OK
|
|
if registry_res.status ~= ngx.HTTP_OK then
|
|
ngx.exit(ngx.HTTP_NOT_FOUND)
|
|
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
|
|
ngx.exit(ngx.HTTP_NOT_FOUND)
|
|
end
|
|
|
|
ngx.var.skylink = skylink
|
|
if request_uri_rest == "/" and skylink_rest ~= nil and skylink_rest ~= "" and skylink_rest ~= "/" then
|
|
ngx.var.rest = skylink_rest
|
|
else
|
|
ngx.var.rest = request_uri_rest
|
|
end
|
|
}
|
|
|
|
# we proxy to another nginx location rather than directly to siad because we don't want to deal with caching here
|
|
proxy_pass http://127.0.0.1/$skylink$rest;
|
|
|
|
# in case siad returns location header, we need to replace the skylink with the domain name
|
|
header_filter_by_lua_block {
|
|
if ngx.header.location then
|
|
-- match hns domain from the request_uri
|
|
local hns_domain_name = string.match(ngx.var.request_uri, "/hns/([^/?]+)")
|
|
|
|
-- match location redirect part after the skylink
|
|
local location_rest = 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 = hns_domain_name .. location_rest
|
|
end
|
|
}
|
|
}
|
|
|
|
location /hnsres {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_pass http://handshake-api:3100;
|
|
}
|
|
|
|
# internal registry endpoint that caches calls for a certain period of time
|
|
# it is not suitable for every registry call but some requests might be cached
|
|
# and we are using it currently for caching registry resolutions from /hns calls
|
|
location /skynet/registry/cached {
|
|
internal; # internal endpoint only
|
|
access_log off; # do not log traffic
|
|
|
|
proxy_cache skynet;
|
|
proxy_cache_key publickey=$arg_publickey&datakey=$arg_datakey; # cache based on publickey and datakey
|
|
proxy_cache_valid 200 30s; # cache only 200 responses and only for 30 seconds
|
|
proxy_cache_lock on; # queue cache requests for the same resource until it is fully cached
|
|
proxy_cache_bypass $cookie_nocache $arg_nocache; # add cache bypass option
|
|
|
|
proxy_pass http://127.0.0.1/skynet/registry$is_args$args;
|
|
}
|
|
|
|
location /skynet/registry {
|
|
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://siad/skynet/registry;
|
|
|
|
access_by_lua_block {
|
|
-- this block runs only when accounts are enabled
|
|
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
|
|
|
|
local res = ngx.location.capture("/accounts/user/limits", { copy_all_vars = true })
|
|
if res.status == ngx.HTTP_OK then
|
|
local json = require('cjson')
|
|
local limits = json.decode(res.body)
|
|
if limits.registry > 0 then
|
|
ngx.sleep(limits.registry / 1000)
|
|
end
|
|
end
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
limit_req zone=uploads_by_ip burst=100 nodelay;
|
|
limit_req zone=uploads_by_ip_throttled;
|
|
|
|
limit_conn upload_conn 10;
|
|
limit_conn upload_conn_rl 1;
|
|
|
|
client_max_body_size 1000M; # 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;
|
|
|
|
# access_by_lua_block {
|
|
# -- this block runs only when accounts are enabled
|
|
# if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
|
|
|
|
# ngx.var.upload_limit_rate = 5 * 1024 * 1024
|
|
# local res = ngx.location.capture("/accounts/user", { copy_all_vars = true })
|
|
# if res.status == ngx.HTTP_OK then
|
|
# local json = require('cjson')
|
|
# local user = json.decode(res.body)
|
|
# ngx.var.upload_limit_rate = ngx.var.upload_limit_rate * (user.tier + 1)
|
|
# end
|
|
# }
|
|
|
|
# proxy this call to siad endpoint (make sure the ip is correct)
|
|
proxy_pass http://siad/skynet/skyfile/$dir1/$dir2/$dir3$is_args$args;
|
|
}
|
|
|
|
# endpoing implementing resumable file uploads open protocol https://tus.io
|
|
location /skynet/tus {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
include /etc/nginx/conf.d/include/track-upload;
|
|
|
|
client_max_body_size 50M; # tus chunks size is 40M + leaving 10M of breathing room
|
|
|
|
# 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 /skynet/tus requests to siad endpoint with all arguments
|
|
proxy_pass http://siad;
|
|
|
|
# rewrite tus headers to use correct uri from env variable
|
|
set_by_lua $SKYNET_SERVER_API 'return os.getenv("SKYNET_SERVER_API")';
|
|
proxy_redirect https://siad $SKYNET_SERVER_API;
|
|
|
|
# extract skylink from base64 encoded upload metadata and assign to a proper header
|
|
header_filter_by_lua_block {
|
|
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;
|
|
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
proxy_pass http://siad$uri?siapath=$dir1/$dir2/$dir3&$args;
|
|
}
|
|
|
|
location /skynet/metadata {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
proxy_pass http://siad;
|
|
}
|
|
|
|
location /skynet/resolve {
|
|
include /etc/nginx/conf.d/include/cors;
|
|
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
proxy_pass http://siad;
|
|
}
|
|
|
|
location ~ "^/(([a-zA-Z0-9-_]{46}|[a-z0-9]{55})(/.*)?)$" {
|
|
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;
|
|
|
|
# redirect purge calls to separate location
|
|
error_page 462 = @purge;
|
|
if ($request_method = PURGE) {
|
|
return 462;
|
|
}
|
|
|
|
limit_conn downloads_by_ip 100; # ddos protection: max 100 downloads at a time
|
|
|
|
# we need to explicitly use set directive here because $2 and $3 will contain values with
|
|
# decoded whitespaces and set will re-encode it for us before passing it to proxy_pass
|
|
set $skylink $2;
|
|
set $path $3;
|
|
|
|
# v2 support
|
|
set $skylink_v1 $skylink;
|
|
set $skylink_v2 $skylink;
|
|
|
|
access_by_lua_block {
|
|
-- disable cache if this is skylink v2
|
|
local isBase32v2 = string.len(ngx.var.skylink) == 55 and string.sub(ngx.var.skylink, 0, 2) == "04"
|
|
local isBase64v2 = string.len(ngx.var.skylink) == 46 and string.sub(ngx.var.skylink, 0, 2) == "AQ"
|
|
|
|
if isBase32v2 or isBase64v2 then
|
|
local res = ngx.location.capture("/skynet/resolve/" .. ngx.var.skylink_v2)
|
|
if res.status == ngx.HTTP_OK then
|
|
local json = require('cjson')
|
|
local resolve = json.decode(res.body)
|
|
ngx.var.skylink_v1 = resolve.skylink
|
|
end
|
|
end
|
|
|
|
-- this block runs only when accounts are enabled
|
|
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then return end
|
|
|
|
local res = ngx.location.capture("/accounts/user/limits", { copy_all_vars = true })
|
|
if res.status == ngx.HTTP_OK then
|
|
local json = require('cjson')
|
|
local limits = json.decode(res.body)
|
|
ngx.var.limit_rate = limits.download
|
|
end
|
|
}
|
|
|
|
proxy_read_timeout 600;
|
|
proxy_set_header User-Agent: Sia-Agent;
|
|
# proxy this call to siad /skynet/skylink/ endpoint (make sure the ip is correct)
|
|
proxy_pass http://siad/skynet/skylink/$skylink_v1$path$is_args$args;
|
|
}
|
|
|
|
location @base32_subdomain {
|
|
include /etc/nginx/conf.d/include/proxy-buffer;
|
|
|
|
proxy_pass http://127.0.0.1/$base32_subdomain/$request_uri;
|
|
}
|
|
|
|
location @hns_domain {
|
|
include /etc/nginx/conf.d/include/proxy-buffer;
|
|
|
|
proxy_pass http://127.0.0.1/hns/$hns_domain/$request_uri;
|
|
}
|
|
|
|
location @purge {
|
|
allow 10.0.0.0/8;
|
|
allow 127.0.0.1/32;
|
|
allow 172.16.0.0/12;
|
|
allow 192.168.0.0/16;
|
|
deny all;
|
|
|
|
set $lua_purge_path "/data/nginx/cache/";
|
|
content_by_lua_file /etc/nginx/conf.d/scripts/purge-multi.lua;
|
|
}
|
|
|
|
location ~ "^/file/([a-zA-Z0-9-_]{46}(/.*)?)$" {
|
|
include /etc/nginx/conf.d/include/proxy-buffer;
|
|
|
|
rewrite /file/(.*) $1 break; # drop the /file/ prefix from uri
|
|
|
|
proxy_pass http://127.0.0.1/$uri?attachment=true&$args;
|
|
}
|
|
|
|
location /__internal/do/not/use/authenticated {
|
|
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')
|
|
-- this block runs only when accounts are enabled
|
|
if os.getenv("ACCOUNTS_ENABLED", "0") == "0" then
|
|
ngx.say(json.encode{authenticated = false})
|
|
return ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
|
|
local res = ngx.location.capture("/accounts/user", { copy_all_vars = true })
|
|
if res.status == ngx.HTTP_OK then
|
|
local limits = json.decode(res.body)
|
|
ngx.say(json.encode{authenticated = limits.tier > 0})
|
|
return ngx.exit(ngx.HTTP_OK)
|
|
end
|
|
|
|
ngx.say(json.encode{authenticated = false})
|
|
return ngx.exit(ngx.HTTP_OK)
|
|
}
|
|
}
|
|
|
|
location /accounts {
|
|
internal; # internal endpoint only
|
|
access_log off; # do not log traffic
|
|
|
|
proxy_cache skynet; # use general nginx cache
|
|
proxy_cache_key $uri+$skynet_jwt; # include skynet-jwt cookie (mapped to skynet_jwt)
|
|
proxy_cache_valid 200 401 1m; # cache success and unauthorized responses for 1 minute
|
|
|
|
rewrite /accounts(.*) $1 break; # drop the /accounts prefix from uri
|
|
proxy_pass http://10.10.10.70:3000; # hardcoded ip because accounts might not be available
|
|
}
|
|
|
|
# include custom locations, specific to the server
|
|
include /etc/nginx/conf.d/server-override/*;
|
|
}
|