ActivityPubでリモートのオブジェクトをGETするときのリクエストをHTTP Signatureで署名するオプション (#6731)
* Sign ActivityPub GET * Fix v12, v12.48.0 UI bug
This commit is contained in:
parent
ba3c62bf9c
commit
85a0f696bc
10 changed files with 298 additions and 8 deletions
|
@ -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();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ export type Source = {
|
|||
};
|
||||
|
||||
mediaProxy?: string;
|
||||
|
||||
signToActivityPubGet?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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']) ?
|
||||
|
|
17
src/services/instance-actor.ts
Normal file
17
src/services/instance-actor.ts
Normal file
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue