Skip to content

Commit 60b6ba2

Browse files
committed
Fix local env
1 parent 9ccfa42 commit 60b6ba2

2 files changed

Lines changed: 207 additions & 107 deletions

File tree

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
import { networkUtls, validateHost } from '@openops/server-shared';
22

3+
const WEBHOOK_SYNC_PATH_REGEX = /^\/v1\/webhooks\/[0-9A-Za-z]{21}\/sync$/;
4+
5+
function normalizeBasePath(pathname: string): string {
6+
return pathname.replace(/\/$/, '');
7+
}
8+
9+
function normalizePath(pathname: string): string {
10+
const withLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`;
11+
return withLeadingSlash.replace(/\/{2,}/g, '/');
12+
}
13+
14+
function stripBasePath(pathname: string, basePath: string): string {
15+
if (!basePath || basePath === '/') {
16+
return pathname;
17+
}
18+
19+
if (!pathname.startsWith(basePath)) {
20+
return pathname;
21+
}
22+
23+
return pathname.slice(basePath.length) || '/';
24+
}
25+
26+
function extractWebhookPath(
27+
pathname: string,
28+
publicBasePath: string,
29+
internalBasePath: string,
30+
): string | null {
31+
const candidates = [
32+
normalizePath(stripBasePath(pathname, publicBasePath)),
33+
normalizePath(stripBasePath(pathname, internalBasePath)),
34+
normalizePath(pathname),
35+
];
36+
37+
return (
38+
candidates.find((candidate) => WEBHOOK_SYNC_PATH_REGEX.test(candidate)) ??
39+
null
40+
);
41+
}
42+
343
export async function validateAndRewritePublicWebhookUrl(
444
userUrl: string,
545
): Promise<string> {
@@ -10,42 +50,35 @@ export async function validateAndRewritePublicWebhookUrl(
1050
try {
1151
await validateHost(userUrl);
1252
return userUrl;
13-
} catch (error) {
14-
const publicUrl = await networkUtls.getPublicUrl();
15-
const internalApiUrl = networkUtls.getInternalApiUrl();
53+
} catch (originalError) {
54+
const [publicUrl, internalApiUrl] = await Promise.all([
55+
networkUtls.getPublicUrl(),
56+
networkUtls.getInternalApiUrl(),
57+
]);
1658

1759
const publicUrlObj = new URL(publicUrl);
1860
const internalUrlObj = new URL(internalApiUrl);
1961
const userUrlObj = new URL(userUrl);
2062

21-
if (userUrlObj.origin !== publicUrlObj.origin) {
22-
throw error;
63+
if (userUrlObj.host !== publicUrlObj.host) {
64+
throw originalError;
2365
}
2466

25-
const internalBasePath = internalUrlObj.pathname.replace(/\/$/, '');
26-
let relativePath = userUrlObj.pathname;
67+
const publicBasePath = normalizeBasePath(publicUrlObj.pathname);
68+
const internalBasePath = normalizeBasePath(internalUrlObj.pathname);
2769

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-
}
70+
const webhookPath = extractWebhookPath(
71+
userUrlObj.pathname,
72+
publicBasePath,
73+
internalBasePath,
74+
);
3975

40-
if (!/^\/v1\/webhooks\/[0-9A-Za-z]{21}\/sync$/.test(relativePath)) {
41-
throw error;
76+
if (!webhookPath) {
77+
throw originalError;
4278
}
4379

44-
const rewrittenPath = `${internalBasePath}${relativePath}`.replace(
45-
/\/{2,}/g,
46-
'/',
47-
);
80+
const rewrittenPath = normalizePath(`${internalBasePath}${webhookPath}`);
4881

49-
return `${internalUrlObj.origin}${rewrittenPath}`;
82+
return `${internalUrlObj.origin}${rewrittenPath}${userUrlObj.search}${userUrlObj.hash}`;
5083
}
5184
}

packages/blocks/http/test/webhook-url-validator.test.ts

Lines changed: 149 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -27,95 +27,162 @@ describe('webhook-url-validator', () => {
2727
expect(validateHost).toHaveBeenCalledWith(userUrl);
2828
});
2929

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(
30+
test.each([
31+
[
32+
'rewrites when public URL has no base path and internal URL has no base path',
3433
'https://public.openops.com',
35-
);
36-
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
3734
'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(
35+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync',
4536
'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(
37+
],
38+
[
39+
'rewrites when public URL has a base path and internal URL has no base path',
40+
'http://localhost:4200/api',
41+
'http://127.0.0.1:3000',
42+
'http://localhost:4200/api/v1/webhooks/123456789012345678901/sync',
43+
'http://127.0.0.1:3000/v1/webhooks/123456789012345678901/sync',
44+
],
45+
[
46+
'rewrites when public URL has a base path and internal URL has a different base path',
47+
'https://public.openops.com/api',
48+
'http://internal-api:3000/internal',
49+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
50+
'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync',
51+
],
52+
[
53+
'rewrites when public URL and internal URL share the same base path',
54+
'https://public.openops.com/api',
5655
'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(
56+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
6457
'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(
58+
],
59+
[
60+
'rewrites when public URL ends with a trailing slash',
61+
'https://public.openops.com/api/',
62+
'http://internal-api:3000',
63+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
64+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync',
65+
],
66+
[
67+
'rewrites when internal URL ends with a trailing slash',
68+
'https://public.openops.com/api',
69+
'http://internal-api:3000/',
70+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
71+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync',
72+
],
73+
[
74+
'rewrites when both public and internal URLs end with trailing slashes',
75+
'https://public.openops.com/api/',
76+
'http://internal-api:3000/internal/',
77+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
78+
'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync',
79+
],
80+
[
81+
'rewrites localhost public URL to internal host',
82+
'http://localhost:4200',
83+
'http://internal-api:3000',
84+
'http://localhost:4200/v1/webhooks/123456789012345678901/sync',
85+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync',
86+
],
87+
[
88+
'rewrites when user URL includes query parameters',
7289
'https://public.openops.com',
73-
);
74-
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
7590
'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(
91+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync?test=true',
92+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync?test=true',
93+
],
94+
[
95+
'rewrites when user URL includes a hash fragment',
9096
'https://public.openops.com',
91-
);
92-
(networkUtls.getInternalApiUrl as jest.Mock).mockReturnValue(
9397
'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-
});
98+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync#section',
99+
'http://internal-api:3000/v1/webhooks/123456789012345678901/sync#section',
100+
],
101+
[
102+
'rewrites when user URL includes both query parameters and a hash fragment',
103+
'https://public.openops.com/api',
104+
'http://internal-api:3000/internal',
105+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync?foo=bar#section',
106+
'http://internal-api:3000/internal/v1/webhooks/123456789012345678901/sync?foo=bar#section',
107+
],
108+
[
109+
'rewrites when user URL already contains the internal base path after public host matching',
110+
'https://public.openops.com',
111+
'http://internal-api:3000/api',
112+
'https://public.openops.com/api/v1/webhooks/123456789012345678901/sync',
113+
'http://internal-api:3000/api/v1/webhooks/123456789012345678901/sync',
114+
],
115+
])(
116+
'%s',
117+
async (
118+
_caseName: string,
119+
publicUrl: string,
120+
internalApiUrl: string,
121+
userUrl: string,
122+
expectedUrl: string,
123+
) => {
124+
const originalError = new Error('Host must not be an internal address');
125+
126+
(validateHost as jest.Mock).mockRejectedValue(originalError);
127+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(publicUrl);
128+
(networkUtls.getInternalApiUrl as jest.Mock).mockResolvedValue(
129+
internalApiUrl,
130+
);
131+
132+
await expect(validateAndRewritePublicWebhookUrl(userUrl)).resolves.toBe(
133+
expectedUrl,
134+
);
135+
136+
expect(validateHost).toHaveBeenCalledWith(userUrl);
137+
expect(networkUtls.getPublicUrl).toHaveBeenCalledTimes(1);
138+
expect(networkUtls.getInternalApiUrl).toHaveBeenCalledTimes(1);
139+
},
140+
);
141+
142+
test.each([
143+
[
144+
'throws when user URL host does not match public URL host',
145+
'https://public.openops.com',
146+
'http://internal-api:3000',
147+
'https://other.openops.com/v1/webhooks/123456789012345678901/sync',
148+
],
149+
[
150+
'throws when path is not a valid webhook sync path',
151+
'https://public.openops.com',
152+
'http://internal-api:3000',
153+
'https://public.openops.com/v1/webhooks/invalid/sync',
154+
],
155+
[
156+
'throws when path does not match webhook route',
157+
'https://public.openops.com/api',
158+
'http://internal-api:3000',
159+
'https://public.openops.com/api/v1/other/123456789012345678901/sync',
160+
],
161+
[
162+
'throws when webhook path has extra suffix',
163+
'https://public.openops.com',
164+
'http://internal-api:3000',
165+
'https://public.openops.com/v1/webhooks/123456789012345678901/sync/extra',
166+
],
167+
])(
168+
'%s',
169+
async (
170+
_caseName: string,
171+
publicUrl: string,
172+
internalApiUrl: string,
173+
userUrl: string,
174+
) => {
175+
const originalError = new Error('Host must not be an internal address');
176+
177+
(validateHost as jest.Mock).mockRejectedValue(originalError);
178+
(networkUtls.getPublicUrl as jest.Mock).mockResolvedValue(publicUrl);
179+
(networkUtls.getInternalApiUrl as jest.Mock).mockResolvedValue(
180+
internalApiUrl,
181+
);
182+
183+
await expect(validateAndRewritePublicWebhookUrl(userUrl)).rejects.toBe(
184+
originalError,
185+
);
186+
},
187+
);
121188
});

0 commit comments

Comments
 (0)