smartproxy/readme.plan2.md

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

  1. Simpler API - Only two action types to understand
  2. Unified handling - Everything is either forwarding or custom socket handling
  3. More flexible - Socket handlers can do anything the old types did and more
  4. Less code - Remove specialized handlers and their dependencies
  5. Context aware - Socket handlers get access to route context (domain, port, clientIp, etc.)
  6. 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

  1. Update TSocketHandler type (15 minutes)

    • Add IRouteContext as second parameter
    • Update type definition in route-types.ts
  2. 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
  3. Remove old action types (30 minutes)

    • Remove 'redirect', 'block', 'static' from TRouteActionType
    • Remove IRouteRedirect, IRouteStatic interfaces
    • Clean up IRouteAction interface
  4. Delete old handlers (45 minutes)

    • Delete handleRedirectAction, handleBlockAction, handleStaticAction methods
    • Delete RedirectHandler and StaticHandler classes
    • Remove imports and exports
  5. Update route connection handler (30 minutes)

    • Simplify switch statement to only handle 'forward' and 'socket-handler'
    • Remove all references to deleted action types
  6. Create new socket handlers (30 minutes)

    • Implement SocketHandlers.block() with context
    • Implement SocketHandlers.httpBlock() with context
    • Implement SocketHandlers.httpRedirect() with context
  7. Update helper functions (30 minutes)

    • Update createHttpToHttpsRedirect to use socket handler
    • Delete createStaticFileRoute entirely
    • Update any other affected helpers
  8. 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
  9. 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

  1. Simplicity wins - Two concepts are easier to understand than five
  2. Power through context - Socket handlers with context are more capable
  3. Clean break - No migration paths means cleaner code
  4. 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) => {
  • 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) => {
  • 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) => {
  • 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) => {
  • 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 file
    • test/test.forwarding.ts - update any redirect tests
    • test/test.forwarding.examples.ts - update any redirect tests
    • test/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:

  1. ts/proxies/smart-proxy/models/route-types.ts - Update types
  2. ts/proxies/smart-proxy/route-connection-handler.ts - Remove handlers, update switch
  3. ts/proxies/smart-proxy/utils/route-helpers.ts - Update handlers, add new ones
  4. ts/proxies/http-proxy/handlers/index.ts - Remove exports
  5. Various test files - Update to use socket handlers

Files to Delete:

  1. ts/proxies/http-proxy/handlers/redirect-handler.ts
  2. ts/proxies/http-proxy/handlers/static-handler.ts
  3. test/test.route-redirects.ts (likely)
  4. 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