@push.rocks/smartproxy 21.1.7 → 22.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +109 -0
- package/dist_rust/rustproxy +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/utils/shared-security-manager.d.ts +17 -0
- package/dist_ts/core/utils/shared-security-manager.js +66 -1
- package/dist_ts/index.d.ts +1 -5
- package/dist_ts/index.js +3 -9
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/http-proxy/default-certificates.d.ts +54 -0
- package/dist_ts/proxies/http-proxy/default-certificates.js +127 -0
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +9 -14
- package/dist_ts/proxies/http-proxy/index.d.ts +5 -1
- package/dist_ts/proxies/http-proxy/index.js +6 -2
- package/dist_ts/proxies/http-proxy/security-manager.d.ts +4 -12
- package/dist_ts/proxies/http-proxy/security-manager.js +66 -99
- package/dist_ts/proxies/index.d.ts +1 -5
- package/dist_ts/proxies/index.js +2 -6
- package/dist_ts/proxies/nftables-proxy/index.d.ts +1 -0
- package/dist_ts/proxies/nftables-proxy/index.js +2 -1
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +4 -26
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +84 -236
- package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +9 -0
- package/dist_ts/proxies/nftables-proxy/utils/index.js +12 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +66 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +131 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +39 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +112 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +59 -0
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +130 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +4 -3
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +13 -2
- package/dist_ts/proxies/smart-proxy/connection-manager.js +16 -6
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +35 -10
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -3
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +72 -9
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/security-manager.d.ts +14 -12
- package/dist_ts/proxies/smart-proxy/security-manager.js +80 -74
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -622
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +2 -9
- package/dist_ts/proxies/smart-proxy/tls-manager.js +3 -26
- package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/utils/index.js +3 -4
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.js +108 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.d.ts +57 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.js +89 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.js +32 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.d.ts +68 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.js +117 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.d.ts +17 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.js +27 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.d.ts +63 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.js +105 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.d.ts +83 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.js +126 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.d.ts +47 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.js +66 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.d.ts +70 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.js +287 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.d.ts +46 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.js +67 -0
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +4 -457
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +6 -950
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +2 -2
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +67 -1
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +251 -3
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/npmextra.json +12 -6
- package/package.json +34 -24
- package/readme.hints.md +184 -1
- package/readme.md +580 -266
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/utils/shared-security-manager.ts +98 -13
- package/ts/index.ts +4 -12
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -9
- package/ts/proxies/nftables-proxy/index.ts +1 -0
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +116 -290
- package/ts/proxies/nftables-proxy/utils/index.ts +38 -0
- package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +162 -0
- package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +125 -0
- package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +156 -0
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -5
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -800
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/proxies/smart-proxy/utils/index.ts +3 -5
- package/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +144 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +124 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +40 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +163 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/index.ts +62 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +154 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +202 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +96 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +337 -0
- package/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +98 -0
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +5 -1302
- package/ts/proxies/smart-proxy/utils/route-utils.ts +1 -1
- package/ts/proxies/smart-proxy/utils/route-validator.ts +274 -4
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/ts/proxies/http-proxy/certificate-manager.ts +0 -244
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -675
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -13
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -433
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -894
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -796
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -187
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1640
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -257
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- package/ts/proxies/smart-proxy/tls-manager.ts +0 -207
- package/ts/proxies/smart-proxy/utils/route-validators.ts +0 -283
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFTables Proxy Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module exports utility functions and classes for NFTables operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Command execution
|
|
8
|
+
export { NftCommandExecutor } from './nft-command-executor.js';
|
|
9
|
+
export type { INftLoggerFn, INftExecutorOptions } from './nft-command-executor.js';
|
|
10
|
+
|
|
11
|
+
// Port specification normalization
|
|
12
|
+
export {
|
|
13
|
+
normalizePortSpec,
|
|
14
|
+
validatePorts,
|
|
15
|
+
formatPortRange,
|
|
16
|
+
portSpecToNftExpr,
|
|
17
|
+
rangesOverlap,
|
|
18
|
+
mergeOverlappingRanges,
|
|
19
|
+
countPorts,
|
|
20
|
+
isPortInSpec
|
|
21
|
+
} from './nft-port-spec-normalizer.js';
|
|
22
|
+
|
|
23
|
+
// Rule validation
|
|
24
|
+
export {
|
|
25
|
+
isValidIP,
|
|
26
|
+
isValidIPv4,
|
|
27
|
+
isValidIPv6,
|
|
28
|
+
isValidHostname,
|
|
29
|
+
isValidTableName,
|
|
30
|
+
isValidRate,
|
|
31
|
+
validateIPs,
|
|
32
|
+
validateHost,
|
|
33
|
+
validateTableName,
|
|
34
|
+
validateQosSettings,
|
|
35
|
+
validateSettings,
|
|
36
|
+
isIPForFamily,
|
|
37
|
+
filterIPsByFamily
|
|
38
|
+
} from './nft-rule-validator.js';
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFTables Command Executor
|
|
3
|
+
*
|
|
4
|
+
* Handles command execution with retry logic, temp file management,
|
|
5
|
+
* and error handling for nftables operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { exec, execSync } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { delay } from '../../../core/utils/async-utils.js';
|
|
11
|
+
import { AsyncFileSystem } from '../../../core/utils/fs-utils.js';
|
|
12
|
+
import { NftExecutionError } from '../models/index.js';
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
export interface INftLoggerFn {
|
|
17
|
+
(level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: Record<string, any>): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface INftExecutorOptions {
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
retryDelayMs?: number;
|
|
23
|
+
tempFilePath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* NFTables command executor with retry logic and temp file support
|
|
28
|
+
*/
|
|
29
|
+
export class NftCommandExecutor {
|
|
30
|
+
private static readonly NFT_CMD = 'nft';
|
|
31
|
+
private maxRetries: number;
|
|
32
|
+
private retryDelayMs: number;
|
|
33
|
+
private tempFilePath: string;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private log: INftLoggerFn,
|
|
37
|
+
options: INftExecutorOptions = {}
|
|
38
|
+
) {
|
|
39
|
+
this.maxRetries = options.maxRetries || 3;
|
|
40
|
+
this.retryDelayMs = options.retryDelayMs || 1000;
|
|
41
|
+
this.tempFilePath = options.tempFilePath || `/tmp/nft-rules-${Date.now()}.nft`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Execute a command with retry capability
|
|
46
|
+
*/
|
|
47
|
+
async executeWithRetry(command: string, maxRetries?: number, retryDelayMs?: number): Promise<string> {
|
|
48
|
+
const retries = maxRetries ?? this.maxRetries;
|
|
49
|
+
const delayMs = retryDelayMs ?? this.retryDelayMs;
|
|
50
|
+
let lastError: Error | undefined;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < retries; i++) {
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execAsync(command);
|
|
55
|
+
return stdout;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
lastError = err as Error;
|
|
58
|
+
this.log('warn', `Command failed (attempt ${i+1}/${retries}): ${command}`, { error: lastError.message });
|
|
59
|
+
|
|
60
|
+
// Wait before retry, unless it's the last attempt
|
|
61
|
+
if (i < retries - 1) {
|
|
62
|
+
await delay(delayMs);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new NftExecutionError(`Failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute system command synchronously (single attempt, no retry)
|
|
72
|
+
* Used only for exit handlers where the process is terminating anyway.
|
|
73
|
+
*/
|
|
74
|
+
executeSync(command: string): string {
|
|
75
|
+
try {
|
|
76
|
+
return execSync(command, { timeout: 5000 }).toString();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
this.log('warn', `Sync command failed: ${command}`, { error: (err as Error).message });
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Execute nftables commands with a temporary file
|
|
85
|
+
*/
|
|
86
|
+
async executeWithTempFile(rulesetContent: string): Promise<void> {
|
|
87
|
+
await AsyncFileSystem.writeFile(this.tempFilePath, rulesetContent);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await this.executeWithRetry(
|
|
91
|
+
`${NftCommandExecutor.NFT_CMD} -f ${this.tempFilePath}`,
|
|
92
|
+
this.maxRetries,
|
|
93
|
+
this.retryDelayMs
|
|
94
|
+
);
|
|
95
|
+
} finally {
|
|
96
|
+
// Always clean up the temp file
|
|
97
|
+
await AsyncFileSystem.remove(this.tempFilePath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if nftables is available
|
|
103
|
+
*/
|
|
104
|
+
async checkAvailability(): Promise<boolean> {
|
|
105
|
+
try {
|
|
106
|
+
await this.executeWithRetry(`${NftCommandExecutor.NFT_CMD} --version`, this.maxRetries, this.retryDelayMs);
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.log('error', `nftables is not available: ${(err as Error).message}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if connection tracking modules are loaded
|
|
116
|
+
*/
|
|
117
|
+
async checkConntrackModules(): Promise<boolean> {
|
|
118
|
+
try {
|
|
119
|
+
await this.executeWithRetry('lsmod | grep nf_conntrack', this.maxRetries, this.retryDelayMs);
|
|
120
|
+
return true;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
this.log('warn', 'Connection tracking modules might not be loaded, advanced NAT features may not work');
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run an nft command directly
|
|
129
|
+
*/
|
|
130
|
+
async nft(args: string): Promise<string> {
|
|
131
|
+
return this.executeWithRetry(`${NftCommandExecutor.NFT_CMD} ${args}`, this.maxRetries, this.retryDelayMs);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Run an nft command synchronously (for cleanup on exit)
|
|
136
|
+
*/
|
|
137
|
+
nftSync(args: string): string {
|
|
138
|
+
return this.executeSync(`${NftCommandExecutor.NFT_CMD} ${args}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the NFT command path
|
|
143
|
+
*/
|
|
144
|
+
static get nftCmd(): string {
|
|
145
|
+
return NftCommandExecutor.NFT_CMD;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update the temp file path
|
|
150
|
+
*/
|
|
151
|
+
setTempFilePath(path: string): void {
|
|
152
|
+
this.tempFilePath = path;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Update retry settings
|
|
157
|
+
*/
|
|
158
|
+
setRetryOptions(maxRetries: number, retryDelayMs: number): void {
|
|
159
|
+
this.maxRetries = maxRetries;
|
|
160
|
+
this.retryDelayMs = retryDelayMs;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFTables Port Specification Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Handles normalization and validation of port specifications
|
|
5
|
+
* for nftables rules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PortRange } from '../models/index.js';
|
|
9
|
+
import { NftValidationError } from '../models/index.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes port specifications into an array of port ranges
|
|
13
|
+
*/
|
|
14
|
+
export function normalizePortSpec(portSpec: number | PortRange | Array<number | PortRange>): PortRange[] {
|
|
15
|
+
const result: PortRange[] = [];
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(portSpec)) {
|
|
18
|
+
// If it's an array, process each element
|
|
19
|
+
for (const spec of portSpec) {
|
|
20
|
+
result.push(...normalizePortSpec(spec));
|
|
21
|
+
}
|
|
22
|
+
} else if (typeof portSpec === 'number') {
|
|
23
|
+
// Single port becomes a range with the same start and end
|
|
24
|
+
result.push({ from: portSpec, to: portSpec });
|
|
25
|
+
} else {
|
|
26
|
+
// Already a range
|
|
27
|
+
result.push(portSpec);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates port numbers or ranges
|
|
35
|
+
*/
|
|
36
|
+
export function validatePorts(port: number | PortRange | Array<number | PortRange>): void {
|
|
37
|
+
if (Array.isArray(port)) {
|
|
38
|
+
port.forEach(p => validatePorts(p));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof port === 'number') {
|
|
43
|
+
if (port < 1 || port > 65535) {
|
|
44
|
+
throw new NftValidationError(`Invalid port number: ${port}`);
|
|
45
|
+
}
|
|
46
|
+
} else if (typeof port === 'object') {
|
|
47
|
+
if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
|
|
48
|
+
throw new NftValidationError(`Invalid port range: ${port.from}-${port.to}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Format port range for nftables rule
|
|
55
|
+
*/
|
|
56
|
+
export function formatPortRange(range: PortRange): string {
|
|
57
|
+
if (range.from === range.to) {
|
|
58
|
+
return String(range.from);
|
|
59
|
+
}
|
|
60
|
+
return `${range.from}-${range.to}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert port spec to nftables expression
|
|
65
|
+
*/
|
|
66
|
+
export function portSpecToNftExpr(portSpec: number | PortRange | Array<number | PortRange>): string {
|
|
67
|
+
const ranges = normalizePortSpec(portSpec);
|
|
68
|
+
|
|
69
|
+
if (ranges.length === 1) {
|
|
70
|
+
return formatPortRange(ranges[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Multiple ports/ranges need to use a set
|
|
74
|
+
const ports = ranges.map(formatPortRange);
|
|
75
|
+
return `{ ${ports.join(', ')} }`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if two port ranges overlap
|
|
80
|
+
*/
|
|
81
|
+
export function rangesOverlap(range1: PortRange, range2: PortRange): boolean {
|
|
82
|
+
return range1.from <= range2.to && range2.from <= range1.to;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Merge overlapping port ranges
|
|
87
|
+
*/
|
|
88
|
+
export function mergeOverlappingRanges(ranges: PortRange[]): PortRange[] {
|
|
89
|
+
if (ranges.length <= 1) return ranges;
|
|
90
|
+
|
|
91
|
+
// Sort by start port
|
|
92
|
+
const sorted = [...ranges].sort((a, b) => a.from - b.from);
|
|
93
|
+
const merged: PortRange[] = [sorted[0]];
|
|
94
|
+
|
|
95
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
96
|
+
const current = sorted[i];
|
|
97
|
+
const lastMerged = merged[merged.length - 1];
|
|
98
|
+
|
|
99
|
+
if (current.from <= lastMerged.to + 1) {
|
|
100
|
+
// Ranges overlap or are adjacent, merge them
|
|
101
|
+
lastMerged.to = Math.max(lastMerged.to, current.to);
|
|
102
|
+
} else {
|
|
103
|
+
// No overlap, add as new range
|
|
104
|
+
merged.push(current);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return merged;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Calculate the total number of ports in a port specification
|
|
113
|
+
*/
|
|
114
|
+
export function countPorts(portSpec: number | PortRange | Array<number | PortRange>): number {
|
|
115
|
+
const ranges = normalizePortSpec(portSpec);
|
|
116
|
+
return ranges.reduce((total, range) => total + (range.to - range.from + 1), 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if a port is within the given specification
|
|
121
|
+
*/
|
|
122
|
+
export function isPortInSpec(port: number, portSpec: number | PortRange | Array<number | PortRange>): boolean {
|
|
123
|
+
const ranges = normalizePortSpec(portSpec);
|
|
124
|
+
return ranges.some(range => port >= range.from && port <= range.to);
|
|
125
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFTables Rule Validator
|
|
3
|
+
*
|
|
4
|
+
* Handles validation of settings and inputs for nftables operations.
|
|
5
|
+
* Prevents command injection and ensures valid values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PortRange, NfTableProxyOptions } from '../models/index.js';
|
|
9
|
+
import { NftValidationError } from '../models/index.js';
|
|
10
|
+
import { validatePorts } from './nft-port-spec-normalizer.js';
|
|
11
|
+
|
|
12
|
+
// IP address validation patterns
|
|
13
|
+
const IPV4_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
|
|
14
|
+
const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
|
|
15
|
+
const HOSTNAME_REGEX = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
|
16
|
+
const TABLE_NAME_REGEX = /^[a-zA-Z0-9_]+$/;
|
|
17
|
+
const RATE_REGEX = /^[0-9]+[kKmMgG]?bps$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validates an IP address (IPv4 or IPv6)
|
|
21
|
+
*/
|
|
22
|
+
export function isValidIP(ip: string): boolean {
|
|
23
|
+
return IPV4_REGEX.test(ip) || IPV6_REGEX.test(ip);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates an IPv4 address
|
|
28
|
+
*/
|
|
29
|
+
export function isValidIPv4(ip: string): boolean {
|
|
30
|
+
return IPV4_REGEX.test(ip);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates an IPv6 address
|
|
35
|
+
*/
|
|
36
|
+
export function isValidIPv6(ip: string): boolean {
|
|
37
|
+
return IPV6_REGEX.test(ip);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates a hostname
|
|
42
|
+
*/
|
|
43
|
+
export function isValidHostname(hostname: string): boolean {
|
|
44
|
+
return HOSTNAME_REGEX.test(hostname);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates a table name for nftables
|
|
49
|
+
*/
|
|
50
|
+
export function isValidTableName(tableName: string): boolean {
|
|
51
|
+
return TABLE_NAME_REGEX.test(tableName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates a rate specification (e.g., "10mbps")
|
|
56
|
+
*/
|
|
57
|
+
export function isValidRate(rate: string): boolean {
|
|
58
|
+
return RATE_REGEX.test(rate);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validates an array of IP addresses
|
|
63
|
+
*/
|
|
64
|
+
export function validateIPs(ips?: string[]): void {
|
|
65
|
+
if (!ips) return;
|
|
66
|
+
|
|
67
|
+
for (const ip of ips) {
|
|
68
|
+
if (!isValidIP(ip)) {
|
|
69
|
+
throw new NftValidationError(`Invalid IP address format: ${ip}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validates a host (can be hostname or IP)
|
|
76
|
+
*/
|
|
77
|
+
export function validateHost(host?: string): void {
|
|
78
|
+
if (!host) return;
|
|
79
|
+
|
|
80
|
+
if (!isValidHostname(host) && !isValidIP(host)) {
|
|
81
|
+
throw new NftValidationError(`Invalid host format: ${host}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validates a table name
|
|
87
|
+
*/
|
|
88
|
+
export function validateTableName(tableName?: string): void {
|
|
89
|
+
if (!tableName) return;
|
|
90
|
+
|
|
91
|
+
if (!isValidTableName(tableName)) {
|
|
92
|
+
throw new NftValidationError(
|
|
93
|
+
`Invalid table name: ${tableName}. Only alphanumeric characters and underscores are allowed.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates QoS settings
|
|
100
|
+
*/
|
|
101
|
+
export function validateQosSettings(qos?: NfTableProxyOptions['qos']): void {
|
|
102
|
+
if (!qos?.enabled) return;
|
|
103
|
+
|
|
104
|
+
if (qos.maxRate && !isValidRate(qos.maxRate)) {
|
|
105
|
+
throw new NftValidationError(
|
|
106
|
+
`Invalid rate format: ${qos.maxRate}. Use format like "10mbps", "1gbps", etc.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (qos.priority !== undefined) {
|
|
111
|
+
if (qos.priority < 1 || qos.priority > 10 || !Number.isInteger(qos.priority)) {
|
|
112
|
+
throw new NftValidationError(
|
|
113
|
+
`Invalid priority: ${qos.priority}. Must be an integer between 1 and 10.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validates all NfTablesProxy settings
|
|
121
|
+
*/
|
|
122
|
+
export function validateSettings(settings: NfTableProxyOptions): void {
|
|
123
|
+
// Validate port numbers
|
|
124
|
+
validatePorts(settings.fromPort);
|
|
125
|
+
validatePorts(settings.toPort);
|
|
126
|
+
|
|
127
|
+
// Validate IP addresses
|
|
128
|
+
validateIPs(settings.ipAllowList);
|
|
129
|
+
validateIPs(settings.ipBlockList);
|
|
130
|
+
|
|
131
|
+
// Validate target host
|
|
132
|
+
validateHost(settings.toHost);
|
|
133
|
+
|
|
134
|
+
// Validate table name
|
|
135
|
+
validateTableName(settings.tableName);
|
|
136
|
+
|
|
137
|
+
// Validate QoS settings
|
|
138
|
+
validateQosSettings(settings.qos);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if an IP matches the given family (ip or ip6)
|
|
143
|
+
*/
|
|
144
|
+
export function isIPForFamily(ip: string, family: 'ip' | 'ip6'): boolean {
|
|
145
|
+
if (family === 'ip6') {
|
|
146
|
+
return ip.includes(':');
|
|
147
|
+
}
|
|
148
|
+
return ip.includes('.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filter IPs by family
|
|
153
|
+
*/
|
|
154
|
+
export function filterIPsByFamily(ips: string[], family: 'ip' | 'ip6'): string[] {
|
|
155
|
+
return ips.filter(ip => isIPForFamily(ip, family));
|
|
156
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SmartProxy implementation
|
|
3
3
|
*
|
|
4
|
-
* Version
|
|
4
|
+
* Version 23.0.0: Rust-backed proxy engine
|
|
5
5
|
*/
|
|
6
6
|
// Re-export models
|
|
7
7
|
export * from './models/index.js';
|
|
@@ -9,21 +9,14 @@ export * from './models/index.js';
|
|
|
9
9
|
// Export the main SmartProxy class
|
|
10
10
|
export { SmartProxy } from './smart-proxy.js';
|
|
11
11
|
|
|
12
|
-
// Export
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
17
|
-
export { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
12
|
+
// Export Rust bridge and helpers
|
|
13
|
+
export { RustProxyBridge } from './rust-proxy-bridge.js';
|
|
14
|
+
export { RoutePreprocessor } from './route-preprocessor.js';
|
|
15
|
+
export { SocketHandlerServer } from './socket-handler-server.js';
|
|
16
|
+
export { RustMetricsAdapter } from './rust-metrics-adapter.js';
|
|
18
17
|
|
|
19
18
|
// Export route-based components
|
|
20
19
|
export { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
21
|
-
export { RouteConnectionHandler } from './route-connection-handler.js';
|
|
22
|
-
export { NFTablesManager } from './nftables-manager.js';
|
|
23
|
-
export { RouteOrchestrator } from './route-orchestrator.js';
|
|
24
|
-
|
|
25
|
-
// Export certificate management
|
|
26
|
-
export { SmartCertManager } from './certificate-manager.js';
|
|
27
20
|
|
|
28
21
|
// Export all helper functions from the utils directory
|
|
29
22
|
export * from './utils/index.js';
|
|
@@ -89,7 +89,6 @@ export interface ISmartProxyOptions {
|
|
|
89
89
|
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
|
90
90
|
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
|
|
91
91
|
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
|
92
|
-
allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
|
|
93
92
|
|
|
94
93
|
// Rate limiting and security
|
|
95
94
|
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
@@ -100,10 +99,6 @@ export interface ISmartProxyOptions {
|
|
|
100
99
|
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
101
100
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
102
101
|
|
|
103
|
-
// HttpProxy integration
|
|
104
|
-
useHttpProxy?: number[]; // Array of ports to forward to HttpProxy
|
|
105
|
-
httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443)
|
|
106
|
-
|
|
107
102
|
// Metrics configuration
|
|
108
103
|
metrics?: {
|
|
109
104
|
enabled?: boolean;
|
|
@@ -140,6 +135,12 @@ export interface ISmartProxyOptions {
|
|
|
140
135
|
* Default: true
|
|
141
136
|
*/
|
|
142
137
|
certProvisionFallbackToAcme?: boolean;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Path to the RustProxy binary. If not set, the binary is located
|
|
141
|
+
* automatically via env var, platform package, local build, or PATH.
|
|
142
|
+
*/
|
|
143
|
+
rustBinaryPath?: string;
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { IRouteConfig, IRouteAction, IRouteTarget } from './models/route-types.js';
|
|
2
|
+
import { logger } from '../../core/utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Preprocesses routes before sending them to Rust.
|
|
6
|
+
*
|
|
7
|
+
* Strips non-serializable fields (functions, callbacks) and classifies
|
|
8
|
+
* routes that must be handled by TypeScript (socket-handler, dynamic host/port).
|
|
9
|
+
*/
|
|
10
|
+
export class RoutePreprocessor {
|
|
11
|
+
/**
|
|
12
|
+
* Map of route name/id → original route config (with JS functions preserved).
|
|
13
|
+
* Used by the socket handler server to look up the original handler.
|
|
14
|
+
*/
|
|
15
|
+
private originalRoutes = new Map<string, IRouteConfig>();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Preprocess routes for the Rust binary.
|
|
19
|
+
*
|
|
20
|
+
* - Routes with `socketHandler` callbacks are marked as socket-handler type
|
|
21
|
+
* (Rust will relay these back to TS)
|
|
22
|
+
* - Routes with dynamic `host`/`port` functions are converted to socket-handler
|
|
23
|
+
* type (Rust relays, TS resolves the function)
|
|
24
|
+
* - Non-serializable fields are stripped
|
|
25
|
+
* - Original routes are preserved in the local map for handler lookup
|
|
26
|
+
*/
|
|
27
|
+
public preprocessForRust(routes: IRouteConfig[]): IRouteConfig[] {
|
|
28
|
+
this.originalRoutes.clear();
|
|
29
|
+
return routes.map((route, index) => this.preprocessRoute(route, index));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the original route config (with JS functions) by route name or id.
|
|
34
|
+
*/
|
|
35
|
+
public getOriginalRoute(routeKey: string): IRouteConfig | undefined {
|
|
36
|
+
return this.originalRoutes.get(routeKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all original routes that have socket handlers or dynamic functions.
|
|
41
|
+
*/
|
|
42
|
+
public getHandlerRoutes(): Map<string, IRouteConfig> {
|
|
43
|
+
return new Map(this.originalRoutes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private preprocessRoute(route: IRouteConfig, index: number): IRouteConfig {
|
|
47
|
+
const routeKey = route.name || route.id || `route_${index}`;
|
|
48
|
+
|
|
49
|
+
// Check if this route needs TS-side handling
|
|
50
|
+
const needsTsHandling = this.routeNeedsTsHandling(route);
|
|
51
|
+
|
|
52
|
+
if (needsTsHandling) {
|
|
53
|
+
// Store the original route for handler lookup
|
|
54
|
+
this.originalRoutes.set(routeKey, route);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create a clean copy for Rust
|
|
58
|
+
const cleanRoute: IRouteConfig = {
|
|
59
|
+
...route,
|
|
60
|
+
action: this.cleanAction(route.action, routeKey, needsTsHandling),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Ensure we have a name for handler lookup
|
|
64
|
+
if (!cleanRoute.name && !cleanRoute.id) {
|
|
65
|
+
cleanRoute.name = routeKey;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return cleanRoute;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private routeNeedsTsHandling(route: IRouteConfig): boolean {
|
|
72
|
+
// Socket handler routes always need TS
|
|
73
|
+
if (route.action.type === 'socket-handler' && route.action.socketHandler) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Routes with dynamic host/port functions need TS
|
|
78
|
+
if (route.action.targets) {
|
|
79
|
+
for (const target of route.action.targets) {
|
|
80
|
+
if (typeof target.host === 'function' || typeof target.port === 'function') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private cleanAction(action: IRouteAction, routeKey: string, needsTsHandling: boolean): IRouteAction {
|
|
90
|
+
const cleanAction: IRouteAction = { ...action };
|
|
91
|
+
|
|
92
|
+
if (needsTsHandling) {
|
|
93
|
+
// Convert to socket-handler type for Rust (Rust will relay back to TS)
|
|
94
|
+
cleanAction.type = 'socket-handler';
|
|
95
|
+
// Remove the JS handler (not serializable)
|
|
96
|
+
delete (cleanAction as any).socketHandler;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Clean targets - replace functions with static values
|
|
100
|
+
if (cleanAction.targets) {
|
|
101
|
+
cleanAction.targets = cleanAction.targets.map(t => this.cleanTarget(t));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return cleanAction;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private cleanTarget(target: IRouteTarget): IRouteTarget {
|
|
108
|
+
const clean: IRouteTarget = { ...target };
|
|
109
|
+
|
|
110
|
+
// Replace function host with placeholder
|
|
111
|
+
if (typeof clean.host === 'function') {
|
|
112
|
+
clean.host = 'localhost';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Replace function port with placeholder
|
|
116
|
+
if (typeof clean.port === 'function') {
|
|
117
|
+
clean.port = 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return clean;
|
|
121
|
+
}
|
|
122
|
+
}
|