diff --git a/package.json b/package.json index d5233d54a6..e5180fddbd 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,9 @@ "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", "@types/js-yaml": "3.11.1", + "@types/koa": "^2.0.45", + "@types/koa-bodyparser": "^4.2.0", + "@types/koa-router": "^7.0.27", "@types/kue": "^0.11.8", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", @@ -140,6 +143,8 @@ "is-url": "1.2.4", "js-yaml": "3.11.0", "jsdom": "11.7.0", + "koa": "^2.5.0", + "koa-router": "^7.4.0", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts new file mode 100644 index 0000000000..ed0311af9d --- /dev/null +++ b/src/server/activitypub.ts @@ -0,0 +1,142 @@ +import * as Router from 'koa-router'; +import { parseRequest } from 'http-signature'; + +import { createHttp } from '../queue'; +import context from '../remote/activitypub/renderer/context'; +import render from '../remote/activitypub/renderer/note'; +import Note from '../models/note'; +import User, { isLocalUser } from '../models/user'; +import renderNote from '../remote/activitypub/renderer/note'; +import renderKey from '../remote/activitypub/renderer/key'; +import renderPerson from '../remote/activitypub/renderer/person'; +import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection'; +//import parseAcct from '../acct/parse'; +import config from '../config'; + +// Init router +const router = new Router(); + +//#region Routing + +// inbox +router.post('/users/:user/inbox', ctx => { + let signature; + + ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; + + try { + signature = parseRequest(ctx.req); + } catch (e) { + ctx.status = 401; + return; + } + + createHttp({ + type: 'processInbox', + activity: ctx.request.body, + signature + }).save(); + + ctx.status = 202; +}); + +// note +router.get('/notes/:note', async (ctx, next) => { + const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json'); + if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) { + next(); + return; + } + + const note = await Note.findOne({ + _id: ctx.params.note + }); + + if (note === null) { + ctx.status = 404; + return; + } + + const rendered = await render(note); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// outbot +router.get('/users/:user/outbox', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + const notes = await Note.find({ userId: user._id }, { + limit: 10, + sort: { _id: -1 } + }); + + const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); + const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// publickey +router.get('/users/:user/publickey', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + if (isLocalUser(user)) { + const rendered = renderKey(user); + rendered['@context'] = context; + + ctx.body = rendered; + } else { + ctx.status = 400; + } +}); + +// user +router.get('/users/:user', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + const rendered = renderPerson(user); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// follow form +router.get('/authorize-follow', async ctx => { + /* TODO + const { username, host } = parseAcct(ctx.query.acct); + if (host === null) { + res.sendStatus(422); + return; + } + + const finger = await request(`https://${host}`) + */ +}); + +//#endregion + +export default router; diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts deleted file mode 100644 index 643d2945bd..0000000000 --- a/src/server/activitypub/inbox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as bodyParser from 'body-parser'; -import * as express from 'express'; -import { parseRequest } from 'http-signature'; -import { createHttp } from '../../queue'; - -const app = express.Router(); - -app.post('/users/:user/inbox', bodyParser.json({ - type() { - return true; - } -}), async (req, res) => { - let signature; - - req.headers.authorization = 'Signature ' + req.headers.signature; - - try { - signature = parseRequest(req); - } catch (exception) { - return res.sendStatus(401); - } - - createHttp({ - type: 'processInbox', - activity: req.body, - signature, - }).save(); - - return res.status(202).end(); -}); - -export default app; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts deleted file mode 100644 index 042579db9d..0000000000 --- a/src/server/activitypub/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as express from 'express'; - -import user from './user'; -import inbox from './inbox'; -import outbox from './outbox'; -import publicKey from './publickey'; -import note from './note'; - -const app = express(); -app.disable('x-powered-by'); - -app.use(user); -app.use(inbox); -app.use(outbox); -app.use(publicKey); -app.use(note); - -export default app; diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts deleted file mode 100644 index 1c2e695b80..0000000000 --- a/src/server/activitypub/note.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/note'; -import Note from '../../models/note'; - -const app = express.Router(); - -app.get('/notes/:note', async (req, res, next) => { - const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); - if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { - return next(); - } - - const note = await Note.findOne({ - _id: req.params.note - }); - - if (note === null) { - return res.sendStatus(404); - } - - const rendered = await render(note); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts deleted file mode 100644 index 1c97c17a2e..0000000000 --- a/src/server/activitypub/outbox.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import renderNote from '../../remote/activitypub/renderer/note'; -import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; -import config from '../../config'; -import Note from '../../models/note'; -import User from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user/outbox', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - const notes = await Note.find({ userId: user._id }, { - limit: 20, - sort: { _id: -1 } - }); - - const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); - const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts deleted file mode 100644 index e874b82729..0000000000 --- a/src/server/activitypub/publickey.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/key'; -import User, { isLocalUser } from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user/publickey', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - if (isLocalUser(user)) { - const rendered = render(user); - rendered['@context'] = context; - - res.json(rendered); - } else { - res.sendStatus(400); - } -}); - -export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts deleted file mode 100644 index 9e98e92b6a..0000000000 --- a/src/server/activitypub/user.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/person'; -import User from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - const rendered = render(user); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/index.ts b/src/server/index.ts index 962d3b5f4f..e9bfa9e10b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,67 +5,40 @@ import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; -import * as express from 'express'; -import * as morgan from 'morgan'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as bodyParser from 'koa-bodyparser'; import activityPub from './activitypub'; import webFinger from './webfinger'; -import log from './log-request'; import config from '../config'; -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); -app.set('trust proxy', 'loopback'); +// Init server +const app = new Koa(); +app.proxy = true; +app.use(bodyParser); -app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { - // create a write stream (in append mode) - stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null -})); - -app.use((req, res, next) => { - log(req); - next(); -}); - -/** - * HSTS - * 6month(15552000sec) - */ +// HSTS +// 6months (15552000sec) if (config.url.startsWith('https')) { - app.use((req, res, next) => { - res.header('strict-transport-security', 'max-age=15552000; preload'); + app.use((ctx, next) => { + ctx.set('strict-transport-security', 'max-age=15552000; preload'); next(); }); } -// Drop request when without 'Host' header -app.use((req, res, next) => { - if (!req.headers['host']) { - res.sendStatus(400); - } else { - next(); - } -}); +// Init router +const router = new Router(); -// 互換性のため -app.post('/meta', (req, res) => { - res.header('Access-Control-Allow-Origin', '*'); - res.json({ - version: 'nighthike' - }); -}); +// Routing +router.use('/api', require('./api')); +router.use('/files', require('./file')); +router.use(activityPub.routes()); +router.use(webFinger.routes()); +router.use(require('./web')); -/** - * Register modules - */ -app.use('/api', require('./api')); -app.use('/files', require('./file')); -app.use(activityPub); -app.use(webFinger); -app.use(require('./web')); +// Register router +app.use(router.routes()); function createServer() { if (config.https) { diff --git a/src/server/log-request.ts b/src/server/log-request.ts deleted file mode 100644 index e431aa271d..0000000000 --- a/src/server/log-request.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as crypto from 'crypto'; -import * as express from 'express'; -import * as proxyAddr from 'proxy-addr'; -import Xev from 'xev'; - -const ev = new Xev(); - -export default function(req: express.Request) { - const ip = proxyAddr(req, () => true); - - const md5 = crypto.createHash('md5'); - md5.update(ip); - const hashedIp = md5.digest('hex').substr(0, 3); - - ev.emit('request', { - ip: hashedIp, - method: req.method, - hostname: req.hostname, - path: req.originalUrl - }); -} diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts index dbf0999f3e..e72592351b 100644 --- a/src/server/webfinger.ts +++ b/src/server/webfinger.ts @@ -1,17 +1,19 @@ -import * as express from 'express'; +import * as Router from 'koa-router'; import config from '../config'; import parseAcct from '../acct/parse'; import User from '../models/user'; -const app = express.Router(); +// Init router +const router = new Router(); -app.get('/.well-known/webfinger', async (req, res) => { - if (typeof req.query.resource !== 'string') { - return res.sendStatus(400); +router.get('/.well-known/webfinger', async ctx => { + if (typeof ctx.query.resource !== 'string') { + ctx.status = 400; + return; } - const resourceLower = req.query.resource.toLowerCase(); + const resourceLower = ctx.query.resource.toLowerCase(); const webPrefix = config.url.toLowerCase() + '/@'; let acctLower; @@ -25,15 +27,21 @@ app.get('/.well-known/webfinger', async (req, res) => { const parsedAcctLower = parseAcct(acctLower); if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) { - return res.sendStatus(422); + ctx.status = 422; + return; } - const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null }); + const user = await User.findOne({ + usernameLower: parsedAcctLower.username, + host: null + }); + if (user === null) { - return res.sendStatus(404); + ctx.status = 404; + return; } - return res.json({ + ctx.body = { subject: `acct:${user.username}@${config.host}`, links: [{ rel: 'self', @@ -47,7 +55,7 @@ app.get('/.well-known/webfinger', async (req, res) => { rel: 'http://ostatus.org/schema/1.0/subscribe', template: `${config.url}/authorize-follow?acct={uri}` }] - }); + }; }); -export default app; +export default router;