2021-05-17 19:39:22 +02:00
|
|
|
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');
|
2021-05-17 21:55:42 +02:00
|
|
|
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
2021-05-17 19:39:22 +02:00
|
|
|
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: {
|
2021-05-17 21:55:42 +02:00
|
|
|
beforeRequest: IS_PRODUCTION ? [] : [(req) => console.log(`${req.method} ${req.url.href}`)],
|
2021-05-17 19:39:22 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
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',
|
2021-05-17 21:54:27 +02:00
|
|
|
headers: {
|
|
|
|
Authorization: ctx.headers['authorization'],
|
|
|
|
},
|
2021-05-17 19:39:22 +02:00
|
|
|
});
|
|
|
|
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;
|
2021-05-23 14:08:24 +02:00
|
|
|
if (
|
|
|
|
typeof ctx.query.width !== 'string' ||
|
|
|
|
typeof ctx.query.height !== 'string' ||
|
|
|
|
typeof ctx.query.method !== 'string'
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { method } = ctx.query;
|
|
|
|
assert(['crop', 'scale'].includes(method));
|
2021-05-17 19:39:22 +02:00
|
|
|
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',
|
2021-05-17 21:54:27 +02:00
|
|
|
headers: {
|
|
|
|
Authorization: ctx.headers['authorization'],
|
|
|
|
},
|
2021-05-17 19:39:22 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (file && file.statusCode === 200) {
|
2021-05-23 14:08:24 +02:00
|
|
|
const resizer = sharp(file.body).resize(width, height, {
|
|
|
|
fit: method === 'crop' ? 'cover' : 'inside',
|
|
|
|
});
|
|
|
|
const img = await resizer.toBuffer({ resolveWithObject: true });
|
2021-05-17 19:39:22 +02:00
|
|
|
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());
|
|
|
|
|
|
|
|
koa.use(router.routes()).use(router.allowedMethods());
|
|
|
|
koa.listen(parseInt(LISTENING_PORT || '8009', 10));
|