diff --git a/css/authorize.css b/css/authorize.css new file mode 100644 index 0000000..9a3e35b --- /dev/null +++ b/css/authorize.css @@ -0,0 +1,21 @@ +button { + font-size: 18px; + margin: 10px; +} + +.center { + text-align: center; + margin: auto; + width: 50%; + border: 3px solid green; + padding: 10px; +} + +body { + background-color: var(--bg-color); + color: var(--text-color); +} + +.hidden { + display: none; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9d78cae..9d220d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "express": "^4.18.2", "express-fileupload": "^1.3.1", "express-useragent": "^1.0.15", + "hcaptcha": "^0.1.1", "html-minifier-terser": "^7.1.0", "lru-cache": "^7.14.1", "mysql2": "^3.1.0", @@ -26,6 +27,9 @@ "uglify-js": "^3.17.4", "unsafe_encrypt": "^1.0.4", "ws": "^8.12.0" + }, + "devDependencies": { + "@hcaptcha/types": "^1.0.3" } }, "node_modules/@colors/colors": { @@ -80,6 +84,12 @@ "node": ">=6" } }, + "node_modules/@hcaptcha/types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@hcaptcha/types/-/types-1.0.3.tgz", + "integrity": "sha512-1mbU6eSGawRrqeahRrOzZo/SVLI6oZ5/azuBpSyVrRRR96CnS3fOVDWfzxpngfxKD0/I9Rwu6c/3ITqD8rXeTQ==", + "dev": true + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -1086,6 +1096,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hcaptcha": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz", + "integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw==" + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -2498,6 +2513,12 @@ "yargs": "^16.2.0" } }, + "@hcaptcha/types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@hcaptcha/types/-/types-1.0.3.tgz", + "integrity": "sha512-1mbU6eSGawRrqeahRrOzZo/SVLI6oZ5/azuBpSyVrRRR96CnS3fOVDWfzxpngfxKD0/I9Rwu6c/3ITqD8rXeTQ==", + "dev": true + }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -3278,6 +3299,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" }, + "hcaptcha": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.1.1.tgz", + "integrity": "sha512-iMrDmH2VpIEKOrcKWidVjI89FdDKTEdZ7PfPWkP27sTazIIkob8YfdY2ezaufAnWBiUUcvzsn0qF+dyXtBH2Vw==" + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", diff --git a/package.json b/package.json index 9479974..1af5c39 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "express": "^4.18.2", "express-fileupload": "^1.3.1", "express-useragent": "^1.0.15", + "hcaptcha": "^0.1.1", "html-minifier-terser": "^7.1.0", "lru-cache": "^7.14.1", "mysql2": "^3.1.0", @@ -36,5 +37,8 @@ "bugs": { "url": "https://github.com/002Hub/IPost/issues" }, - "homepage": "https://github.com/002Hub/IPost#readme" + "homepage": "https://github.com/002Hub/IPost#readme", + "devDependencies": { + "@hcaptcha/types": "^1.0.3" + } } diff --git a/routes/api/all.js b/routes/api/all.js index 4ce6f88..a72e15a 100644 --- a/routes/api/all.js +++ b/routes/api/all.js @@ -18,7 +18,7 @@ export const setup = function (router, con, server) { console.log("error parsing header",err) } } - if(req.body.auth != undefined) { + if(req.body.auth !== undefined && req.originalUrl!=="/redeemauthcode") { if(typeof req.body.auth === "string") { try{ req.body.auth = JSON.parse(req.body.auth) diff --git a/routes/authorize.js b/routes/authorize.js new file mode 100644 index 0000000..28b6ba6 --- /dev/null +++ b/routes/authorize.js @@ -0,0 +1,125 @@ +import {randomBytes} from "crypto" +import {SHA256} from "../extra_modules/SHA.js"; +import {unsign} from "../extra_modules/unsign.js"; + +export const setup = function (router, con, server) { + const temp_code_to_token = {} + router.post("/authorize",async (req,res) => { + if (!unsign(req.cookies.AUTH_COOKIE, req, res)){ + return + } + + let data = await server.hcaptcha.verify(req.body["h-captcha-response"]) + + if(data.success) { + + console.log("captcha success") + + let appid = req.body.application_id + if(typeof appid === "string") { + appid = Number(appid) + } + if(typeof appid === "number") { + + const token = randomBytes(150).toString("base64") + + let tokencode; + while(tokencode===undefined || temp_code_to_token[tokencode]!==undefined) { + tokencode = randomBytes(15).toString("base64") + } + temp_code_to_token[tokencode]={ + "userid":res.locals.userid, + "appid":appid, + "token":token + } + setTimeout(() => { + let data = temp_code_to_token[tokencode] + if(data !== undefined && data.token===token && data.appid === appid && data.userid === userid) { + temp_code_to_token[tokencode]=undefined + } + }, 300000); //wait for 5 minutes + + const sql = "SELECT application_auth_url FROM ipost.application where application_id=?" + + con.query(sql,[appid],(err,result) => { + if(err || result.length !== 1) { + console.err(err) + res.redirect(`/authorize?id=${req.body.application_id}`) + return + } + res.redirect(`${result[0].application_auth_url}?code=${tokencode}`) + }) + + + + return + } + } + + res.redirect(`/authorize?id=${req.body.application_id}`) + }) + + router.post("/redeemauthcode", (req,res) => { + + if(temp_code_to_token[req.body.authcode]===undefined) { + res.status(400) + res.json({"status":400,"message":"invalid code given"}) + return + } + + if(typeof req.body.auth === "string") { + try{ + req.body.auth = JSON.parse(req.body.auth) + } catch(err) { + console.log("error parsing",err) + } + } + if( + typeof req.body.auth !== "object" || + typeof req.body.auth.secret !== "string" || + typeof req.body.auth.appid !== "number" || + req.body.auth.secret.length !== 200 || + Buffer.from(req.body.auth.secret,"base64").length !== 150 || + req.body.auth.appid !== temp_code_to_token[req.body.authcode].appid + ) { + //console.log(1,req.body.auth,temp_code_to_token[req.body.authcode].appid) + res.status(420).send("invalid authentication object") + return; + } + + const appid = req.body.auth.appid + + const checksecret = SHA256(req.body.auth.secret,appid,10000) + + const checksql = "SELECT application_id from ipost.application where application_secret=? and application_id=?" + const checkvalues = [checksecret,appid] + + con.query(checksql,checkvalues,(error,result_object) => { + + if(error || result_object[0]===undefined || result_object[0].application_id!==appid) { + res.status(400) + res.json({"status":400,"message":"invalid code given"}) + return + } + + let data = temp_code_to_token[req.body.authcode] + temp_code_to_token[req.body.authcode] = undefined + + + const sql = "INSERT INTO `ipost`.`auth_tokens`(`auth_token`,`auth_token_u_id`,`auth_token_isfrom_application_id`) VALUES(?,?,?);" + + const values = [SHA256(data.token,appid,10000),data.userid,data.appid] //token,id,appid + con.query(sql,values,(err,result) => { + if(err) { + res.json({"status":500,"message":"error redeeming code"}) + console.err(err) + } else { + res.json({"status":200,"message":"successfully redeemed code","token":data.token}) + } + }) + }) + + + + }) +} \ No newline at end of file diff --git a/routes/setup_all_routes.js b/routes/setup_all_routes.js index 52f6486..8dd4d4d 100644 --- a/routes/setup_all_routes.js +++ b/routes/setup_all_routes.js @@ -11,6 +11,7 @@ import { setup as userroutessetup } from "./api/userRoutes.js"; import { setup as servefilessetup} from "./serve_static_files.js" import { setup as userfilessetup} from "./userfiles.js" import { setup as userauthsetup} from "./user_auth.js" +import { setup as applicationsetup} from "./authorize.js" export const setup = function (router, con, server) { const setuproute = handler => handler(router,con,server) @@ -34,4 +35,6 @@ export const setup = function (router, con, server) { setuproute(userfilessetup) //needs getPID and getDMPID setuproute(userauthsetup) //login & register + + setuproute(applicationsetup) } \ No newline at end of file diff --git a/routes/userfiles.js b/routes/userfiles.js index c8880a8..59a5887 100644 --- a/routes/userfiles.js +++ b/routes/userfiles.js @@ -80,7 +80,8 @@ export const setup = function (router, con, server) { newrelic: load_var("./extra_modules/newrelic_monitor.html"), getPID: server.global_page_variables.getPID, getDMPID: server.global_page_variables.getDMPID, - unauthorized_description: "Chat now by creating an account on IPost" + unauthorized_description: "Chat now by creating an account on IPost", + hcaptcha_sitekey: server.hcaptcha.sitekey } diff --git a/server.js b/server.js index df8fac0..3d6c764 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,7 @@ import {unsign} from "./extra_modules/unsign.js"; import { readFileSync, appendFile } from "fs"; import { format } from "util"; import { setup as SETUP_ROUTES} from "./routes/setup_all_routes.js" +import { verify as verifyHCaptcha_int } from "hcaptcha" import { ensureExists } from "./extra_modules/ensureExists.js" @@ -58,6 +59,13 @@ function log_info(level, ...info) { } console.log = log_info; + +const hcaptcha_secret = config.hcaptcha_secret +// wrapper for the HCaptcha verify function +function verifyHCaptcha(token) { + return verifyHCaptcha_int(hcaptcha_secret,token,undefined,config.hcaptcha_sitekey) +} + const WebSocket = ws.WebSocketServer; const router = Router(); @@ -259,7 +267,7 @@ app.use(bodyParser.default.json({ limit: "100mb" })); app.use(bodyParser.default.urlencoded({ limit: "100mb", extended: true })); app.use(cookieParser(cookiesecret)); app.use(compression()) -var blocked_headers = [ +let blocked_headers = [ 'HTTP_VIA', 'HTTP_X_FORWARDED_FOR', 'HTTP_FORWARDED_FOR', @@ -334,7 +342,11 @@ var commonfunctions = { ensureExists, "dirname": __dirname, config, - DID_I_FINALLY_ADD_HTTPS + DID_I_FINALLY_ADD_HTTPS, + hcaptcha: { + "verify":verifyHCaptcha, + "sitekey":config.hcaptcha_sitekey + } }; SETUP_ROUTES(router,con,commonfunctions) diff --git a/server_config.json b/server_config.json index 7dc96c1..fdddc9f 100644 --- a/server_config.json +++ b/server_config.json @@ -155,12 +155,14 @@ "level": 5 }, "ssl": { - "privateKey": "/etc/letsencrypt/live/ipost.rocks/privkey.pem", - "certificate" : "/etc/letsencrypt/live/ipost.rocks/cert.pem" + "privateKey": "./etc/letsencrypt/live/ipost.rocks/privkey.pem", + "certificate" : "./etc/letsencrypt/live/ipost.rocks/fullchain.pem" }, "ports": { "http": 9999, "https": 9998 }, - "disallow_proxies_by_headers": true + "disallow_proxies_by_headers": true, + "hcaptcha_secret": "0x0000000000000000000000000000000000000000", + "hcaptcha_sitekey": "10000000-ffff-ffff-ffff-000000000001" } diff --git a/views/authorize.html b/views/authorize.html new file mode 100644 index 0000000..e21ed52 --- /dev/null +++ b/views/authorize.html @@ -0,0 +1,31 @@ + + + + <% if(user.username === undefined) { %> + + <% } else { %> + + <% } %> + Authorize App + + <% if(user.username === undefined) { %> + + <% } else { %> + + <% } %> + + +
+

Authorize App

+

Please authorize the app to access your information:

+
+ class="hidden" name="application_id" id="application_id"> +
+ +
+
+ +