smartproxy/readme.plan2.md
2025-05-29 01:00:20 +00:00

764 lines
25 KiB
Markdown

# 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
```typescript
export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler';
export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;
```
## Target State
```typescript
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
```typescript
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
```typescript
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:
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
/**
* 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
```typescript
// 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
```typescript
// BEFORE
{
action: { type: 'block' }
}
// AFTER
{
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.block()
}
}
```
### HTTP Redirect
```typescript
// 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
```typescript
// 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)
- [x] Open `ts/proxies/smart-proxy/models/route-types.ts`
- [x] Find line 14: `export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise<void>;`
- [x] Import IRouteContext at top of file: `import type { IRouteContext } from '../../../core/models/route-context.js';`
- [x] Update TSocketHandler to: `export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise<void>;`
- [x] Save file
### Step 2: Update Socket Handler Implementation (30 minutes)
- [x] Open `ts/proxies/smart-proxy/route-connection-handler.ts`
- [x] Find `handleSocketHandlerAction` method (around line 790)
- [x] Add route context creation after line 809:
```typescript
// 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,
});
```
- [x] Update line 812 from `const result = route.action.socketHandler(socket);`
- [x] To: `const result = route.action.socketHandler(socket, routeContext);`
- [x] Save file
### Step 3: Update Existing Socket Handlers in route-helpers.ts (20 minutes)
- [x] Open `ts/proxies/smart-proxy/utils/route-helpers.ts`
- [x] Update `echo` handler (line 856):
- From: `echo: (socket: plugins.net.Socket) => {`
- To: `echo: (socket: plugins.net.Socket, context: IRouteContext) => {`
- [x] 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) => {`
- [x] 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:
```typescript
/**
* 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();
});
}
```
- [x] Save file
### Step 9: Update Helper Functions (20 minutes)
- [x] Still in `route-helpers.ts`
- [x] Update `createHttpToHttpsRedirect` function (around line 109):
- Change the action to use socket handler:
```typescript
action: {
type: 'socket-handler',
socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301)
}
```
- [x] Delete entire `createStaticFileRoute` function (lines 277-322)
- [x] 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