/.well-known 周りをいい感じに (#4141)
* Enhance /.well-known and their friends * Fix bug
This commit is contained in:
		
							parent
							
								
									2f4434b0d8
								
							
						
					
					
						commit
						9dd06a7621
					
				
					 9 changed files with 202 additions and 89 deletions
				
			
		
							
								
								
									
										9
									
								
								src/@types/package.json.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/@types/package.json.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,10 @@
 | 
			
		|||
declare module '*/package.json' {
 | 
			
		||||
	const version: string;
 | 
			
		||||
	interface IRepository {
 | 
			
		||||
		type: string;
 | 
			
		||||
		url: string;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export const name: string;
 | 
			
		||||
	export const version: string;
 | 
			
		||||
	export const repository: IRepository;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
export default (acct: string) => {
 | 
			
		||||
import Acct from './type';
 | 
			
		||||
 | 
			
		||||
export default (acct: string): Acct => {
 | 
			
		||||
	if (acct.startsWith('@')) acct = acct.substr(1);
 | 
			
		||||
	const split = acct.split('@', 2);
 | 
			
		||||
	return { username: split[0], host: split[1] || null };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,5 @@
 | 
			
		|||
type UserLike = {
 | 
			
		||||
	host: string;
 | 
			
		||||
	username: string;
 | 
			
		||||
};
 | 
			
		||||
import Acct from './type';
 | 
			
		||||
 | 
			
		||||
export default (user: UserLike) => {
 | 
			
		||||
export default (user: Acct) => {
 | 
			
		||||
	return user.host === null ? user.username : `${user.username}@${user.host}`;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								src/misc/acct/type.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/misc/acct/type.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
type Acct = {
 | 
			
		||||
	username: string;
 | 
			
		||||
	host: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Acct;
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,8 @@ import * as requestStats from 'request-stats';
 | 
			
		|||
import * as slow from 'koa-slow';
 | 
			
		||||
 | 
			
		||||
import activityPub from './activitypub';
 | 
			
		||||
import webFinger from './webfinger';
 | 
			
		||||
import nodeinfo from './nodeinfo';
 | 
			
		||||
import wellKnown from './well-known';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import networkChart from '../chart/network';
 | 
			
		||||
import apiServer from './api';
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +69,8 @@ const router = new Router();
 | 
			
		|||
 | 
			
		||||
// Routing
 | 
			
		||||
router.use(activityPub.routes());
 | 
			
		||||
router.use(webFinger.routes());
 | 
			
		||||
router.use(nodeinfo.routes());
 | 
			
		||||
router.use(wellKnown.routes());
 | 
			
		||||
 | 
			
		||||
router.get('/verify-email/:code', async ctx => {
 | 
			
		||||
	const user = await User.findOne({ emailVerifyCode: ctx.params.code });
 | 
			
		||||
| 
						 | 
				
			
			@ -88,11 +90,6 @@ router.get('/verify-email/:code', async ctx => {
 | 
			
		|||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Return 404 for other .well-known
 | 
			
		||||
router.all('/.well-known/*', async ctx => {
 | 
			
		||||
	ctx.status = 404;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Register router
 | 
			
		||||
app.use(router.routes());
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										73
									
								
								src/server/nodeinfo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/server/nodeinfo.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
import * as Router from 'koa-router';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import fetchMeta from '../misc/fetch-meta';
 | 
			
		||||
import User from '../models/user';
 | 
			
		||||
import { name as softwareName, version, repository } from '../../package.json';
 | 
			
		||||
import Note from '../models/note';
 | 
			
		||||
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
const nodeinfo2_1path = '/nodeinfo/2.1';
 | 
			
		||||
const nodeinfo2_0path = '/nodeinfo/2.0';
 | 
			
		||||
 | 
			
		||||
export const links = [/* (awaiting release) {
 | 
			
		||||
	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
 | 
			
		||||
	href: config.url + nodeinfo2_1path
 | 
			
		||||
}, */{
 | 
			
		||||
	rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
 | 
			
		||||
	href: config.url + nodeinfo2_0path
 | 
			
		||||
}];
 | 
			
		||||
 | 
			
		||||
const nodeinfo2 = async () => {
 | 
			
		||||
	const [
 | 
			
		||||
		{ name, description, maintainer, langs, broadcasts, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker },
 | 
			
		||||
		total,
 | 
			
		||||
		activeHalfyear,
 | 
			
		||||
		activeMonth,
 | 
			
		||||
		localPosts,
 | 
			
		||||
		localComments
 | 
			
		||||
	] = await Promise.all([
 | 
			
		||||
		fetchMeta(),
 | 
			
		||||
		User.count({ host: null }),
 | 
			
		||||
		User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
 | 
			
		||||
		User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
 | 
			
		||||
		Note.count({ '_user.host': null, replyId: null }),
 | 
			
		||||
		Note.count({ '_user.host': null, replyId: { $ne: null } })
 | 
			
		||||
	]);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		software: {
 | 
			
		||||
			name: softwareName,
 | 
			
		||||
			version,
 | 
			
		||||
			repository: repository.url
 | 
			
		||||
		},
 | 
			
		||||
		protocols: ['activitypub'],
 | 
			
		||||
		services: {
 | 
			
		||||
			inbound: [] as string[],
 | 
			
		||||
			outbound: ['atom1.0', 'rss2.0']
 | 
			
		||||
		},
 | 
			
		||||
		openRegistrations: !disableRegistration,
 | 
			
		||||
		usage: {
 | 
			
		||||
			users: { total, activeHalfyear, activeMonth },
 | 
			
		||||
			localPosts,
 | 
			
		||||
			localComments
 | 
			
		||||
		},
 | 
			
		||||
		metadata: { name, description, maintainer, langs, broadcasts, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
router.get(nodeinfo2_1path, async ctx => {
 | 
			
		||||
	const base = await nodeinfo2();
 | 
			
		||||
 | 
			
		||||
	ctx.body = { version: '2.1', ...base };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get(nodeinfo2_0path, async ctx => {
 | 
			
		||||
	const base = await nodeinfo2();
 | 
			
		||||
 | 
			
		||||
	delete base.software.repository;
 | 
			
		||||
 | 
			
		||||
	ctx.body = { version: '2.0', ...base };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,75 +0,0 @@
 | 
			
		|||
import * as mongo from 'mongodb';
 | 
			
		||||
import * as Router from 'koa-router';
 | 
			
		||||
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import parseAcct from '../misc/acct/parse';
 | 
			
		||||
import User, { IUser } from '../models/user';
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
router.get('/.well-known/webfinger', async ctx => {
 | 
			
		||||
	if (typeof ctx.query.resource !== 'string') {
 | 
			
		||||
		ctx.status = 400;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const resourceLower = ctx.query.resource.toLowerCase();
 | 
			
		||||
	let acctLower;
 | 
			
		||||
	let id;
 | 
			
		||||
 | 
			
		||||
	if (resourceLower.startsWith(config.url.toLowerCase() + '/@')) {
 | 
			
		||||
		acctLower = resourceLower.split('/').pop();
 | 
			
		||||
	} else if (resourceLower.startsWith(config.url.toLowerCase() + '/users/')) {
 | 
			
		||||
		id = new mongo.ObjectID(resourceLower.split('/').pop());
 | 
			
		||||
	} else if (resourceLower.startsWith('acct:')) {
 | 
			
		||||
		acctLower = resourceLower.slice('acct:'.length);
 | 
			
		||||
	} else {
 | 
			
		||||
		acctLower = resourceLower;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let user: IUser;
 | 
			
		||||
 | 
			
		||||
	if (acctLower) {
 | 
			
		||||
		const parsedAcctLower = parseAcct(acctLower);
 | 
			
		||||
		if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) {
 | 
			
		||||
			ctx.status = 422;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user = await User.findOne({
 | 
			
		||||
			usernameLower: parsedAcctLower.username,
 | 
			
		||||
			host: null
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		user = await User.findOne({
 | 
			
		||||
			_id: id,
 | 
			
		||||
			host: null
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (user === null) {
 | 
			
		||||
		ctx.status = 404;
 | 
			
		||||
		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}`
 | 
			
		||||
		}]
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	ctx.set('Cache-Control', 'public, max-age=180');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router;
 | 
			
		||||
							
								
								
									
										102
									
								
								src/server/well-known.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/server/well-known.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
import * as mongo from 'mongodb';
 | 
			
		||||
import * as Router from 'koa-router';
 | 
			
		||||
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import parseAcct from '../misc/acct/parse';
 | 
			
		||||
import User from '../models/user';
 | 
			
		||||
import Acct from '../misc/acct/type';
 | 
			
		||||
import { links } from './nodeinfo';
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
const webFingerPath = '/.well-known/webfinger';
 | 
			
		||||
 | 
			
		||||
router.get('/.well-known/host-meta', async ctx => {
 | 
			
		||||
	ctx.set('Content-Type', 'application/xrd+xml');
 | 
			
		||||
	ctx.body = `<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
 | 
			
		||||
  <Link rel="lrdd" type="application/xrd+xml" template="${config.url}${webFingerPath}?resource={uri}"/>
 | 
			
		||||
</XRD>
 | 
			
		||||
`;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/.well-known/host-meta.json', async ctx => {
 | 
			
		||||
	ctx.set('Content-Type', 'application/jrd+json');
 | 
			
		||||
	ctx.body = {
 | 
			
		||||
		links: [{
 | 
			
		||||
			rel: 'lrdd',
 | 
			
		||||
			type: 'application/xrd+xml',
 | 
			
		||||
			template: `${config.url}${webFingerPath}?resource={uri}`
 | 
			
		||||
		}]
 | 
			
		||||
	};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/.well-known/nodeinfo', async ctx => {
 | 
			
		||||
	ctx.body = { links };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get(webFingerPath, async ctx => {
 | 
			
		||||
	const generateQuery = (resource: string) =>
 | 
			
		||||
		resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
 | 
			
		||||
			fromId(new mongo.ObjectID(resource.split('/').pop())) :
 | 
			
		||||
			fromAcct(parseAcct(
 | 
			
		||||
				resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() :
 | 
			
		||||
				resource.startsWith('acct:') ? resource.slice('acct:'.length) :
 | 
			
		||||
				resource));
 | 
			
		||||
 | 
			
		||||
	const fromId = (_id: mongo.ObjectID): Record<string, any> => ({
 | 
			
		||||
			_id,
 | 
			
		||||
			host: null
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	const fromAcct = (acct: Acct): Record<string, any> | number =>
 | 
			
		||||
		!acct.host || acct.host === config.host.toLowerCase() ? {
 | 
			
		||||
			usernameLower: acct.username,
 | 
			
		||||
			host: null
 | 
			
		||||
		} : 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 User.findOne(query);
 | 
			
		||||
 | 
			
		||||
	if (user === null) {
 | 
			
		||||
		ctx.status = 404;
 | 
			
		||||
		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}`
 | 
			
		||||
		}]
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	ctx.set('Cache-Control', 'public, max-age=180');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Return 404 for other .well-known
 | 
			
		||||
router.all('/.well-known/*', async ctx => {
 | 
			
		||||
	ctx.status = 404;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue