feat: add SmartProxy Docker image
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.nogit
|
||||
node_modules
|
||||
dist
|
||||
dist_*
|
||||
coverage
|
||||
.cache
|
||||
*.log
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Docker (non-tag pushes)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @git.zone/tsdocker@latest
|
||||
pnpm install
|
||||
|
||||
- name: Build daemon
|
||||
run: pnpm test
|
||||
|
||||
- name: Build image
|
||||
run: tsdocker build
|
||||
|
||||
- name: Test image
|
||||
run: tsdocker test
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Docker (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:szci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
||||
NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: code.foss.global/host.today/ht-docker-dbase:szci
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @git.zone/tsdocker@latest
|
||||
pnpm install
|
||||
|
||||
- name: Login to registries
|
||||
run: tsdocker login
|
||||
|
||||
- name: List images
|
||||
run: tsdocker list
|
||||
|
||||
- name: Build images
|
||||
run: tsdocker build
|
||||
|
||||
- name: Test images
|
||||
run: tsdocker test
|
||||
|
||||
- name: Push to code.foss.global
|
||||
run: tsdocker push code.foss.global
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
.nogit/
|
||||
*.log
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"@git.zone/tsdocker": {
|
||||
"registries": ["code.foss.global"],
|
||||
"registryRepoMap": {
|
||||
"code.foss.global": "host.today/ht-docker-smartproxy"
|
||||
},
|
||||
"platforms": ["linux/amd64", "linux/arm64"],
|
||||
"testDir": "./test"
|
||||
},
|
||||
"@git.zone/cli": {
|
||||
"projectType": "docker-image",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "host.today",
|
||||
"gitrepo": "ht-docker-smartproxy",
|
||||
"shortDescription": "Docker image for the Rust-backed SmartProxy daemon",
|
||||
"npmPackagename": "@host.today/ht-docker-smartproxy",
|
||||
"license": "MIT",
|
||||
"description": "A multi-architecture Docker image that runs @push.rocks/smartproxy as a managed daemon with an HTTP admin API.",
|
||||
"keywords": [
|
||||
"Docker",
|
||||
"SmartProxy",
|
||||
"reverse proxy",
|
||||
"TLS",
|
||||
"Rust",
|
||||
"Node.js",
|
||||
"multi-arch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
FROM code.foss.global/host.today/ht-docker-node:lts AS build
|
||||
LABEL author="Task Venture Capital GmbH <hello@task.vc>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY ts ./ts
|
||||
COPY tsconfig.json ./
|
||||
RUN pnpm build:daemon && pnpm prune --prod
|
||||
|
||||
FROM code.foss.global/host.today/ht-docker-node:lts
|
||||
LABEL author="Task Venture Capital GmbH <hello@task.vc>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV SMARTPROXY_CONFIG=/etc/smartproxy/config.json
|
||||
ENV SMARTPROXY_ADMIN_HOST=0.0.0.0
|
||||
ENV SMARTPROXY_ADMIN_PORT=3000
|
||||
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/dist_ts ./dist_ts
|
||||
|
||||
EXPOSE 80 443 3000
|
||||
CMD ["node", "dist_ts/daemon.js"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "ht-docker-smartproxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Multi-architecture Docker image for the Rust-backed SmartProxy daemon.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsdocker build",
|
||||
"build:daemon": "tsbuild",
|
||||
"test": "pnpm build:daemon && node dist_ts/daemon.js --check",
|
||||
"release:docker": "tsdocker push --verbose"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "ssh://code.foss.global:29419/host.today/ht-docker-smartproxy.git"
|
||||
},
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"Docker",
|
||||
"SmartProxy",
|
||||
"reverse proxy",
|
||||
"TLS",
|
||||
"Rust",
|
||||
"Node.js",
|
||||
"multi-arch"
|
||||
],
|
||||
"dependencies": {
|
||||
"@push.rocks/smartproxy": "^27.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsdocker": "^2.1.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
}
|
||||
Generated
+4768
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
# ht-docker-smartproxy
|
||||
|
||||
Multi-architecture Docker image for running `@push.rocks/smartproxy` as a daemon.
|
||||
|
||||
The image wraps SmartProxy with a small Node.js admin API so orchestrators such as Onebox can update routes without embedding the Node/Rust SmartProxy runtime into their own process.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
`tsdocker` builds `linux/amd64` and `linux/arm64` according to `.smartconfig.json`.
|
||||
|
||||
## Release
|
||||
|
||||
```bash
|
||||
pnpm release:docker
|
||||
```
|
||||
|
||||
The image is pushed as `code.foss.global/host.today/ht-docker-smartproxy`.
|
||||
|
||||
## Runtime
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 3000:3000 \
|
||||
-v ./config.json:/etc/smartproxy/config.json:ro \
|
||||
code.foss.global/host.today/ht-docker-smartproxy:latest
|
||||
```
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `SMARTPROXY_CONFIG`: config path, default `/etc/smartproxy/config.json`.
|
||||
- `SMARTPROXY_ADMIN_HOST`: admin bind host, default `0.0.0.0`.
|
||||
- `SMARTPROXY_ADMIN_PORT`: admin bind port, default `3000`.
|
||||
- `SMARTPROXY_ADMIN_TOKEN`: optional bearer token for admin endpoints.
|
||||
|
||||
## Admin API
|
||||
|
||||
- `GET /health`: health status.
|
||||
- `GET /routes`: current raw routes.
|
||||
- `PUT /routes`: replace routes with either an array or `{ "routes": [...] }`.
|
||||
- `POST /reload`: reload config from `SMARTPROXY_CONFIG` and restart SmartProxy.
|
||||
- `POST /security-policy`: update global security policy.
|
||||
- `GET /statistics`: SmartProxy runtime statistics.
|
||||
- `GET /listening-ports`: currently listening proxy ports.
|
||||
|
||||
## Config
|
||||
|
||||
The config is regular `ISmartProxyOptions` JSON with one daemon extension: `httpToHttpsRedirect`.
|
||||
|
||||
```json
|
||||
{
|
||||
"httpToHttpsRedirect": {
|
||||
"enabled": true,
|
||||
"httpPort": 80,
|
||||
"httpsPort": 443,
|
||||
"statusCode": 301
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"name": "app-example-com",
|
||||
"match": {
|
||||
"ports": 443,
|
||||
"domains": "app.example.com",
|
||||
"protocol": "http"
|
||||
},
|
||||
"action": {
|
||||
"type": "forward",
|
||||
"targets": [{ "host": "app", "port": 3000 }],
|
||||
"tls": {
|
||||
"mode": "terminate",
|
||||
"certificate": {
|
||||
"key": "-----BEGIN PRIVATE KEY-----\\n...",
|
||||
"cert": "-----BEGIN CERTIFICATE-----\\n..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Testing ht-docker-smartproxy image..."
|
||||
|
||||
node --version
|
||||
node -e "const mod = await import('@push.rocks/smartproxy'); if (!mod.SmartProxy) process.exit(1); console.log('SmartProxy import ok');"
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) rust_suffix="linux_amd64" ;;
|
||||
aarch64|arm64) rust_suffix="linux_arm64" ;;
|
||||
*) echo "Unsupported test architecture: $(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
test -x "/app/node_modules/@push.rocks/smartproxy/dist_rust/rustproxy_${rust_suffix}"
|
||||
node /app/dist_ts/daemon.js --check
|
||||
|
||||
echo "ht-docker-smartproxy tests passed!"
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import {
|
||||
SmartProxy,
|
||||
SocketHandlers,
|
||||
type IRouteConfig,
|
||||
type ISmartProxyOptions,
|
||||
type ISmartProxySecurityPolicy,
|
||||
} from '@push.rocks/smartproxy';
|
||||
|
||||
interface IHttpToHttpsRedirectConfig {
|
||||
enabled?: boolean;
|
||||
httpPort?: number;
|
||||
httpsPort?: number;
|
||||
statusCode?: number;
|
||||
targetTemplate?: string;
|
||||
}
|
||||
|
||||
interface ISmartProxyDaemonConfig extends Omit<ISmartProxyOptions, 'routes'> {
|
||||
routes?: IRouteConfig[];
|
||||
httpToHttpsRedirect?: IHttpToHttpsRedirectConfig;
|
||||
}
|
||||
|
||||
const getEnvNumber = (envNameArg: string, defaultArg: number) => {
|
||||
const value = process.env[envNameArg];
|
||||
if (!value) {
|
||||
return defaultArg;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
||||
throw new Error(`${envNameArg} must be a TCP port number`);
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const getConfigPath = () => process.env.SMARTPROXY_CONFIG || '/etc/smartproxy/config.json';
|
||||
|
||||
const readJsonBody = async (reqArg: IncomingMessage) => {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of reqArg) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
|
||||
const body = Buffer.concat(chunks).toString('utf8').trim();
|
||||
if (!body) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(body) as unknown;
|
||||
};
|
||||
|
||||
const sendJson = (resArg: ServerResponse, statusCodeArg: number, dataArg: unknown) => {
|
||||
resArg.writeHead(statusCodeArg, {
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
});
|
||||
resArg.end(JSON.stringify(dataArg));
|
||||
};
|
||||
|
||||
const isRouteArray = (valueArg: unknown): valueArg is IRouteConfig[] => {
|
||||
return Array.isArray(valueArg);
|
||||
};
|
||||
|
||||
const getRoutesFromPayload = (payloadArg: unknown): IRouteConfig[] => {
|
||||
if (isRouteArray(payloadArg)) {
|
||||
return payloadArg;
|
||||
}
|
||||
|
||||
if (
|
||||
payloadArg &&
|
||||
typeof payloadArg === 'object' &&
|
||||
'routes' in payloadArg &&
|
||||
isRouteArray((payloadArg as { routes: unknown }).routes)
|
||||
) {
|
||||
return (payloadArg as { routes: IRouteConfig[] }).routes;
|
||||
}
|
||||
|
||||
throw new Error('Expected request body to be a route array or an object with a routes array');
|
||||
};
|
||||
|
||||
const getSecurityPolicyFromPayload = (payloadArg: unknown): ISmartProxySecurityPolicy => {
|
||||
if (!payloadArg || typeof payloadArg !== 'object' || Array.isArray(payloadArg)) {
|
||||
throw new Error('Expected request body to be a SmartProxy security policy object');
|
||||
}
|
||||
return payloadArg as ISmartProxySecurityPolicy;
|
||||
};
|
||||
|
||||
class SmartProxyDaemon {
|
||||
private smartProxy: SmartProxy | undefined;
|
||||
private adminServer: Server | undefined;
|
||||
private rawConfig: ISmartProxyDaemonConfig = { routes: [] };
|
||||
private rawRoutes: IRouteConfig[] = [];
|
||||
private activeRoutes: IRouteConfig[] = [];
|
||||
private readonly configPath = getConfigPath();
|
||||
private readonly adminHost = process.env.SMARTPROXY_ADMIN_HOST || '0.0.0.0';
|
||||
private readonly adminPort = getEnvNumber('SMARTPROXY_ADMIN_PORT', 3000);
|
||||
private readonly adminToken = process.env.SMARTPROXY_ADMIN_TOKEN;
|
||||
|
||||
public async check() {
|
||||
this.rawConfig = await this.loadConfig();
|
||||
this.rawRoutes = this.rawConfig.routes || [];
|
||||
this.activeRoutes = this.createActiveRoutes();
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
configPath: this.configPath,
|
||||
configuredRoutes: this.rawRoutes.length,
|
||||
activeRoutes: this.activeRoutes.length,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.rawConfig = await this.loadConfig();
|
||||
this.rawRoutes = this.rawConfig.routes || [];
|
||||
await this.startSmartProxy();
|
||||
await this.startAdminServer();
|
||||
|
||||
console.log(
|
||||
`SmartProxy daemon started: admin=${this.adminHost}:${this.adminPort}, routes=${this.activeRoutes.length}`
|
||||
);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.adminServer) {
|
||||
await new Promise<void>((resolveArg, rejectArg) => {
|
||||
this.adminServer!.close((errorArg) => {
|
||||
if (errorArg) {
|
||||
rejectArg(errorArg);
|
||||
} else {
|
||||
resolveArg();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.adminServer = undefined;
|
||||
}
|
||||
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.stop();
|
||||
this.smartProxy = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadConfig(): Promise<ISmartProxyDaemonConfig> {
|
||||
if (!existsSync(this.configPath)) {
|
||||
return { routes: [] };
|
||||
}
|
||||
|
||||
const configFile = await readFile(this.configPath, 'utf8');
|
||||
const parsedConfig = JSON.parse(configFile) as ISmartProxyDaemonConfig;
|
||||
return {
|
||||
...parsedConfig,
|
||||
routes: parsedConfig.routes || [],
|
||||
};
|
||||
}
|
||||
|
||||
private createActiveRoutes() {
|
||||
const routes = [...this.rawRoutes];
|
||||
const redirectConfig = this.rawConfig.httpToHttpsRedirect;
|
||||
|
||||
if (redirectConfig?.enabled) {
|
||||
const httpPort = redirectConfig.httpPort || 80;
|
||||
const httpsPort = redirectConfig.httpsPort || 443;
|
||||
const statusCode = redirectConfig.statusCode || 301;
|
||||
const targetTemplate =
|
||||
redirectConfig.targetTemplate || `https://{domain}${httpsPort === 443 ? '' : `:${httpsPort}`}{path}`;
|
||||
|
||||
routes.unshift({
|
||||
name: 'http-to-https-redirect',
|
||||
match: {
|
||||
ports: httpPort,
|
||||
protocol: 'http',
|
||||
},
|
||||
action: {
|
||||
type: 'socket-handler',
|
||||
socketHandler: SocketHandlers.httpRedirect(targetTemplate, statusCode),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private createSmartProxyOptions(): ISmartProxyOptions {
|
||||
const { httpToHttpsRedirect: _httpToHttpsRedirect, routes: _routes, ...smartProxyOptions } = this.rawConfig;
|
||||
this.activeRoutes = this.createActiveRoutes();
|
||||
return {
|
||||
...smartProxyOptions,
|
||||
routes: this.activeRoutes,
|
||||
};
|
||||
}
|
||||
|
||||
private async startSmartProxy() {
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.stop();
|
||||
}
|
||||
|
||||
this.smartProxy = new SmartProxy(this.createSmartProxyOptions());
|
||||
await this.smartProxy.start();
|
||||
}
|
||||
|
||||
private async updateRoutes(routesArg: IRouteConfig[]) {
|
||||
this.rawRoutes = routesArg;
|
||||
this.rawConfig.routes = routesArg;
|
||||
this.activeRoutes = this.createActiveRoutes();
|
||||
|
||||
if (!this.smartProxy) {
|
||||
throw new Error('SmartProxy is not running');
|
||||
}
|
||||
await this.smartProxy.updateRoutes(this.activeRoutes);
|
||||
}
|
||||
|
||||
private async reloadFromDisk() {
|
||||
this.rawConfig = await this.loadConfig();
|
||||
this.rawRoutes = this.rawConfig.routes || [];
|
||||
await this.startSmartProxy();
|
||||
}
|
||||
|
||||
private isAuthorized(reqArg: IncomingMessage) {
|
||||
if (!this.adminToken) {
|
||||
return true;
|
||||
}
|
||||
return reqArg.headers.authorization === `Bearer ${this.adminToken}`;
|
||||
}
|
||||
|
||||
private async startAdminServer() {
|
||||
this.adminServer = createServer(async (reqArg, resArg) => {
|
||||
const requestUrl = new URL(reqArg.url || '/', 'http://localhost');
|
||||
const method = reqArg.method || 'GET';
|
||||
|
||||
try {
|
||||
if (method === 'GET' && (requestUrl.pathname === '/health' || requestUrl.pathname === '/ready')) {
|
||||
sendJson(resArg, 200, {
|
||||
ok: true,
|
||||
routes: this.rawRoutes.length,
|
||||
activeRoutes: this.activeRoutes.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isAuthorized(reqArg)) {
|
||||
sendJson(resArg, 401, { ok: false, error: 'unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && requestUrl.pathname === '/routes') {
|
||||
sendJson(resArg, 200, {
|
||||
routes: this.rawRoutes,
|
||||
activeRoutes: this.activeRoutes,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((method === 'PUT' || method === 'POST') && requestUrl.pathname === '/routes') {
|
||||
const payload = await readJsonBody(reqArg);
|
||||
await this.updateRoutes(getRoutesFromPayload(payload));
|
||||
sendJson(resArg, 200, {
|
||||
ok: true,
|
||||
routes: this.rawRoutes.length,
|
||||
activeRoutes: this.activeRoutes.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'POST' && requestUrl.pathname === '/reload') {
|
||||
await this.reloadFromDisk();
|
||||
sendJson(resArg, 200, {
|
||||
ok: true,
|
||||
routes: this.rawRoutes.length,
|
||||
activeRoutes: this.activeRoutes.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'POST' && requestUrl.pathname === '/security-policy') {
|
||||
if (!this.smartProxy) {
|
||||
throw new Error('SmartProxy is not running');
|
||||
}
|
||||
const payload = await readJsonBody(reqArg);
|
||||
const policy = getSecurityPolicyFromPayload(payload);
|
||||
await this.smartProxy.updateSecurityPolicy(policy);
|
||||
this.rawConfig.securityPolicy = policy;
|
||||
sendJson(resArg, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && requestUrl.pathname === '/statistics') {
|
||||
if (!this.smartProxy) {
|
||||
throw new Error('SmartProxy is not running');
|
||||
}
|
||||
sendJson(resArg, 200, await this.smartProxy.getStatistics());
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'GET' && requestUrl.pathname === '/listening-ports') {
|
||||
if (!this.smartProxy) {
|
||||
throw new Error('SmartProxy is not running');
|
||||
}
|
||||
sendJson(resArg, 200, { ports: await this.smartProxy.getListeningPorts() });
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(resArg, 404, { ok: false, error: 'not found' });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
sendJson(resArg, 500, { ok: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolveArg) => {
|
||||
this.adminServer!.listen(this.adminPort, this.adminHost, resolveArg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const daemon = new SmartProxyDaemon();
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
await daemon.check();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const stopAndExit = async (signalArg: NodeJS.Signals) => {
|
||||
console.log(`Received ${signalArg}, stopping SmartProxy daemon...`);
|
||||
await daemon.stop();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
void stopAndExit('SIGTERM');
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
void stopAndExit('SIGINT');
|
||||
});
|
||||
|
||||
await daemon.start();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": false,
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"outDir": "dist_ts"
|
||||
},
|
||||
"include": ["ts/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user