Compare commits

...

2 Commits

Author SHA1 Message Date
Lamp 6f3dacb565 finish wip: whitelist & readme 2022-12-23 02:19:20 -06:00
Lamp caa44a4c19 commit dumb work-in-progress code before redoing 2022-12-23 01:06:52 -06:00
2 changed files with 63 additions and 11 deletions

44
README.md Normal file
View File

@ -0,0 +1,44 @@
# ActivityPub Proxy
A **simple** proxy for ActivityPub that lets you circumvent blocks by masquerading as another domain name. All it does is replace all hostnames in the text proxied through, and for signed POST requests, it swaps the public keys and re-signs the requests.
To use it, type a user's handle, replace the dots in their domain with hyphens, and add `.yourproxydomain` to the end. (If the domain already has hyphens, replace them with double hypens.) So for example, say you want to follow @kingu_platypus_gidora@octodon.social but the woke administrator has blocked you (or your instance blocked them wtf mastodon.social??), and you have a proxy at *.activitypub-proxy.cf: that would make @kingu_platypus_gidora@octodon-social.activitypub-proxy.cf which you can theoretically follow and fully interact with just like the real user. The person on the other side will see your tag proxied the same way, `@yourname@your-domain.your.proxy`, and they can follow and interact with you back.
## Installation
You will need a host with Node.js 15 or newer, and a wildcard domain with HTTPS pointed to your server. Cloudflare may be easiest, as you can bind the app to an extra IP address and connect Cloudflare directly to it.
Download the repository and `npm i`. Then you can run it with the following environment variables.
- `PORT`: The port to listen for HTTP, default: 80.
- `BIND_IP`: The IP address to bind to. Default: all.
- `DOMAIN_WHITELIST`: Comma-separated list of domains that can use the proxy. Recommended to use this to prevent abuse as otherwise anyone can proxy anything. Remember to include the domains you want to follow from as well as the domains you want to follow, as it needs to work both ways.
- `USER_WHITELIST`: Comma-separated list of names that can be looked up. Maybe useful if you don't want other users messing with other users... 🤷 Note that this only restricts the webfinger. (todo could be circumvented? 🤔) Default: any
- `NODE_ENV`: Set to `development` to see debug logs.
Install by copying and pasting this example systemd file to `/etc/systemd/system/ap-proxy.service` (or similar), and editing as needed:
```systemd
[Unit]
Description=Simple ActivityPub Proxy
Documentation=https://gitea.moe/lamp/activitypub-proxy
After=network.target
[Service]
Environment= PORT=80 BIND_IP=0.0.0.0 DOMAIN_WHITELIST=mastodon.social,mstdn.social
WorkingDirectory=/path/to/activitypub-proxy/
ExecStart=/usr/bin/node .
[Install]
WantedBy=multi-user.target
```
`systemctl enable --now ap-proxy` and Bob's your uncle.
## Known issues
- Sending AP messages to Pleroma makes it 500 Internal Server Error for no obvious reason
- Since it simply replaces all instances of the domain names in the raw JSON text, mentions of those names in post content will be replaced as well. Although this makes it more likely to work with _anything_ with less code, a stricter version that parses deeper into the protocol might be of better quality.
If any issues please submit!

View File

@ -1,4 +1,11 @@
if (process.env.NODE_ENV == "production") console.debug = () => {};
if (process.env.NODE_ENV != "development") {
process.env.NODE_ENV = "production";
console.debug = () => {};
}
if (process.env.DOMAIN_WHITELIST) var DOMAIN_WHITELIST = process.env.DOMAIN_WHITELIST.split(',').map(x=>x.trim().toLowerCase()).filter(x=>x);
if (process.env.USER_WHITELIST) var USER_WHITELIST = process.env.USER_WHITELIST.split(',').map(x=>x.trim().toLowerCase()).filter(x=>x);
var express = require("express");
require("express-async-errors");
var fetch = require("node-fetch");
@ -10,7 +17,7 @@ var keystore = new Keyv("sqlite://keys.sqlite");
var app = express();
app.set("trust proxy", true);
app.listen(80, "2605:a140:2045:1635:99bc:ddc5:1227:c27c");
app.listen(process.env.PORT || 80, process.env.BIND_IP);
app.use(express.text({
type: [
@ -34,9 +41,16 @@ app.use(async (req, res, next) => {
if (!req.subdomains[0]) return next();
var TARGET_NODE = req.subdomains[0].replaceAll(/(?<!-)-(?!-)/g, '.').replaceAll('--','-');
if (DOMAIN_WHITELIST && !DOMAIN_WHITELIST.includes(TARGET_NODE)) return res.status(403).send(`target ${TARGET_NODE} is not whitelisted`);
var TARGET_MASQUERADE = req.hostname;
var TARGET_REGEXP = new RegExp(`(?<!\\.)${TARGET_NODE.replaceAll('.','\\.')}`, 'gi');
var CLIENT_NODE = req.get("User-Agent").match(/(?<=https:\/\/)[a-z0-9-\.]+/i)?.[0];
if (DOMAIN_WHITELIST && !DOMAIN_WHITELIST.includes(CLIENT_NODE)) return res.status(403).send(`client ${CLIENT_NODE} is not whitelisted`);
if (CLIENT_NODE) var CLIENT_MASQUERADE = [CLIENT_NODE.replaceAll('-','--').replaceAll('.','-'), ...req.hostname.split('.').slice(-2)].join('.');
if (USER_WHITELIST && req.url.startsWith("/.well-known/webfinger") && !USER_WHITELIST.includes(req.query.resource?.replace('acct:','').split('@')[0])) return res.status(403).send("user not whitelisted");
var url = `https://${TARGET_NODE}${req.url.replaceAll(TARGET_MASQUERADE, TARGET_NODE)}`;
var opts = {method: req.method, headers: {
host: TARGET_NODE,
@ -52,7 +66,7 @@ app.use(async (req, res, next) => {
if (!publicKeyPem) {
console.debug("fetching public key", signature.keyId);
let user_res = await fetch(signature.keyId, {headers:{Accept:"application/json"}});
if (!user_res.ok) return res.status(400).send("cannot verify");
if (!user_res.ok || !user_res.headers.get("content-type").includes("json")) return res.status(400).send("cannot verify");
let user_json = await user_res.json();
console.debug({user_json});
publicKeyPem = user_json.publicKey.publicKeyPem;
@ -64,13 +78,7 @@ app.use(async (req, res, next) => {
return;
}
var CLIENT_NODE = req.get("User-Agent").match(/(?<=https:\/\/)[a-z0-9-\.]+/i)[0];
var CLIENT_MASQUERADE = [CLIENT_NODE.replaceAll('-','--').replaceAll('.','-'), ...req.hostname.split('.').slice(-2)].join('.');
var modifiedPayload = req.body
.replaceAll(TARGET_MASQUERADE, TARGET_NODE)
.replaceAll(CLIENT_NODE, CLIENT_MASQUERADE);
var modifiedPayload = req.body.replaceAll(TARGET_MASQUERADE, TARGET_NODE).replaceAll(CLIENT_NODE, CLIENT_MASQUERADE);
console.debug({CLIENT_NODE,CLIENT_MASQUERADE,original:req.body,modified:modifiedPayload});
var digest = crypto.createHash("sha256").update(modifiedPayload, "utf-8").digest("base64");
@ -110,7 +118,7 @@ app.use(async (req, res, next) => {
"application/xml",
"text/",
"charset=utf-8"
].some(t => target_res.headers.get("content-type").toLowerCase().includes(t))) {
].some(t => target_res.headers.get("content-type")?.toLowerCase().includes(t))) {
var originalText = await target_res.text(), modifiedText = originalText;
console.debug({originalText})
if (target_res.headers.get("content-type").includes("json")) {