diff --git a/authenticate.html b/authenticate.html new file mode 100644 index 0000000..2831c2f --- /dev/null +++ b/authenticate.html @@ -0,0 +1,14 @@ + + + Authentication required + + +

Authentication required

+
+ + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 5d49f4f..301ea09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,149 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "bagpipe": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz", + "integrity": "sha1-40HRZPyyTN8E6n4Ft2XsEMiupqE=" + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "http": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "kruptein": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-2.2.3.tgz", + "integrity": "sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==", + "requires": { + "asn1.js": "^5.4.1" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "querystring": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "session-file-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.5.0.tgz", + "integrity": "sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==", + "requires": { + "bagpipe": "^0.3.5", + "fs-extra": "^8.0.1", + "kruptein": "^2.0.4", + "object-assign": "^4.1.1", + "retry": "^0.12.0", + "write-file-atomic": "3.0.3" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } } } } diff --git a/package.json b/package.json index a8ec756..bcb26b7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "author": "Leon Etienne", "license": "BSD-2-Clause", "dependencies": { - "http": "0.0.1-security" + "crypto": "^1.0.1", + "http": "0.0.1-security", + "querystring": "^0.2.1", + "session-file-store": "^1.5.0" } } diff --git a/server.js b/server.js index 57f1fab..b51c402 100755 --- a/server.js +++ b/server.js @@ -1,9 +1,14 @@ -#!/home/menethil/.nvm/versions/node/v14.16.1/bin/node - var http = require('http'); var fs = require('fs'); var path = require('path'); +var querystring = require('querystring'); +var crypto = require('crypto'); +var execSync = require('child_process').execSync; +//! How many seconds (from the last interaction) a session stays valid +const SESSION_DURATION = 10*60; + +// Just a few mime types const contentTypes = { '.html': 'text/html', '.css': 'text/css', @@ -19,9 +24,150 @@ const contentTypes = { '.webm': 'video/webm', }; -var server = http.createServer(function (request, response) { - // Handle requests here... +sessions = []; +//! Will create a session and return it's id. +function createSession() { + const timestamp = Date.now(); + const sessionId = SHA512Digest(timestamp.toString() + Math.floor(Math.random() * 100000).toString()); + + sessions.push({ + 'sessionId': sessionId, + 'timestamp': timestamp + }); + + return sessionId; +} + +//! Will check if a session of a given id exsists, and if it has been expired. +//! Will removed if expired. +//! Will also renew a sessions timestamp +function isSessisionValid(id) { + // Get an array of all sessions matching this id (should be 1 or 0) + var filteredSessions = sessions.filter((value, index, array) => { + return value.sessionId === id; + }); + + // Quick-reject: No session of this id + if (filteredSessions.length === 0) { + console.log('No session of this id...'); + return false; + } + + // Else: fetch the session + var sessionById = filteredSessions[0]; + + // Is the session still valid? + if (Date.now() - sessionById.timestamp > SESSION_DURATION * 1000) { + console.log('Session is no longer valid, because it expired... Removing it...'); + + // Remove the session from the list of sessions + const indexOfSession = sessions.indexOf(sessionById); + sessions.splice(indexOfSession, 1); + + return false; + } + + // Else: It must be valid. We should update its timestamp. + console.log('Session is active. Bumping timestamp...'); + sessionById.timestamp = Date.now(); + return true; +} + +//! Will decode cookies to an array +//! Source: https://stackoverflow.com/a/3409200 +//! I know this fails if a cookie contains '='. Mine don't! +function parseCookies(request) { + const list = {}; + const cookieHeader = request.headers?.cookie; + if (!cookieHeader) return list; + + cookieHeader.split(`;`).forEach(function(cookie) { + let [ name, ...rest] = cookie.split(`=`); + name = name?.trim(); + if (!name) return; + const value = rest.join(`=`).trim(); + if (!value) return; + list[name] = decodeURIComponent(value); + }); + + return list; +} + +function SHA512Digest(string) { + return crypto.createHash('sha512').update(string, 'utf-8').digest('hex'); +} + +function serveAuthenticatePage(request, response) { + fs.readFile(__dirname + '/authenticate.html', function (error, data) { + if (!error) { + response.writeHead(200, { + 'Content-Type': 'text/html' + }); + response.end(data); + return; + } else { + response.writeHead(500, { + 'Content-Type': 'text/html' + }); + + console.error('Unable to read authentication html file: ' + JSON.stringify(error)); + response.end('Internal server error.'); + return; + } + }); +} + +// FIX THIS BS! +const PASSWD_HASH = 'a3c1443b087cf5338d3696f6029fdf791ee4829a27e19c9f257a06ca0d88b5b518ac9868bb13199e807553bda62d3dc15b6354862f34fcab0a7c4c45530349ea'; + +function testAuthentication(request, response) { + // Wait for the request to have been received completely (including request body) + console.log('Request is trying to authenticate... Waiting for request body...'); + response.writeHead(200, { + 'Content-Type': 'text/html' + }); + + // Collect post data (request body) + var requestBody = ''; + request.on('data', function(data) { + requestBody += data.toString(); + }); + + // Process post data + request.on('end', function() { + console.log('Received complete request body.'); + + // Extract password from the request and hash it + const postData = querystring.parse(requestBody); + const password = postData['password']; + const passwordHash = SHA512Digest(password); + + // Is the password good? + if (passwordHash === PASSWD_HASH) { + // Yes, it is: + // Create session + const sessionId = createSession(); + + response.writeHead(200, { + 'Content-Type': 'text/html', + 'Set-Cookie': 'sesid=' + sessionId + }); + response.end('Access granted! You\'re in!'); + return; + } else { + response.writeHead(401, { + 'Content-Type': 'text/html' + }); + response.end('WOOP! WOOP! Invalid password!

Need to reset your password? Replace the password hash in access.yaml with a new one.
This password hashes to: ' + passwordHash + '.'); + return; + } + + return; + }); +} + +function serverStaticFiles(request, response) { // Fetch requested file fs.readFile(__dirname + request.url, function (error, data) { if(!error) { @@ -52,6 +198,33 @@ var server = http.createServer(function (request, response) { return; } }); +} + +var server = http.createServer(function (request, response) { + // Handle requests here... + + // If request is trying to authenticate + if (request.url == '/api--authenticate') { + testAuthentication(request, response); + return; + } + else /* Request is not trying to authenticate */ { + + // Parse request cookies + const cookies = parseCookies(request); + console.log(cookies); + + // Check if the user is authenticated + if ((cookies.hasOwnProperty('sesid')) && (isSessisionValid(cookies['sesid']))) { + // Session is authenticated. File access is granted. + serverStaticFiles(request, response); + return; + + } else /* Session is not authenticated */ { + serveAuthenticatePage(request, response); + return; + } + } }); const port = 80;