diff options
| author | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
|---|---|---|
| committer | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
| commit | 6b554c178b81f13f83a69b19d44b72b282a0c119 (patch) | |
| tree | f5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/backend/src/core/HttpRequestService.ts | |
| parent | merge: Security fixes (!970) (diff) | |
| parent | bump version for release (diff) | |
| download | sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.gz sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.bz2 sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.zip | |
merge: release 2025.4.2 (!1051)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1051
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/backend/src/core/HttpRequestService.ts')
| -rw-r--r-- | packages/backend/src/core/HttpRequestService.ts | 122 |
1 files changed, 71 insertions, 51 deletions
diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 19992a7597..5c271b81e3 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -12,20 +12,44 @@ import fetch from 'node-fetch'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { Config } from '@/config.js'; +import type { Config, PrivateNetwork } from '@/config.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import { IObject } from '@/core/activitypub/type.js'; +import type { IObject, IObjectWithId } from '@/core/activitypub/type.js'; import { ApUtilityService } from './activitypub/ApUtilityService.js'; import type { Response } from 'node-fetch'; import type { URL } from 'node:url'; +import type { Socket } from 'node:net'; export type HttpRequestSendOptions = { throwErrorWhenResponseNotOk: boolean; validators?: ((res: Response) => void)[]; }; +export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean { + const parsedIp = ipaddr.parse(ip); + + for (const { cidr, ports } of allowedPrivateNetworks ?? []) { + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(cidr)) { + if (ports == null || (port != null && ports.includes(port))) { + return false; + } + } + } + + return parsedIp.range() !== 'unicast'; +} + +export function validateSocketConnect(allowedPrivateNetworks: PrivateNetwork[] | undefined, socket: Socket): void { + const address = socket.remoteAddress; + if (address && ipaddr.isValid(address)) { + if (isPrivateIp(allowedPrivateNetworks, address, socket.remotePort)) { + socket.destroy(new Error(`Blocked address: ${address}`)); + } + } +} + declare module 'node:http' { interface Agent { createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket; @@ -44,31 +68,12 @@ class HttpRequestServiceAgent extends http.Agent { public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } + validateSocketConnect(this.config.allowedPrivateNetworks, socket); } }); return socket; } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } } class HttpsRequestServiceAgent extends https.Agent { @@ -83,31 +88,12 @@ class HttpsRequestServiceAgent extends https.Agent { public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket { const socket = super.createConnection(options, callback) .on('connect', () => { - const address = socket.remoteAddress; if (process.env.NODE_ENV === 'production') { - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } - } + validateSocketConnect(this.config.allowedPrivateNetworks, socket); } }); return socket; } - - @bindThis - private isPrivateIp(ip: string): boolean { - const parsedIp = ipaddr.parse(ip); - - for (const net of this.config.allowedPrivateNetworks ?? []) { - const cidr = ipaddr.parseCIDR(net); - if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { - return false; - } - } - - return parsedIp.range() !== 'unicast'; - } } @Injectable() @@ -115,32 +101,32 @@ export class HttpRequestService { /** * Get http non-proxy agent (without local address filtering) */ - private httpNative: http.Agent; + private readonly httpNative: http.Agent; /** * Get https non-proxy agent (without local address filtering) */ - private httpsNative: https.Agent; + private readonly httpsNative: https.Agent; /** * Get http non-proxy agent */ - private http: http.Agent; + private readonly http: http.Agent; /** * Get https non-proxy agent */ - private https: https.Agent; + private readonly https: https.Agent; /** * Get http proxy or non-proxy agent */ - public httpAgent: http.Agent; + public readonly httpAgent: http.Agent; /** * Get https proxy or non-proxy agent */ - public httpsAgent: https.Agent; + public readonly httpsAgent: https.Agent; constructor( @Inject(DI.config) @@ -198,7 +184,7 @@ export class HttpRequestService { /** * Get agent by URL * @param url URL - * @param bypassProxy Allways bypass proxy + * @param bypassProxy Always bypass proxy * @param isLocalAddressAllowed */ @bindThis @@ -216,8 +202,42 @@ export class HttpRequestService { } } + /** + * Get agent for http by URL + * @param url URL + * @param isLocalAddressAllowed + */ @bindThis - public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> { + public getAgentForHttp(url: URL, isLocalAddressAllowed = false): http.Agent { + if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + return isLocalAddressAllowed + ? this.httpNative + : this.http; + } else { + return this.httpAgent; + } + } + + /** + * Get agent for https by URL + * @param url URL + * @param isLocalAddressAllowed + */ + @bindThis + public getAgentForHttps(url: URL, isLocalAddressAllowed = false): https.Agent { + if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) { + return isLocalAddressAllowed + ? this.httpsNative + : this.https; + } else { + return this.httpsAgent; + } + } + + @bindThis + public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObjectWithId> { + this.apUtilityService.assertApUrl(url); + const res = await this.send(url, { method: 'GET', headers: { @@ -237,7 +257,7 @@ export class HttpRequestService { // The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match. this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url); - return activity; + return activity as IObjectWithId; } @bindThis |