smartproxy/readme.plan.md

8.4 KiB

SmartProxy Development Plan

Implementation Plan: Socket Handler Function Support (Simplified) COMPLETED

Overview

Add support for custom socket handler functions with the simplest possible API - just pass a function that receives the socket.

User Experience Goal

const proxy = new SmartProxy({
  routes: [{
    name: 'my-custom-protocol',
    match: { ports: 9000, domains: 'custom.example.com' },
    action: {
      type: 'socket-handler',
      socketHandler: (socket) => {
        // User has full control of the socket
        socket.write('Welcome!\n');
        socket.on('data', (data) => {
          socket.write(`Echo: ${data}`);
        });
      }
    }
  }]
});

That's it. Simple and powerful.


Phase 1: Minimal Type Changes

1.1 Add Socket Handler Action Type

File: ts/proxies/smart-proxy/models/route-types.ts

// Update action type
export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler';

// Add simple socket handler type
export type TSocketHandler = (socket: net.Socket) => void | Promise<void>;

// Extend IRouteAction
export interface IRouteAction {
  // ... existing properties
  
  // Socket handler function (when type is 'socket-handler')
  socketHandler?: TSocketHandler;
}

Phase 2: Simple Implementation

2.1 Update Route Connection Handler

File: ts/proxies/smart-proxy/route-connection-handler.ts

In the handleConnection method, add handling for socket-handler:

// After route matching...
if (matchedRoute) {
  const action = matchedRoute.action;
  
  if (action.type === 'socket-handler') {
    if (!action.socketHandler) {
      logger.error('socket-handler action missing socketHandler function');
      socket.destroy();
      return;
    }
    
    try {
      // Simply call the handler with the socket
      const result = action.socketHandler(socket);
      
      // If it returns a promise, handle errors
      if (result instanceof Promise) {
        result.catch(error => {
          logger.error('Socket handler error:', error);
          if (!socket.destroyed) {
            socket.destroy();
          }
        });
      }
    } catch (error) {
      logger.error('Socket handler error:', error);
      if (!socket.destroyed) {
        socket.destroy();
      }
    }
    
    return; // Done - user has control now
  }
  
  // ... rest of existing action handling
}

Phase 3: Optional Context (If Needed)

If users need more info, we can optionally pass a minimal context as a second parameter:

export type TSocketHandler = (
  socket: net.Socket, 
  context?: {
    route: IRouteConfig;
    clientIp: string;
    localPort: number;
  }
) => void | Promise<void>;

Usage:

socketHandler: (socket, context) => {
  console.log(`Connection from ${context.clientIp} to port ${context.localPort}`);
  // Handle socket...
}

Phase 4: Helper Utilities (Optional)

4.1 Common Patterns

File: ts/proxies/smart-proxy/utils/route-helpers.ts

// Simple helper to create socket handler routes
export function createSocketHandlerRoute(
  domains: string | string[],
  ports: TPortRange,
  handler: TSocketHandler,
  options?: { name?: string; priority?: number }
): IRouteConfig {
  return {
    name: options?.name || 'socket-handler-route',
    priority: options?.priority || 50,
    match: { domains, ports },
    action: {
      type: 'socket-handler',
      socketHandler: handler
    }
  };
}

// Pre-built handlers for common cases
export const SocketHandlers = {
  // Simple echo server
  echo: (socket: net.Socket) => {
    socket.on('data', data => socket.write(data));
  },
  
  // TCP proxy
  proxy: (targetHost: string, targetPort: number) => (socket: net.Socket) => {
    const target = net.connect(targetPort, targetHost);
    socket.pipe(target);
    target.pipe(socket);
    socket.on('close', () => target.destroy());
    target.on('close', () => socket.destroy());
  },
  
  // Line-based protocol
  lineProtocol: (handler: (line: string, socket: net.Socket) => void) => (socket: net.Socket) => {
    let buffer = '';
    socket.on('data', (data) => {
      buffer += data.toString();
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';
      lines.forEach(line => handler(line, socket));
    });
  }
};

Usage Examples

Example 1: Custom Protocol

{
  name: 'custom-protocol',
  match: { ports: 9000 },
  action: {
    type: 'socket-handler',
    socketHandler: (socket) => {
      socket.write('READY\n');
      socket.on('data', (data) => {
        const cmd = data.toString().trim();
        if (cmd === 'PING') socket.write('PONG\n');
        else if (cmd === 'QUIT') socket.end();
        else socket.write('ERROR: Unknown command\n');
      });
    }
  }
}

Example 2: Simple TCP Proxy

{
  name: 'tcp-proxy',
  match: { ports: 8080, domains: 'proxy.example.com' },
  action: {
    type: 'socket-handler',
    socketHandler: SocketHandlers.proxy('backend.local', 3000)
  }
}

Example 3: WebSocket with Custom Auth

{
  name: 'custom-websocket',
  match: { ports: [80, 443], path: '/ws' },
  action: {
    type: 'socket-handler',
    socketHandler: async (socket) => {
      // Read HTTP headers
      const headers = await readHttpHeaders(socket);
      
      // Custom auth check
      if (!headers.authorization || !validateToken(headers.authorization)) {
        socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
        socket.end();
        return;
      }
      
      // Proceed with WebSocket upgrade
      const ws = new WebSocket(socket, headers);
      // ... handle WebSocket
    }
  }
}

Benefits of This Approach

  1. Dead Simple API: Just pass a function that gets the socket
  2. No New Classes: No ForwardingHandler subclass needed
  3. Minimal Changes: Only touches type definitions and one handler method
  4. Full Power: Users have complete control over the socket
  5. Backward Compatible: No changes to existing functionality
  6. Easy to Test: Just test the socket handler functions directly

Implementation Steps

  1. Add 'socket-handler' to TRouteActionType (5 minutes)
  2. Add socketHandler?: TSocketHandler to IRouteAction (5 minutes)
  3. Add socket-handler case in RouteConnectionHandler.handleConnection() (15 minutes)
  4. Add helper functions (optional, 30 minutes)
  5. Write tests (2 hours)
  6. Update documentation (1 hour)

Total implementation time: ~4 hours (vs 6 weeks for the complex version)


What We're NOT Doing

  • Creating new ForwardingHandler classes
  • Complex context objects with utils
  • HTTP request handling for socket handlers
  • Complex protocol detection mechanisms
  • Middleware patterns
  • Lifecycle hooks

Keep it simple. The user just wants to handle a socket.


Success Criteria

  • Users can define a route with type: 'socket-handler'
  • Users can provide a function that receives the socket
  • The function is called when a connection matches the route
  • Error handling prevents crashes
  • No performance impact on existing routes
  • Clean, simple API that's easy to understand

Implementation Notes (Completed)

What Was Implemented

  1. Type Definitions - Added 'socket-handler' to TRouteActionType and TSocketHandler type
  2. Route Handler - Added socket-handler case in RouteConnectionHandler switch statement
  3. Error Handling - Both sync and async errors are caught and logged
  4. Initial Data Handling - Initial chunks are re-emitted to handler's listeners
  5. Helper Functions - Added createSocketHandlerRoute and pre-built handlers (echo, proxy, etc.)
  6. Full Test Coverage - All test cases pass including async handlers and error handling

Key Implementation Details

  • Socket handlers require initial data from client to trigger routing (not TLS handshake)
  • The handler receives the raw socket after route matching
  • Both sync and async handlers are supported
  • Errors in handlers terminate the connection gracefully
  • Helper utilities provide common patterns (echo server, TCP proxy, line protocol)

Usage Notes

  • Clients must send initial data to trigger the handler (even just a newline)
  • The socket is passed directly to the handler function
  • Handler has complete control over the socket lifecycle
  • No special context object needed - keeps it simple

Total implementation time: ~3 hours