From 5065e66b466d00a477138a194a85b75e551bce37 Mon Sep 17 00:00:00 2001 From: Karol Wypchlo Date: Thu, 13 Jan 2022 21:41:56 +0100 Subject: [PATCH 1/4] support auth only portal mode in health-check module --- packages/health-check/src/checks/critical.js | 11 ++-- packages/health-check/src/checks/extended.js | 5 +- packages/health-check/src/index.js | 14 +++++- packages/health-check/src/utils.js | 53 ++++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/health-check/src/checks/critical.js b/packages/health-check/src/checks/critical.js index d5527b50..dd614beb 100644 --- a/packages/health-check/src/checks/critical.js +++ b/packages/health-check/src/checks/critical.js @@ -1,7 +1,7 @@ const got = require("got"); const FormData = require("form-data"); const { isEqual } = require("lodash"); -const { calculateElapsedTime, getResponseContent } = require("../utils"); +const { calculateElapsedTime, getResponseContent, getAuthCookie } = require("../utils"); const { SkynetClient, stringToUint8ArrayUtf8, genKeyPairAndSeed } = require("skynet-js"); const skynetClient = new SkynetClient(process.env.SKYNET_PORTAL_API); @@ -34,6 +34,7 @@ async function skydConfigCheck(done) { // uploadCheck returns the result of uploading a sample file async function uploadCheck(done) { + const authCookie = await getAuthCookie(); const time = process.hrtime(); const form = new FormData(); const payload = Buffer.from(new Date()); // current date to ensure data uniqueness @@ -42,7 +43,10 @@ async function uploadCheck(done) { form.append("file", payload, { filename: "time.txt", contentType: "text/plain" }); try { - const response = await got.post(`${process.env.SKYNET_PORTAL_API}/skynet/skyfile`, { body: form }); + const response = await got.post(`${process.env.SKYNET_PORTAL_API}/skynet/skyfile`, { + body: form, + headers: { cookie: authCookie }, + }); data.statusCode = response.statusCode; data.up = true; @@ -170,11 +174,12 @@ async function accountHealthCheck(done) { } async function genericAccessCheck(name, url) { + const authCookie = await getAuthCookie(); const time = process.hrtime(); const data = { up: false, url }; try { - const response = await got(url, { headers: { cookie: "nocache=true" } }); + const response = await got(url, { headers: { cookie: `nocache=true;${authCookie}` } }); data.statusCode = response.statusCode; data.up = true; diff --git a/packages/health-check/src/checks/extended.js b/packages/health-check/src/checks/extended.js index 01953d75..28a96f0e 100644 --- a/packages/health-check/src/checks/extended.js +++ b/packages/health-check/src/checks/extended.js @@ -2,7 +2,7 @@ const got = require("got"); const hasha = require("hasha"); const { detailedDiff } = require("deep-object-diff"); const { isEqual } = require("lodash"); -const { calculateElapsedTime, ensureValidJSON, getResponseContent } = require("../utils"); +const { calculateElapsedTime, ensureValidJSON, getResponseContent, getAuthCookie } = require("../utils"); const { parseSkylink } = require("skynet-js"); // audioExampleCheck returns the result of trying to download the skylink @@ -1130,12 +1130,13 @@ function parseHeaderString(header) { // skylinkVerification verifies a skylink against provided information. async function skylinkVerification(done, expected, { followRedirect = true, method = "get" } = {}) { + const authCookie = await getAuthCookie(); const time = process.hrtime(); const details = { name: expected.name, skylink: expected.skylink }; try { const query = `${process.env.SKYNET_PORTAL_API}/${expected.skylink}`; - const response = await got[method](query, { followRedirect, headers: { cookie: "nocache=true" } }); + const response = await got[method](query, { followRedirect, headers: { cookie: `nocache=true;${authCookie}` } }); const entry = { ...details, up: true, statusCode: response.statusCode, time: calculateElapsedTime(time) }; const info = {}; diff --git a/packages/health-check/src/index.js b/packages/health-check/src/index.js index 9d928156..5bf10868 100644 --- a/packages/health-check/src/index.js +++ b/packages/health-check/src/index.js @@ -4,8 +4,18 @@ if (!process.env.SKYNET_PORTAL_API) { throw new Error("You need to provide SKYNET_PORTAL_API environment variable"); } -if (process.env.ACCOUNTS_ENABLED === "true" && !process.env.SKYNET_DASHBOARD_URL) { - throw new Error("You need to provide SKYNET_DASHBOARD_URL environment variable when accounts are enabled"); +if (process.env.ACCOUNTS_ENABLED === "true") { + if (!process.env.SKYNET_DASHBOARD_URL) { + throw new Error("You need to provide SKYNET_DASHBOARD_URL environment variable when accounts are enabled"); + } + if (process.env.ACCOUNTS_LIMIT_ACCESS === "authenticated") { + if (!process.env.ACCOUNTS_TEST_USER_EMAIL) { + throw new Error("ACCOUNTS_TEST_USER_EMAIL cannot be empty"); + } + if (!process.env.ACCOUNTS_TEST_USER_PASSWORD) { + throw new Error("ACCOUNTS_TEST_USER_PASSWORD cannot be empty"); + } + } } const express = require("express"); diff --git a/packages/health-check/src/utils.js b/packages/health-check/src/utils.js index cebdb426..c8362f05 100644 --- a/packages/health-check/src/utils.js +++ b/packages/health-check/src/utils.js @@ -1,3 +1,5 @@ +const got = require("got"); + /** * Get the time between start and now in milliseconds */ @@ -39,9 +41,60 @@ function ensureValidJSON(object) { return JSON.parse(stringified); } +/** + * Authenticate with given credentials and return auth cookie + * Creates new account if username does not exist + * Only authenticates when portal is set to authenticated users only mode + */ +function getAuthCookie() { + // cache auth promise so only one actual request will be made + if (getAuthCookie.cache) return getAuthCookie.cache; + + // do not authenticate if it is not necessary + if (process.env.ACCOUNTS_LIMIT_ACCESS !== "authenticated") return {}; + + const email = process.env.ACCOUNTS_TEST_USER_EMAIL; + const password = process.env.ACCOUNTS_TEST_USER_PASSWORD; + + if (!email) throw new Error("ACCOUNTS_TEST_USER_EMAIL cannot be empty"); + if (!password) throw new Error("ACCOUNTS_TEST_USER_PASSWORD cannot be empty"); + + async function authenticate() { + try { + // authenticate with given test credentials + const response = await got.post(`${process.env.SKYNET_DASHBOARD_URL}/api/login`, { + json: { email, password }, + }); + + // extract set-cookie from successful authentication request + const cookies = response.headers["set-cookie"]; + + // find the skynet-jwt cookie + const jwtcookie = cookies.find((cookie) => cookie.startsWith("skynet-jwt")); + + // extract just the cookie value (no set-cookie props) from set-cookie + return jwtcookie.match(/skynet-jwt=[^;]+;/)[0]; + } catch (error) { + // 401 means that service worked but user could not have been authenticated + if (error.response && error.response.statusCode === 401) { + // sign up with the given credentials + await got.post(`${process.env.SKYNET_DASHBOARD_URL}/api/user`, { + json: { email, password }, + }); + + // retry authentication + return authenticate(); + } + } + } + + return (getAuthCookie.cache = authenticate()); +} + module.exports = { calculateElapsedTime, getYesterdayISOString, getResponseContent, ensureValidJSON, + getAuthCookie, }; From 330f220e1a9669420fcc341fe538e8b9c9f90a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wypch=C5=82o?= Date: Fri, 14 Jan 2022 10:51:09 +0100 Subject: [PATCH 2/4] Update packages/health-check/src/utils.js --- packages/health-check/src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health-check/src/utils.js b/packages/health-check/src/utils.js index c8362f05..595fbb1e 100644 --- a/packages/health-check/src/utils.js +++ b/packages/health-check/src/utils.js @@ -61,7 +61,7 @@ function getAuthCookie() { async function authenticate() { try { - // authenticate with given test credentials + // authenticate with given test user credentials const response = await got.post(`${process.env.SKYNET_DASHBOARD_URL}/api/login`, { json: { email, password }, }); From 185e9b898216b0f9076a0780de0b9686d69f6975 Mon Sep 17 00:00:00 2001 From: Karol Wypchlo Date: Fri, 14 Jan 2022 10:54:24 +0100 Subject: [PATCH 3/4] rethrow unhandled exception --- packages/health-check/src/utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/health-check/src/utils.js b/packages/health-check/src/utils.js index 595fbb1e..7e5b09b8 100644 --- a/packages/health-check/src/utils.js +++ b/packages/health-check/src/utils.js @@ -85,6 +85,9 @@ function getAuthCookie() { // retry authentication return authenticate(); } + + // rethrow unhandled exception + throw error; } } From fbe108a573f292cfa51773862b24d545d209def3 Mon Sep 17 00:00:00 2001 From: Karol Wypchlo Date: Fri, 14 Jan 2022 11:32:24 +0100 Subject: [PATCH 4/4] throw meaningful errors --- packages/health-check/src/utils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/health-check/src/utils.js b/packages/health-check/src/utils.js index 7e5b09b8..7ff84bec 100644 --- a/packages/health-check/src/utils.js +++ b/packages/health-check/src/utils.js @@ -69,9 +69,15 @@ function getAuthCookie() { // extract set-cookie from successful authentication request const cookies = response.headers["set-cookie"]; + // throw meaningful error when set-cookie header is missing + if (!cookies) throw new Error(`Auth successful (code ${response.statusCode}) but 'set-cookie' header is missing`); + // find the skynet-jwt cookie const jwtcookie = cookies.find((cookie) => cookie.startsWith("skynet-jwt")); + // throw meaningful error when skynet-jwt cookie is missing + if (!jwtcookie) throw new Error(`Header 'set-cookie' found but 'skynet-jwt' cookie is missing`); + // extract just the cookie value (no set-cookie props) from set-cookie return jwtcookie.match(/skynet-jwt=[^;]+;/)[0]; } catch (error) {