why-is-synapse/src/index.ts

173 lines
5.1 KiB
TypeScript

import Koa from 'koa';
import Router from '@koa/router';
import Got, { Response } from 'got';
import sharp from 'sharp';
import dns from 'dns/promises';
import assert from 'assert';
const { DEST_SERVER, SERVER_PRETTY_NAME, LISTENING_PORT } = process.env;
assert(DEST_SERVER, 'missing DEST_SERVER env variable');
assert(SERVER_PRETTY_NAME, 'missing SERVER_PRETTY_NAME env variable');
const FUCKED_UP_SERVERS = ['matrix.org'];
const got = Got.extend({
headers: {
'User-Agent': 'why-is-synapse (https://git.sakamoto.pl/selfisekai/why-is-synapse)',
},
throwHttpErrors: false,
hooks: {
beforeRequest: [(req) => console.log(`${req.method} ${req.url.href}`)],
},
});
const serverHostnameCache = new Map<string, string>();
const getServerHostname = async (serverName: string) => {
const cached = serverHostnameCache.get(serverName);
if (cached) return cached;
const jsonRes = await got.get(`https://${serverName}/.well-known/matrix/server`);
if (jsonRes.statusCode === 200) {
let res = JSON.parse(jsonRes.body)['m.server'];
if (typeof res === 'string') {
if (!res.includes(':')) {
res += ':8448'; // default port per spec
}
serverHostnameCache.set(serverName, res);
return res;
}
}
const dnsRes = await dns.resolveSrv(`_matrix._tcp.${serverName}`).catch(() => []);
if (dnsRes.length > 0) {
dnsRes.sort((a, b) => a.priority - b.priority);
console.log(dnsRes);
const res = `${dnsRes[0].name}:${dnsRes[0].port}`;
serverHostnameCache.set(serverName, res);
return res;
}
// if there are no SRV/well-known specs, assume the defaults
return `${serverName}:8448`;
};
const koa = new Koa();
const router = new Router({
prefix: '/_matrix',
});
const media = new Router();
media.get(
['/download/:serverName/:mediaId', '/download/:serverName/:mediaId/:filename'],
async (ctx) => {
const { serverName, mediaId, filename } = ctx.params;
function sendFile(f: Response<Buffer>) {
ctx.res.statusCode = 200;
(
['content-type', filename ? 'content-disposition' : null].filter(
(header) => header,
) as string[]
).forEach((header) => {
if (!f) return; // typescript is bork
const val = f.headers[header];
if (!val) return;
ctx.res.setHeader(header, val);
});
if (filename) {
ctx.res.setHeader('content-disposition', `attachment; filename="${filename}"`);
}
ctx.res.write(f.body);
ctx.res.end();
}
let file: Response<Buffer> | null = null;
if (!FUCKED_UP_SERVERS.includes(serverName)) {
file = await got.get(`${DEST_SERVER}/_matrix/media/r0/download/${serverName}/${mediaId}`, {
responseType: 'buffer',
});
if (file && file.statusCode === 200) {
sendFile(file);
return;
}
}
if (serverName === SERVER_PRETTY_NAME) {
ctx.res.statusCode = 500;
return;
}
const remoteHost = await getServerHostname(serverName);
file = await got.get(
`https://${remoteHost}/_matrix/media/r0/download/${serverName}/${mediaId}`,
{
responseType: 'buffer',
},
);
if (file && file.statusCode === 200) {
sendFile(file);
return;
}
},
);
media.get('/thumbnail/:serverName/:mediaId', async (ctx) => {
const { serverName, mediaId } = ctx.params;
if (typeof ctx.query.width !== 'string' || typeof ctx.query.height !== 'string') return;
const width = parseInt(ctx.query.width, 10);
const height = parseInt(ctx.query.height, 10);
let file: Response<Buffer> | null = null;
if (!FUCKED_UP_SERVERS.includes(serverName)) {
file = await got.get(`${DEST_SERVER}/_matrix/media/r0/download/${serverName}/${mediaId}`, {
responseType: 'buffer',
});
}
if (!file || file.statusCode !== 200) {
if (serverName === SERVER_PRETTY_NAME) {
ctx.res.statusCode = 500;
return;
}
const remoteHost = await getServerHostname(serverName);
file = await got.get(
`https://${remoteHost}/_matrix/media/r0/download/${serverName}/${mediaId}`,
{
responseType: 'buffer',
},
);
}
if (file && file.statusCode === 200) {
const img = await sharp(file.body).resize(width, height).toBuffer({ resolveWithObject: true });
ctx.res.statusCode = 200;
ctx.res.setHeader('content-type', `image/${img.info.format}`);
ctx.res.write(img.data);
ctx.res.end();
return;
}
});
router.use('/media/r0', media.routes(), media.allowedMethods());
router.all('/:idc+', async (ctx) => {
const req = await got(DEST_SERVER + ctx.path + '?' + ctx.querystring, {
// @ts-ignore
method: ctx.req.method,
body: ctx.body,
responseType: 'buffer',
headers: {
Authorization: ctx.headers['authorization'],
},
});
ctx.res.statusCode = req.statusCode;
Object.keys(req.headers).forEach((h) => {
const val = req.headers[h];
if (typeof val === 'undefined') return;
ctx.res.setHeader(h, val);
});
ctx.res.write(req.body);
ctx.res.end();
});
koa.use(router.routes()).use(router.allowedMethods());
koa.listen(parseInt(LISTENING_PORT || '8009', 10));