25 KiB
SmartProxy Simplification Plan: Unify Action Types
Summary
Complete removal of 'redirect', 'block', and 'static' action types, leaving only 'forward' and 'socket-handler'. All old code will be deleted entirely - no migration paths or backwards compatibility. Socket handlers will be enhanced to receive IRouteContext as a second parameter.
Goal
Create a dramatically simpler SmartProxy with only two action types, where everything is either proxied (forward) or handled by custom code (socket-handler).
Current State
export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler';
export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;
Target State
export type TRouteActionType = 'forward' | 'socket-handler';
export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;
Benefits
- Simpler API - Only two action types to understand
- Unified handling - Everything is either forwarding or custom socket handling
- More flexible - Socket handlers can do anything the old types did and more
- Less code - Remove specialized handlers and their dependencies
- Context aware - Socket handlers get access to route context (domain, port, clientIp, etc.)
- Clean codebase - No legacy code or migration paths
Phase 1: Code to Remove
1.1 Action Type Handlers
RouteConnectionHandler.handleRedirectAction()
RouteConnectionHandler.handleBlockAction()
RouteConnectionHandler.handleStaticAction()
1.2 Handler Classes
RedirectHandler
class (http-proxy/handlers/)StaticHandler
class (http-proxy/handlers/)
1.3 Type Definitions
- 'redirect', 'block', 'static' from TRouteActionType
- IRouteRedirect interface
- IRouteStatic interface
- Related properties in IRouteAction
1.4 Helper Functions
createStaticFileRoute()
- Any other helpers that create redirect/block/static routes
Phase 2: Create Predefined Socket Handlers
2.1 Block Handler
export const SocketHandlers = {
// ... existing handlers
/**
* Block connection immediately
*/
block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
// Can use context for logging or custom messages
const finalMessage = message || `Connection blocked from ${context.clientIp}`;
if (finalMessage) {
socket.write(finalMessage);
}
socket.end();
},
/**
* HTTP block response
*/
httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
// Can customize message based on context
const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`;
const finalMessage = message || defaultMessage;
const response = [
`HTTP/1.1 ${statusCode} ${finalMessage}`,
'Content-Type: text/plain',
`Content-Length: ${finalMessage.length}`,
'Connection: close',
'',
finalMessage
].join('\r\n');
socket.write(response);
socket.end();
}
};
2.2 Redirect Handler
export const SocketHandlers = {
// ... existing handlers
/**
* HTTP redirect handler
*/
httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => {
let buffer = '';
socket.once('data', (data) => {
buffer += data.toString();
// Parse HTTP request
const lines = buffer.split('\r\n');
const requestLine = lines[0];
const [method, path] = requestLine.split(' ');
// Use domain from context (more reliable than Host header)
const domain = context.domain || 'localhost';
const port = context.port;
// Replace placeholders in location using context
let finalLocation = locationTemplate
.replace('{domain}', domain)
.replace('{port}', String(port))
.replace('{path}', path)
.replace('{clientIp}', context.clientIp);
const message = `Redirecting to ${finalLocation}`;
const response = [
`HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`,
`Location: ${finalLocation}`,
'Content-Type: text/plain',
`Content-Length: ${message.length}`,
'Connection: close',
'',
message
].join('\r\n');
socket.write(response);
socket.end();
});
}
};
2.3 Benefits of Context in Socket Handlers
With routeContext as a second parameter, socket handlers can:
- Access client IP for logging or rate limiting
- Use domain information for multi-tenant handling
- Check if connection is TLS and what version
- Access route name/ID for metrics
- Build more intelligent responses based on context
Example advanced handler:
const rateLimitHandler = (maxRequests: number) => {
const ipCounts = new Map<string, number>();
return (socket: net.Socket, context: IRouteContext) => {
const count = (ipCounts.get(context.clientIp) || 0) + 1;
ipCounts.set(context.clientIp, count);
if (count > maxRequests) {
socket.write(`Rate limit exceeded for ${context.clientIp}\n`);
socket.end();
return;
}
// Process request...
};
};
Phase 3: Update Helper Functions
3.1 Update createHttpToHttpsRedirect
export function createHttpToHttpsRedirect(
domains: string | string[],
httpsPort: number = 443,
options: Partial<IRouteConfig> = {}
): IRouteConfig {
return {
name: options.name || `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
match: {
ports: options.match?.ports || 80,
domains
},
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
},
...options
};
}
3.2 Update createSocketHandlerRoute
export function createSocketHandlerRoute(
domains: string | string[],
ports: TPortRange,
handler: TSocketHandler,
options: { name?: string; priority?: number; path?: string } = {}
): IRouteConfig {
return {
name: options.name || 'socket-handler-route',
priority: options.priority !== undefined ? options.priority : 50,
match: {
domains,
ports,
...(options.path && { path: options.path })
},
action: {
type: 'socket-handler',
socketHandler: handler
}
};
}
Phase 4: Core Implementation Changes
4.1 Update Route Connection Handler
// Remove these methods:
// - handleRedirectAction()
// - handleBlockAction()
// - handleStaticAction()
// Update switch statement to only have:
switch (route.action.type) {
case 'forward':
return this.handleForwardAction(socket, record, route, initialChunk);
case 'socket-handler':
this.handleSocketHandlerAction(socket, record, route, initialChunk);
return;
default:
logger.log('error', `Unknown action type '${(route.action as any).type}'`);
socket.end();
this.connectionManager.cleanupConnection(record, 'unknown_action');
}
4.2 Update Socket Handler to Pass Context
private async handleSocketHandlerAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig,
initialChunk?: Buffer
): Promise<void> {
const connectionId = record.id;
// Create route context for the handler
const routeContext = this.createRouteContext({
connectionId: record.id,
port: record.localPort,
domain: record.lockedDomain,
clientIp: record.remoteIP,
serverIp: socket.localAddress || '',
isTls: record.isTLS || false,
tlsVersion: record.tlsVersion,
routeName: route.name,
routeId: route.id,
});
try {
// Call the handler with socket AND context
const result = route.action.socketHandler(socket, routeContext);
// Rest of implementation stays the same...
} catch (error) {
// Error handling...
}
}
4.3 Clean Up Imports and Exports
- Remove imports of deleted handler classes
- Update index.ts files to remove exports
- Clean up any unused imports
Phase 5: Test Updates
5.1 Remove Old Tests
- Delete tests for redirect action type
- Delete tests for block action type
- Delete tests for static action type
5.2 Add New Socket Handler Tests
- Test block socket handler with context
- Test HTTP redirect socket handler with context
- Test that context is properly passed to all handlers
Phase 6: Documentation Updates
6.1 Update README.md
- Remove documentation for redirect, block, static action types
- Document the two remaining action types: forward and socket-handler
- Add examples using socket handlers with context
6.2 Update Type Documentation
/**
* Route action types
* - 'forward': Proxy the connection to a target host:port
* - 'socket-handler': Pass the socket to a custom handler function
*/
export type TRouteActionType = 'forward' | 'socket-handler';
/**
* Socket handler function
* @param socket - The incoming socket connection
* @param context - Route context with connection information
*/
export type TSocketHandler = (socket: net.Socket, context: IRouteContext) => void | Promise<void>;
6.3 Example Documentation
// Example: Block connections from specific IPs
const ipBlocker = (socket: net.Socket, context: IRouteContext) => {
if (context.clientIp.startsWith('192.168.')) {
socket.write('Internal IPs not allowed\n');
socket.end();
return;
}
// Forward to backend...
};
// Example: Domain-based routing
const domainRouter = (socket: net.Socket, context: IRouteContext) => {
const backend = context.domain === 'api.example.com' ? 'api-server' : 'web-server';
// Forward to appropriate backend...
};
Implementation Steps
-
Update TSocketHandler type (15 minutes)
- Add IRouteContext as second parameter
- Update type definition in route-types.ts
-
Update socket handler implementation (30 minutes)
- Create routeContext in handleSocketHandlerAction
- Pass context to socket handler function
- Update all existing socket handlers in route-helpers.ts
-
Remove old action types (30 minutes)
- Remove 'redirect', 'block', 'static' from TRouteActionType
- Remove IRouteRedirect, IRouteStatic interfaces
- Clean up IRouteAction interface
-
Delete old handlers (45 minutes)
- Delete handleRedirectAction, handleBlockAction, handleStaticAction methods
- Delete RedirectHandler and StaticHandler classes
- Remove imports and exports
-
Update route connection handler (30 minutes)
- Simplify switch statement to only handle 'forward' and 'socket-handler'
- Remove all references to deleted action types
-
Create new socket handlers (30 minutes)
- Implement SocketHandlers.block() with context
- Implement SocketHandlers.httpBlock() with context
- Implement SocketHandlers.httpRedirect() with context
-
Update helper functions (30 minutes)
- Update createHttpToHttpsRedirect to use socket handler
- Delete createStaticFileRoute entirely
- Update any other affected helpers
-
Clean up tests (1.5 hours)
- Delete all tests for removed action types
- Update socket handler tests to verify context parameter
- Add new tests for block/redirect socket handlers
-
Update documentation (30 minutes)
- Update README.md
- Update type documentation
- Add examples of context usage
Total estimated time: ~5 hours
Considerations
Benefits
- Dramatically simpler API - Only 2 action types instead of 5
- Consistent handling model - Everything is either forwarding or custom handling
- More powerful - Socket handlers with context can do much more than old static types
- Less code to maintain - Removing hundreds of lines of specialized handler code
- Better extensibility - Easy to add new socket handlers for any use case
- Context awareness - All handlers get full connection context
Trade-offs
- Static file serving removed (users should use nginx/apache behind proxy)
- HTTP-specific logic (redirects) now in socket handlers (but more flexible)
- Slightly more verbose configuration for simple blocks/redirects
Why This Approach
- Simplicity wins - Two concepts are easier to understand than five
- Power through context - Socket handlers with context are more capable
- Clean break - No migration paths means cleaner code
- Future proof - Easy to add new handlers without changing core
Code Examples: Before and After
Block Action
// BEFORE
{
action: { type: 'block' }
}
// AFTER
{
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.block()
}
}
HTTP Redirect
// BEFORE
{
action: {
type: 'redirect',
redirect: {
to: 'https://{domain}:443{path}',
status: 301
}
}
}
// AFTER
{
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
}
}
Custom Handler with Context
// NEW CAPABILITY - Access to full context
{
action: {
type: 'socket-handler',
socketHandler: (socket, context) => {
console.log(`Connection from ${context.clientIp} to ${context.domain}:${context.port}`);
// Custom handling based on context...
}
}
}
Detailed Implementation Tasks
Step 1: Update TSocketHandler Type (15 minutes)
- Open
ts/proxies/smart-proxy/models/route-types.ts
- Find line 14:
export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;
- Import IRouteContext at top of file:
import type { IRouteContext } from '../../../core/models/route-context.js';
- Update TSocketHandler to:
export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;
- Save file
Step 2: Update Socket Handler Implementation (30 minutes)
- Open
ts/proxies/smart-proxy/route-connection-handler.ts
- Find
handleSocketHandlerAction
method (around line 790) - Add route context creation after line 809:
// Create route context for the handler const routeContext = this.createRouteContext({ connectionId: record.id, port: record.localPort, domain: record.lockedDomain, clientIp: record.remoteIP, serverIp: socket.localAddress || '', isTls: record.isTLS || false, tlsVersion: record.tlsVersion, routeName: route.name, routeId: route.id, });
- Update line 812 from
const result = route.action.socketHandler(socket);
- To:
const result = route.action.socketHandler(socket, routeContext);
- Save file
Step 3: Update Existing Socket Handlers in route-helpers.ts (20 minutes)
- Open
ts/proxies/smart-proxy/utils/route-helpers.ts
- Update
echo
handler (line 856):- From:
echo: (socket: plugins.net.Socket) => {
- To:
echo: (socket: plugins.net.Socket, context: IRouteContext) => {
- From:
- Update
proxy
handler (line 864):- From:
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket) => {
- To:
proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {
- From:
- Update
lineProtocol
handler (line 879):- From:
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket) => {
- To:
lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {
- From:
- Update
httpResponse
handler (line 896):- From:
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket) => {
- To:
httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => {
- From:
- Save file
Step 4: Remove Old Action Types from Type Definitions (15 minutes)
- Open
ts/proxies/smart-proxy/models/route-types.ts
- Find line with TRouteActionType (around line 10)
- Change from:
export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler';
- To:
export type TRouteActionType = 'forward' | 'socket-handler';
- Find and delete IRouteRedirect interface (around line 123-126)
- Find and delete IRouteStatic interface (if exists)
- Find IRouteAction interface
- Remove these properties:
redirect?: IRouteRedirect;
static?: IRouteStatic;
- Save file
Step 5: Delete Handler Classes (15 minutes)
- Delete file:
ts/proxies/http-proxy/handlers/redirect-handler.ts
- Delete file:
ts/proxies/http-proxy/handlers/static-handler.ts
- Open
ts/proxies/http-proxy/handlers/index.ts
- Delete all content (the file only exports RedirectHandler and StaticHandler)
- Save empty file or delete it
Step 6: Remove Handler Methods from RouteConnectionHandler (30 minutes)
- Open
ts/proxies/smart-proxy/route-connection-handler.ts
- Find and delete entire
handleRedirectAction
method (around line 723) - Find and delete entire
handleBlockAction
method (around line 750) - Find and delete entire
handleStaticAction
method (around line 773) - Remove imports at top:
import { RedirectHandler, StaticHandler } from '../http-proxy/handlers/index.js';
- Save file
Step 7: Update Switch Statement (15 minutes)
- Still in
route-connection-handler.ts
- Find switch statement (around line 388)
- Remove these cases:
case 'redirect': return this.handleRedirectAction(...)
case 'block': return this.handleBlockAction(...)
case 'static': this.handleStaticAction(...); return;
- Verify only 'forward' and 'socket-handler' cases remain
- Save file
Step 8: Add New Socket Handlers to route-helpers.ts (30 minutes)
- Open
ts/proxies/smart-proxy/utils/route-helpers.ts
- Add import at top:
import type { IRouteContext } from '../../../core/models/route-context.js';
- Add to SocketHandlers object:
/** * Block connection immediately */ block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const finalMessage = message || `Connection blocked from ${context.clientIp}`; if (finalMessage) { socket.write(finalMessage); } socket.end(); }, /** * HTTP block response */ httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`; const finalMessage = message || defaultMessage; const response = [ `HTTP/1.1 ${statusCode} ${finalMessage}`, 'Content-Type: text/plain', `Content-Length: ${finalMessage.length}`, 'Connection: close', '', finalMessage ].join('\r\n'); socket.write(response); socket.end(); }, /** * HTTP redirect handler */ httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => { let buffer = ''; socket.once('data', (data) => { buffer += data.toString(); const lines = buffer.split('\r\n'); const requestLine = lines[0]; const [method, path] = requestLine.split(' '); const domain = context.domain || 'localhost'; const port = context.port; let finalLocation = locationTemplate .replace('{domain}', domain) .replace('{port}', String(port)) .replace('{path}', path) .replace('{clientIp}', context.clientIp); const message = `Redirecting to ${finalLocation}`; const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`, `Location: ${finalLocation}`, 'Content-Type: text/plain', `Content-Length: ${message.length}`, 'Connection: close', '', message ].join('\r\n'); socket.write(response); socket.end(); }); }
- Save file
Step 9: Update Helper Functions (20 minutes)
- Still in
route-helpers.ts
- Update
createHttpToHttpsRedirect
function (around line 109):- Change the action to use socket handler:
action: { type: 'socket-handler', socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301) }
- Delete entire
createStaticFileRoute
function (lines 277-322) - Save file
Step 10: Update Test Files (1.5 hours)
10.1 Update Socket Handler Tests
- Open
test/test.socket-handler.ts
- Update all handler functions to accept context parameter
- Open
test/test.socket-handler.simple.ts
- Update handler to accept context parameter
- Open
test/test.socket-handler-race.ts
- Update handler to accept context parameter
10.2 Find and Update/Delete Redirect Tests
- Search for files containing
type: 'redirect'
in test directory - For each file:
- If it's a redirect-specific test, delete the file
- If it's a mixed test, update redirect actions to use socket handlers
- Files to check:
test/test.route-redirects.ts
- likely delete entire filetest/test.forwarding.ts
- update any redirect teststest/test.forwarding.examples.ts
- update any redirect teststest/test.route-config.ts
- update any redirect tests
10.3 Find and Update/Delete Block Tests
- Search for files containing
type: 'block'
in test directory - Update or delete as appropriate
10.4 Find and Delete Static Tests
- Search for files containing
type: 'static'
in test directory - Delete static-specific test files
- Remove static tests from mixed test files
Step 11: Clean Up Imports and Exports (20 minutes)
- Open
ts/proxies/smart-proxy/utils/index.ts
- Ensure route-helpers.ts is exported
- Remove any exports of deleted functions
- Open
ts/index.ts
- Remove any exports of deleted types/interfaces
- Search for any remaining imports of RedirectHandler or StaticHandler
- Remove any found imports
Step 12: Documentation Updates (30 minutes)
- Update README.md:
- Remove any mention of redirect, block, static action types
- Add examples of socket handlers with context
- Document the two action types: forward and socket-handler
- Update any JSDoc comments in modified files
- Add examples showing context usage
Step 13: Final Verification (15 minutes)
- Run build:
pnpm build
- Fix any compilation errors
- Run tests:
pnpm test
- Fix any failing tests
- Search codebase for any remaining references to:
- 'redirect' action type
- 'block' action type
- 'static' action type
- RedirectHandler
- StaticHandler
- IRouteRedirect
- IRouteStatic
Step 14: Test New Functionality (30 minutes)
- Create test for block socket handler with context
- Create test for httpBlock socket handler with context
- Create test for httpRedirect socket handler with context
- Verify context is properly passed in all scenarios
Files to be Modified/Deleted
Files to Modify:
ts/proxies/smart-proxy/models/route-types.ts
- Update typests/proxies/smart-proxy/route-connection-handler.ts
- Remove handlers, update switchts/proxies/smart-proxy/utils/route-helpers.ts
- Update handlers, add new onests/proxies/http-proxy/handlers/index.ts
- Remove exports- Various test files - Update to use socket handlers
Files to Delete:
ts/proxies/http-proxy/handlers/redirect-handler.ts
ts/proxies/http-proxy/handlers/static-handler.ts
test/test.route-redirects.ts
(likely)- Any static-specific test files
Test Files Requiring Updates (15 files found):
- test/test.acme-http01-challenge.ts
- test/test.logger-error-handling.ts
- test/test.port80-management.node.ts
- test/test.route-update-callback.node.ts
- test/test.acme-state-manager.node.ts
- test/test.acme-route-creation.ts
- test/test.forwarding.ts
- test/test.route-redirects.ts
- test/test.forwarding.examples.ts
- test/test.acme-simple.ts
- test/test.acme-http-challenge.ts
- test/test.certificate-provisioning.ts
- test/test.route-config.ts
- test/test.route-utils.ts
- test/test.certificate-simple.ts
Success Criteria
- ✅ Only 'forward' and 'socket-handler' action types remain
- ✅ Socket handlers receive IRouteContext as second parameter
- ✅ All old handler code completely removed
- ✅ Redirect functionality works via context-aware socket handlers
- ✅ Block functionality works via context-aware socket handlers
- ✅ All tests updated and passing
- ✅ Documentation updated with new examples
- ✅ No performance regression
- ✅ Cleaner, simpler codebase