/.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", | 	"version": "10.81.0", | ||||||
| 	"clientVersion": "2.0.14026", | 	"clientVersion": "2.0.14026", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
|  | 	"repository": { | ||||||
|  | 		"type": "git", | ||||||
|  | 		"url": "https://github.com/syuilo/misskey.git" | ||||||
|  | 	}, | ||||||
| 	"main": "./index.js", | 	"main": "./index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"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' { | 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); | 	if (acct.startsWith('@')) acct = acct.substr(1); | ||||||
| 	const split = acct.split('@', 2); | 	const split = acct.split('@', 2); | ||||||
| 	return { username: split[0], host: split[1] || null }; | 	return { username: split[0], host: split[1] || null }; | ||||||
|  |  | ||||||
|  | @ -1,8 +1,5 @@ | ||||||
| type UserLike = { | import Acct from './type'; | ||||||
| 	host: string; |  | ||||||
| 	username: string; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| export default (user: UserLike) => { | export default (user: Acct) => { | ||||||
| 	return user.host === null ? user.username : `${user.username}@${user.host}`; | 	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 * as slow from 'koa-slow'; | ||||||
| 
 | 
 | ||||||
| import activityPub from './activitypub'; | import activityPub from './activitypub'; | ||||||
| import webFinger from './webfinger'; | import nodeinfo from './nodeinfo'; | ||||||
|  | import wellKnown from './well-known'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import networkChart from '../chart/network'; | import networkChart from '../chart/network'; | ||||||
| import apiServer from './api'; | import apiServer from './api'; | ||||||
|  | @ -68,7 +69,8 @@ const router = new Router(); | ||||||
| 
 | 
 | ||||||
| // Routing
 | // Routing
 | ||||||
| router.use(activityPub.routes()); | router.use(activityPub.routes()); | ||||||
| router.use(webFinger.routes()); | router.use(nodeinfo.routes()); | ||||||
|  | router.use(wellKnown.routes()); | ||||||
| 
 | 
 | ||||||
| router.get('/verify-email/:code', async ctx => { | router.get('/verify-email/:code', async ctx => { | ||||||
| 	const user = await User.findOne({ emailVerifyCode: ctx.params.code }); | 	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
 | // Register router
 | ||||||
| app.use(router.routes()); | 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