feat: replace onebox ingress with SmartProxy
This commit is contained in:
+15
-16
@@ -44,42 +44,42 @@ ts/database/
|
|||||||
- All methods delegate to the appropriate repository
|
- All methods delegate to the appropriate repository
|
||||||
- No breaking changes for existing code
|
- No breaking changes for existing code
|
||||||
|
|
||||||
## Current Migration Version: 8
|
## Current Migration Version: 15
|
||||||
|
|
||||||
Migration 8 converted certificate storage from file paths to PEM content.
|
Migration 15 renames the core reverse proxy platform service from `caddy` to `smartproxy`.
|
||||||
|
|
||||||
## Reverse Proxy (November 2025 - Caddy Docker Service)
|
## Reverse Proxy (April 2026 - SmartProxy Docker Service)
|
||||||
|
|
||||||
The reverse proxy uses **Caddy** running as a Docker Swarm service for production-grade reverse proxying with native SNI support, HTTP/2, HTTP/3, and WebSocket handling.
|
The reverse proxy uses **SmartProxy** running as a Docker Swarm service for production-grade reverse proxying with TLS termination and WebSocket handling.
|
||||||
|
|
||||||
**Architecture:**
|
**Architecture:**
|
||||||
|
|
||||||
- Caddy runs as Docker Swarm service (`onebox-caddy`) on the overlay network
|
- SmartProxy runs as Docker Swarm service (`onebox-smartproxy`) on the overlay network
|
||||||
- No binary download required - uses `caddy:2-alpine` Docker image
|
- No host binary download required - uses `code.foss.global/host.today/ht-docker-smartproxy:latest`
|
||||||
- Configuration pushed dynamically via Caddy Admin API (port 2019)
|
- Routes are pushed dynamically via the SmartProxy admin API (host port 2019)
|
||||||
- Automatic HTTPS disabled - certificates managed externally via SmartACME
|
- Automatic HTTPS disabled - certificates managed externally via SmartACME
|
||||||
- Zero-downtime configuration updates
|
- Zero-downtime configuration updates
|
||||||
- Services reached by Docker service name (e.g., `onebox-hello-world:80`)
|
- Services reached by Docker service name (e.g., `onebox-hello-world:80`)
|
||||||
|
|
||||||
**Key files:**
|
**Key files:**
|
||||||
|
|
||||||
- `ts/classes/caddy.ts` - CaddyManager class for Docker service and Admin API
|
- `ts/classes/smartproxy.ts` - SmartProxyManager class for Docker service and Admin API
|
||||||
- `ts/classes/reverseproxy.ts` - Delegates to CaddyManager
|
- `ts/classes/reverseproxy.ts` - Delegates to SmartProxyManager
|
||||||
|
|
||||||
**Certificate workflow:**
|
**Certificate workflow:**
|
||||||
|
|
||||||
1. `CertRequirementManager` creates requirements for domains
|
1. `CertRequirementManager` creates requirements for domains
|
||||||
2. Daemon processes requirements via `certmanager.ts`
|
2. Daemon processes requirements via `certmanager.ts`
|
||||||
3. Certificates stored in database (PEM content)
|
3. Certificates stored in database (PEM content)
|
||||||
4. `reverseProxy.addCertificate()` passes PEM content to Caddy via `load_pem` (inline in config)
|
4. `reverseProxy.addCertificate()` passes PEM content to SmartProxy route config
|
||||||
5. Caddy serves TLS with the loaded certificates (no volume mounts needed)
|
5. SmartProxy serves TLS with the loaded certificates (no volume mounts needed)
|
||||||
|
|
||||||
**Docker Service Configuration:**
|
**Docker Service Configuration:**
|
||||||
|
|
||||||
- Service name: `onebox-caddy`
|
- Service name: `onebox-smartproxy`
|
||||||
- Image: `caddy:2-alpine`
|
- Image: `code.foss.global/host.today/ht-docker-smartproxy:latest`
|
||||||
- Network: `onebox-network` (overlay, attachable)
|
- Network: `onebox-network` (overlay, attachable)
|
||||||
- Startup: Writes initial config with `admin.listen: 0.0.0.0:2019` for host access
|
- Startup: SmartProxy daemon admin API listens on container port 3000, published on host port 2019
|
||||||
|
|
||||||
**Port Mapping:**
|
**Port Mapping:**
|
||||||
|
|
||||||
@@ -89,5 +89,4 @@ The reverse proxy uses **Caddy** running as a Docker Swarm service for productio
|
|||||||
|
|
||||||
**Log Receiver:**
|
**Log Receiver:**
|
||||||
|
|
||||||
- Caddy sends access logs to `tcp/172.17.0.1:9999` (Docker bridge gateway)
|
- `ProxyLogReceiver` remains the host-side access-log stream endpoint for proxy log integrations
|
||||||
- `CaddyLogReceiver` on host receives and processes logs
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# @serve.zone/onebox
|
# @serve.zone/onebox
|
||||||
|
|
||||||
> 🚀 Self-hosted Docker Swarm platform with Caddy reverse proxy, automatic SSL, and real-time WebSocket updates
|
> 🚀 Self-hosted Docker Swarm platform with SmartProxy reverse proxy, automatic SSL, and real-time WebSocket updates
|
||||||
|
|
||||||
**Onebox** transforms any Linux server into a powerful container hosting platform. Deploy Docker Swarm services with automatic HTTPS, DNS configuration, and Caddy reverse proxy running as a Docker service - all managed through a modern web interface with real-time updates.
|
**Onebox** transforms any Linux server into a powerful container hosting platform. Deploy Docker Swarm services with automatic HTTPS, DNS configuration, and SmartProxy reverse proxy running as a Docker service - all managed through a modern web interface with real-time updates.
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -10,12 +10,12 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
|
|
||||||
## What Makes Onebox Different? 🎯
|
## What Makes Onebox Different? 🎯
|
||||||
|
|
||||||
- **Caddy Reverse Proxy in Docker** - Production-grade HTTP/HTTPS proxy running as a Swarm service with native service discovery, HTTP/2, HTTP/3, and bidirectional WebSocket proxying
|
- **SmartProxy Reverse Proxy in Docker** - Production-grade HTTP/HTTPS proxy running as a Swarm service with native service discovery, TLS termination, and bidirectional WebSocket proxying
|
||||||
- **Docker Swarm First** - All workloads (including the reverse proxy!) run as Swarm services on the overlay network for seamless service-to-service communication
|
- **Docker Swarm First** - All workloads (including the reverse proxy!) run as Swarm services on the overlay network for seamless service-to-service communication
|
||||||
- **Real-time Everything** - WebSocket-powered live updates for service status, logs, and metrics across all connected clients
|
- **Real-time Everything** - WebSocket-powered live updates for service status, logs, and metrics across all connected clients
|
||||||
- **Single Executable** - Compiles to a standalone binary - just run it, no dependencies
|
- **Single Executable** - Compiles to a standalone binary - just run it, no dependencies
|
||||||
- **Private Registry Included** - Built-in Docker registry with token-based auth and auto-deploy on push
|
- **Private Registry Included** - Built-in Docker registry with token-based auth and auto-deploy on push
|
||||||
- **Zero Config SSL** - Automatic Let's Encrypt certificates with inline `load_pem` (no volume mounts needed)
|
- **Zero Config SSL** - Automatic Let's Encrypt certificates passed directly into SmartProxy routes
|
||||||
- **Cloudflare Integration** - Automatic DNS record management and zone synchronization
|
- **Cloudflare Integration** - Automatic DNS record management and zone synchronization
|
||||||
- **Modern Stack** - Deno runtime + SQLite database + typed web UI
|
- **Modern Stack** - Deno runtime + SQLite database + typed web UI
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
### Core Platform
|
### Core Platform
|
||||||
|
|
||||||
- 🐳 **Docker Swarm Management** - Deploy, scale, and orchestrate services with Swarm mode
|
- 🐳 **Docker Swarm Management** - Deploy, scale, and orchestrate services with Swarm mode
|
||||||
- 🌐 **Caddy Reverse Proxy** - Production-grade proxy running as Docker service with SNI, HTTP/2, HTTP/3
|
- 🌐 **SmartProxy Reverse Proxy** - Production-grade proxy running as Docker service with TLS termination and WebSocket support
|
||||||
- 🔒 **Automatic SSL Certificates** - Let's Encrypt integration with hot-reload and renewal monitoring
|
- 🔒 **Automatic SSL Certificates** - Let's Encrypt integration with hot-reload and renewal monitoring
|
||||||
- ☁️ **Cloudflare DNS Integration** - Automatic DNS record creation and zone synchronization
|
- ☁️ **Cloudflare DNS Integration** - Automatic DNS record creation and zone synchronization
|
||||||
- 📦 **Built-in Registry** - Private Docker registry with per-service tokens and auto-update
|
- 📦 **Built-in Registry** - Private Docker registry with per-service tokens and auto-update
|
||||||
@@ -117,7 +117,7 @@ Onebox is built with modern technologies for performance and developer experienc
|
|||||||
│ ┌──────────────────────────────┐ │
|
│ ┌──────────────────────────────┐ │
|
||||||
│ │ onebox-network (overlay) │ │
|
│ │ onebox-network (overlay) │ │
|
||||||
│ ├──────────────────────────────┤ │
|
│ ├──────────────────────────────┤ │
|
||||||
│ │ onebox-caddy (Caddy proxy) │ │
|
│ │ onebox-smartproxy (proxy) │ │
|
||||||
│ │ HTTP (80) + HTTPS (443) │ │
|
│ │ HTTP (80) + HTTPS (443) │ │
|
||||||
│ │ Admin API → config updates │ │
|
│ │ Admin API → config updates │ │
|
||||||
│ ├──────────────────────────────┤ │
|
│ ├──────────────────────────────┤ │
|
||||||
@@ -137,7 +137,7 @@ Onebox is built with modern technologies for performance and developer experienc
|
|||||||
| Component | Description |
|
| Component | Description |
|
||||||
| ----------------------- | -------------------------------------------------------------------- |
|
| ----------------------- | -------------------------------------------------------------------- |
|
||||||
| **Deno Runtime** | Modern TypeScript with built-in security |
|
| **Deno Runtime** | Modern TypeScript with built-in security |
|
||||||
| **Caddy Reverse Proxy** | Docker Swarm service with HTTP/2, HTTP/3, SNI, and WebSocket support |
|
| **SmartProxy Reverse Proxy** | Docker Swarm service with TLS termination and WebSocket support |
|
||||||
| **Docker Swarm** | Container orchestration (all workloads run as services) |
|
| **Docker Swarm** | Container orchestration (all workloads run as services) |
|
||||||
| **SQLite Database** | Configuration, metrics, and user data |
|
| **SQLite Database** | Configuration, metrics, and user data |
|
||||||
| **OpsServer** | TypedRequest API and TypedSocket real-time updates |
|
| **OpsServer** | TypedRequest API and TypedSocket real-time updates |
|
||||||
@@ -321,7 +321,7 @@ onebox/
|
|||||||
│ ├── classes/ # Core implementations
|
│ ├── classes/ # Core implementations
|
||||||
│ │ ├── onebox.ts # Main coordinator
|
│ │ ├── onebox.ts # Main coordinator
|
||||||
│ │ ├── reverseproxy.ts # Reverse proxy orchestration
|
│ │ ├── reverseproxy.ts # Reverse proxy orchestration
|
||||||
│ │ ├── caddy.ts # Caddy Docker service management
|
│ │ ├── smartproxy.ts # SmartProxy Docker service management
|
||||||
│ │ ├── docker.ts # Docker Swarm API
|
│ │ ├── docker.ts # Docker Swarm API
|
||||||
│ │ ├── services.ts # Service orchestration
|
│ │ ├── services.ts # Service orchestration
|
||||||
│ │ ├── certmanager.ts # SSL certificate management
|
│ │ ├── certmanager.ts # SSL certificate management
|
||||||
|
|||||||
@@ -1,592 +0,0 @@
|
|||||||
/**
|
|
||||||
* Caddy Manager for Onebox
|
|
||||||
*
|
|
||||||
* Manages Caddy as a Docker Swarm service instead of a host binary.
|
|
||||||
* This allows Caddy to access services on the Docker overlay network.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as plugins from '../plugins.ts';
|
|
||||||
import { logger } from '../logging.ts';
|
|
||||||
import { getErrorMessage } from '../utils/error.ts';
|
|
||||||
|
|
||||||
const CADDY_SERVICE_NAME = 'onebox-caddy';
|
|
||||||
const CADDY_IMAGE = 'caddy:2-alpine';
|
|
||||||
const DOCKER_GATEWAY_IP = '172.17.0.1'; // Docker bridge gateway for container-to-host communication
|
|
||||||
|
|
||||||
export interface ICaddyRoute {
|
|
||||||
domain: string;
|
|
||||||
upstream: string; // e.g., "onebox-hello-world:80"
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICaddyCertificate {
|
|
||||||
domain: string;
|
|
||||||
certPem: string;
|
|
||||||
keyPem: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICaddyLoggingConfig {
|
|
||||||
logs: {
|
|
||||||
[name: string]: {
|
|
||||||
writer: {
|
|
||||||
output: string;
|
|
||||||
address?: string;
|
|
||||||
dial_timeout?: string;
|
|
||||||
soft_start?: boolean;
|
|
||||||
};
|
|
||||||
encoder?: { format: string };
|
|
||||||
level?: string;
|
|
||||||
include?: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICaddyConfig {
|
|
||||||
admin: {
|
|
||||||
listen: string;
|
|
||||||
};
|
|
||||||
logging?: ICaddyLoggingConfig;
|
|
||||||
apps: {
|
|
||||||
http: {
|
|
||||||
servers: {
|
|
||||||
[key: string]: {
|
|
||||||
listen: string[];
|
|
||||||
routes: ICaddyRouteConfig[];
|
|
||||||
automatic_https?: {
|
|
||||||
disable?: boolean;
|
|
||||||
disable_redirects?: boolean;
|
|
||||||
};
|
|
||||||
logs?: {
|
|
||||||
default_logger_name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
tls?: {
|
|
||||||
automation?: {
|
|
||||||
policies: Array<{ issuers: never[] }>;
|
|
||||||
};
|
|
||||||
certificates?: {
|
|
||||||
load_pem?: Array<{
|
|
||||||
certificate: string;
|
|
||||||
key: string;
|
|
||||||
tags?: string[];
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ICaddyRouteConfig {
|
|
||||||
match: Array<{ host: string[] }>;
|
|
||||||
handle: Array<{
|
|
||||||
handler: string;
|
|
||||||
upstreams?: Array<{ dial: string }>;
|
|
||||||
routes?: ICaddyRouteConfig[];
|
|
||||||
}>;
|
|
||||||
terminal?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CaddyManager {
|
|
||||||
private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
|
|
||||||
private certsDir: string;
|
|
||||||
private adminUrl: string;
|
|
||||||
private httpPort: number;
|
|
||||||
private httpsPort: number;
|
|
||||||
private logReceiverPort: number;
|
|
||||||
private loggingEnabled: boolean;
|
|
||||||
private routes: Map<string, ICaddyRoute> = new Map();
|
|
||||||
private certificates: Map<string, ICaddyCertificate> = new Map();
|
|
||||||
private networkName = 'onebox-network';
|
|
||||||
private serviceRunning = false;
|
|
||||||
|
|
||||||
constructor(options?: {
|
|
||||||
certsDir?: string;
|
|
||||||
adminPort?: number;
|
|
||||||
httpPort?: number;
|
|
||||||
httpsPort?: number;
|
|
||||||
logReceiverPort?: number;
|
|
||||||
loggingEnabled?: boolean;
|
|
||||||
}) {
|
|
||||||
this.certsDir = options?.certsDir || './.nogit/certs';
|
|
||||||
this.adminUrl = `http://localhost:${options?.adminPort || 2019}`;
|
|
||||||
this.httpPort = options?.httpPort || 8080;
|
|
||||||
this.httpsPort = options?.httpsPort || 8443;
|
|
||||||
this.logReceiverPort = options?.logReceiverPort || 9999;
|
|
||||||
this.loggingEnabled = options?.loggingEnabled ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize Docker client for Caddy service management
|
|
||||||
*/
|
|
||||||
private async ensureDockerClient(): Promise<void> {
|
|
||||||
if (!this.dockerClient) {
|
|
||||||
this.dockerClient = new plugins.docker.Docker({
|
|
||||||
socketPath: 'unix:///var/run/docker.sock',
|
|
||||||
});
|
|
||||||
await this.dockerClient.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update listening ports (must call reloadConfig after if running)
|
|
||||||
*/
|
|
||||||
setPorts(httpPort: number, httpsPort: number): void {
|
|
||||||
this.httpPort = httpPort;
|
|
||||||
this.httpsPort = httpsPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start Caddy as a Docker Swarm service
|
|
||||||
*/
|
|
||||||
async start(): Promise<void> {
|
|
||||||
if (this.serviceRunning) {
|
|
||||||
logger.warn('Caddy service is already running');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.ensureDockerClient();
|
|
||||||
|
|
||||||
// Create certs directory for backup/persistence
|
|
||||||
await Deno.mkdir(this.certsDir, { recursive: true });
|
|
||||||
|
|
||||||
logger.info('Starting Caddy Docker service...');
|
|
||||||
|
|
||||||
// Check if service already exists
|
|
||||||
const existingService = await this.getExistingService();
|
|
||||||
if (existingService) {
|
|
||||||
logger.info('Caddy service exists, removing old service...');
|
|
||||||
await this.removeService();
|
|
||||||
// Wait for service to be removed
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get network ID
|
|
||||||
const networkId = await this.getNetworkId();
|
|
||||||
|
|
||||||
// Create Caddy Docker service
|
|
||||||
const response = await this.dockerClient!.request('POST', '/services/create', {
|
|
||||||
Name: CADDY_SERVICE_NAME,
|
|
||||||
Labels: {
|
|
||||||
'managed-by': 'onebox',
|
|
||||||
'onebox-type': 'caddy',
|
|
||||||
},
|
|
||||||
TaskTemplate: {
|
|
||||||
ContainerSpec: {
|
|
||||||
Image: CADDY_IMAGE,
|
|
||||||
// Start Caddy with admin listening on all interfaces so we can reach it from host
|
|
||||||
// Write minimal config to /tmp and start Caddy with that config
|
|
||||||
Command: ['sh', '-c', 'printf \'{"admin":{"listen":"0.0.0.0:2019"}}\' > /tmp/caddy.json && caddy run --config /tmp/caddy.json'],
|
|
||||||
},
|
|
||||||
Networks: [
|
|
||||||
{
|
|
||||||
Target: networkId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
RestartPolicy: {
|
|
||||||
Condition: 'any',
|
|
||||||
MaxAttempts: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Mode: {
|
|
||||||
Replicated: {
|
|
||||||
Replicas: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
EndpointSpec: {
|
|
||||||
Ports: [
|
|
||||||
{
|
|
||||||
Protocol: 'tcp',
|
|
||||||
TargetPort: 80,
|
|
||||||
PublishedPort: this.httpPort,
|
|
||||||
PublishMode: 'host',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Protocol: 'tcp',
|
|
||||||
TargetPort: 443,
|
|
||||||
PublishedPort: this.httpsPort,
|
|
||||||
PublishMode: 'host',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Protocol: 'tcp',
|
|
||||||
TargetPort: 2019,
|
|
||||||
PublishedPort: 2019,
|
|
||||||
PublishMode: 'host',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode >= 300) {
|
|
||||||
throw new Error(`Failed to create Caddy service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`Caddy service created: ${response.body.ID}`);
|
|
||||||
|
|
||||||
// Wait for Admin API to be ready
|
|
||||||
await this.waitForReady();
|
|
||||||
|
|
||||||
this.serviceRunning = true;
|
|
||||||
|
|
||||||
// Now configure via Admin API with current routes and certificates
|
|
||||||
await this.reloadConfig();
|
|
||||||
|
|
||||||
logger.success(`Caddy started (HTTP: ${this.httpPort}, HTTPS: ${this.httpsPort}, Admin: ${this.adminUrl})`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to start Caddy: ${getErrorMessage(error)}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get existing Caddy service if any
|
|
||||||
*/
|
|
||||||
private async getExistingService(): Promise<any | null> {
|
|
||||||
try {
|
|
||||||
const response = await this.dockerClient!.request('GET', `/services/${CADDY_SERVICE_NAME}`, {});
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
return response.body;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the Caddy service
|
|
||||||
*/
|
|
||||||
private async removeService(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.dockerClient!.request('DELETE', `/services/${CADDY_SERVICE_NAME}`, {});
|
|
||||||
} catch {
|
|
||||||
// Service may not exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get network ID by name
|
|
||||||
*/
|
|
||||||
private async getNetworkId(): Promise<string> {
|
|
||||||
const networks = await this.dockerClient!.listNetworks();
|
|
||||||
const network = networks.find((n: any) => n.Name === this.networkName);
|
|
||||||
if (!network) {
|
|
||||||
throw new Error(`Network not found: ${this.networkName}`);
|
|
||||||
}
|
|
||||||
return network.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for Caddy Admin API to be ready
|
|
||||||
*/
|
|
||||||
private async waitForReady(maxAttempts = 60, intervalMs = 500): Promise<void> {
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.adminUrl}/config/`);
|
|
||||||
if (response.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not ready yet
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
||||||
}
|
|
||||||
throw new Error('Caddy service failed to start within timeout');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop Caddy Docker service
|
|
||||||
*/
|
|
||||||
async stop(): Promise<void> {
|
|
||||||
if (!this.serviceRunning && !(await this.getExistingService())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.ensureDockerClient();
|
|
||||||
|
|
||||||
logger.info('Stopping Caddy service...');
|
|
||||||
|
|
||||||
await this.removeService();
|
|
||||||
|
|
||||||
this.serviceRunning = false;
|
|
||||||
logger.info('Caddy service stopped');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to stop Caddy: ${getErrorMessage(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Caddy Admin API is healthy
|
|
||||||
*/
|
|
||||||
async isHealthy(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.adminUrl}/config/`);
|
|
||||||
return response.ok;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Caddy service is running
|
|
||||||
*/
|
|
||||||
async isRunning(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.ensureDockerClient();
|
|
||||||
const service = await this.getExistingService();
|
|
||||||
if (!service) return false;
|
|
||||||
|
|
||||||
// Check if service has running tasks
|
|
||||||
const tasksResponse = await this.dockerClient!.request(
|
|
||||||
'GET',
|
|
||||||
`/tasks?filters=${encodeURIComponent(JSON.stringify({ service: [CADDY_SERVICE_NAME] }))}`,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tasksResponse.statusCode !== 200) return false;
|
|
||||||
|
|
||||||
const tasks = tasksResponse.body;
|
|
||||||
return tasks.some((task: any) => task.Status?.State === 'running');
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build Caddy JSON configuration from current routes and certificates
|
|
||||||
*/
|
|
||||||
private buildConfig(): ICaddyConfig {
|
|
||||||
const routes: ICaddyRouteConfig[] = [];
|
|
||||||
|
|
||||||
// Add routes
|
|
||||||
for (const [domain, route] of this.routes) {
|
|
||||||
routes.push({
|
|
||||||
match: [{ host: [domain] }],
|
|
||||||
handle: [
|
|
||||||
{
|
|
||||||
handler: 'reverse_proxy',
|
|
||||||
upstreams: [{ dial: route.upstream }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
terminal: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build certificate load_pem entries (inline PEM content)
|
|
||||||
const loadPem: Array<{ certificate: string; key: string; tags?: string[] }> = [];
|
|
||||||
for (const [domain, cert] of this.certificates) {
|
|
||||||
loadPem.push({
|
|
||||||
certificate: cert.certPem,
|
|
||||||
key: cert.keyPem,
|
|
||||||
tags: [domain],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: ICaddyConfig = {
|
|
||||||
admin: {
|
|
||||||
listen: '0.0.0.0:2019', // Listen on all interfaces inside container
|
|
||||||
},
|
|
||||||
apps: {
|
|
||||||
http: {
|
|
||||||
servers: {
|
|
||||||
main: {
|
|
||||||
listen: [':80', ':443'],
|
|
||||||
routes,
|
|
||||||
// Disable automatic HTTPS to prevent Caddy from trying to obtain certs
|
|
||||||
automatic_https: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add access logging configuration if enabled
|
|
||||||
if (this.loggingEnabled) {
|
|
||||||
config.logging = {
|
|
||||||
logs: {
|
|
||||||
access: {
|
|
||||||
writer: {
|
|
||||||
output: 'net',
|
|
||||||
// Use Docker bridge gateway IP to reach log receiver on host
|
|
||||||
address: `tcp/${DOCKER_GATEWAY_IP}:${this.logReceiverPort}`,
|
|
||||||
dial_timeout: '5s',
|
|
||||||
soft_start: true, // Continue even if log receiver is down
|
|
||||||
},
|
|
||||||
encoder: { format: 'json' },
|
|
||||||
level: 'INFO',
|
|
||||||
include: ['http.log.access'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Associate server with access logger
|
|
||||||
config.apps.http.servers.main.logs = {
|
|
||||||
default_logger_name: 'access',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add TLS config if we have certificates
|
|
||||||
if (loadPem.length > 0) {
|
|
||||||
config.apps.tls = {
|
|
||||||
automation: {
|
|
||||||
// Disable automatic HTTPS - we manage certs ourselves
|
|
||||||
policies: [{ issuers: [] }],
|
|
||||||
},
|
|
||||||
certificates: {
|
|
||||||
load_pem: loadPem,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload Caddy configuration via Admin API
|
|
||||||
*/
|
|
||||||
async reloadConfig(): Promise<void> {
|
|
||||||
const isRunning = await this.isRunning();
|
|
||||||
if (!isRunning) {
|
|
||||||
logger.warn('Caddy not running, cannot reload config');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = this.buildConfig();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${this.adminUrl}/load`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(config),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Failed to reload Caddy config: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('Caddy configuration reloaded');
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to reload Caddy config: ${getErrorMessage(error)}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or update a route
|
|
||||||
*/
|
|
||||||
async addRoute(domain: string, upstream: string): Promise<void> {
|
|
||||||
this.routes.set(domain, { domain, upstream });
|
|
||||||
|
|
||||||
if (await this.isRunning()) {
|
|
||||||
await this.reloadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.success(`Added Caddy route: ${domain} -> ${upstream}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a route
|
|
||||||
*/
|
|
||||||
async removeRoute(domain: string): Promise<void> {
|
|
||||||
if (this.routes.delete(domain)) {
|
|
||||||
if (await this.isRunning()) {
|
|
||||||
await this.reloadConfig();
|
|
||||||
}
|
|
||||||
logger.success(`Removed Caddy route: ${domain}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or update a TLS certificate
|
|
||||||
* Stores PEM content in memory for Admin API, also writes to disk for backup
|
|
||||||
*/
|
|
||||||
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
|
|
||||||
// Store PEM content in memory for buildConfig()
|
|
||||||
this.certificates.set(domain, {
|
|
||||||
domain,
|
|
||||||
certPem,
|
|
||||||
keyPem,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also write to disk for backup/persistence
|
|
||||||
try {
|
|
||||||
await Deno.mkdir(this.certsDir, { recursive: true });
|
|
||||||
await Deno.writeTextFile(`${this.certsDir}/${domain}.crt`, certPem);
|
|
||||||
await Deno.writeTextFile(`${this.certsDir}/${domain}.key`, keyPem);
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Failed to write certificate backup for ${domain}: ${getErrorMessage(error)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.isRunning()) {
|
|
||||||
await this.reloadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.success(`Added TLS certificate for ${domain}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a TLS certificate
|
|
||||||
*/
|
|
||||||
async removeCertificate(domain: string): Promise<void> {
|
|
||||||
if (this.certificates.delete(domain)) {
|
|
||||||
// Remove backup files
|
|
||||||
try {
|
|
||||||
await Deno.remove(`${this.certsDir}/${domain}.crt`);
|
|
||||||
await Deno.remove(`${this.certsDir}/${domain}.key`);
|
|
||||||
} catch {
|
|
||||||
// Files may not exist
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.isRunning()) {
|
|
||||||
await this.reloadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.success(`Removed TLS certificate for ${domain}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all current routes
|
|
||||||
*/
|
|
||||||
getRoutes(): ICaddyRoute[] {
|
|
||||||
return Array.from(this.routes.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all current certificates
|
|
||||||
*/
|
|
||||||
getCertificates(): ICaddyCertificate[] {
|
|
||||||
return Array.from(this.certificates.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all routes and certificates (useful for reload from database)
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this.routes.clear();
|
|
||||||
this.certificates.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get status
|
|
||||||
*/
|
|
||||||
getStatus(): {
|
|
||||||
running: boolean;
|
|
||||||
httpPort: number;
|
|
||||||
httpsPort: number;
|
|
||||||
routes: number;
|
|
||||||
certificates: number;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
running: this.serviceRunning,
|
|
||||||
httpPort: this.httpPort,
|
|
||||||
httpsPort: this.httpsPort,
|
|
||||||
routes: this.routes.size,
|
|
||||||
certificates: this.certificates.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-11
@@ -21,7 +21,7 @@ import { CertRequirementManager } from './cert-requirement-manager.ts';
|
|||||||
import { RegistryManager } from './registry.ts';
|
import { RegistryManager } from './registry.ts';
|
||||||
import { PlatformServicesManager } from './platform-services/index.ts';
|
import { PlatformServicesManager } from './platform-services/index.ts';
|
||||||
import { AppStoreManager } from './appstore.ts';
|
import { AppStoreManager } from './appstore.ts';
|
||||||
import { CaddyLogReceiver } from './caddy-log-receiver.ts';
|
import { ProxyLogReceiver } from './proxy-log-receiver.ts';
|
||||||
import { BackupManager } from './backup-manager.ts';
|
import { BackupManager } from './backup-manager.ts';
|
||||||
import { BackupScheduler } from './backup-scheduler.ts';
|
import { BackupScheduler } from './backup-scheduler.ts';
|
||||||
import { OpsServer } from '../opsserver/index.ts';
|
import { OpsServer } from '../opsserver/index.ts';
|
||||||
@@ -41,7 +41,7 @@ export class Onebox {
|
|||||||
public registry: RegistryManager;
|
public registry: RegistryManager;
|
||||||
public platformServices: PlatformServicesManager;
|
public platformServices: PlatformServicesManager;
|
||||||
public appStore: AppStoreManager;
|
public appStore: AppStoreManager;
|
||||||
public caddyLogReceiver: CaddyLogReceiver;
|
public proxyLogReceiver: ProxyLogReceiver;
|
||||||
public backupManager: BackupManager;
|
public backupManager: BackupManager;
|
||||||
public backupScheduler: BackupScheduler;
|
public backupScheduler: BackupScheduler;
|
||||||
public opsServer: OpsServer;
|
public opsServer: OpsServer;
|
||||||
@@ -77,8 +77,8 @@ export class Onebox {
|
|||||||
// Initialize App Store manager
|
// Initialize App Store manager
|
||||||
this.appStore = new AppStoreManager(this);
|
this.appStore = new AppStoreManager(this);
|
||||||
|
|
||||||
// Initialize Caddy log receiver
|
// Initialize reverse proxy log receiver
|
||||||
this.caddyLogReceiver = new CaddyLogReceiver(9999);
|
this.proxyLogReceiver = new ProxyLogReceiver(9999);
|
||||||
|
|
||||||
// Initialize Backup manager
|
// Initialize Backup manager
|
||||||
this.backupManager = new BackupManager(this);
|
this.backupManager = new BackupManager(this);
|
||||||
@@ -106,11 +106,11 @@ export class Onebox {
|
|||||||
// Initialize Docker
|
// Initialize Docker
|
||||||
await this.docker.init();
|
await this.docker.init();
|
||||||
|
|
||||||
// Start Caddy log receiver BEFORE reverse proxy (so Caddy can connect to it)
|
// Start proxy log receiver before reverse proxy startup.
|
||||||
try {
|
try {
|
||||||
await this.caddyLogReceiver.start();
|
await this.proxyLogReceiver.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`Failed to start Caddy log receiver: ${getErrorMessage(error)}`);
|
logger.warn(`Failed to start proxy log receiver: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Reverse Proxy
|
// Initialize Reverse Proxy
|
||||||
@@ -268,9 +268,9 @@ export class Onebox {
|
|||||||
const providers = this.platformServices.getAllProviders();
|
const providers = this.platformServices.getAllProviders();
|
||||||
const platformServicesStatus = providers.map((provider) => {
|
const platformServicesStatus = providers.map((provider) => {
|
||||||
const service = platformServices.find((s) => s.type === provider.type);
|
const service = platformServices.find((s) => s.type === provider.type);
|
||||||
// For Caddy, check actual runtime status since it starts without a DB record
|
// For SmartProxy, check actual runtime status since it starts without a DB record
|
||||||
let status = service?.status || 'not-deployed';
|
let status = service?.status || 'not-deployed';
|
||||||
if (provider.type === 'caddy') {
|
if (provider.type === 'smartproxy') {
|
||||||
status = proxyStatus.http.running ? 'running' : 'stopped';
|
status = proxyStatus.http.running ? 'running' : 'stopped';
|
||||||
}
|
}
|
||||||
// Count resources for this platform service
|
// Count resources for this platform service
|
||||||
@@ -432,8 +432,8 @@ export class Onebox {
|
|||||||
// Stop reverse proxy if running
|
// Stop reverse proxy if running
|
||||||
await this.reverseProxy.stop();
|
await this.reverseProxy.stop();
|
||||||
|
|
||||||
// Stop Caddy log receiver
|
// Stop proxy log receiver
|
||||||
await this.caddyLogReceiver.stop();
|
await this.proxyLogReceiver.stop();
|
||||||
|
|
||||||
// Close backup archive
|
// Close backup archive
|
||||||
await this.backupManager.close();
|
await this.backupManager.close();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
import type { IPlatformServiceProvider } from './providers/base.ts';
|
import type { IPlatformServiceProvider } from './providers/base.ts';
|
||||||
import { MongoDBProvider } from './providers/mongodb.ts';
|
import { MongoDBProvider } from './providers/mongodb.ts';
|
||||||
import { MinioProvider } from './providers/minio.ts';
|
import { MinioProvider } from './providers/minio.ts';
|
||||||
import { CaddyProvider } from './providers/caddy.ts';
|
import { SmartProxyProvider } from './providers/smartproxy.ts';
|
||||||
import { ClickHouseProvider } from './providers/clickhouse.ts';
|
import { ClickHouseProvider } from './providers/clickhouse.ts';
|
||||||
import { MariaDBProvider } from './providers/mariadb.ts';
|
import { MariaDBProvider } from './providers/mariadb.ts';
|
||||||
import { RedisProvider } from './providers/redis.ts';
|
import { RedisProvider } from './providers/redis.ts';
|
||||||
@@ -41,7 +41,7 @@ export class PlatformServicesManager {
|
|||||||
// Register providers
|
// Register providers
|
||||||
this.registerProvider(new MongoDBProvider(this.oneboxRef));
|
this.registerProvider(new MongoDBProvider(this.oneboxRef));
|
||||||
this.registerProvider(new MinioProvider(this.oneboxRef));
|
this.registerProvider(new MinioProvider(this.oneboxRef));
|
||||||
this.registerProvider(new CaddyProvider(this.oneboxRef));
|
this.registerProvider(new SmartProxyProvider(this.oneboxRef));
|
||||||
this.registerProvider(new ClickHouseProvider(this.oneboxRef));
|
this.registerProvider(new ClickHouseProvider(this.oneboxRef));
|
||||||
this.registerProvider(new MariaDBProvider(this.oneboxRef));
|
this.registerProvider(new MariaDBProvider(this.oneboxRef));
|
||||||
this.registerProvider(new RedisProvider(this.oneboxRef));
|
this.registerProvider(new RedisProvider(this.oneboxRef));
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
/**
|
|
||||||
* Caddy Platform Service Provider
|
|
||||||
*
|
|
||||||
* Caddy is a core infrastructure service that provides reverse proxy functionality.
|
|
||||||
* Unlike other platform services:
|
|
||||||
* - It doesn't provision resources for user services
|
|
||||||
* - It's started automatically by Onebox and cannot be stopped by users
|
|
||||||
* - It delegates to the existing CaddyManager for actual operations
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BasePlatformServiceProvider } from './base.ts';
|
|
||||||
import type {
|
|
||||||
IService,
|
|
||||||
IPlatformResource,
|
|
||||||
IPlatformServiceConfig,
|
|
||||||
IProvisionedResource,
|
|
||||||
IEnvVarMapping,
|
|
||||||
TPlatformServiceType,
|
|
||||||
TPlatformResourceType,
|
|
||||||
} from '../../../types.ts';
|
|
||||||
import { logger } from '../../../logging.ts';
|
|
||||||
import type { Onebox } from '../../onebox.ts';
|
|
||||||
|
|
||||||
export class CaddyProvider extends BasePlatformServiceProvider {
|
|
||||||
readonly type: TPlatformServiceType = 'caddy';
|
|
||||||
readonly displayName = 'Caddy Reverse Proxy';
|
|
||||||
readonly resourceTypes: TPlatformResourceType[] = []; // Caddy doesn't provision resources
|
|
||||||
readonly isCore = true; // Core infrastructure - cannot be stopped by users
|
|
||||||
|
|
||||||
constructor(oneboxRef: Onebox) {
|
|
||||||
super(oneboxRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultConfig(): IPlatformServiceConfig {
|
|
||||||
return {
|
|
||||||
image: 'caddy:2-alpine',
|
|
||||||
port: 80,
|
|
||||||
volumes: [],
|
|
||||||
environment: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getEnvVarMappings(): IEnvVarMapping[] {
|
|
||||||
// Caddy doesn't inject any env vars into user services
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deploy Caddy container - delegates to CaddyManager via reverseProxy
|
|
||||||
*/
|
|
||||||
async deployContainer(): Promise<string> {
|
|
||||||
logger.info('Starting Caddy via reverse proxy manager...');
|
|
||||||
|
|
||||||
// Get the reverse proxy which manages Caddy
|
|
||||||
const reverseProxy = this.oneboxRef.reverseProxy;
|
|
||||||
|
|
||||||
// Start reverse proxy (which starts Caddy)
|
|
||||||
await reverseProxy.startHttp();
|
|
||||||
|
|
||||||
// Get Caddy status to find container ID
|
|
||||||
const status = reverseProxy.getStatus();
|
|
||||||
|
|
||||||
// Update platform service record
|
|
||||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
|
||||||
if (platformService) {
|
|
||||||
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
|
||||||
status: 'running',
|
|
||||||
containerId: 'onebox-caddy', // Service name for Swarm services
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.success('Caddy platform service started');
|
|
||||||
return 'onebox-caddy';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop Caddy container - NOT ALLOWED for core infrastructure
|
|
||||||
*/
|
|
||||||
async stopContainer(_containerId: string): Promise<void> {
|
|
||||||
throw new Error('Caddy is a core infrastructure service and cannot be stopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Caddy is healthy via the reverse proxy
|
|
||||||
*/
|
|
||||||
async healthCheck(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const reverseProxy = this.oneboxRef.reverseProxy;
|
|
||||||
const status = reverseProxy.getStatus();
|
|
||||||
return status.http.running;
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug(`Caddy health check failed: ${error}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caddy doesn't provision resources for user services
|
|
||||||
*/
|
|
||||||
async provisionResource(_userService: IService): Promise<IProvisionedResource> {
|
|
||||||
throw new Error('Caddy does not provision resources for user services');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Caddy doesn't deprovision resources
|
|
||||||
*/
|
|
||||||
async deprovisionResource(_resource: IPlatformResource, _credentials: Record<string, string>): Promise<void> {
|
|
||||||
throw new Error('Caddy does not manage resources for user services');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* SmartProxy Platform Service Provider
|
||||||
|
*
|
||||||
|
* SmartProxy is a core infrastructure service that provides reverse proxy functionality.
|
||||||
|
* Unlike other platform services:
|
||||||
|
* - It doesn't provision resources for user services
|
||||||
|
* - It's started automatically by Onebox and cannot be stopped by users
|
||||||
|
* - It delegates to the existing reverse proxy manager for actual operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BasePlatformServiceProvider } from './base.ts';
|
||||||
|
import type {
|
||||||
|
IService,
|
||||||
|
IPlatformResource,
|
||||||
|
IPlatformServiceConfig,
|
||||||
|
IProvisionedResource,
|
||||||
|
IEnvVarMapping,
|
||||||
|
TPlatformServiceType,
|
||||||
|
TPlatformResourceType,
|
||||||
|
} from '../../../types.ts';
|
||||||
|
import { logger } from '../../../logging.ts';
|
||||||
|
import type { Onebox } from '../../onebox.ts';
|
||||||
|
|
||||||
|
export class SmartProxyProvider extends BasePlatformServiceProvider {
|
||||||
|
readonly type: TPlatformServiceType = 'smartproxy';
|
||||||
|
readonly displayName = 'SmartProxy Reverse Proxy';
|
||||||
|
readonly resourceTypes: TPlatformResourceType[] = [];
|
||||||
|
readonly isCore = true;
|
||||||
|
|
||||||
|
constructor(oneboxRef: Onebox) {
|
||||||
|
super(oneboxRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultConfig(): IPlatformServiceConfig {
|
||||||
|
return {
|
||||||
|
image: 'code.foss.global/host.today/ht-docker-smartproxy:latest',
|
||||||
|
port: 80,
|
||||||
|
volumes: [],
|
||||||
|
environment: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnvVarMappings(): IEnvVarMapping[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deployContainer(): Promise<string> {
|
||||||
|
logger.info('Starting SmartProxy via reverse proxy manager...');
|
||||||
|
|
||||||
|
const reverseProxy = this.oneboxRef.reverseProxy;
|
||||||
|
await reverseProxy.startHttp();
|
||||||
|
|
||||||
|
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
||||||
|
if (platformService) {
|
||||||
|
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
||||||
|
status: 'running',
|
||||||
|
containerId: 'onebox-smartproxy',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success('SmartProxy platform service started');
|
||||||
|
return 'onebox-smartproxy';
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopContainer(_containerId: string): Promise<void> {
|
||||||
|
throw new Error('SmartProxy is a core infrastructure service and cannot be stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
async healthCheck(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const reverseProxy = this.oneboxRef.reverseProxy;
|
||||||
|
const status = reverseProxy.getStatus();
|
||||||
|
return status.http.running;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`SmartProxy health check failed: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async provisionResource(_userService: IService): Promise<IProvisionedResource> {
|
||||||
|
throw new Error('SmartProxy does not provision resources for user services');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deprovisionResource(_resource: IPlatformResource, _credentials: Record<string, string>): Promise<void> {
|
||||||
|
throw new Error('SmartProxy does not manage resources for user services');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Caddy Log Receiver for Onebox
|
* Proxy Log Receiver for Onebox
|
||||||
*
|
*
|
||||||
* TCP server that receives access logs from Caddy and broadcasts them to WebSocket clients.
|
* TCP server that receives reverse proxy access logs and broadcasts them to WebSocket clients.
|
||||||
* Supports per-client filtering by domain and adaptive sampling at high volume.
|
* Supports per-client filtering by domain and adaptive sampling at high volume.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ export interface ILogFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caddy access log entry structure (from Caddy JSON format)
|
* Reverse proxy access log entry structure.
|
||||||
*/
|
*/
|
||||||
export interface ICaddyAccessLog {
|
export interface IProxyAccessLog {
|
||||||
ts: number;
|
ts: number;
|
||||||
level?: string;
|
level?: string;
|
||||||
logger?: string;
|
logger?: string;
|
||||||
@@ -60,9 +60,9 @@ interface ILogClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CaddyLogReceiver - TCP server for Caddy access logs
|
* ProxyLogReceiver - TCP server for reverse proxy access logs
|
||||||
*/
|
*/
|
||||||
export class CaddyLogReceiver {
|
export class ProxyLogReceiver {
|
||||||
private server: Deno.TcpListener | null = null;
|
private server: Deno.TcpListener | null = null;
|
||||||
private clients: Map<string, ILogClient> = new Map();
|
private clients: Map<string, ILogClient> = new Map();
|
||||||
private port: number;
|
private port: number;
|
||||||
@@ -76,7 +76,7 @@ export class CaddyLogReceiver {
|
|||||||
private logCounter = 0;
|
private logCounter = 0;
|
||||||
|
|
||||||
// Ring buffer for recent logs (for late-joining clients)
|
// Ring buffer for recent logs (for late-joining clients)
|
||||||
private recentLogs: ICaddyAccessLog[] = [];
|
private recentLogs: IProxyAccessLog[] = [];
|
||||||
private maxRecentLogs = 100;
|
private maxRecentLogs = 100;
|
||||||
|
|
||||||
// Traffic stats aggregation (hourly rolling window)
|
// Traffic stats aggregation (hourly rolling window)
|
||||||
@@ -137,7 +137,7 @@ export class CaddyLogReceiver {
|
|||||||
/**
|
/**
|
||||||
* Record a request in traffic stats
|
* Record a request in traffic stats
|
||||||
*/
|
*/
|
||||||
private recordTrafficStats(log: ICaddyAccessLog): void {
|
private recordTrafficStats(log: IProxyAccessLog): void {
|
||||||
const bucket = this.getCurrentStatsBucket();
|
const bucket = this.getCurrentStatsBucket();
|
||||||
|
|
||||||
bucket.requestCount++;
|
bucket.requestCount++;
|
||||||
@@ -164,25 +164,25 @@ export class CaddyLogReceiver {
|
|||||||
*/
|
*/
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
logger.warn('CaddyLogReceiver is already running');
|
logger.warn('ProxyLogReceiver is already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.server = Deno.listen({ port: this.port, transport: 'tcp' });
|
this.server = Deno.listen({ port: this.port, transport: 'tcp' });
|
||||||
this.running = true;
|
this.running = true;
|
||||||
logger.success(`CaddyLogReceiver started on TCP port ${this.port}`);
|
logger.success(`ProxyLogReceiver started on TCP port ${this.port}`);
|
||||||
|
|
||||||
// Start accepting connections in background
|
// Start accepting connections in background
|
||||||
this.acceptConnections();
|
this.acceptConnections();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to start CaddyLogReceiver: ${getErrorMessage(error)}`);
|
logger.error(`Failed to start ProxyLogReceiver: ${getErrorMessage(error)}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept incoming TCP connections from Caddy
|
* Accept incoming TCP connections from the reverse proxy
|
||||||
*/
|
*/
|
||||||
private async acceptConnections(): Promise<void> {
|
private async acceptConnections(): Promise<void> {
|
||||||
if (!this.server) return;
|
if (!this.server) return;
|
||||||
@@ -194,17 +194,17 @@ export class CaddyLogReceiver {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
logger.error(`CaddyLogReceiver accept error: ${getErrorMessage(error)}`);
|
logger.error(`ProxyLogReceiver accept error: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a single TCP connection from Caddy
|
* Handle a single TCP connection from the reverse proxy
|
||||||
*/
|
*/
|
||||||
private async handleConnection(conn: Deno.TcpConn): Promise<void> {
|
private async handleConnection(conn: Deno.TcpConn): Promise<void> {
|
||||||
const remoteAddr = conn.remoteAddr as Deno.NetAddr;
|
const remoteAddr = conn.remoteAddr as Deno.NetAddr;
|
||||||
logger.debug(`CaddyLogReceiver: Connection from ${remoteAddr.hostname}:${remoteAddr.port}`);
|
logger.debug(`ProxyLogReceiver: Connection from ${remoteAddr.hostname}:${remoteAddr.port}`);
|
||||||
|
|
||||||
const reader = conn.readable.getReader();
|
const reader = conn.readable.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
@@ -217,7 +217,7 @@ export class CaddyLogReceiver {
|
|||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
// Process complete lines (Caddy sends newline-delimited JSON)
|
// Process complete newline-delimited JSON log lines.
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split('\n');
|
||||||
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export class CaddyLogReceiver {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
logger.debug(`CaddyLogReceiver connection closed: ${getErrorMessage(error)}`);
|
logger.debug(`ProxyLogReceiver connection closed: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.connections.delete(conn);
|
this.connections.delete(conn);
|
||||||
@@ -242,18 +242,18 @@ export class CaddyLogReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a single log line from Caddy
|
* Process a single access log line
|
||||||
*/
|
*/
|
||||||
private processLogLine(line: string): void {
|
private processLogLine(line: string): void {
|
||||||
try {
|
try {
|
||||||
const log = JSON.parse(line) as ICaddyAccessLog;
|
const log = JSON.parse(line) as IProxyAccessLog;
|
||||||
|
|
||||||
// Only process access logs (check for http.log.access or just access, or any log with request/status)
|
// Only process access logs (check for http.log.access or just access, or any log with request/status)
|
||||||
const isAccessLog = log.logger === 'http.log.access' ||
|
const isAccessLog = log.logger === 'http.log.access' ||
|
||||||
log.logger === 'access' ||
|
log.logger === 'access' ||
|
||||||
(log.request && typeof log.status === 'number');
|
(log.request && typeof log.status === 'number');
|
||||||
if (!isAccessLog) {
|
if (!isAccessLog) {
|
||||||
logger.debug(`CaddyLogReceiver: Skipping non-access log: ${log.logger || 'unknown'}`);
|
logger.debug(`ProxyLogReceiver: Skipping non-access log: ${log.logger || 'unknown'}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ export class CaddyLogReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`CaddyLogReceiver: Access log received - ${log.request?.method} ${log.request?.host}${log.request?.uri} (status: ${log.status})`);
|
logger.debug(`ProxyLogReceiver: Access log received - ${log.request?.method} ${log.request?.host}${log.request?.uri} (status: ${log.status})`);
|
||||||
|
|
||||||
// Add to recent logs buffer
|
// Add to recent logs buffer
|
||||||
this.recentLogs.push(log);
|
this.recentLogs.push(log);
|
||||||
@@ -277,10 +277,10 @@ export class CaddyLogReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast to WebSocket clients (log how many clients)
|
// Broadcast to WebSocket clients (log how many clients)
|
||||||
logger.debug(`CaddyLogReceiver: Broadcasting to ${this.clients.size} clients`);
|
logger.debug(`ProxyLogReceiver: Broadcasting to ${this.clients.size} clients`);
|
||||||
this.broadcast(log);
|
this.broadcast(log);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`Failed to parse Caddy log line: ${getErrorMessage(error)}`);
|
logger.debug(`Failed to parse proxy log line: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ export class CaddyLogReceiver {
|
|||||||
/**
|
/**
|
||||||
* Broadcast a log entry to all connected WebSocket clients
|
* Broadcast a log entry to all connected WebSocket clients
|
||||||
*/
|
*/
|
||||||
private broadcast(log: ICaddyAccessLog): void {
|
private broadcast(log: IProxyAccessLog): void {
|
||||||
const message = JSON.stringify({
|
const message = JSON.stringify({
|
||||||
type: 'access_log',
|
type: 'access_log',
|
||||||
data: {
|
data: {
|
||||||
@@ -365,7 +365,7 @@ export class CaddyLogReceiver {
|
|||||||
/**
|
/**
|
||||||
* Check if a log entry matches a client's filter
|
* Check if a log entry matches a client's filter
|
||||||
*/
|
*/
|
||||||
private matchesFilter(log: ICaddyAccessLog, filter: ILogFilter): boolean {
|
private matchesFilter(log: IProxyAccessLog, filter: ILogFilter): boolean {
|
||||||
// Domain filter
|
// Domain filter
|
||||||
if (filter.domain) {
|
if (filter.domain) {
|
||||||
const logHost = log.request.host.toLowerCase();
|
const logHost = log.request.host.toLowerCase();
|
||||||
@@ -385,7 +385,7 @@ export class CaddyLogReceiver {
|
|||||||
*/
|
*/
|
||||||
addClient(clientId: string, ws: WebSocket, filter: ILogFilter = {}): void {
|
addClient(clientId: string, ws: WebSocket, filter: ILogFilter = {}): void {
|
||||||
this.clients.set(clientId, { id: clientId, ws, filter });
|
this.clients.set(clientId, { id: clientId, ws, filter });
|
||||||
logger.debug(`CaddyLogReceiver: Added client ${clientId} (${this.clients.size} total)`);
|
logger.debug(`ProxyLogReceiver: Added client ${clientId} (${this.clients.size} total)`);
|
||||||
|
|
||||||
// Send recent logs to new client
|
// Send recent logs to new client
|
||||||
for (const log of this.recentLogs) {
|
for (const log of this.recentLogs) {
|
||||||
@@ -422,7 +422,7 @@ export class CaddyLogReceiver {
|
|||||||
*/
|
*/
|
||||||
removeClient(clientId: string): void {
|
removeClient(clientId: string): void {
|
||||||
if (this.clients.delete(clientId)) {
|
if (this.clients.delete(clientId)) {
|
||||||
logger.debug(`CaddyLogReceiver: Removed client ${clientId} (${this.clients.size} remaining)`);
|
logger.debug(`ProxyLogReceiver: Removed client ${clientId} (${this.clients.size} remaining)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ export class CaddyLogReceiver {
|
|||||||
const client = this.clients.get(clientId);
|
const client = this.clients.get(clientId);
|
||||||
if (client) {
|
if (client) {
|
||||||
client.filter = filter;
|
client.filter = filter;
|
||||||
logger.debug(`CaddyLogReceiver: Updated filter for client ${clientId}`);
|
logger.debug(`ProxyLogReceiver: Updated filter for client ${clientId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ export class CaddyLogReceiver {
|
|||||||
// Clear clients
|
// Clear clients
|
||||||
this.clients.clear();
|
this.clients.clear();
|
||||||
|
|
||||||
logger.info('CaddyLogReceiver stopped');
|
logger.info('ProxyLogReceiver stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+39
-42
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Reverse Proxy for Onebox
|
* Reverse Proxy for Onebox
|
||||||
*
|
*
|
||||||
* Delegates to Caddy (running as Docker service) for production-grade reverse proxy
|
* Delegates to SmartProxy (running as Docker service) for production-grade reverse proxy
|
||||||
* with native SNI support, HTTP/2, WebSocket proxying, and zero-downtime configuration updates.
|
* with TLS termination, WebSocket proxying, and zero-downtime configuration updates.
|
||||||
*
|
*
|
||||||
* Routes use Docker service names (e.g., onebox-hello-world:80) for container-to-container
|
* Routes use Docker service names (e.g., onebox-hello-world:80) for container-to-container
|
||||||
* communication within the Docker overlay network.
|
* communication within the Docker overlay network.
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import { logger } from '../logging.ts';
|
import { logger } from '../logging.ts';
|
||||||
import { getErrorMessage } from '../utils/error.ts';
|
import { getErrorMessage } from '../utils/error.ts';
|
||||||
import { OneboxDatabase } from './database.ts';
|
import { OneboxDatabase } from './database.ts';
|
||||||
import { CaddyManager } from './caddy.ts';
|
import { SmartProxyManager } from './smartproxy.ts';
|
||||||
|
|
||||||
interface IProxyRoute {
|
interface IProxyRoute {
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -24,7 +24,7 @@ interface IProxyRoute {
|
|||||||
export class OneboxReverseProxy {
|
export class OneboxReverseProxy {
|
||||||
private oneboxRef: any;
|
private oneboxRef: any;
|
||||||
private database: OneboxDatabase;
|
private database: OneboxDatabase;
|
||||||
private caddy: CaddyManager;
|
private smartProxy: SmartProxyManager;
|
||||||
private routes: Map<string, IProxyRoute> = new Map();
|
private routes: Map<string, IProxyRoute> = new Map();
|
||||||
private httpPort = 8080; // Default to dev ports (will be overridden if production)
|
private httpPort = 8080; // Default to dev ports (will be overridden if production)
|
||||||
private httpsPort = 8443;
|
private httpsPort = 8443;
|
||||||
@@ -32,33 +32,32 @@ export class OneboxReverseProxy {
|
|||||||
constructor(oneboxRef: any) {
|
constructor(oneboxRef: any) {
|
||||||
this.oneboxRef = oneboxRef;
|
this.oneboxRef = oneboxRef;
|
||||||
this.database = oneboxRef.database;
|
this.database = oneboxRef.database;
|
||||||
this.caddy = new CaddyManager({
|
this.smartProxy = new SmartProxyManager({
|
||||||
httpPort: this.httpPort,
|
httpPort: this.httpPort,
|
||||||
httpsPort: this.httpsPort,
|
httpsPort: this.httpsPort,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize reverse proxy - Caddy runs as Docker service, no setup needed
|
* Initialize reverse proxy - SmartProxy runs as Docker service, no setup needed
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
logger.info('Reverse proxy initialized (Caddy Docker service)');
|
logger.info('Reverse proxy initialized (SmartProxy Docker service)');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the HTTP/HTTPS reverse proxy server
|
* Start the HTTP/HTTPS reverse proxy server
|
||||||
* Caddy handles both HTTP and HTTPS on the configured ports
|
* SmartProxy handles both HTTP and HTTPS on the configured ports
|
||||||
*/
|
*/
|
||||||
async startHttp(port?: number): Promise<void> {
|
async startHttp(port?: number): Promise<void> {
|
||||||
if (port) {
|
if (port) {
|
||||||
this.httpPort = port;
|
this.httpPort = port;
|
||||||
this.caddy.setPorts(this.httpPort, this.httpsPort);
|
this.smartProxy.setPorts(this.httpPort, this.httpsPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Start Caddy (handles both HTTP and HTTPS)
|
await this.smartProxy.start();
|
||||||
await this.caddy.start();
|
logger.success(`Reverse proxy started on port ${this.httpPort} (SmartProxy Docker service)`);
|
||||||
logger.success(`Reverse proxy started on port ${this.httpPort} (Caddy Docker service)`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`);
|
logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -66,21 +65,19 @@ export class OneboxReverseProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start HTTPS - Caddy already handles HTTPS when started
|
* Start HTTPS - SmartProxy already handles HTTPS when started
|
||||||
* This method exists for interface compatibility
|
* This method exists for interface compatibility
|
||||||
*/
|
*/
|
||||||
async startHttps(port?: number): Promise<void> {
|
async startHttps(port?: number): Promise<void> {
|
||||||
if (port) {
|
if (port) {
|
||||||
this.httpsPort = port;
|
this.httpsPort = port;
|
||||||
this.caddy.setPorts(this.httpPort, this.httpsPort);
|
this.smartProxy.setPorts(this.httpPort, this.httpsPort);
|
||||||
}
|
}
|
||||||
// Caddy handles both HTTP and HTTPS together
|
const status = this.smartProxy.getStatus();
|
||||||
// If already running, just log and optionally reload with new port
|
|
||||||
const status = this.caddy.getStatus();
|
|
||||||
if (status.running) {
|
if (status.running) {
|
||||||
logger.info(`HTTPS already running on port ${this.httpsPort} via Caddy`);
|
logger.info(`HTTPS already running on port ${this.httpsPort} via SmartProxy`);
|
||||||
} else {
|
} else {
|
||||||
await this.caddy.start();
|
await this.smartProxy.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +85,13 @@ export class OneboxReverseProxy {
|
|||||||
* Stop the reverse proxy
|
* Stop the reverse proxy
|
||||||
*/
|
*/
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
await this.caddy.stop();
|
await this.smartProxy.stop();
|
||||||
logger.info('Reverse proxy stopped');
|
logger.info('Reverse proxy stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a route for a service
|
* Add a route for a service
|
||||||
* Uses Docker service name for upstream (Caddy runs in same Docker network)
|
* Uses Docker service name for upstream (SmartProxy runs in same Docker network)
|
||||||
*/
|
*/
|
||||||
async addRoute(serviceId: number, domain: string, targetPort: number): Promise<void> {
|
async addRoute(serviceId: number, domain: string, targetPort: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -105,7 +102,7 @@ export class OneboxReverseProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use Docker service name as upstream target
|
// Use Docker service name as upstream target
|
||||||
// Caddy runs on the same Docker network, so it can resolve service names directly
|
// SmartProxy runs on the same Docker network, so it can resolve service names directly
|
||||||
const serviceName = `onebox-${service.name}`;
|
const serviceName = `onebox-${service.name}`;
|
||||||
const targetHost = serviceName;
|
const targetHost = serviceName;
|
||||||
|
|
||||||
@@ -119,9 +116,9 @@ export class OneboxReverseProxy {
|
|||||||
|
|
||||||
this.routes.set(domain, route);
|
this.routes.set(domain, route);
|
||||||
|
|
||||||
// Add route to Caddy using Docker service name
|
// Add route to SmartProxy using Docker service name
|
||||||
const upstream = `${targetHost}:${targetPort}`;
|
const upstream = `${targetHost}:${targetPort}`;
|
||||||
await this.caddy.addRoute(domain, upstream);
|
await this.smartProxy.addRoute(domain, upstream);
|
||||||
|
|
||||||
logger.success(`Added proxy route: ${domain} -> ${upstream}`);
|
logger.success(`Added proxy route: ${domain} -> ${upstream}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -135,9 +132,9 @@ export class OneboxReverseProxy {
|
|||||||
*/
|
*/
|
||||||
removeRoute(domain: string): void {
|
removeRoute(domain: string): void {
|
||||||
if (this.routes.delete(domain)) {
|
if (this.routes.delete(domain)) {
|
||||||
// Remove from Caddy (async but we don't wait)
|
// Remove from SmartProxy (async but we don't wait)
|
||||||
this.caddy.removeRoute(domain).catch((error) => {
|
this.smartProxy.removeRoute(domain).catch((error) => {
|
||||||
logger.error(`Failed to remove Caddy route for ${domain}: ${getErrorMessage(error)}`);
|
logger.error(`Failed to remove SmartProxy route for ${domain}: ${getErrorMessage(error)}`);
|
||||||
});
|
});
|
||||||
logger.success(`Removed proxy route: ${domain}`);
|
logger.success(`Removed proxy route: ${domain}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -159,9 +156,9 @@ export class OneboxReverseProxy {
|
|||||||
try {
|
try {
|
||||||
logger.info('Reloading proxy routes...');
|
logger.info('Reloading proxy routes...');
|
||||||
|
|
||||||
// Clear local and Caddy routes
|
// Clear local and SmartProxy routes
|
||||||
this.routes.clear();
|
this.routes.clear();
|
||||||
this.caddy.clear();
|
this.smartProxy.clear();
|
||||||
|
|
||||||
const services = this.database.getAllServices();
|
const services = this.database.getAllServices();
|
||||||
|
|
||||||
@@ -181,7 +178,7 @@ export class OneboxReverseProxy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add TLS certificate for a domain
|
* Add TLS certificate for a domain
|
||||||
* Sends PEM content to Caddy via Admin API
|
* Sends PEM content to SmartProxy via Admin API
|
||||||
*/
|
*/
|
||||||
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
|
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
|
||||||
if (!certPem || !keyPem) {
|
if (!certPem || !keyPem) {
|
||||||
@@ -189,14 +186,14 @@ export class OneboxReverseProxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.caddy.addCertificate(domain, certPem, keyPem);
|
await this.smartProxy.addCertificate(domain, certPem, keyPem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove TLS certificate for a domain
|
* Remove TLS certificate for a domain
|
||||||
*/
|
*/
|
||||||
removeCertificate(domain: string): void {
|
removeCertificate(domain: string): void {
|
||||||
this.caddy.removeCertificate(domain).catch((error) => {
|
this.smartProxy.removeCertificate(domain).catch((error) => {
|
||||||
logger.error(`Failed to remove certificate for ${domain}: ${getErrorMessage(error)}`);
|
logger.error(`Failed to remove certificate for ${domain}: ${getErrorMessage(error)}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -213,13 +210,13 @@ export class OneboxReverseProxy {
|
|||||||
for (const cert of certificates) {
|
for (const cert of certificates) {
|
||||||
// Use fullchainPem for the cert (includes intermediates) and keyPem for the key
|
// Use fullchainPem for the cert (includes intermediates) and keyPem for the key
|
||||||
if (cert.domain && cert.fullchainPem && cert.keyPem) {
|
if (cert.domain && cert.fullchainPem && cert.keyPem) {
|
||||||
await this.caddy.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem);
|
await this.smartProxy.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Skipping certificate for ${cert.domain}: missing PEM content`);
|
logger.warn(`Skipping certificate for ${cert.domain}: missing PEM content`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`Loaded ${this.caddy.getCertificates().length} TLS certificates`);
|
logger.success(`Loaded ${this.smartProxy.getCertificates().length} TLS certificates`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to reload certificates: ${getErrorMessage(error)}`);
|
logger.error(`Failed to reload certificates: ${getErrorMessage(error)}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -230,19 +227,19 @@ export class OneboxReverseProxy {
|
|||||||
* Get status of reverse proxy
|
* Get status of reverse proxy
|
||||||
*/
|
*/
|
||||||
getStatus() {
|
getStatus() {
|
||||||
const caddyStatus = this.caddy.getStatus();
|
const smartProxyStatus = this.smartProxy.getStatus();
|
||||||
return {
|
return {
|
||||||
http: {
|
http: {
|
||||||
running: caddyStatus.running,
|
running: smartProxyStatus.running,
|
||||||
port: caddyStatus.httpPort,
|
port: smartProxyStatus.httpPort,
|
||||||
},
|
},
|
||||||
https: {
|
https: {
|
||||||
running: caddyStatus.running,
|
running: smartProxyStatus.running,
|
||||||
port: caddyStatus.httpsPort,
|
port: smartProxyStatus.httpsPort,
|
||||||
certificates: caddyStatus.certificates,
|
certificates: smartProxyStatus.certificates,
|
||||||
},
|
},
|
||||||
routes: caddyStatus.routes,
|
routes: smartProxyStatus.routes,
|
||||||
backend: 'caddy-docker',
|
backend: 'smartproxy-docker',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
/**
|
||||||
|
* SmartProxy Manager for Onebox
|
||||||
|
*
|
||||||
|
* Manages SmartProxy as a Docker Swarm service so it can route to services on
|
||||||
|
* the Onebox overlay network.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as plugins from '../plugins.ts';
|
||||||
|
import { logger } from '../logging.ts';
|
||||||
|
import { getErrorMessage } from '../utils/error.ts';
|
||||||
|
|
||||||
|
const SMARTPROXY_SERVICE_NAME = 'onebox-smartproxy';
|
||||||
|
const SMARTPROXY_IMAGE = 'code.foss.global/host.today/ht-docker-smartproxy:latest';
|
||||||
|
const SMARTPROXY_ADMIN_CONTAINER_PORT = 3000;
|
||||||
|
const SMARTPROXY_HTTP_CONTAINER_PORT = 80;
|
||||||
|
const SMARTPROXY_HTTPS_CONTAINER_PORT = 443;
|
||||||
|
|
||||||
|
export interface ISmartProxyRoute {
|
||||||
|
domain: string;
|
||||||
|
upstream: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISmartProxyCertificate {
|
||||||
|
domain: string;
|
||||||
|
certPem: string;
|
||||||
|
keyPem: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISmartProxyRouteConfig {
|
||||||
|
name: string;
|
||||||
|
match: {
|
||||||
|
ports: number;
|
||||||
|
domains: string;
|
||||||
|
protocol?: 'http' | 'tcp' | 'udp' | 'quic' | 'http3';
|
||||||
|
};
|
||||||
|
action: {
|
||||||
|
type: 'forward';
|
||||||
|
targets: Array<{ host: string; port: number }>;
|
||||||
|
tls?: {
|
||||||
|
mode: 'terminate';
|
||||||
|
certificate: {
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
websocket?: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmartProxyManager {
|
||||||
|
private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
|
||||||
|
private certsDir: string;
|
||||||
|
private adminUrl: string;
|
||||||
|
private adminPort: number;
|
||||||
|
private httpPort: number;
|
||||||
|
private httpsPort: number;
|
||||||
|
private routes: Map<string, ISmartProxyRoute> = new Map();
|
||||||
|
private certificates: Map<string, ISmartProxyCertificate> = new Map();
|
||||||
|
private networkName = 'onebox-network';
|
||||||
|
private serviceRunning = false;
|
||||||
|
|
||||||
|
constructor(options?: {
|
||||||
|
certsDir?: string;
|
||||||
|
adminPort?: number;
|
||||||
|
httpPort?: number;
|
||||||
|
httpsPort?: number;
|
||||||
|
}) {
|
||||||
|
this.certsDir = options?.certsDir || './.nogit/certs';
|
||||||
|
this.adminPort = options?.adminPort || 2019;
|
||||||
|
this.adminUrl = `http://localhost:${this.adminPort}`;
|
||||||
|
this.httpPort = options?.httpPort || 8080;
|
||||||
|
this.httpsPort = options?.httpsPort || 8443;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureDockerClient(): Promise<void> {
|
||||||
|
if (!this.dockerClient) {
|
||||||
|
this.dockerClient = new plugins.docker.Docker({
|
||||||
|
socketPath: 'unix:///var/run/docker.sock',
|
||||||
|
});
|
||||||
|
await this.dockerClient.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPorts(httpPort: number, httpsPort: number): void {
|
||||||
|
this.httpPort = httpPort;
|
||||||
|
this.httpsPort = httpsPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.serviceRunning) {
|
||||||
|
logger.warn('SmartProxy service is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureDockerClient();
|
||||||
|
await Deno.mkdir(this.certsDir, { recursive: true });
|
||||||
|
|
||||||
|
logger.info('Starting SmartProxy Docker service...');
|
||||||
|
|
||||||
|
const existingService = await this.getExistingService();
|
||||||
|
if (existingService) {
|
||||||
|
logger.info('SmartProxy service exists, removing old service...');
|
||||||
|
await this.removeService();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkId = await this.getNetworkId();
|
||||||
|
|
||||||
|
const response = await this.dockerClient!.request('POST', '/services/create', {
|
||||||
|
Name: SMARTPROXY_SERVICE_NAME,
|
||||||
|
Labels: {
|
||||||
|
'managed-by': 'onebox',
|
||||||
|
'onebox-type': 'smartproxy',
|
||||||
|
},
|
||||||
|
TaskTemplate: {
|
||||||
|
ContainerSpec: {
|
||||||
|
Image: SMARTPROXY_IMAGE,
|
||||||
|
Env: [
|
||||||
|
'SMARTPROXY_ADMIN_HOST=0.0.0.0',
|
||||||
|
`SMARTPROXY_ADMIN_PORT=${SMARTPROXY_ADMIN_CONTAINER_PORT}`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Networks: [
|
||||||
|
{
|
||||||
|
Target: networkId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
RestartPolicy: {
|
||||||
|
Condition: 'any',
|
||||||
|
MaxAttempts: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mode: {
|
||||||
|
Replicated: {
|
||||||
|
Replicas: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EndpointSpec: {
|
||||||
|
Ports: [
|
||||||
|
{
|
||||||
|
Protocol: 'tcp',
|
||||||
|
TargetPort: SMARTPROXY_HTTP_CONTAINER_PORT,
|
||||||
|
PublishedPort: this.httpPort,
|
||||||
|
PublishMode: 'host',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Protocol: 'tcp',
|
||||||
|
TargetPort: SMARTPROXY_HTTPS_CONTAINER_PORT,
|
||||||
|
PublishedPort: this.httpsPort,
|
||||||
|
PublishMode: 'host',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Protocol: 'tcp',
|
||||||
|
TargetPort: SMARTPROXY_ADMIN_CONTAINER_PORT,
|
||||||
|
PublishedPort: this.adminPort,
|
||||||
|
PublishMode: 'host',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode >= 300) {
|
||||||
|
throw new Error(`Failed to create SmartProxy service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`SmartProxy service created: ${response.body.ID}`);
|
||||||
|
|
||||||
|
await this.waitForReady();
|
||||||
|
this.serviceRunning = true;
|
||||||
|
await this.reloadConfig();
|
||||||
|
|
||||||
|
logger.success(`SmartProxy started (HTTP: ${this.httpPort}, HTTPS: ${this.httpsPort}, Admin: ${this.adminUrl})`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to start SmartProxy: ${getErrorMessage(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getExistingService(): Promise<any | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.dockerClient!.request('GET', `/services/${SMARTPROXY_SERVICE_NAME}`, {});
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeService(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.dockerClient!.request('DELETE', `/services/${SMARTPROXY_SERVICE_NAME}`, {});
|
||||||
|
} catch {
|
||||||
|
// Service may not exist.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getNetworkId(): Promise<string> {
|
||||||
|
const networks = await this.dockerClient!.listNetworks();
|
||||||
|
const network = networks.find((n: any) => n.Name === this.networkName);
|
||||||
|
if (!network) {
|
||||||
|
throw new Error(`Network not found: ${this.networkName}`);
|
||||||
|
}
|
||||||
|
return network.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForReady(maxAttempts = 120, intervalMs = 1000): Promise<void> {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.adminUrl}/ready`);
|
||||||
|
if (response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not ready yet.
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
|
}
|
||||||
|
throw new Error('SmartProxy service failed to start within timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.serviceRunning && !(await this.getExistingService())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureDockerClient();
|
||||||
|
|
||||||
|
logger.info('Stopping SmartProxy service...');
|
||||||
|
await this.removeService();
|
||||||
|
|
||||||
|
this.serviceRunning = false;
|
||||||
|
logger.info('SmartProxy service stopped');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to stop SmartProxy: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isHealthy(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.adminUrl}/health`);
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isRunning(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.ensureDockerClient();
|
||||||
|
const service = await this.getExistingService();
|
||||||
|
if (!service) return false;
|
||||||
|
|
||||||
|
const tasksResponse = await this.dockerClient!.request(
|
||||||
|
'GET',
|
||||||
|
`/tasks?filters=${encodeURIComponent(JSON.stringify({ service: [SMARTPROXY_SERVICE_NAME] }))}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tasksResponse.statusCode !== 200) return false;
|
||||||
|
|
||||||
|
const tasks = tasksResponse.body;
|
||||||
|
return tasks.some((task: any) => task.Status?.State === 'running');
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private routeName(prefixArg: string, domainArg: string): string {
|
||||||
|
return `${prefixArg}-${domainArg.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUpstream(upstreamArg: string): { host: string; port: number } {
|
||||||
|
const separatorIndex = upstreamArg.lastIndexOf(':');
|
||||||
|
if (separatorIndex <= 0 || separatorIndex === upstreamArg.length - 1) {
|
||||||
|
throw new Error(`Invalid upstream target: ${upstreamArg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = upstreamArg.slice(0, separatorIndex);
|
||||||
|
const port = Number(upstreamArg.slice(separatorIndex + 1));
|
||||||
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||||
|
throw new Error(`Invalid upstream port in target: ${upstreamArg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { host, port };
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildRoutes(): ISmartProxyRouteConfig[] {
|
||||||
|
const routeConfigs: ISmartProxyRouteConfig[] = [];
|
||||||
|
|
||||||
|
for (const [domain, route] of this.routes) {
|
||||||
|
const target = this.parseUpstream(route.upstream);
|
||||||
|
const baseAction = {
|
||||||
|
type: 'forward' as const,
|
||||||
|
targets: [target],
|
||||||
|
websocket: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
routeConfigs.push({
|
||||||
|
name: this.routeName('http', domain),
|
||||||
|
match: {
|
||||||
|
ports: SMARTPROXY_HTTP_CONTAINER_PORT,
|
||||||
|
domains: domain,
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: baseAction,
|
||||||
|
priority: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificate = this.certificates.get(domain);
|
||||||
|
if (certificate) {
|
||||||
|
routeConfigs.push({
|
||||||
|
name: this.routeName('https', domain),
|
||||||
|
match: {
|
||||||
|
ports: SMARTPROXY_HTTPS_CONTAINER_PORT,
|
||||||
|
domains: domain,
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
...baseAction,
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: {
|
||||||
|
key: certificate.keyPem,
|
||||||
|
cert: certificate.certPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
priority: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadConfig(): Promise<void> {
|
||||||
|
const isRunning = await this.isRunning();
|
||||||
|
if (!isRunning) {
|
||||||
|
logger.warn('SmartProxy not running, cannot reload config');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = this.buildRoutes();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.adminUrl}/routes`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ routes }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(`Failed to reload SmartProxy routes: ${response.status} ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('SmartProxy routes reloaded');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to reload SmartProxy routes: ${getErrorMessage(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRoute(domain: string, upstream: string): Promise<void> {
|
||||||
|
this.routes.set(domain, { domain, upstream });
|
||||||
|
|
||||||
|
if (await this.isRunning()) {
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`Added SmartProxy route: ${domain} -> ${upstream}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRoute(domain: string): Promise<void> {
|
||||||
|
if (this.routes.delete(domain)) {
|
||||||
|
if (await this.isRunning()) {
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
logger.success(`Removed SmartProxy route: ${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addCertificate(domain: string, certPem: string, keyPem: string): Promise<void> {
|
||||||
|
this.certificates.set(domain, {
|
||||||
|
domain,
|
||||||
|
certPem,
|
||||||
|
keyPem,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Deno.mkdir(this.certsDir, { recursive: true });
|
||||||
|
await Deno.writeTextFile(`${this.certsDir}/${domain}.crt`, certPem);
|
||||||
|
await Deno.writeTextFile(`${this.certsDir}/${domain}.key`, keyPem);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to write certificate backup for ${domain}: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.isRunning()) {
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`Added TLS certificate for ${domain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCertificate(domain: string): Promise<void> {
|
||||||
|
if (this.certificates.delete(domain)) {
|
||||||
|
try {
|
||||||
|
await Deno.remove(`${this.certsDir}/${domain}.crt`);
|
||||||
|
await Deno.remove(`${this.certsDir}/${domain}.key`);
|
||||||
|
} catch {
|
||||||
|
// Files may not exist.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.isRunning()) {
|
||||||
|
await this.reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.success(`Removed TLS certificate for ${domain}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoutes(): ISmartProxyRoute[] {
|
||||||
|
return Array.from(this.routes.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
getCertificates(): ISmartProxyCertificate[] {
|
||||||
|
return Array.from(this.certificates.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.routes.clear();
|
||||||
|
this.certificates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): {
|
||||||
|
running: boolean;
|
||||||
|
httpPort: number;
|
||||||
|
httpsPort: number;
|
||||||
|
routes: number;
|
||||||
|
certificates: number;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
running: this.serviceRunning,
|
||||||
|
httpPort: this.httpPort,
|
||||||
|
httpsPort: this.httpsPort,
|
||||||
|
routes: this.routes.size,
|
||||||
|
certificates: this.certificates.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { BaseMigration } from './base-migration.ts';
|
||||||
|
import type { TQueryFunction } from '../types.ts';
|
||||||
|
|
||||||
|
export class Migration015SmartProxyPlatformService extends BaseMigration {
|
||||||
|
readonly version = 15;
|
||||||
|
readonly description = 'Rename Caddy platform service to SmartProxy';
|
||||||
|
|
||||||
|
up(query: TQueryFunction): void {
|
||||||
|
query(
|
||||||
|
`UPDATE platform_services
|
||||||
|
SET name = 'onebox-smartproxy',
|
||||||
|
type = 'smartproxy',
|
||||||
|
container_id = CASE
|
||||||
|
WHEN container_id = 'onebox-caddy' THEN 'onebox-smartproxy'
|
||||||
|
ELSE container_id
|
||||||
|
END,
|
||||||
|
config = ?,
|
||||||
|
updated_at = ?
|
||||||
|
WHERE type = 'caddy'`,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
image: 'code.foss.global/host.today/ht-docker-smartproxy:latest',
|
||||||
|
port: 80,
|
||||||
|
volumes: [],
|
||||||
|
environment: {},
|
||||||
|
}),
|
||||||
|
Date.now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { Migration011ScopeColumns } from './migration-011-scope-columns.ts';
|
|||||||
import { Migration012GfsRetention } from './migration-012-gfs-retention.ts';
|
import { Migration012GfsRetention } from './migration-012-gfs-retention.ts';
|
||||||
import { Migration013AppTemplateVersion } from './migration-013-app-template-version.ts';
|
import { Migration013AppTemplateVersion } from './migration-013-app-template-version.ts';
|
||||||
import { Migration014ContainerArchive } from './migration-014-containerarchive.ts';
|
import { Migration014ContainerArchive } from './migration-014-containerarchive.ts';
|
||||||
|
import { Migration015SmartProxyPlatformService } from './migration-015-smartproxy-platform-service.ts';
|
||||||
import type { BaseMigration } from './base-migration.ts';
|
import type { BaseMigration } from './base-migration.ts';
|
||||||
|
|
||||||
export class MigrationRunner {
|
export class MigrationRunner {
|
||||||
@@ -46,6 +47,7 @@ export class MigrationRunner {
|
|||||||
new Migration012GfsRetention(),
|
new Migration012GfsRetention(),
|
||||||
new Migration013AppTemplateVersion(),
|
new Migration013AppTemplateVersion(),
|
||||||
new Migration014ContainerArchive(),
|
new Migration014ContainerArchive(),
|
||||||
|
new Migration015SmartProxyPlatformService(),
|
||||||
].sort((a, b) => a.version - b.version);
|
].sort((a, b) => a.version - b.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,20 +166,20 @@ export class LogsHandler {
|
|||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const clientId = crypto.randomUUID();
|
const clientId = crypto.randomUUID();
|
||||||
|
|
||||||
// Create a mock WebSocket-like object for the CaddyLogReceiver
|
// Create a mock WebSocket-like object for the proxy log receiver.
|
||||||
const mockSocket = {
|
const mockSocket = {
|
||||||
readyState: 1, // WebSocket.OPEN
|
readyState: 1, // WebSocket.OPEN
|
||||||
send: (data: string) => {
|
send: (data: string) => {
|
||||||
try {
|
try {
|
||||||
virtualStream.sendData(encoder.encode(data));
|
virtualStream.sendData(encoder.encode(data));
|
||||||
} catch {
|
} catch {
|
||||||
this.opsServerRef.oneboxRef.caddyLogReceiver.removeClient(clientId);
|
this.opsServerRef.oneboxRef.proxyLogReceiver.removeClient(clientId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = dataArg.filter || {};
|
const filter = dataArg.filter || {};
|
||||||
this.opsServerRef.oneboxRef.caddyLogReceiver.addClient(
|
this.opsServerRef.oneboxRef.proxyLogReceiver.addClient(
|
||||||
clientId,
|
clientId,
|
||||||
mockSocket as any,
|
mockSocket as any,
|
||||||
filter,
|
filter,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class NetworkHandler {
|
|||||||
redis: 6379,
|
redis: 6379,
|
||||||
postgresql: 5432,
|
postgresql: 5432,
|
||||||
rabbitmq: 5672,
|
rabbitmq: 5672,
|
||||||
caddy: 80,
|
smartproxy: 80,
|
||||||
clickhouse: 8123,
|
clickhouse: 8123,
|
||||||
mariadb: 3306,
|
mariadb: 3306,
|
||||||
};
|
};
|
||||||
@@ -85,7 +85,7 @@ export class NetworkHandler {
|
|||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||||
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
||||||
const logReceiverStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getStats();
|
const logReceiverStats = this.opsServerRef.oneboxRef.proxyLogReceiver.getStats();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stats: {
|
stats: {
|
||||||
@@ -115,7 +115,7 @@ export class NetworkHandler {
|
|||||||
'getTrafficStats',
|
'getTrafficStats',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||||
const trafficStats = this.opsServerRef.oneboxRef.caddyLogReceiver.getTrafficStats(60);
|
const trafficStats = this.opsServerRef.oneboxRef.proxyLogReceiver.getTrafficStats(60);
|
||||||
return { stats: trafficStats };
|
return { stats: trafficStats };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export class PlatformHandler {
|
|||||||
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
||||||
|
|
||||||
let status: string = service?.status || 'not-deployed';
|
let status: string = service?.status || 'not-deployed';
|
||||||
if (provider.type === 'caddy') {
|
if (provider.type === 'smartproxy') {
|
||||||
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
||||||
status = (proxyStatus.running ?? proxyStatus.http?.running) ? 'running' : 'stopped';
|
status = (proxyStatus.running ?? proxyStatus.http?.running) ? 'running' : 'stopped';
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ export class PlatformHandler {
|
|||||||
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
||||||
|
|
||||||
let rawStatus: string = service?.status || 'not-deployed';
|
let rawStatus: string = service?.status || 'not-deployed';
|
||||||
if (dataArg.serviceType === 'caddy') {
|
if (dataArg.serviceType === 'smartproxy') {
|
||||||
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
const proxyStatus = this.opsServerRef.oneboxRef.reverseProxy.getStatus() as any;
|
||||||
rawStatus = (proxyStatus.running ?? proxyStatus.http?.running) ? 'running' : 'stopped';
|
rawStatus = (proxyStatus.running ?? proxyStatus.http?.running) ? 'running' : 'stopped';
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ export interface ITokenCreatedResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Platform service types
|
// Platform service types
|
||||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'caddy' | 'clickhouse' | 'mariadb';
|
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'smartproxy' | 'clickhouse' | 'mariadb';
|
||||||
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||||
export type TPlatformServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
export type TPlatformServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -41,7 +41,7 @@ export interface ITrafficStats {
|
|||||||
errorRate: number;
|
errorRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICaddyAccessLog {
|
export interface IProxyAccessLog {
|
||||||
ts: number;
|
ts: number;
|
||||||
request: {
|
request: {
|
||||||
remote_ip: string;
|
remote_ip: string;
|
||||||
@@ -59,6 +59,6 @@ export interface INetworkLogMessage {
|
|||||||
type: 'connected' | 'access_log' | 'filter_updated';
|
type: 'connected' | 'access_log' | 'filter_updated';
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
filter?: { domain?: string; sampleRate?: number };
|
filter?: { domain?: string; sampleRate?: number };
|
||||||
data?: ICaddyAccessLog;
|
data?: IProxyAccessLog;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Platform service data shapes for Onebox
|
* Platform service data shapes for Onebox
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'caddy' | 'clickhouse' | 'mariadb';
|
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'smartproxy' | 'clickhouse' | 'mariadb';
|
||||||
export type TPlatformServiceStatus = 'not-deployed' | 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
export type TPlatformServiceStatus = 'not-deployed' | 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||||
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||||
|
|
||||||
|
|||||||
@@ -609,7 +609,7 @@ export class ObViewServices extends DeesElement {
|
|||||||
mongodb: { host: 'onebox-mongodb', port: 27017, version: '4.4', config: { engine: 'WiredTiger', authEnabled: true } },
|
mongodb: { host: 'onebox-mongodb', port: 27017, version: '4.4', config: { engine: 'WiredTiger', authEnabled: true } },
|
||||||
minio: { host: 'onebox-minio', port: 9000, version: 'latest', config: { consolePort: 9001, region: 'us-east-1' } },
|
minio: { host: 'onebox-minio', port: 9000, version: 'latest', config: { consolePort: 9001, region: 'us-east-1' } },
|
||||||
clickhouse: { host: 'onebox-clickhouse', port: 8123, version: 'latest', config: { nativePort: 9000, httpPort: 8123 } },
|
clickhouse: { host: 'onebox-clickhouse', port: 8123, version: 'latest', config: { nativePort: 9000, httpPort: 8123 } },
|
||||||
caddy: { host: 'onebox-caddy', port: 80, version: '2-alpine', config: { httpsPort: 443, adminApi: 2019 } },
|
smartproxy: { host: 'onebox-smartproxy', port: 80, version: 'latest', config: { httpsPort: 443, adminApi: 2019 } },
|
||||||
mariadb: { host: 'onebox-mariadb', port: 3306, version: '11', config: { engine: 'InnoDB', authEnabled: true } },
|
mariadb: { host: 'onebox-mariadb', port: 3306, version: '11', config: { engine: 'InnoDB', authEnabled: true } },
|
||||||
redis: { host: 'onebox-redis', port: 6379, version: '7-alpine', config: { appendonly: true, maxDatabases: 16 } },
|
redis: { host: 'onebox-redis', port: 6379, version: '7-alpine', config: { appendonly: true, maxDatabases: 16 } },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user