diff --git a/src/prelude/xml.ts b/src/prelude/xml.ts new file mode 100644 index 0000000000..0773f75d47 --- /dev/null +++ b/src/prelude/xml.ts @@ -0,0 +1,41 @@ +const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''' +}; + +const beginingOfCDATA = ''; + +export function escapeValue(x: string): string { + let insideOfCDATA = false; + let builder = ''; + for ( + let i = 0; + i < x.length; + ) { + if (insideOfCDATA) { + if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) { + insideOfCDATA = true; + i += beginingOfCDATA.length; + } else { + builder += x[i++]; + } + } else { + if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) { + insideOfCDATA = false; + i += endOfCDATA.length; + } else { + const b = x[i++]; + builder += map[b] || b; + } + } + } + return builder; +} + +export function escapeAttribute(x: string): string { + return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x); +} diff --git a/src/server/well-known.ts b/src/server/well-known.ts index 3c994793e1..18c080acc7 100644 --- a/src/server/well-known.ts +++ b/src/server/well-known.ts @@ -6,27 +6,37 @@ import parseAcct from '../misc/acct/parse'; import User from '../models/user'; import Acct from '../misc/acct/type'; import { links } from './nodeinfo'; +import { escapeAttribute, escapeValue } from '../prelude/xml'; // Init router const router = new Router(); +const XRD = (...x: { element: string, value?: string, attributes?: Record }[]) => + `${x.map(({ element, value, attributes }) => + `<${ + Object.entries(typeof attributes === 'object' && attributes || {}).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element) + }${ + typeof value === 'string' ? `>${escapeValue(value)}`).reduce((a, c) => a + c, '')}`; + const webFingerPath = '/.well-known/webfinger'; +const jrd = 'application/jrd+json'; +const xrd = 'application/xrd+xml'; router.get('/.well-known/host-meta', async ctx => { - ctx.set('Content-Type', 'application/xrd+xml'); - ctx.body = ` - - - -`; + ctx.set('Content-Type', xrd); + ctx.body = XRD({ element: 'Link', attributes: { + type: xrd, + template: `${config.url}${webFingerPath}?resource={uri}` + }}); }); router.get('/.well-known/host-meta.json', async ctx => { - ctx.set('Content-Type', 'application/jrd+json'); + ctx.set('Content-Type', jrd); ctx.body = { links: [{ rel: 'lrdd', - type: 'application/xrd+xml', + type: jrd, template: `${config.url}${webFingerPath}?resource={uri}` }] }; @@ -75,22 +85,38 @@ router.get(webFingerPath, async ctx => { return; } - ctx.body = { - subject: `acct:${user.username}@${config.host}`, - links: [{ - rel: 'self', - type: 'application/activity+json', - href: `${config.url}/users/${user._id}` - }, { - rel: 'http://webfinger.net/rel/profile-page', - type: 'text/html', - href: `${config.url}/@${user.username}` - }, { - rel: 'http://ostatus.org/schema/1.0/subscribe', - template: `${config.url}/authorize-follow?acct={uri}` - }] + const subject = `acct:${user.username}@${config.host}`; + const self = { + rel: 'self', + type: 'application/activity+json', + href: `${config.url}/users/${user._id}` + }; + const profilePage = { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${config.url}/@${user.username}` + }; + const subscribe = { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${config.url}/authorize-follow?acct={uri}` }; + if (ctx.accepts(jrd, xrd) === xrd) { + ctx.body = XRD( + { element: 'Subject', value: subject }, + { element: 'Link', attributes: self }, + { element: 'Link', attributes: profilePage }, + { element: 'Link', attributes: subscribe }); + ctx.type = xrd; + } else { + ctx.body = { + subject, + links: [self, profilePage, subscribe] + }; + ctx.type = jrd; + } + + ctx.vary('Accept'); ctx.set('Cache-Control', 'public, max-age=180'); });