summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMeiMei <30769358+mei23@users.noreply.github.com>2020-10-18 01:46:40 +0900
committerGitHub <noreply@github.com>2020-10-18 01:46:40 +0900
commit85a0f696bcea779b02749dae596fff94a1df2467 (patch)
tree747ee51f96c7ada22a835dfc7faee0b13cdfd0e4 /src
parentFix lint (#6732) (diff)
downloadsharkey-85a0f696bcea779b02749dae596fff94a1df2467.tar.gz
sharkey-85a0f696bcea779b02749dae596fff94a1df2467.tar.bz2
sharkey-85a0f696bcea779b02749dae596fff94a1df2467.zip
ActivityPubでリモートのオブジェクトをGETするときのリクエストをHTTP Signatureで署名するオプション (#6731)
* Sign ActivityPub GET * Fix v12, v12.48.0 UI bug
Diffstat (limited to 'src')
-rw-r--r--src/client/pages/follow.vue8
-rw-r--r--src/client/scripts/search.ts6
-rw-r--r--src/config/types.ts2
-rw-r--r--src/misc/fetch.ts5
-rw-r--r--src/remote/activitypub/request.ts97
-rw-r--r--src/remote/activitypub/resolver.ts13
-rw-r--r--src/services/instance-actor.ts17
7 files changed, 141 insertions, 7 deletions
diff --git a/src/client/pages/follow.vue b/src/client/pages/follow.vue
index 35d5cc3b26..13e0a62a0d 100644
--- a/src/client/pages/follow.vue
+++ b/src/client/pages/follow.vue
@@ -12,6 +12,7 @@ export default defineComponent({
const acct = new URL(location.href).searchParams.get('acct');
if (acct == null) return;
+ /*
const dialog = os.dialog({
type: 'waiting',
text: this.$t('fetchingAsApObject') + '...',
@@ -19,6 +20,7 @@ export default defineComponent({
showCancelButton: false,
cancelableByBgClick: false
});
+ */
if (acct.startsWith('https://')) {
os.api('ap/show', {
@@ -26,6 +28,8 @@ export default defineComponent({
}).then(res => {
if (res.type == 'User') {
this.follow(res.object);
+ } else if (res.type === 'Note') {
+ this.$router.push(`/notes/${res.object.id}`);
} else {
os.dialog({
type: 'error',
@@ -42,7 +46,7 @@ export default defineComponent({
window.close();
});
}).finally(() => {
- dialog.close();
+ //dialog.close();
});
} else {
os.api('users/show', parseAcct(acct)).then(user => {
@@ -55,7 +59,7 @@ export default defineComponent({
window.close();
});
}).finally(() => {
- dialog.close();
+ //dialog.close();
});
}
},
diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts
index 45cc691fe4..fbdc32dfb1 100644
--- a/src/client/scripts/search.ts
+++ b/src/client/scripts/search.ts
@@ -48,6 +48,7 @@ export async function search(q?: string | null | undefined) {
}
if (q.startsWith('https://')) {
+ /*
const dialog = os.dialog({
type: 'waiting',
text: i18n.global.t('fetchingAsApObject') + '...',
@@ -55,19 +56,20 @@ export async function search(q?: string | null | undefined) {
showCancelButton: false,
cancelableByBgClick: false
});
+ */
try {
const res = await os.api('ap/show', {
uri: q
});
- dialog.cancel();
+ //dialog.cancel();
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
} catch (e) {
- dialog.cancel();
+ //dialog.cancel();
// TODO: Show error
}
diff --git a/src/config/types.ts b/src/config/types.ts
index 4f025750b0..8084be1864 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -58,6 +58,8 @@ export type Source = {
};
mediaProxy?: string;
+
+ signToActivityPubGet?: boolean;
};
/**
diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts
index 7be0e53fd4..90d89a4392 100644
--- a/src/misc/fetch.ts
+++ b/src/misc/fetch.ts
@@ -5,6 +5,7 @@ import fetch, { HeadersInit } from 'node-fetch';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import config from '../config';
+import { URL } from 'url';
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: HeadersInit) {
const res = await fetch(url, {
@@ -69,14 +70,14 @@ const _https = new https.Agent({
* Get http proxy or non-proxy agent
*/
export const httpAgent = config.proxy
- ? new HttpProxyAgent(config.proxy)
+ ? new HttpProxyAgent(config.proxy) as unknown as http.Agent
: _http;
/**
* Get https proxy or non-proxy agent
*/
export const httpsAgent = config.proxy
- ? new HttpsProxyAgent(config.proxy)
+ ? new HttpsProxyAgent(config.proxy) as unknown as https.Agent
: _https;
/**
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index ab51fdd93c..0edfcee1e3 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -1,3 +1,4 @@
+import * as http from 'http';
import * as https from 'https';
import { sign } from 'http-signature';
import * as crypto from 'crypto';
@@ -7,6 +8,9 @@ import { ILocalUser } from '../../models/entities/user';
import { UserKeypairs } from '../../models';
import { ensure } from '../../prelude/ensure';
import { getAgentByUrl } from '../../misc/fetch';
+import { URL } from 'url';
+import got from 'got';
+import * as Got from 'got';
export default async (user: ILocalUser, url: string, object: any) => {
const timeout = 10 * 1000;
@@ -62,3 +66,96 @@ export default async (user: ILocalUser, url: string, object: any) => {
req.end(data);
});
};
+
+/**
+ * Get AP object with http-signature
+ * @param user http-signature user
+ * @param url URL to fetch
+ */
+export async function signedGet(url: string, user: ILocalUser) {
+ const timeout = 10 * 1000;
+
+ const keypair = await UserKeypairs.findOne({
+ userId: user.id
+ }).then(ensure);
+
+ const req = got.get<any>(url, {
+ headers: {
+ 'Accept': 'application/activity+json, application/ld+json',
+ 'User-Agent': config.userAgent,
+ },
+ responseType: 'json',
+ timeout,
+ hooks: {
+ beforeRequest: [
+ options => {
+ options.request = (url: URL, opt: http.RequestOptions, callback?: (response: any) => void) => {
+ // Select custom agent by URL
+ opt.agent = getAgentByUrl(url, false);
+
+ // Wrap original https?.request
+ const requestFunc = url.protocol === 'http:' ? http.request : https.request;
+ const clientRequest = requestFunc(url, opt, callback) as http.ClientRequest;
+
+ // HTTP-Signature
+ sign(clientRequest, {
+ authorizationHeaderName: 'Signature',
+ key: keypair.privateKey,
+ keyId: `${config.url}/users/${user.id}#main-key`,
+ headers: ['(request-target)', 'host', 'date', 'accept']
+ });
+
+ return clientRequest;
+ };
+ },
+ ],
+ },
+ retry: 0,
+ });
+
+ const res = await receiveResponce(req, 10 * 1024 * 1024);
+
+ return res.body;
+}
+
+/**
+ * Receive response (with size limit)
+ * @param req Request
+ * @param maxSize size limit
+ */
+export async function receiveResponce<T>(req: Got.CancelableRequest<Got.Response<T>>, maxSize: number) {
+ // 応答ヘッダでサイズチェック
+ req.on('response', (res: Got.Response) => {
+ const contentLength = res.headers['content-length'];
+ if (contentLength != null) {
+ const size = Number(contentLength);
+ if (size > maxSize) {
+ req.cancel();
+ }
+ }
+ });
+
+ // 受信中のデータでサイズチェック
+ req.on('downloadProgress', (progress: Got.Progress) => {
+ if (progress.transferred > maxSize) {
+ req.cancel();
+ }
+ });
+
+ // 応答取得 with ステータスコードエラーの整形
+ const res = await req.catch(e => {
+ if (e.name === 'HTTPError') {
+ const statusCode = (e as Got.HTTPError).response.statusCode;
+ const statusMessage = (e as Got.HTTPError).response.statusMessage;
+ throw {
+ name: `StatusError`,
+ statusCode,
+ message: `${statusCode} ${statusMessage}`,
+ };
+ } else {
+ throw e;
+ }
+ });
+
+ return res;
+}
diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts
index f4bf8f94f9..2871c1cb41 100644
--- a/src/remote/activitypub/resolver.ts
+++ b/src/remote/activitypub/resolver.ts
@@ -1,8 +1,13 @@
+import config from '../../config';
import { getJson } from '../../misc/fetch';
+import { ILocalUser } from '../../models/entities/user';
+import { getInstanceActor } from '../../services/instance-actor';
+import { signedGet } from './request';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type';
export default class Resolver {
private history: Set<string>;
+ private user?: ILocalUser;
constructor() {
this.history = new Set();
@@ -39,7 +44,13 @@ export default class Resolver {
this.history.add(value);
- const object = await getJson(value, 'application/activity+json, application/ld+json');
+ if (config.signToActivityPubGet && !this.user) {
+ this.user = await getInstanceActor();
+ }
+
+ const object = this.user
+ ? await signedGet(value, this.user)
+ : await getJson(value, 'application/activity+json, application/ld+json');
if (object == null || (
Array.isArray(object['@context']) ?
diff --git a/src/services/instance-actor.ts b/src/services/instance-actor.ts
new file mode 100644
index 0000000000..74591846fa
--- /dev/null
+++ b/src/services/instance-actor.ts
@@ -0,0 +1,17 @@
+import { createSystemUser } from './create-system-user';
+import { ILocalUser } from '../models/entities/user';
+import { Users } from '../models';
+
+const ACTOR_USERNAME = 'instance.actor' as const;
+
+export async function getInstanceActor(): Promise<ILocalUser> {
+ const user = await Users.findOne({
+ host: null,
+ username: ACTOR_USERNAME
+ });
+
+ if (user) return user as ILocalUser;
+
+ const created = await createSystemUser(ACTOR_USERNAME);
+ return created as ILocalUser;
+}