summaryrefslogtreecommitdiff
path: root/packages/backend/src/core/HttpRequestService.ts
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
committerJulia <julia@insertdomain.name>2025-05-29 00:07:38 +0000
commit6b554c178b81f13f83a69b19d44b72b282a0c119 (patch)
treef5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/backend/src/core/HttpRequestService.ts
parentmerge: Security fixes (!970) (diff)
parentbump version for release (diff)
downloadsharkey-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.ts122
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