From b75184ec8e3436200bacdcd832e3324702553d20 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 18 Sep 2022 03:27:08 +0900 Subject: なんかもうめっちゃ変えた MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/WellKnownServerService.ts | 168 +++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 packages/backend/src/server/WellKnownServerService.ts (limited to 'packages/backend/src/server/WellKnownServerService.ts') diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts new file mode 100644 index 0000000000..7f827d439b --- /dev/null +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -0,0 +1,168 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Router from '@koa/router'; +import { IsNull, MoreThan } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { UsersRepository } from '@/models/index.js'; +import { Config } from '@/config.js'; +import { escapeAttribute, escapeValue } from '@/misc/prelude/xml.js'; +import type { User } from '@/models/entities/User.js'; +import * as Acct from '@/misc/acct.js'; +import { NodeinfoServerService } from './NodeinfoServerService.js'; +import type { FindOptionsWhere } from 'typeorm'; + +@Injectable() +export class WellKnownServerService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private nodeinfoServerService: NodeinfoServerService, + ) { + } + + public createRouter() { + 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 allPath = '/.well-known/(.*)'; + const webFingerPath = '/.well-known/webfinger'; + const jrd = 'application/jrd+json'; + const xrd = 'application/xrd+xml'; + + router.use(allPath, async (ctx, next) => { + ctx.set({ + 'Access-Control-Allow-Headers': 'Accept', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Expose-Headers': 'Vary', + }); + await next(); + }); + + router.options(allPath, async ctx => { + ctx.status = 204; + }); + + router.get('/.well-known/host-meta', async ctx => { + ctx.set('Content-Type', xrd); + ctx.body = XRD({ element: 'Link', attributes: { + rel: 'lrdd', + type: xrd, + template: `${this.config.url}${webFingerPath}?resource={uri}`, + } }); + }); + + router.get('/.well-known/host-meta.json', async ctx => { + ctx.set('Content-Type', jrd); + ctx.body = { + links: [{ + rel: 'lrdd', + type: jrd, + template: `${this.config.url}${webFingerPath}?resource={uri}`, + }], + }; + }); + + router.get('/.well-known/nodeinfo', async ctx => { + ctx.body = { links: this.nodeinfoServerService.getLinks() }; + }); + + /* TODO +router.get('/.well-known/change-password', async ctx => { +}); +*/ + + router.get(webFingerPath, async ctx => { + const fromId = (id: User['id']): FindOptionsWhere => ({ + id, + host: IsNull(), + isSuspended: false, + }); + + const generateQuery = (resource: string): FindOptionsWhere | number => + resource.startsWith(`${this.config.url.toLowerCase()}/users/`) ? + fromId(resource.split('/').pop()!) : + fromAcct(Acct.parse( + resource.startsWith(`${this.config.url.toLowerCase()}/@`) ? resource.split('/').pop()! : + resource.startsWith('acct:') ? resource.slice('acct:'.length) : + resource)); + + const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => + !acct.host || acct.host === this.config.host.toLowerCase() ? { + usernameLower: acct.username, + host: IsNull(), + isSuspended: false, + } : 422; + + if (typeof ctx.query.resource !== 'string') { + ctx.status = 400; + return; + } + + const query = generateQuery(ctx.query.resource.toLowerCase()); + + if (typeof query === 'number') { + ctx.status = query; + return; + } + + const user = await this.usersRepository.findOneBy(query); + + if (user == null) { + ctx.status = 404; + return; + } + + const subject = `acct:${user.username}@${this.config.host}`; + const self = { + rel: 'self', + type: 'application/activity+json', + href: `${this.config.url}/users/${user.id}`, + }; + const profilePage = { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${this.config.url}/@${user.username}`, + }; + const subscribe = { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${this.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'); + }); + + // Return 404 for other .well-known + router.all(allPath, async ctx => { + ctx.status = 404; + }); + + return router; + } +} -- cgit v1.2.3-freya