-
Notifications
You must be signed in to change notification settings - Fork 2
feat: tunnel package #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: tunnel package #115
Conversation
This package provides: - DevhookClient for connecting to a devhook server and handling proxied requests - Cloudflare Worker server implementation with Durable Objects for session state - Local server implementation for testing - Secure URL generation using HMAC-SHA256 - Support for both wildcard subdomain and subpath routing modes - Full HTTP and WebSocket proxy support
Added tests for: - Crypto: ID generation, verification, edge cases (empty, unicode) - Local server: health check, 404s, non-WebSocket rejection - Client-server integration: - Connection establishment and public URL - GET/POST request proxying - Query parameters and headers preservation - Response headers from client - Various HTTP status codes - 503 when no client connected - Client disconnection handling - Reconnection with same secret - Multiple concurrent clients - Request error handling - Large request/response bodies (100KB) - Server callbacks: onClientConnect, onClientDisconnect, onReady
Changed from hex encoding (16^16 = 2^64 possible IDs) to base36 encoding (36^16 ≈ 2^82.7 possible IDs), providing ~18 additional bits of entropy. The base36 alphabet uses all lowercase letters (a-z) and digits (0-9), which are all valid in DNS subdomains. Added test to verify the full alphabet is being used.
Verifies that: - HMAC-SHA256 signature verification works (GitHub-style webhooks) - Form-urlencoded POST bodies are handled correctly - All headers are preserved through the proxy
Added 10 new webhook-specific tests: - Concurrent webhooks to the same client (10 simultaneous) - Concurrent webhooks to different clients (3 clients, 5 webhooks each) - Body integrity preservation with signature verification (20 concurrent) - Varying payload sizes concurrently (100B to 50KB) - Rapid sequential webhooks (50 in sequence) - Slow processing without blocking others - GitHub-style webhook with all headers - Stripe-style webhook with timestamp signature - Webhook retry scenarios - Binary webhook payloads Total test count: 39 tests, all passing.
- Implement WebSocket upgrade handling in local server for proxied connections - Add transformUrl option to DevhookClient for WebSocket URL transformation - Add try/catch guards for stream writes on WebSocket close/error - Fix close code validation to handle invalid codes gracefully - Add comprehensive WebSocket proxy tests: - Basic bidirectional messaging - Close propagation (external -> local and local -> external) - Binary message support - Multiple concurrent connections to same client - Multiple devhook clients simultaneously - Rapid message exchange across connections - Connection isolation (closing one doesnt affect others) - Cleanup on client disconnect - Update README with transformUrl documentation
Refactor tests to use a shared test suite that runs against both server implementations to ensure API compatibility: - Add shared.test-suite.ts with tests for HTTP proxying and WebSocket - Add test adapters for local and Cloudflare servers - Add test-utils.ts with common test helpers - Fix Cloudflare DO initialization to work via request headers - Fix getPublicUrl() to include /devhook/ prefix in subpath mode - Skip WebSocket tests for Cloudflare in local mode (miniflare limitation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Reorder DO fetch handler to check proxy requests before WebSocket upgrade (proxied WebSocket connections also have upgrade header) - Handle binary messages sent as strings in miniflare environment - Complete WebSocket close handshake on server side of WebSocketPair - Add proper timeout handling for slow WebSocket close propagation - Remove WebSocket test skip options now that all tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add ~50 new tests covering edge cases for both local and Cloudflare adapters: - Multi-value headers: Set-Cookie with multiple cookies, Expires dates, attributes - Cookie handling: URL-encoded values, long cookies, unicode, empty values - Header edge cases: empty values, long headers, many headers, case sensitivity - WebSocket edge cases: UTF-8 messages, large binaries, close codes, query params - Body edge cases: large bodies (5MB), binary data, null bytes, unicode JSON - Connection edge cases: rapid reconnects, high concurrency, client replacement - Error handling: 502 responses, rejected promises, various status codes - URL handling: encoded paths, special query chars, double slashes - HTTP methods: all standard methods, HEAD, OPTIONS/CORS All 75 tests pass on local adapter, 68 tests pass on Cloudflare adapter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove duplicate PROXY_WEBSOCKET_MESSAGE and PROXY_WEBSOCKET_CLOSE handling from proxy() method. For WebSocket upgrades, bindStream() is already called, which registers an onData handler for these message types. Having both handlers caused duplicate event emissions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Change WebSocket message count assertions from toBeGreaterThanOrEqual to toBe for exact counts, now that the duplicate event emission bug has been fixed in worker.ts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Set-Cookie headers require special handling because: - Multiple cookies MUST be sent as separate headers (not comma-joined) - Cookie Expires dates contain commas (e.g., "Thu, 01 Jan 2026") Changes: - Add set_cookies field to ProxyInitResponse schema - Extract Set-Cookie headers separately using getSetCookie() on client - Reconstruct multiple Set-Cookie headers using headers.append() on server - Handle Set-Cookie as array in local server HTTP response 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace the previous base36 conversion with a new algorithm that uses rejection sampling to avoid modulo bias, ensuring perfectly uniform distribution of devhook IDs. Also adds domain separation with "blink-devhook" for better cryptographic hygiene. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
| JSON.stringify({ | ||
| error: "Proxy error", | ||
| message: err instanceof Error ? err.message : String(err), | ||
| }) |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace Medium
stack trace information
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 6 hours ago
In general, the fix is to avoid returning exception-derived details (message/stack) directly to the client, and instead send a generic error response while logging the detailed error on the server. This ensures developers retain diagnostic information without exposing internal implementation details to users.
For this specific code, the best fix with minimal functional impact is to change the catch (err) block around lines 224–231. Instead of serializing err.message into the HTTP response, we should:
- Log the error on the server (e.g., using
console.error) including the stack if available. - Return a generic JSON error body such as
{ error: "Proxy error" }or{ error: "Proxy error", message: "Upstream request failed" }that does not depend onerr.
No change is needed elsewhere in the file. We can use console.error without adding imports. Optionally, we can normalize err to an Error to log stack/message safely.
Concretely:
- In
packages/tunnel/src/server/local.ts, replace thecatch (err) { ... }block starting at line 224 so that:- It calls
console.error("Proxy error", err);(or a slightly more detailed logging with stack). - It writes an HTTP 502 JSON response with a constant error and message string, not derived from
err.
- It calls
-
Copy modified lines R225-R230 -
Copy modified line R235
| @@ -222,11 +222,17 @@ | ||
|
|
||
| res.end(); | ||
| } catch (err) { | ||
| // Log detailed error information on the server, but do not expose it to the client. | ||
| if (err instanceof Error) { | ||
| console.error("Proxy error:", err.stack || err.message); | ||
| } else { | ||
| console.error("Proxy error:", err); | ||
| } | ||
| res.writeHead(502, { "content-type": "application/json" }); | ||
| res.end( | ||
| JSON.stringify({ | ||
| error: "Proxy error", | ||
| message: err instanceof Error ? err.message : String(err), | ||
| message: "An error occurred while proxying the request.", | ||
| }) | ||
| ); | ||
| } |
037bd92 to
d6eba8d
Compare
d6eba8d to
bb79c4d
Compare
No description provided.