Skip to content

Commit ff2b4f9

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/nodemailer-8.0.5
2 parents 387e1c9 + 9ccfa42 commit ff2b4f9

12 files changed

Lines changed: 500 additions & 136 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default {
2+
displayName: 'blocks-http',
3+
preset: '../../../jest.preset.js',
4+
setupFiles: ['../../../jest.env.js'],
5+
testEnvironment: 'node',
6+
transform: {
7+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
8+
},
9+
moduleFileExtensions: ['ts', 'js', 'html'],
10+
coverageDirectory: '../../../coverage/packages/blocks/http',
11+
};

packages/blocks/http/project.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
"updateBuildableProjectDepsInPackageJson": true
1818
}
1919
},
20+
"test": {
21+
"executor": "@nx/jest:jest",
22+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
23+
"options": {
24+
"jestConfig": "packages/blocks/http/jest.config.ts"
25+
}
26+
},
2027
"lint": {
2128
"executor": "@nx/eslint:lint",
2229
"outputs": ["{options.outputFile}"]

packages/blocks/http/src/lib/actions/send-http-request-action.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import {
1010
DynamicPropsValue,
1111
Property,
1212
} from '@openops/blocks-framework';
13-
import { validateHostAllowingPublicWebhookUrl } from '@openops/server-shared';
13+
import { validateHost } from '@openops/server-shared';
1414
import { assertNotNullOrUndefined } from '@openops/shared';
1515
import axios from 'axios';
1616
import FormData from 'form-data';
1717
import { HttpsProxyAgent } from 'https-proxy-agent';
1818
import { httpAuth } from '../common/auth';
1919
import { httpMethodDropdown } from '../common/props';
20+
import { validateAndRewritePublicWebhookUrl } from '../common/webhook-url-validator';
2021

2122
const toLowerCaseKeys = (obj: HttpHeaders) =>
2223
Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v]));
@@ -170,10 +171,8 @@ export const httpSendRequestAction = createAction({
170171
assertNotNullOrUndefined(method, 'Method');
171172
assertNotNullOrUndefined(url, 'URL');
172173

173-
await validateHostAllowingPublicWebhookUrl(url);
174-
await validateHostAllowingPublicWebhookUrl(
175-
context.propsValue.proxy_settings?.proxy_host,
176-
);
174+
const newUrl = await validateAndRewritePublicWebhookUrl(url);
175+
await validateHost(context.propsValue.proxy_settings?.proxy_host);
177176

178177
const headersArray =
179178
(context.auth?.headers as
@@ -193,7 +192,7 @@ export const httpSendRequestAction = createAction({
193192

194193
const request: HttpRequest = {
195194
method,
196-
url,
195+
url: newUrl,
197196
headers: mergedHeaders,
198197
queryParams: (queryParams ?? {}) as QueryParams,
199198
timeout: timeout ? timeout * 1000 : 0,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { networkUtls, validateHost } from '@openops/server-shared';
2+
3+
export async function validateAndRewritePublicWebhookUrl(
4+
userUrl: string,
5+
): Promise<string> {
6+
if (!userUrl) {
7+
return userUrl;
8+
}
9+
10+
try {
11+
await validateHost(userUrl);
12+
return userUrl;
13+
} catch (error) {
14+
const publicUrl = await networkUtls.getPublicUrl();
15+
const internalApiUrl = networkUtls.getInternalApiUrl();
16+
17+
const publicUrlObj = new URL(publicUrl);
18+
const internalUrlObj = new URL(internalApiUrl);
19+
const userUrlObj = new URL(userUrl);
20+
21+
if (userUrlObj.origin !== publicUrlObj.origin) {
22+
throw error;
23+
}
24+
25+
const internalBasePath = internalUrlObj.pathname.replace(/\/$/, '');
26+
let relativePath = userUrlObj.pathname;
27+
28+
if (
29+
internalBasePath &&
30+
internalBasePath !== '/' &&
31+
relativePath.startsWith(internalBasePath)
32+
) {
33+
relativePath = relativePath.slice(internalBasePath.length);
34+
}
35+
36+
if (!relativePath.startsWith('/')) {
37+
relativePath = `/${relativePath}`;
38+
}
39+
40+
if (!/^\/v1\/webhooks\/[0-9A-Za-z]{21}\/sync$/.test(relativePath)) {
41+
throw error;
42+
}
43+
44+
const rewrittenPath = `${internalBasePath}${relativePath}`.replace(
45+
/\/{2,}/g,
46+
'/',
47+
);
48+
49+
return `${internalUrlObj.origin}${rewrittenPath}`;
50+
}
51+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { networkUtls, validateHost } from '@openops/server-shared';
2+
import { validateAndRewritePublicWebhookUrl } from '../src/lib/common/webhook-url-validator';
3+
4+
jest.mock('@openops/server-shared', () => ({
5+
validateHost: jest.fn(),
6+
networkUtls: {
7+
getPublicUrl: jest.fn(),
8+
getInternalApiUrl: jest.fn(),
9+
},
10+
}));
11+
12+
describe('webhook-url-validator', () => {
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
});
16+
17+
it('should return the original URL if it is empty', async () => {
18+
const result = await validateAndRewritePublicWebhookUrl('');
19+
expect(result).toBe('');
20+
});
21+
22+
it('should return the original URL if validateHost passes', async () => {
23+
(validateHost as jest.Mock).mockResolvedValue(undefined);
24+
const userUrl = 'https://example.com/webhook';
25+
const result = await validateAndRewritePublicWebhookUrl(userUrl);
26+
expect(result).toBe(userUrl);
27+
expect(validateHost).toHaveBeenCalledWith(userUrl);
28+
});
29+
30+
it('should rewrite the URL if it matches the public URL origin and valid webhook path', async () => {
31+
const error = new Error('Host must not be an internal address');
32+
(validateHost as jest.Mock).mockRejectedValue(error);
33+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(
34+
'https://public.openops.com',
35+
);
36+
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
37+
'http://internal-api:3000',
38+
);
39+
40+
const userUrl =
41+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync';
42+
const result = await validateAndRewritePublicWebhookUrl(userUrl);
43+
44+
expect(result).toBe(
45+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync',
46+
);
47+
});
48+
49+
it('should rewrite the URL when public URL has a base path', async () => {
50+
const error = new Error('Host must not be an internal address');
51+
(validateHost as jest.Mock).mockRejectedValue(error);
52+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(
53+
'https://openops.com/',
54+
);
55+
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
56+
'http://internal-api:3000/api',
57+
);
58+
59+
const userUrl =
60+
'https://openops.com/api/v1/webhooks/123456789012345678901/sync';
61+
const result = await validateAndRewritePublicWebhookUrl(userUrl);
62+
63+
expect(result).toBe(
64+
'http://internal-api:3000/api/v1/webhooks/123456789012345678901/sync',
65+
);
66+
});
67+
68+
it('should throw the original error if origin does not match public URL origin', async () => {
69+
const error = new Error('Host must not be an internal address');
70+
(validateHost as jest.Mock).mockRejectedValue(error);
71+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(
72+
'https://public.openops.com',
73+
);
74+
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
75+
'http://internal-api:3000',
76+
);
77+
78+
const userUrl =
79+
'https://other-domain.com/v1/webhooks/123456789012345678901/sync';
80+
81+
await expect(validateAndRewritePublicWebhookUrl(userUrl)).rejects.toThrow(
82+
error,
83+
);
84+
});
85+
86+
it('should throw the original error if the path does not match the webhook pattern', async () => {
87+
const error = new Error('Host must not be an internal address');
88+
(validateHost as jest.Mock).mockRejectedValue(error);
89+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(
90+
'https://public.openops.com',
91+
);
92+
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
93+
'http://internal-api:3000',
94+
);
95+
96+
const userUrl = 'https://public.openops.com/v1/webhooks/invalid-id/sync';
97+
98+
await expect(validateAndRewritePublicWebhookUrl(userUrl)).rejects.toThrow(
99+
error,
100+
);
101+
});
102+
103+
it('should handle multiple slashes correctly during rewrite', async () => {
104+
const error = new Error('Host must not be an internal address');
105+
(validateHost as jest.Mock).mockRejectedValue(error);
106+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(
107+
'https://public.openops.com/',
108+
);
109+
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
110+
'http://internal-api:3000/',
111+
);
112+
113+
const userUrl =
114+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync';
115+
const result = await validateAndRewritePublicWebhookUrl(userUrl);
116+
117+
expect(result).toBe(
118+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync',
119+
);
120+
});
121+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../../dist/out-tsc",
5+
"module": "commonjs",
6+
"types": ["jest", "node"]
7+
},
8+
"include": [
9+
"jest.config.ts",
10+
"src/**/*.test.ts",
11+
"src/**/*.spec.ts",
12+
"src/**/*.d.ts"
13+
]
14+
}

0 commit comments

Comments
 (0)