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(); 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) { 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 | 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 | 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));