179 lines
5.2 KiB
TypeScript
179 lines
5.2 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',
|
|
headers: {
|
|
Authorization: ctx.headers['authorization'],
|
|
},
|
|
});
|
|
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',
|
|
headers: {
|
|
Authorization: ctx.headers['authorization'],
|
|
},
|
|
},
|
|
);
|
|
}
|
|
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));
|