Merge pull request #1856 from SkynetLabs/ivo/nginx_passes_apikey
Nginx to pass Skynet-Api-Key and Authorization headers
This commit is contained in:
commit
a292a9e2a2
|
@ -1,8 +1,9 @@
|
|||
# register the download in accounts service (cookies should contain jwt)
|
||||
log_by_lua_block {
|
||||
-- this block runs only when accounts are enabled
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
local function track(premature, skylink, status, body_bytes_sent, jwt)
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
-- tracking runs only when request comes from authenticated user
|
||||
if skynet_account.is_authenticated() then
|
||||
local function track(premature, skylink, status, body_bytes_sent, auth_headers)
|
||||
if premature then return end
|
||||
|
||||
local httpc = require("resty.http").new()
|
||||
|
@ -11,7 +12,7 @@ log_by_lua_block {
|
|||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/track/download/" .. skylink .. "?" .. query, {
|
||||
method = "POST",
|
||||
headers = { ["Cookie"] = "skynet-jwt=" .. jwt },
|
||||
headers = auth_headers,
|
||||
})
|
||||
|
||||
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
|
||||
|
@ -19,8 +20,9 @@ log_by_lua_block {
|
|||
end
|
||||
end
|
||||
|
||||
if ngx.header["Skynet-Skylink"] and ngx.var.skynet_jwt ~= "" and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
|
||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.status, ngx.var.body_bytes_sent, ngx.var.skynet_jwt)
|
||||
if ngx.header["Skynet-Skylink"] and ngx.status >= ngx.HTTP_OK and ngx.status < ngx.HTTP_SPECIAL_RESPONSE then
|
||||
local auth_headers = skynet_account.get_auth_headers()
|
||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.status, ngx.var.body_bytes_sent, auth_headers)
|
||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
# register the registry access in accounts service (cookies should contain jwt)
|
||||
log_by_lua_block {
|
||||
-- this block runs only when accounts are enabled
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
local function track(premature, request_method, jwt)
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
-- tracking runs only when request comes from authenticated user
|
||||
if skynet_account.is_authenticated() then
|
||||
local function track(premature, request_method, auth_headers)
|
||||
if premature then return end
|
||||
|
||||
local httpc = require("resty.http").new()
|
||||
|
||||
-- based on request method we assign a registry action string used
|
||||
-- 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 },
|
||||
headers = auth_headers,
|
||||
})
|
||||
|
||||
|
||||
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 ngx.status == ngx.HTTP_OK or ngx.status == ngx.HTTP_NOT_FOUND then
|
||||
local auth_headers = skynet_account.get_auth_headers()
|
||||
local ok, err = ngx.timer.at(0, track, ngx.req.get_method(), auth_headers)
|
||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# register the upload in accounts service (cookies should contain jwt)
|
||||
log_by_lua_block {
|
||||
-- this block runs only when accounts are enabled
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
local function track(premature, skylink, jwt)
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
-- tracking runs only when request comes from authenticated user
|
||||
if skynet_account.is_authenticated() then
|
||||
local function track(premature, skylink, auth_headers)
|
||||
if premature then return end
|
||||
|
||||
local httpc = require("resty.http").new()
|
||||
|
@ -10,7 +11,7 @@ log_by_lua_block {
|
|||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/track/upload/" .. skylink, {
|
||||
method = "POST",
|
||||
headers = { ["Cookie"] = "skynet-jwt=" .. jwt },
|
||||
headers = auth_headers,
|
||||
})
|
||||
|
||||
if err or (res and res.status ~= ngx.HTTP_NO_CONTENT) then
|
||||
|
@ -19,8 +20,9 @@ log_by_lua_block {
|
|||
end
|
||||
|
||||
-- report all skylinks (header empty if request failed) but only if jwt is preset (user is authenticated)
|
||||
if ngx.header["Skynet-Skylink"] and ngx.var.skynet_jwt ~= "" then
|
||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], ngx.var.skynet_jwt)
|
||||
if ngx.header["Skynet-Skylink"] then
|
||||
local auth_headers = skynet_account.get_auth_headers()
|
||||
local ok, err = ngx.timer.at(0, track, ngx.header["Skynet-Skylink"], auth_headers)
|
||||
if err then ngx.log(ngx.ERR, "Failed to create timer: ", err) end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -174,24 +174,25 @@ location /skynet/registry/subscription {
|
|||
set $notificationdelay "0";
|
||||
|
||||
rewrite_by_lua_block {
|
||||
-- this block runs only when accounts are enabled
|
||||
if os.getenv("PORTAL_MODULES"):match("a") then
|
||||
local httpc = require("resty.http").new()
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
-- fetch account limits and set download bandwidth and registry delays accordingly
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
|
||||
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?unit=byte: ", 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
|
||||
if skynet_account.accounts_enabled() then
|
||||
-- check if portal is in authenticated only mode
|
||||
if skynet_account.is_access_unauthorized() then
|
||||
return skynet_account.exit_access_unauthorized()
|
||||
end
|
||||
|
||||
-- check if portal is in subscription only mode
|
||||
if skynet_account.is_access_forbidden() then
|
||||
return skynet_account.exit_access_forbidden()
|
||||
end
|
||||
|
||||
-- get account limits of currently authenticated user
|
||||
local limits = skynet_account.get_account_limits()
|
||||
|
||||
-- apply bandwidth limit and notification delay
|
||||
ngx.var.bandwidthlimit = limits.download
|
||||
ngx.var.notificationdelay = limits.registry
|
||||
end
|
||||
}
|
||||
|
||||
|
@ -259,19 +260,21 @@ location /skynet/tus {
|
|||
proxy_pass http://sia:9980;
|
||||
|
||||
access_by_lua_block {
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
if skynet_account.accounts_enabled() then
|
||||
-- check if portal is in authenticated only mode
|
||||
if require("skynet.account").is_access_unauthorized() then
|
||||
return require("skynet.account").exit_access_unauthorized()
|
||||
if skynet_account.is_access_unauthorized() then
|
||||
return skynet_account.exit_access_unauthorized()
|
||||
end
|
||||
|
||||
-- check if portal is in subscription only mode
|
||||
if require("skynet.account").is_access_forbidden() then
|
||||
return require("skynet.account").exit_access_forbidden()
|
||||
if skynet_account.is_access_forbidden() then
|
||||
return skynet_account.exit_access_forbidden()
|
||||
end
|
||||
|
||||
-- get account limits of currently authenticated user
|
||||
local limits = require("skynet.account").get_account_limits()
|
||||
local limits = skynet_account.get_account_limits()
|
||||
|
||||
-- apply upload size limits
|
||||
ngx.req.set_header("SkynetMaxUploadSize", limits.maxUploadSize)
|
||||
|
@ -362,19 +365,21 @@ location /skynet/trustless/basesector {
|
|||
set $limit_rate 0;
|
||||
|
||||
access_by_lua_block {
|
||||
if require("skynet.account").accounts_enabled() then
|
||||
local skynet_account = require("skynet.account")
|
||||
|
||||
if skynet_account.accounts_enabled() then
|
||||
-- check if portal is in authenticated only mode
|
||||
if require("skynet.account").is_access_unauthorized() then
|
||||
return require("skynet.account").exit_access_unauthorized()
|
||||
if skynet_account.is_access_unauthorized() then
|
||||
return skynet_account.exit_access_unauthorized()
|
||||
end
|
||||
|
||||
-- check if portal is in subscription only mode
|
||||
if require("skynet.account").is_access_forbidden() then
|
||||
return require("skynet.account").exit_access_forbidden()
|
||||
if skynet_account.is_access_forbidden() then
|
||||
return skynet_account.exit_access_forbidden()
|
||||
end
|
||||
|
||||
-- get account limits of currently authenticated user
|
||||
local limits = require("skynet.account").get_account_limits()
|
||||
local limits = skynet_account.get_account_limits()
|
||||
|
||||
-- apply download speed limit
|
||||
ngx.var.limit_rate = limits.download
|
||||
|
|
|
@ -14,6 +14,34 @@ local anon_limits = {
|
|||
["registry"] = 250
|
||||
}
|
||||
|
||||
-- get all non empty authentication headers from request, we want to return
|
||||
-- all of them and let accounts service deal with validation and prioritisation
|
||||
function _M.get_auth_headers()
|
||||
local utils = require("utils")
|
||||
local request_headers = ngx.req.get_headers()
|
||||
local headers = {}
|
||||
|
||||
-- try to extract skynet-jwt cookie from cookie header
|
||||
local skynet_jwt_cookie = utils.extract_cookie(request_headers["Cookie"], "skynet[-]jwt")
|
||||
|
||||
-- if skynet-jwt cookie is present, pass it as is
|
||||
if skynet_jwt_cookie then
|
||||
headers["Cookie"] = skynet_jwt_cookie
|
||||
end
|
||||
|
||||
-- if authorization header is set, pass it as is
|
||||
if request_headers["Authorization"] then
|
||||
headers["Authorization"] = request_headers["Authorization"]
|
||||
end
|
||||
|
||||
-- if skynet api key header is set, pass it as is
|
||||
if request_headers["Skynet-Api-Key"] then
|
||||
headers["Skynet-Api-Key"] = request_headers["Skynet-Api-Key"]
|
||||
end
|
||||
|
||||
return headers
|
||||
end
|
||||
|
||||
-- handle request exit when access to portal should be restricted to authenticated users only
|
||||
function _M.exit_access_unauthorized(message)
|
||||
ngx.status = ngx.HTTP_UNAUTHORIZED
|
||||
|
@ -36,8 +64,11 @@ end
|
|||
|
||||
function _M.get_account_limits()
|
||||
local cjson = require('cjson')
|
||||
local utils = require('utils')
|
||||
local auth_headers = _M.get_auth_headers()
|
||||
|
||||
if ngx.var.skynet_jwt == "" then
|
||||
-- simple case of anonymous request - none of available auth headers exist
|
||||
if utils.is_table_empty(auth_headers) then
|
||||
return anon_limits
|
||||
end
|
||||
|
||||
|
@ -46,7 +77,7 @@ function _M.get_account_limits()
|
|||
|
||||
-- 10.10.10.70 points to accounts service (alias not available when using resty-http)
|
||||
local res, err = httpc:request_uri("http://10.10.10.70:3000/user/limits?unit=byte", {
|
||||
headers = { ["Cookie"] = "skynet-jwt=" .. ngx.var.skynet_jwt }
|
||||
headers = auth_headers,
|
||||
})
|
||||
|
||||
-- fail gracefully in case /user/limits failed
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
local _M = {}
|
||||
|
||||
-- utility function for checking if table is empty
|
||||
function _M.is_table_empty(check)
|
||||
-- bind next to local variable to achieve ultimate efficiency
|
||||
-- https://stackoverflow.com/a/1252776
|
||||
local next = next
|
||||
|
||||
return next(check) == nil
|
||||
end
|
||||
|
||||
-- extract full cookie name and value by its name from cookie string
|
||||
-- note: name matcher argument is a pattern so you will need to escape
|
||||
-- any special characters, read more https://www.lua.org/pil/20.2.html
|
||||
function _M.extract_cookie(cookie_string, name_matcher)
|
||||
-- nil cookie string safeguard
|
||||
if cookie_string == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local start, stop = string.find(cookie_string, name_matcher .. "=[^;]+")
|
||||
|
||||
if start then
|
||||
return string.sub(cookie_string, start, stop)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- extract just the cookie value by its name from cookie string
|
||||
-- note: name matcher argument is a pattern so you will need to escape
|
||||
-- any special characters, read more https://www.lua.org/pil/20.2.html
|
||||
function _M.extract_cookie_value(cookie_string, name_matcher)
|
||||
local cookie = _M.extract_cookie(cookie_string, name_matcher)
|
||||
|
||||
if cookie == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
local value_start = string.find(cookie, "=") + 1
|
||||
|
||||
return string.sub(cookie, value_start)
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,79 @@
|
|||
local utils = require('utils')
|
||||
|
||||
describe("is_table_empty", function()
|
||||
it("should return true for empty table", function()
|
||||
assert.is_true(utils.is_table_empty({}))
|
||||
end)
|
||||
|
||||
it("should return false for not empty table", function()
|
||||
assert.is_false(utils.is_table_empty({ ["foo"] = "bar" }))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("extract_cookie", function()
|
||||
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
|
||||
|
||||
it("should return nil if cookie string is nil", function()
|
||||
local cookie = utils.extract_cookie_value(nil, "aaa")
|
||||
|
||||
assert.is_nil(cookie)
|
||||
end)
|
||||
|
||||
it("should return nil if cookie name is not found", function()
|
||||
local cookie = utils.extract_cookie(cookie_string, "foo")
|
||||
|
||||
assert.is_nil(cookie)
|
||||
end)
|
||||
|
||||
it("should return cookie if cookie_string starts with that cookie name", function()
|
||||
local cookie = utils.extract_cookie(cookie_string, "aaa")
|
||||
|
||||
assert.are.equals(cookie, "aaa=bbb")
|
||||
end)
|
||||
|
||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
||||
local cookie = utils.extract_cookie(cookie_string, "xxx")
|
||||
|
||||
assert.are.equals(cookie, "xxx=yyy")
|
||||
end)
|
||||
|
||||
it("should return cookie with custom matcher", function()
|
||||
local cookie = utils.extract_cookie(cookie_string, "skynet[-]jwt")
|
||||
|
||||
assert.are.equals(cookie, "skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("extract_cookie_value", function()
|
||||
local cookie_string = "aaa=bbb; skynet-jwt=MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==; xxx=yyy"
|
||||
|
||||
it("should return nil if cookie string is nil", function()
|
||||
local value = utils.extract_cookie_value(nil, "aaa")
|
||||
|
||||
assert.is_nil(value)
|
||||
end)
|
||||
|
||||
it("should return nil if cookie name is not found", function()
|
||||
local value = utils.extract_cookie_value(cookie_string, "foo")
|
||||
|
||||
assert.is_nil(value)
|
||||
end)
|
||||
|
||||
it("should return value if cookie_string starts with that cookie name", function()
|
||||
local value = utils.extract_cookie_value(cookie_string, "aaa")
|
||||
|
||||
assert.are.equals(value, "bbb")
|
||||
end)
|
||||
|
||||
it("should return cookie if cookie_string ends with that cookie name", function()
|
||||
local value = utils.extract_cookie_value(cookie_string, "xxx")
|
||||
|
||||
assert.are.equals(value, "yyy")
|
||||
end)
|
||||
|
||||
it("should return cookie with custom matcher", function()
|
||||
local value = utils.extract_cookie_value(cookie_string, "skynet[-]jwt")
|
||||
|
||||
assert.are.equals(value, "MTY0NzUyr8jD-ytiWtspm0tGabKfooxeIDuWcXhJ3lnY0eEw==")
|
||||
end)
|
||||
end)
|
|
@ -117,13 +117,6 @@ http {
|
|||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# skynet-jwt contains dash so we cannot use $cookie_skynet-jwt
|
||||
# https://richardhart.me/2012/03/18/logging-nginx-cookies-with-dashes/
|
||||
map $http_cookie $skynet_jwt {
|
||||
default '';
|
||||
~skynet-jwt=(?<match>[^\;]+) $match;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/conf.extra.d/*.conf;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import ky from "ky";
|
||||
|
||||
const prefix = process.env.NEXT_PUBLIC_PORTAL_DOMAIN ? `https://account.${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}` : "";
|
||||
|
||||
export default ky.create({ prefixUrl: `${prefix}/api` });
|
||||
export default ky.create({ prefixUrl: "/api" });
|
||||
|
|
|
@ -2,8 +2,6 @@ import useSWR from "swr";
|
|||
import { useRouter } from "next/router";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const prefix = process.env.NEXT_PUBLIC_PORTAL_DOMAIN ? `https://account.${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}` : "";
|
||||
|
||||
const fetcher = (url, router) => {
|
||||
return fetch(url).then((res) => {
|
||||
if (res.status === StatusCodes.UNAUTHORIZED) {
|
||||
|
@ -17,5 +15,5 @@ const fetcher = (url, router) => {
|
|||
export default function useAccountsApi(key, config) {
|
||||
const router = useRouter();
|
||||
|
||||
return useSWR(`${prefix}/api/${key}`, (url) => fetcher(url, router), config);
|
||||
return useSWR(`/api/${key}`, (url) => fetcher(url, router), config);
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import useSWR from "swr";
|
|||
import { useRouter } from "next/router";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
const prefix = process.env.NEXT_PUBLIC_PORTAL_DOMAIN ? `https://account.${process.env.NEXT_PUBLIC_PORTAL_DOMAIN}` : "";
|
||||
|
||||
const fetcher = (url, router) => {
|
||||
return fetch(url).then((res) => {
|
||||
if (res.status === StatusCodes.OK) router.push("/");
|
||||
|
@ -13,5 +11,5 @@ const fetcher = (url, router) => {
|
|||
export default function useAnonRoute() {
|
||||
const router = useRouter();
|
||||
|
||||
return useSWR(`${prefix}/api/user`, (url) => fetcher(url, router));
|
||||
return useSWR("/api/user", (url) => fetcher(url, router));
|
||||
}
|
||||
|
|
Reference in New Issue