Skip to content

Commit 3e47845

Browse files
committed
Improve IP-Extract middleware and Bun websockets
1 parent 79abd7d commit 3e47845

9 files changed

Lines changed: 271 additions & 68 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web-monorepo",
3-
"version": "0.17.0",
3+
"version": "0.18.0",
44
"description": "High-performance web framework monorepo",
55
"private": true,
66
"type": "module",

packages/core/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.17.0",
3+
"version": "0.18.0",
44
"license": "MIT",
55
"exports": "./src/index.ts",
66
"publish": {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rabbit-company/web",
3-
"version": "0.17.0",
3+
"version": "0.18.0",
44
"description": "High-performance web framework",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

packages/core/src/index.ts

Lines changed: 126 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import type {
1212
NodeServerInstance,
1313
Route,
1414
Server,
15+
WebSocketData,
1516
} from "./types";
16-
17+
import type { ServerWebSocket } from "bun";
1718
/**
1819
* Runtime detection utilities for identifying the current JavaScript runtime environment.
1920
* @internal
@@ -615,7 +616,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
615616
return response;
616617
},
617618
],
618-
optionsId
619+
optionsId,
619620
);
620621
}
621622

@@ -1133,7 +1134,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
11331134
});
11341135
}
11351136
return res;
1136-
})
1137+
}),
11371138
);
11381139
return this;
11391140
}
@@ -1154,7 +1155,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
11541155
params: Record<string, string>,
11551156
parsedUrl: { pathname: string; searchParams?: URLSearchParams },
11561157
clientIp?: string,
1157-
env?: B
1158+
env?: B,
11581159
): Context<T, B> {
11591160
// Initialize response headers storage
11601161
const responseHeaders = new Headers();
@@ -1424,7 +1425,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
14241425
headers: new Headers({ "Content-Type": "text/html; charset=utf-8" }),
14251426
}),
14261427
query: () => parsedUrl.searchParams || EMPTY_SEARCH_PARAMS,
1427-
body: async () => ({} as any),
1428+
body: async () => ({}) as any,
14281429
header: () => {},
14291430
set: () => {},
14301431
get: () => undefined as any,
@@ -1598,7 +1599,7 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
15981599
headers: new Headers({ "Content-Type": "text/html; charset=utf-8" }),
15991600
}),
16001601
query: () => parsedUrl.searchParams || EMPTY_SEARCH_PARAMS,
1601-
body: async () => ({} as any),
1602+
body: async () => ({}) as any,
16021603
header: () => {},
16031604
set: () => {},
16041605
get: () => undefined as any,
@@ -1610,9 +1611,70 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
16101611
}
16111612
}
16121613

1614+
/**
1615+
* Processes middleware for WebSocket upgrade requests to extract client IP and other context.
1616+
* This runs middleware (like ipExtract) before upgrading the connection.
1617+
*
1618+
* @param req - The incoming Request object
1619+
* @param directIp - The direct connection IP from Bun
1620+
* @returns Promise resolving to WebSocket data to attach during upgrade
1621+
* @private
1622+
*/
1623+
private async processWebSocketUpgrade(req: Request, directIp?: string): Promise<WebSocketData> {
1624+
const method = "GET" as Method; // WebSocket upgrades are always GET
1625+
const parsedUrl = this.parseUrl(req.url);
1626+
const path = parsedUrl.pathname;
1627+
1628+
// Match route to get params
1629+
const matched = this.match(method, path);
1630+
const params = matched?.params || {};
1631+
1632+
// Start with direct IP
1633+
let clientIp = directIp;
1634+
1635+
// If we have middleware, run them to extract IP (like ipExtract)
1636+
if (this.middlewares.length > 0) {
1637+
const methodMiddlewares = this.getMethodMiddlewares(method);
1638+
1639+
// Create context for middleware
1640+
const ctx = this.createContext(req, params, parsedUrl, directIp);
1641+
1642+
// Run middleware that matches this path
1643+
for (const mw of methodMiddlewares) {
1644+
if (mw.pathPrefix && !path.startsWith(mw.pathPrefix)) {
1645+
continue;
1646+
}
1647+
1648+
const matchResult = mw.match(path);
1649+
if (matchResult.matched) {
1650+
try {
1651+
// Run the middleware with a no-op next
1652+
await mw.handler(ctx, async () => {});
1653+
1654+
// Check if middleware set clientIp (like ipExtract does)
1655+
if (ctx.clientIp) {
1656+
clientIp = ctx.clientIp;
1657+
}
1658+
} catch {
1659+
// Ignore middleware errors for WebSocket upgrade
1660+
}
1661+
}
1662+
}
1663+
}
1664+
1665+
return {
1666+
clientIp,
1667+
url: path,
1668+
params,
1669+
query: parsedUrl.searchParams || EMPTY_SEARCH_PARAMS,
1670+
};
1671+
}
1672+
16131673
/**
16141674
* Request handler optimized for Bun runtime with automatic IP extraction.
16151675
* Uses Bun's server request info for reliable client IP detection.
1676+
* For WebSocket upgrades, runs middleware (like ipExtract) before upgrading
1677+
* to ensure the real client IP is available via ws.data.clientIp.
16161678
*
16171679
* @param req - The incoming Request object
16181680
* @param server - Bun server instance
@@ -1629,21 +1691,23 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
16291691
*/
16301692
async handleBun(req: Request, server: unknown): Promise<Response> {
16311693
// Extract client IP from Bun's server object
1632-
const clientIp = (server as any)?.requestIP?.(req)?.address;
1694+
const directIp = (server as any)?.requestIP?.(req)?.address;
16331695

16341696
// Check if this is a WebSocket upgrade request
16351697
if (this.bunWebSocket && req.headers.get("upgrade") === "websocket") {
1636-
// Try to upgrade to WebSocket
1698+
// Process middleware to extract IP and other context data
1699+
const wsData = await this.processWebSocketUpgrade(req, directIp);
1700+
1701+
// Try to upgrade to WebSocket with the extracted data
16371702
const bunServer = server as BunServerInstance;
1638-
if (bunServer.upgrade(req)) {
1703+
if (bunServer.upgrade(req, { data: wsData })) {
16391704
// Return empty response to indicate upgrade was handled
16401705
return new Response(null, { status: 101 });
16411706
}
16421707
}
16431708

1644-
return this.handleWithIp(req, clientIp);
1709+
return this.handleWithIp(req, directIp);
16451710
}
1646-
16471711
/**
16481712
* Request handler optimized for Deno runtime with automatic IP extraction.
16491713
* Uses Deno's ServeHandlerInfo for reliable client IP detection.
@@ -2085,42 +2149,86 @@ export class Web<T extends Record<string, unknown> = Record<string, unknown>, B
20852149
* Sets up WebSocket handlers for Bun runtime.
20862150
* This method must be called before starting the server with listen().
20872151
*
2152+
* The WebSocket handlers receive typed data via ws.data that includes:
2153+
* - clientIp: The real client IP extracted by middleware (like ipExtract)
2154+
* - url: The original request URL pathname
2155+
* - params: URL parameters extracted from the route
2156+
* - query: Query parameters from the upgrade request
2157+
*
20882158
* @param handlers - WebSocket handler configuration
20892159
* @returns The Web instance for method chaining
20902160
*
20912161
* @example
20922162
* ```typescript
2163+
* // Use ipExtract middleware to get real client IP behind proxies
2164+
* app.use(ipExtract("cloudflare"));
2165+
*
20932166
* app.websocket({
20942167
* idleTimeout: 120,
20952168
* maxPayloadLength: 1024 * 1024,
20962169
* open(ws) {
2097-
* console.log('WebSocket connected');
2170+
* // Access the real client IP (extracted by ipExtract middleware)
2171+
* const clientIp = ws.data.clientIp;
2172+
* console.log(`Client connected from: ${clientIp}`);
2173+
*
2174+
* // Access URL params (e.g., from /ws/room/:roomId)
2175+
* const roomId = ws.data.params.roomId;
2176+
*
2177+
* // Access query params
2178+
* const token = ws.data.query.get('token');
2179+
*
20982180
* ws.subscribe('prices');
20992181
* },
21002182
* message(ws, message) {
2101-
* console.log('Received:', message);
2183+
* console.log(`Message from ${ws.data.clientIp}: ${message}`);
21022184
* },
21032185
* close(ws) {
21042186
* ws.unsubscribe('prices');
21052187
* }
21062188
* });
21072189
*
2108-
* // Then use in a route
2109-
* app.get('/ws', (ctx) => {
2190+
* // WebSocket route with parameters
2191+
* app.get('/ws/room/:roomId', (ctx) => {
21102192
* if (ctx.req.headers.get('upgrade') === 'websocket') {
2111-
* // The upgrade will be handled automatically by the framework
21122193
* return new Response(null, { status: 101 });
21132194
* }
21142195
* return ctx.text('WebSocket endpoint');
21152196
* });
21162197
* ```
21172198
*/
2118-
websocket(handlers: BunWebSocketHandler): this {
2119-
this.bunWebSocket = handlers;
2199+
websocket<D extends Record<string, unknown> = Record<string, unknown>>(handlers: BunWebSocketHandler<D>): this {
2200+
this.bunWebSocket = handlers as BunWebSocketHandler;
21202201
return this;
21212202
}
21222203
}
21232204

2205+
/**
2206+
* Helper function to get the real client IP from a WebSocket connection.
2207+
* Use this instead of ws.remoteAddress to get the real client IP when behind proxies.
2208+
*
2209+
* @param ws - The WebSocket connection
2210+
* @returns The client IP address or undefined
2211+
*
2212+
* @example
2213+
* ```typescript
2214+
* import { getWebSocketClientIp } from "@rabbit-company/web";
2215+
*
2216+
* app.websocket({
2217+
* open(ws) {
2218+
* const ip = getWebSocketClientIp(ws);
2219+
* console.log(`Client connected from: ${ip}`);
2220+
* },
2221+
* message(ws, message) {
2222+
* const ip = getWebSocketClientIp(ws);
2223+
* console.log(`Message from ${ip}: ${message}`);
2224+
* }
2225+
* });
2226+
* ```
2227+
*/
2228+
export function getWebSocketClientIp<D extends Record<string, unknown> = Record<string, unknown>>(ws: ServerWebSocket<WebSocketData<D>>): string | undefined {
2229+
return ws.data?.clientIp;
2230+
}
2231+
21242232
/**
21252233
* Extracts the static prefix from a path pattern by finding the longest initial segment
21262234
* that doesn't contain parameters (:) or wildcards (*). Used for quick middleware filtering.

0 commit comments

Comments
 (0)