/.well-known 周りをいい感じに (#4141)

* Enhance /.well-known and their friends

* Fix bug
This commit is contained in:
Acid Chicken (硫酸鶏) 2019-02-05 17:42:55 +09:00 committed by GitHub
parent 0236405970
commit 753684e7ea
9 changed files with 202 additions and 89 deletions

View File

@ -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": {

View File

@ -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;
} }

View File

@ -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 };

View File

@ -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
View File

@ -0,0 +1,6 @@
type Acct = {
username: string;
host: string;
};
export default Acct;

View File

@ -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
View 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;

View File

@ -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
View 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;