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