From a7a4714ea0358a1d92efac6fcb846520986d47a4 Mon Sep 17 00:00:00 2001 From: wgroeneveld Date: Tue, 23 Mar 2021 21:04:17 +0100 Subject: [PATCH] phase one of pingback receive XML-RPC implementation --- src/pingback/receive.js | 43 ++++++++ src/pingback/route.js | 74 +++++++++++++ src/serve.js | 1 + test/pingback/receive-validate.test.js | 143 +++++++++++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 src/pingback/receive.js create mode 100644 src/pingback/route.js create mode 100644 test/pingback/receive-validate.test.js diff --git a/src/pingback/receive.js b/src/pingback/receive.js new file mode 100644 index 0000000..8282a00 --- /dev/null +++ b/src/pingback/receive.js @@ -0,0 +1,43 @@ + +const config = require('./../config') +const parser = require("fast-xml-parser") + +/** +Sample XML: + + + pingback.ping + + + https://brainbaking.com/kristien.html + + + https://kristienthoelen.be/2021/03/22/de-stadia-van-een-burn-out-in-welk-stadium-zit-jij/ + + + +*/ +const isValidDomain = (url) => { + return config.allowedWebmentionSources.some(domain => { + return url.indexOf(domain) !== -1 + }) +} + +function validate(body) { + const xml = parser.parse(body) + if(!xml) return false + if(!xml.methodCall || xml.methodCall.methodName !== "pingback.ping") return false + if(!xml.methodCall.params || !xml.methodCall.params.param || xml.methodCall.params.param.length !== 2) return false + if(!xml.methodCall.params.param.every(param => param?.value?.string?.startsWith('http'))) return false + if(!isValidDomain(xml.methodCall.params.param[1].value.string)) return false + return true +} + +async function receive(body) { + +} + +module.exports = { + receive, + validate +} diff --git a/src/pingback/route.js b/src/pingback/route.js new file mode 100644 index 0000000..0a6fc56 --- /dev/null +++ b/src/pingback/route.js @@ -0,0 +1,74 @@ + +const pingbackReceiver = require('./receive') + +function success(msg) { + return ` + + + + + + ${msg} + + + + + +` +} + +function err(e) { + console.err(` -- pingback receive went wrong: ${e.message}`) + return ` + + + + + + + faultCode + + + + 0 + + + + + + faultString + + + + ${e.message} + + + + + + +` +} + +function route(router) { + router.post("pingback receive endpoint", "/pingback", async (ctx) => { + try { + if(!pingbackReceiver.validate(ctx.request.body)) { + throw "malformed pingback request" + } + + // we do NOT await this on purpose. + pingbackReceiver.receive(ctx.request.body) + + ctx.status = 200 + ctx.body = success("Thanks, bro. Will process this pingback soon, pinky swear!") + } catch(e) { + ctx.status = 200 + ctx.body = err(e) + } + }); +} + +module.exports = { + route +} diff --git a/src/serve.js b/src/serve.js index 4dfd78b..49af8aa 100644 --- a/src/serve.js +++ b/src/serve.js @@ -29,6 +29,7 @@ app.use(bodyParser({ // route docs: https://github.com/koajs/router/blob/HEAD/API.md#module_koa-router--Router+get%7Cput%7Cpost%7Cpatch%7Cdelete%7Cdel require("./webmention/route").route(router); +require("./pingback/route").route(router); const config = require("./config"); config.setupDataDirs(); diff --git a/test/pingback/receive-validate.test.js b/test/pingback/receive-validate.test.js new file mode 100644 index 0000000..fd22ab7 --- /dev/null +++ b/test/pingback/receive-validate.test.js @@ -0,0 +1,143 @@ + +describe("pingback receive validation tests", () => { + + const { validate } = require('../../src/pingback/receive') + + test("not valid if malformed XML as body", () => { + const result = validate("ola pola") + expect(result).toBe(false) + }) + + test("not valid if methodName is not pingback.ping", () => { + const result = validate(` + + + ka.tsjing + + + https://cool.site + + + https://brainbaking.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(false) + }) + + test("not valid if less than two parameters", () => { + const result = validate(` + + + pingback.ping + + + https://brainbaking.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(false) + }) + + test("not valid if more than two parameters", () => { + const xml = ` + + pingback.ping + + + https://cool.site + + + https://brainbaking.com/post/2021/03/cool-ness + + + https://brainbaking.com/post/2021/03/cool-ness + + + + ` + + expect(validate(xml)).toBe(false) + }) + + test("not valid if target is not in trusted domains from config", () => { + const result = validate(` + + + pingback.ping + + + https://cool.site + + + https://flashballz.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(false) + }) + + test("not valid if target is not http(s)", () => { + const result = validate(` + + + pingback.ping + + + https://cool.site + + + gemini://brainbaking.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(false) + }) + + test("not valid if source is not http(s)", () => { + const result = validate(` + + + pingback.ping + + + gemini://cool.site + + + https://brainbaking.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(false) + }) + + test("is valid if pingback.ping and two http(s) parameters of which target is trusted", () => { + const result = validate(` + + + pingback.ping + + + https://cool.site + + + https://brainbaking.com/post/2021/03/cool-ness + + + + `) + + expect(result).toBe(true) + }) + +}) \ No newline at end of file