Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server inboxのテストを追加 #2498

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/server/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ const router = new Router();

async function inbox(ctx: Router.RouterContext) {
if (ctx.req.headers.host !== config.host) {
logger.warn(`inbox: Invalid Host`);
ctx.status = 400;
ctx.message = 'Invalid Host';
return;
}

Expand All @@ -50,6 +52,12 @@ async function inbox(ctx: Router.RouterContext) {
} catch (e) {
logger.warn(`inbox: signature parse error: ${inspect(e)}`);
ctx.status = 401;

if (e instanceof Error) {
if (e.name === 'ExpiredRequestError') ctx.message = 'Expired Request Error';
if (e.name === 'MissingHeaderError') ctx.message = 'Missing Required Header';
}

return;
}

Expand All @@ -60,6 +68,7 @@ async function inbox(ctx: Router.RouterContext) {
if (typeof digest !== 'string') {
logger.warn(`inbox: unrecognized digest header 1`);
ctx.status = 401;
ctx.message = 'Invalid Digest Header';
return;
}

Expand All @@ -68,23 +77,26 @@ async function inbox(ctx: Router.RouterContext) {
if (match == null) {
logger.warn(`inbox: unrecognized digest header 2`);
ctx.status = 401;
ctx.message = 'Invalid Digest Header';
return;
}

const digestAlgo = match[1];
const digestExpected = match[2];

if (digestAlgo.toUpperCase() !== 'SHA-256') {
logger.warn(`inbox: unsupported algorithm`);
logger.warn(`inbox: Unsupported Digest Algorithm`);
ctx.status = 401;
ctx.message = 'Unsupported Digest Algorithm';
return;
}

const digestActual = crypto.createHash('sha256').update(raw).digest('base64');

if (digestExpected !== digestActual) {
logger.warn(`inbox: digest missmatch`);
logger.warn(`inbox: Digest Missmatch`);
ctx.status = 401;
ctx.message = 'Digest Missmatch';
return;
}

Expand Down
235 changes: 233 additions & 2 deletions test/fetch-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { async, startServer, signup, post, api, simpleGet, port, shutdownServer,
import * as openapi from '@redocly/openapi-core';
import rndstr from 'rndstr';
import { randomUUID } from 'crypto';
import { createSignedPost } from '../src/remote/activitypub/ap-request';
import { genRsaKeyPair } from '../src/misc/gen-key-pair';
import { StatusError, getResponse } from '../src/misc/fetch';
import * as crypto from 'crypto';

// Request Accept
const ONLY_AP = 'application/activity+json';
Expand All @@ -25,7 +29,7 @@ const UNSPECIFIED = '*/*';

// Response Contet-Type
const AP = 'application/activity+json; charset=utf-8';
const JSON = 'application/json; charset=utf-8';
const TYPE_JSON = 'application/json; charset=utf-8';
const HTML = 'text/html; charset=utf-8';

const CSP = `base-uri 'none'; default-src 'none'; script-src 'self' https://www.recaptcha.net/recaptcha/ https://www.gstatic.com/recaptcha/; img-src 'self' https: data: blob:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; frame-src 'self' https:; manifest-src 'self'; connect-src 'self' data: blob: ws://misskey.local https://api.rss2json.com; frame-ancestors 'none'`;
Expand Down Expand Up @@ -167,7 +171,7 @@ describe('Fetch resource', () => {
it('GET api.json', async(async () => {
const res = await simpleGet('/api.json');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.type, JSON);
assert.strictEqual(res.type, TYPE_JSON);
}));

it('Validate api.json', async(async () => {
Expand Down Expand Up @@ -515,4 +519,231 @@ describe('Fetch resource', () => {
});
}));
});

describe('inbox', async () => {
const myInbox = `http://localhost:${port}/inbox`;

const myHost = 'misskey.local';
const xHost = 'xxx.local';

const inboxPost = async (url: string, headers: Record<string, string>, body: string) => {
const res = await getResponse({
url,
method: 'POST',
headers,
body,
timeout: 10 * 1000,
}).then(r => {
return {
statusCode: r.statusCode,
statusMessage: r.statusMessage,
body: r.body,
};
}).catch(err => {
if (err instanceof StatusError) {
return {
statusCode: err.statusCode,
statusMessage: err.statusMessage,
};
} else {
throw err;
}
});
return res;
};

// 鍵はここでは検証しないのでなんでもいい
let keyPair: any;
let key: any;

before(async () => {
keyPair = await genRsaKeyPair();
key = {
privateKeyPem: keyPair.privateKey,
keyId: `https://${myHost}/users/a#main-key`,
};
});

it('Accepted', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 202);
});

it('Invalid Host', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: xHost, // ★署名されているが違うホスト向け
},
});

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 400);
assert.strictEqual(res.statusMessage, 'Invalid Host');
});

it('Payload Too Large', async () => {
const object = { a: 1, b: 'x'.repeat(70000), }; // ★でかすぎ
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 413);
});

it('Missing Required Header in the request - signature', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

delete req.request.headers.signature; // ★署名されてない

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Missing Required Header'); // TODO: どのheaderがどこに足りないのか
});

it('Missing Required Header in the request - digest', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

delete req.request.headers.digest; // ★署名されているがrequestにDigestヘッダーがない

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Missing Required Header'); // TODO: どのheaderがどこに足りないのか
});

it('Expired Request Error', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
Date: new Date(new Date().getTime() - 600 * 1000).toISOString(), // ★署名されてるがDateが古すぎる
},
});

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Expired Request Error');
});

// TODO: signatureの方に必須ヘッダーがないパターン

it('Invalid Digest Header', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

req.request.headers.digest = 'puee'; // ★

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Invalid Digest Header');
});

it('Unsupported Digest Algorithm', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

req.request.headers.digest = 'SHA-5000=abc'; // ★

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Unsupported Digest Algorithm');
});

it('Digest Missmath', async () => {
const object = { a: 1, b: 2, };
const body = JSON.stringify(object);

const req = createSignedPost({
key,
url: myInbox,
body,
additionalHeaders: {
Host: myHost,
},
});

req.request.headers.digest = `SHA-256=${crypto.createHash('sha256').update('puppukupu-').digest('base64')}`; // ★

const res = await inboxPost(myInbox, req.request.headers, body);

assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.statusMessage, 'Digest Missmatch');
});
});
});
Loading