mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-12-13 10:28:25 +00:00
allow HTTP connections to private IPs
This commit is contained in:
parent
af967fe6be
commit
3c59a7ae01
3 changed files with 42 additions and 9 deletions
|
|
@ -20,7 +20,6 @@ import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
import type { URL } from 'node:url';
|
|
||||||
import type { Socket } from 'node:net';
|
import type { Socket } from 'node:net';
|
||||||
|
|
||||||
export type HttpRequestSendOptions = {
|
export type HttpRequestSendOptions = {
|
||||||
|
|
@ -28,6 +27,15 @@ export type HttpRequestSendOptions = {
|
||||||
validators?: ((res: Response) => void)[];
|
validators?: ((res: Response) => void)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isPrivateUrl(url: URL): boolean {
|
||||||
|
if (!ipaddr.isValid(url.hostname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = ipaddr.parse(url.hostname);
|
||||||
|
return ip.range() !== 'unicast';
|
||||||
|
}
|
||||||
|
|
||||||
export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean {
|
export function isPrivateIp(allowedPrivateNetworks: PrivateNetwork[] | undefined, ip: string, port?: number): boolean {
|
||||||
const parsedIp = ipaddr.parse(ip);
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
|
@ -303,6 +311,7 @@ export class HttpRequestService {
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
size?: number,
|
size?: number,
|
||||||
isLocalAddressAllowed?: boolean,
|
isLocalAddressAllowed?: boolean,
|
||||||
|
allowHttp?: boolean,
|
||||||
} = {},
|
} = {},
|
||||||
extra: HttpRequestSendOptions = {
|
extra: HttpRequestSendOptions = {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
|
|
@ -311,7 +320,9 @@ export class HttpRequestService {
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const timeout = args.timeout ?? 5000;
|
const timeout = args.timeout ?? 5000;
|
||||||
|
|
||||||
this.utilityService.assertUrl(url);
|
const parsedUrl = new URL(url);
|
||||||
|
const allowHttp = args.allowHttp || isPrivateUrl(parsedUrl);
|
||||||
|
this.utilityService.assertUrl(parsedUrl, allowHttp);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -320,7 +331,7 @@ export class HttpRequestService {
|
||||||
|
|
||||||
const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false;
|
const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false;
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(parsedUrl, {
|
||||||
method: args.method ?? 'GET',
|
method: args.method ?? 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': this.config.userAgent,
|
'User-Agent': this.config.userAgent,
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export class UtilityService {
|
||||||
* @throws {IdentifiableError} If URL contains credentials
|
* @throws {IdentifiableError} If URL contains credentials
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public assertUrl(url: string | URL): URL | never {
|
public assertUrl(url: string | URL, allowHttp?: boolean): URL | never {
|
||||||
// If string, parse and validate
|
// If string, parse and validate
|
||||||
if (typeof(url) === 'string') {
|
if (typeof(url) === 'string') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -237,7 +237,7 @@ export class UtilityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must be HTTPS
|
// Must be HTTPS
|
||||||
if (!this.checkHttps(url)) {
|
if (!this.checkHttps(url, allowHttp)) {
|
||||||
throw new IdentifiableError('0bedd29b-e3bf-4604-af51-d3352e2518af', `invalid url ${url}: unsupported protocol ${url.protocol}`);
|
throw new IdentifiableError('0bedd29b-e3bf-4604-af51-d3352e2518af', `invalid url ${url}: unsupported protocol ${url.protocol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,12 +255,12 @@ export class UtilityService {
|
||||||
* Based on check-https.ts.
|
* Based on check-https.ts.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public checkHttps(url: string | URL): boolean {
|
public checkHttps(url: string | URL, allowHttp = false): boolean {
|
||||||
const isNonProd = this.envService.env.NODE_ENV !== 'production';
|
const isNonProd = this.envService.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proto = new URL(url).protocol;
|
const proto = new URL(url).protocol;
|
||||||
return proto === 'https:' || (proto === 'http:' && isNonProd);
|
return proto === 'https:' || (proto === 'http:' && (isNonProd || allowHttp));
|
||||||
} catch {
|
} catch {
|
||||||
// Invalid URLs don't "count" as HTTPS
|
// Invalid URLs don't "count" as HTTPS
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { jest } from '@jest/globals';
|
import { describe, jest } from '@jest/globals';
|
||||||
import type { Mock } from 'jest-mock';
|
import type { Mock } from 'jest-mock';
|
||||||
import type { PrivateNetwork } from '@/config.js';
|
import type { PrivateNetwork } from '@/config.js';
|
||||||
import type { Socket } from 'net';
|
import type { Socket } from 'net';
|
||||||
import { HttpRequestService, isPrivateIp, validateSocketConnect } from '@/core/HttpRequestService.js';
|
import { HttpRequestService, isPrivateIp, isPrivateUrl, validateSocketConnect } from '@/core/HttpRequestService.js';
|
||||||
import { parsePrivateNetworks } from '@/config.js';
|
import { parsePrivateNetworks } from '@/config.js';
|
||||||
|
|
||||||
describe(HttpRequestService, () => {
|
describe(HttpRequestService, () => {
|
||||||
|
|
@ -53,6 +53,28 @@ describe(HttpRequestService, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isPrivateUrl', () => {
|
||||||
|
it('should return false when URL is not an IP', () => {
|
||||||
|
const result = isPrivateUrl(new URL('https://example.com'));
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when IP is public', () => {
|
||||||
|
const result = isPrivateUrl(new URL('https://23.192.228.80'));
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when IP is private', () => {
|
||||||
|
const result = isPrivateUrl(new URL('https://127.0.0.1'));
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when IP is private with port and path', () => {
|
||||||
|
const result = isPrivateUrl(new URL('https://127.0.0.1:443/some/path'));
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('validateSocketConnect', () => {
|
describe('validateSocketConnect', () => {
|
||||||
let fakeSocket: Socket;
|
let fakeSocket: Socket;
|
||||||
let fakeSocketMutable: {
|
let fakeSocketMutable: {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue