/.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
				
			
		|  | @ -4,6 +4,10 @@ | |||
| 	"version": "10.81.0", | ||||
| 	"clientVersion": "2.0.14026", | ||||
| 	"codename": "nighthike", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "https://github.com/syuilo/misskey.git" | ||||
| 	}, | ||||
| 	"main": "./index.js", | ||||
| 	"private": true, | ||||
| 	"scripts": { | ||||
|  |  | |||
							
								
								
									
										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