@robiki/proxy 1.0.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/LICENSE +21 -0
- package/README.md +588 -0
- package/dist/config-_6LOsppp.d.ts +180 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +542 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +226 -0
- package/package.json +86 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
class ProxyConfig {
|
|
5
|
+
config;
|
|
6
|
+
sslConfig;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.initializeSSL();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Initialize SSL configuration
|
|
13
|
+
*/
|
|
14
|
+
initializeSSL() {
|
|
15
|
+
if (!this.config.ssl) return;
|
|
16
|
+
try {
|
|
17
|
+
const key = this.config.ssl.key.includes("-----BEGIN") ? Buffer.from(this.config.ssl.key) : readFileSync(resolve(this.config.ssl.key));
|
|
18
|
+
const cert = this.config.ssl.cert.includes("-----BEGIN") ? Buffer.from(this.config.ssl.cert) : readFileSync(resolve(this.config.ssl.cert));
|
|
19
|
+
const ca = this.config.ssl.ca ? this.config.ssl.ca.includes("-----BEGIN") ? Buffer.from(this.config.ssl.ca) : readFileSync(resolve(this.config.ssl.ca)) : void 0;
|
|
20
|
+
this.sslConfig = {
|
|
21
|
+
key,
|
|
22
|
+
cert,
|
|
23
|
+
ca,
|
|
24
|
+
allowHTTP1: this.config.ssl.allowHTTP1 ?? true
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("Failed to load SSL certificates:", error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get SSL configuration
|
|
33
|
+
*/
|
|
34
|
+
getSSL() {
|
|
35
|
+
return this.sslConfig;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get route configuration for a host
|
|
39
|
+
*/
|
|
40
|
+
getRoute(host) {
|
|
41
|
+
if (this.config.routes[host]) {
|
|
42
|
+
return this.config.routes[host];
|
|
43
|
+
}
|
|
44
|
+
const hostWithoutPort = host.split(":")[0];
|
|
45
|
+
if (this.config.routes[hostWithoutPort]) {
|
|
46
|
+
return this.config.routes[hostWithoutPort];
|
|
47
|
+
}
|
|
48
|
+
for (const [pattern, route] of Object.entries(this.config.routes)) {
|
|
49
|
+
if (pattern.includes("*")) {
|
|
50
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
51
|
+
if (regex.test(host) || regex.test(hostWithoutPort)) {
|
|
52
|
+
return route;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get target for a host
|
|
60
|
+
*/
|
|
61
|
+
getTarget(host) {
|
|
62
|
+
const route = this.getRoute(host);
|
|
63
|
+
if (!route) {
|
|
64
|
+
return { target: void 0, ssl: void 0, remap: void 0 };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
target: route.target,
|
|
68
|
+
ssl: route.ssl ? this.sslConfig : void 0,
|
|
69
|
+
remap: route.remap
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get CORS headers for a request
|
|
74
|
+
*/
|
|
75
|
+
getCorsHeaders(origin, host) {
|
|
76
|
+
const route = host ? this.getRoute(host) : void 0;
|
|
77
|
+
const corsConfig = route?.cors || this.config.cors;
|
|
78
|
+
if (!corsConfig) {
|
|
79
|
+
return {
|
|
80
|
+
"access-control-allow-origin": origin,
|
|
81
|
+
"access-control-allow-methods": "*",
|
|
82
|
+
"access-control-allow-headers": "*",
|
|
83
|
+
"access-control-allow-credentials": "true"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const headers = {};
|
|
87
|
+
if (corsConfig.origin === "*") {
|
|
88
|
+
headers["access-control-allow-origin"] = "*";
|
|
89
|
+
} else if (Array.isArray(corsConfig.origin)) {
|
|
90
|
+
if (corsConfig.origin.includes(origin)) {
|
|
91
|
+
headers["access-control-allow-origin"] = origin;
|
|
92
|
+
}
|
|
93
|
+
} else if (corsConfig.origin) {
|
|
94
|
+
headers["access-control-allow-origin"] = corsConfig.origin;
|
|
95
|
+
} else {
|
|
96
|
+
headers["access-control-allow-origin"] = origin;
|
|
97
|
+
}
|
|
98
|
+
if (corsConfig.methods) {
|
|
99
|
+
headers["access-control-allow-methods"] = corsConfig.methods.join(", ");
|
|
100
|
+
} else {
|
|
101
|
+
headers["access-control-allow-methods"] = "*";
|
|
102
|
+
}
|
|
103
|
+
if (corsConfig.allowedHeaders) {
|
|
104
|
+
headers["access-control-allow-headers"] = corsConfig.allowedHeaders.join(", ");
|
|
105
|
+
} else {
|
|
106
|
+
headers["access-control-allow-headers"] = "*";
|
|
107
|
+
}
|
|
108
|
+
if (corsConfig.exposedHeaders) {
|
|
109
|
+
headers["access-control-expose-headers"] = corsConfig.exposedHeaders.join(", ");
|
|
110
|
+
}
|
|
111
|
+
if (corsConfig.credentials !== void 0) {
|
|
112
|
+
headers["access-control-allow-credentials"] = corsConfig.credentials ? "true" : "false";
|
|
113
|
+
} else {
|
|
114
|
+
headers["access-control-allow-credentials"] = "true";
|
|
115
|
+
}
|
|
116
|
+
if (corsConfig.maxAge) {
|
|
117
|
+
headers["access-control-max-age"] = corsConfig.maxAge.toString();
|
|
118
|
+
}
|
|
119
|
+
return headers;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Validate a request
|
|
123
|
+
*/
|
|
124
|
+
async validate(info) {
|
|
125
|
+
const route = this.getRoute(info.authority);
|
|
126
|
+
if (route?.validate) return route.validate(info);
|
|
127
|
+
if (this.config.validate) return this.config.validate(info);
|
|
128
|
+
return { status: true };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get ports to listen on
|
|
132
|
+
*/
|
|
133
|
+
getPorts() {
|
|
134
|
+
return [443, 8080, 9229];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the full configuration
|
|
138
|
+
*/
|
|
139
|
+
getConfig() {
|
|
140
|
+
return this.config;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let globalConfig;
|
|
144
|
+
function initConfig(config) {
|
|
145
|
+
globalConfig = new ProxyConfig(config);
|
|
146
|
+
return globalConfig;
|
|
147
|
+
}
|
|
148
|
+
function getConfig() {
|
|
149
|
+
if (!globalConfig) {
|
|
150
|
+
throw new Error("Configuration not initialized. Call initConfig() first.");
|
|
151
|
+
}
|
|
152
|
+
return globalConfig;
|
|
153
|
+
}
|
|
154
|
+
function deepMerge(...objects) {
|
|
155
|
+
const result = {};
|
|
156
|
+
for (const obj of objects) {
|
|
157
|
+
if (!obj) continue;
|
|
158
|
+
for (const key in obj) {
|
|
159
|
+
if (obj[key] === void 0) continue;
|
|
160
|
+
if (typeof obj[key] === "object" && !Array.isArray(obj[key]) && obj[key] !== null) {
|
|
161
|
+
result[key] = deepMerge(result[key] || {}, obj[key]);
|
|
162
|
+
} else {
|
|
163
|
+
result[key] = obj[key];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
function getConfigFromEnv() {
|
|
170
|
+
const config = {};
|
|
171
|
+
if (process.env.SSL_KEY && process.env.SSL_CERT) {
|
|
172
|
+
config.ssl = {
|
|
173
|
+
key: process.env.SSL_KEY,
|
|
174
|
+
cert: process.env.SSL_CERT,
|
|
175
|
+
ca: process.env.SSL_CA,
|
|
176
|
+
allowHTTP1: process.env.SSL_ALLOW_HTTP1 === "true"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (process.env.CORS_ORIGIN) {
|
|
180
|
+
config.cors = {
|
|
181
|
+
origin: process.env.CORS_ORIGIN === "*" ? "*" : process.env.CORS_ORIGIN.split(","),
|
|
182
|
+
methods: process.env.CORS_METHODS?.split(","),
|
|
183
|
+
allowedHeaders: process.env.CORS_HEADERS?.split(","),
|
|
184
|
+
credentials: process.env.CORS_CREDENTIALS === "true"
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return config;
|
|
188
|
+
}
|
|
189
|
+
function getConfigFromFile() {
|
|
190
|
+
const configPath = process.env.PROXY_CONFIG || "./proxy.config.json";
|
|
191
|
+
try {
|
|
192
|
+
const configFile = readFileSync(resolve(configPath), "utf-8");
|
|
193
|
+
return JSON.parse(configFile);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function loadConfig(programmaticConfig) {
|
|
199
|
+
const defaults = {
|
|
200
|
+
routes: {},
|
|
201
|
+
cors: {
|
|
202
|
+
origin: "*",
|
|
203
|
+
credentials: true
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const fileConfig = getConfigFromFile();
|
|
207
|
+
const envConfig = getConfigFromEnv();
|
|
208
|
+
const merged = deepMerge(defaults, fileConfig, envConfig, programmaticConfig || {});
|
|
209
|
+
return initConfig(merged);
|
|
210
|
+
}
|
|
211
|
+
function loadConfigFromFile(path) {
|
|
212
|
+
try {
|
|
213
|
+
const configFile = readFileSync(resolve(path), "utf-8");
|
|
214
|
+
const config = JSON.parse(configFile);
|
|
215
|
+
return initConfig(config);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error("Failed to load configuration file:", error);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function loadConfigFromEnv() {
|
|
222
|
+
const config = getConfigFromEnv();
|
|
223
|
+
return initConfig(config);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { ProxyConfig, getConfig, initConfig, loadConfig, loadConfigFromEnv, loadConfigFromFile };
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@robiki/proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A flexible HTTP/2 proxy server with WebSocket support, configurable routing, CORS, and validation. Use as npm package or Docker container.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"proxy",
|
|
7
|
+
"http2",
|
|
8
|
+
"websocket",
|
|
9
|
+
"reverse-proxy",
|
|
10
|
+
"docker",
|
|
11
|
+
"cors",
|
|
12
|
+
"ssl",
|
|
13
|
+
"tls",
|
|
14
|
+
"routing",
|
|
15
|
+
"middleware"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/robiki-ai/robiki-proxy#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/robiki-ai/robiki-proxy/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/robiki-ai/robiki-proxy.git"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Robiki sp. z o.o.",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./proxy": {
|
|
36
|
+
"import": "./dist/proxy.js",
|
|
37
|
+
"types": "./dist/proxy.d.ts"
|
|
38
|
+
},
|
|
39
|
+
"./config": {
|
|
40
|
+
"import": "./dist/utils/config.js",
|
|
41
|
+
"types": "./dist/utils/config.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"bin": {
|
|
45
|
+
"robiki-proxy": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "pkgroll",
|
|
54
|
+
"dev": "node --watch src/index.ts",
|
|
55
|
+
"start": "node dist/index.js",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"test:ui": "vitest --ui",
|
|
59
|
+
"test:coverage": "vitest run --coverage",
|
|
60
|
+
"test:docker": "make test-docker-full",
|
|
61
|
+
"test:docker:config": "make test-docker-config",
|
|
62
|
+
"test:all": "yarn test && yarn test:docker",
|
|
63
|
+
"prepublishOnly": "yarn build"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "25.0.3",
|
|
67
|
+
"@types/supertest": "6.0.2",
|
|
68
|
+
"@vitest/coverage-v8": "4.0.16",
|
|
69
|
+
"@vitest/ui": "2.1.8",
|
|
70
|
+
"pkgroll": "2.21.4",
|
|
71
|
+
"supertest": "7.1.4",
|
|
72
|
+
"typescript": "5.9.3",
|
|
73
|
+
"vitest": "4.0.16"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@types/ws": "8.18.1",
|
|
77
|
+
"ws": "8.19.0"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=21.0.0"
|
|
81
|
+
},
|
|
82
|
+
"packageManager": "yarn@1.22.22",
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public"
|
|
85
|
+
}
|
|
86
|
+
}
|