Skip to content

Commit 43f98da

Browse files
committed
Simplify
1 parent 10c9b68 commit 43f98da

6 files changed

Lines changed: 61 additions & 96 deletions

File tree

src/http/handlers/config.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import type { Request, Response, NextFn, Handler } from '../index';
1+
import { originNotAllowed, type Request, type Response, type NextFn, type Handler } from '../index';
22
import type { TidewaveConfig } from '../../core';
33
import { default as tidewavePackage } from '../../../package.json' with { type: 'json' };
44

55
export function createHandleConfig(config: TidewaveConfig): Handler {
6-
return async function handleConfig(_req: Request, res: Response, next: NextFn): Promise<void> {
6+
return async function handleConfig(req: Request, res: Response, next: NextFn): Promise<void> {
7+
if (req.headers.origin) {
8+
originNotAllowed(res);
9+
return;
10+
}
11+
712
try {
813
const tidewaveConfig = {
914
project_name: config.projectName || 'app',

src/http/handlers/mcp.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2-
import { methodNotAllowed, type Request, type Response, type NextFn } from '..';
2+
import { methodNotAllowed, originNotAllowed, type Request, type Response, type NextFn } from '..';
33
import { serveMcp } from '../../mcp';
44

55
export async function handleMcp(req: Request, res: Response, next: NextFn): Promise<void> {
66
try {
7+
if (req.headers.origin) {
8+
originNotAllowed(res);
9+
return;
10+
}
11+
712
if (req.method !== 'POST') {
813
methodNotAllowed(res);
914
return;

src/http/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ServerResponse } from 'http';
22
import type { IncomingMessage, NextFunction, Server } from 'connect';
33
import connect from 'connect';
4-
import { checkOrigin, checkRemoteIp } from './security';
4+
import { checkRemoteIp } from './security';
55
import { handleMcp } from './handlers/mcp';
66
import { createHandleHtml } from './handlers/html';
77
import { createHandleConfig } from './handlers/config';
@@ -49,7 +49,6 @@ export function configureServer(
4949
export function checkSecurity(config: TidewaveConfig) {
5050
return (req: Request, res: Response, next: NextFn): void => {
5151
if (!checkRemoteIp(req, res, config)) return;
52-
if (!checkOrigin(req, res, config)) return;
5352
next();
5453
};
5554
}
@@ -58,7 +57,14 @@ export function methodNotAllowed(res: Response): void {
5857
res.statusCode = 405;
5958
res.setHeader('Allow', 'POST');
6059
res.end();
61-
return;
60+
}
61+
62+
export function originNotAllowed(res: Response): void {
63+
const message =
64+
'For security reasons, Tidewave does not accept requests with an origin header for this endpoint.';
65+
console.warn(message);
66+
res.statusCode = 403;
67+
res.end(message);
6268
}
6369

6470
// Export for use by framework integrations

src/http/security.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,3 @@ export function isLocalIp(ip?: string): boolean {
4444

4545
return false;
4646
}
47-
48-
export function checkOrigin(req: Request, res: Response, _config: TidewaveConfig): boolean {
49-
const { origin } = req.headers;
50-
const url = req.url || '/';
51-
const [pathname] = url.split('?');
52-
53-
// GET / (root HTML page) allows any origin
54-
if (
55-
pathname === '/' ||
56-
pathname === '' ||
57-
pathname === '/tidewave' ||
58-
pathname === '/tidewave/'
59-
) {
60-
return true;
61-
}
62-
63-
// No origin header means non-browser request (e.g. Claude Code, Cursor)
64-
if (!origin) return true;
65-
66-
// /config and /mcp refuse if origin header is set
67-
const message =
68-
'For security reasons, Tidewave does not accept requests with an origin header for this endpoint.';
69-
console.warn(message);
70-
res.statusCode = 403;
71-
res.end(message);
72-
return false;
73-
}

test/http/index.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { methodNotAllowed } from '../../src/http';
3-
import type { Response } from '../../src/http';
3+
import type { Request, Response } from '../../src/http';
4+
import { handleMcp } from '../../src/http/handlers/mcp';
5+
import { createHandleConfig } from '../../src/http/handlers/config';
46

57
// Mock request/response helpers
8+
const createMockRequest = (headers: Record<string, string> = {}): Partial<Request> => ({
9+
socket: { remoteAddress: '127.0.0.1' } as any,
10+
headers,
11+
});
12+
613
const createMockResponse = () => {
714
const mockEnd = vi.fn();
815
const mockSetHeader = vi.fn();
@@ -23,6 +30,7 @@ const createMockResponse = () => {
2330
describe('HTTP Utilities', () => {
2431
beforeEach(() => {
2532
vi.clearAllMocks();
33+
console.warn = vi.fn();
2634
console.error = vi.fn();
2735
});
2836

@@ -45,4 +53,31 @@ describe('HTTP Utilities', () => {
4553
expect(result).toBeUndefined();
4654
});
4755
});
56+
57+
describe('handleMcp', () => {
58+
it('should return 403 if origin header is set', async () => {
59+
const req = createMockRequest({ origin: 'http://localhost:4000' });
60+
const { res, mockEnd } = createMockResponse();
61+
const next = vi.fn();
62+
63+
await handleMcp(req as Request, res as Response, next);
64+
65+
expect(res.statusCode).toBe(403);
66+
expect(mockEnd).toHaveBeenCalledWith(expect.stringContaining('origin'));
67+
});
68+
});
69+
70+
describe('handleConfig', () => {
71+
it('should return 403 if origin header is set', async () => {
72+
const req = createMockRequest({ origin: 'http://localhost:4000' });
73+
const { res, mockEnd } = createMockResponse();
74+
const next = vi.fn();
75+
76+
const handler = createHandleConfig({});
77+
await handler(req as Request, res as Response, next);
78+
79+
expect(res.statusCode).toBe(403);
80+
expect(mockEnd).toHaveBeenCalledWith(expect.stringContaining('origin'));
81+
});
82+
});
4883
});

test/http/security.test.ts

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { checkRemoteIp, checkOrigin, isLocalIp } from '../../src/http/security';
2+
import { checkRemoteIp, isLocalIp } from '../../src/http/security';
33
import type { Request, Response } from '../../src/http';
44
import type { TidewaveConfig } from '../../src/core';
55

66
// Mock request/response helpers
7-
const createMockRequest = (
8-
remoteAddress = '127.0.0.1',
9-
origin?: string,
10-
url = '/',
11-
): Partial<Request> => ({
7+
const createMockRequest = (remoteAddress = '127.0.0.1'): Partial<Request> => ({
128
socket: { remoteAddress } as any,
13-
headers: origin ? { origin } : {},
14-
url,
9+
headers: {},
1510
});
1611

1712
const createMockResponse = () => {
@@ -104,58 +99,4 @@ describe('HTTP Security', () => {
10499
expect(res.statusCode).toBe(200);
105100
});
106101
});
107-
108-
describe('checkOrigin', () => {
109-
it('/mcp and /config refuse requests with origin header', () => {
110-
// /mcp should refuse any request with origin header
111-
const req1 = createMockRequest('127.0.0.1', 'http://localhost:4001', '/mcp');
112-
const { res: res1, mockEnd: mockEnd1 } = createMockResponse();
113-
const config: TidewaveConfig = {};
114-
115-
const result1 = checkOrigin(req1 as Request, res1 as Response, config);
116-
117-
expect(result1).toBe(false);
118-
expect(res1.statusCode).toBe(403);
119-
120-
// /config should refuse any request with origin header
121-
const req2 = createMockRequest('127.0.0.1', 'http://localhost:4000', '/config');
122-
const { res: res2, mockEnd: mockEnd2 } = createMockResponse();
123-
124-
const result2 = checkOrigin(req2 as Request, res2 as Response, config);
125-
126-
expect(result2).toBe(false);
127-
expect(res2.statusCode).toBe(403);
128-
});
129-
130-
it('/ (root) allows any origin', () => {
131-
// / should allow any origin
132-
const req1 = createMockRequest('127.0.0.1', 'http://example.com', '/');
133-
const { res: res1 } = createMockResponse();
134-
const config: TidewaveConfig = {};
135-
136-
const result1 = checkOrigin(req1 as Request, res1 as Response, config);
137-
138-
expect(result1).toBe(true);
139-
expect(res1.statusCode).toBe(200);
140-
141-
const req2 = createMockRequest('127.0.0.1', 'http://localhost:4000', '/');
142-
const { res: res2 } = createMockResponse();
143-
144-
const result2 = checkOrigin(req2 as Request, res2 as Response, config);
145-
146-
expect(result2).toBe(true);
147-
expect(res2.statusCode).toBe(200);
148-
});
149-
150-
it('should allow requests without origin header', () => {
151-
const req = createMockRequest('127.0.0.1', undefined, '/mcp');
152-
const { res } = createMockResponse();
153-
const config: TidewaveConfig = {};
154-
155-
const result = checkOrigin(req as Request, res as Response, config);
156-
157-
expect(result).toBe(true);
158-
expect(res.statusCode).toBe(200);
159-
});
160-
});
161102
});

0 commit comments

Comments
 (0)