@cloud-cli/px 2.0.1 → 3.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/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { HttpProxy, Domain } from './proxy-manager.js';
1
+ import { init } from '@cloud-cli/cli';
2
+ import { DomainAndTarget, Domain } from './proxy-manager.js';
3
+ import { ProxyEntry } from './proxy.js';
2
4
  declare const _default: {
3
- add(options: HttpProxy): Promise<unknown>;
4
- remove(options: HttpProxy): Promise<unknown>;
5
- get(options: Domain): HttpProxy[];
5
+ add(options: ProxyEntry): Promise<unknown>;
6
+ remove(options: DomainAndTarget): Promise<unknown>;
7
+ list(): Promise<ProxyEntry[]>;
8
+ get(options: Domain): Promise<ProxyEntry[]>;
6
9
  reload(): void;
7
- domains(): string[];
8
- list(): string[];
10
+ domains(): Promise<string[]>;
11
+ [init](): void;
9
12
  };
10
13
  export default _default;
package/dist/index.js CHANGED
@@ -1,22 +1,35 @@
1
+ import { init } from '@cloud-cli/cli';
2
+ import { Resource, SQLiteDriver } from '@cloud-cli/store';
1
3
  import { ProxyManager } from './proxy-manager.js';
4
+ import { ProxyEntry, ProxyServer } from './proxy.js';
5
+ const px = new ProxyServer();
2
6
  const manager = new ProxyManager();
3
7
  export default {
4
- add(options) {
5
- return manager.addProxy(options);
8
+ async add(options) {
9
+ const proxy = await manager.addProxy(options);
10
+ px.reload();
11
+ return proxy;
6
12
  },
7
- remove(options) {
8
- return manager.removeProxy(options);
13
+ async remove(options) {
14
+ const removed = await manager.removeProxy(options);
15
+ px.reload();
16
+ return removed;
17
+ },
18
+ list() {
19
+ return manager.getProxyList();
9
20
  },
10
21
  get(options) {
11
22
  return manager.getProxyListForDomain(options);
12
23
  },
13
24
  reload() {
14
- return manager.reloadProxies();
25
+ this[init]();
15
26
  },
16
27
  domains() {
17
28
  return manager.getDomainList();
18
29
  },
19
- list() {
20
- return manager.getProxyList();
30
+ [init]() {
31
+ Resource.use(new SQLiteDriver());
32
+ Resource.create(ProxyEntry);
33
+ return px.start();
21
34
  },
22
35
  };
@@ -1,23 +1,21 @@
1
+ import { ProxyEntry } from './proxy.js';
1
2
  export interface Domain {
2
3
  domain: string;
3
4
  }
4
- export interface HttpProxy extends Domain {
5
+ export interface DomainAndTarget extends Domain {
5
6
  target: string;
6
7
  }
8
+ type KeysOf<T> = {
9
+ [K in keyof T]: T[K] extends () => any ? never : K;
10
+ }[keyof T];
11
+ type ClassProperties<T> = {
12
+ [K in KeysOf<T>]: T[K] extends never ? never : T[K];
13
+ };
7
14
  export declare class ProxyManager {
8
- private proxyList;
9
- private _proxy;
10
- private get proxy();
11
- addProxy(proxy: HttpProxy): Promise<unknown>;
12
- removeProxy(options: HttpProxy): Promise<unknown>;
13
- getDomainList(): string[];
14
- getProxyList(): string[];
15
- getProxyListForDomain(options: Domain): HttpProxy[];
16
- reloadProxies(): void;
17
- private readProxyList;
18
- private disconnectDomainAndTarget;
19
- private connectDomainToTarget;
20
- private findRootDomain;
21
- private saveProxies;
22
- private closeProxy;
15
+ addProxy(proxy: ClassProperties<ProxyEntry>): Promise<unknown>;
16
+ removeProxy(options: DomainAndTarget): Promise<unknown>;
17
+ getDomainList(): Promise<string[]>;
18
+ getProxyList(): Promise<ProxyEntry[]>;
19
+ getProxyListForDomain(options: Domain): Promise<ProxyEntry[]>;
23
20
  }
21
+ export {};
@@ -1,117 +1,61 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'fs';
2
- import { join } from 'path';
3
- import redbird from 'redbird';
4
- const certificatesFolder = process.env.PX_CERTS_FOLDER || '/etc/letsencrypt/live';
5
- const certificateFile = 'fullchain.pem';
6
- const keyFile = 'privkey.pem';
7
- const configurationFile = join(process.cwd(), 'data', 'px.json');
1
+ import { Query, Resource } from '@cloud-cli/store';
2
+ import { ProxyEntry } from './proxy.js';
8
3
  const domainNotSpecifiedError = new Error('Domain not specified');
9
4
  const targetNotSpecifiedError = new Error('Target not specified');
10
5
  const notFoundError = new Error('Proxy not found');
11
- const proxyKey = ({ domain, target }) => `${domain} => ${target}`;
12
6
  export class ProxyManager {
13
- constructor() {
14
- this.proxyList = [];
15
- }
16
- get proxy() {
17
- if (!this._proxy) {
18
- this._proxy = redbird({
19
- port: 80,
20
- ssl: {
21
- port: 443,
22
- },
23
- bunyan: false,
24
- });
25
- }
26
- return this._proxy;
27
- }
28
7
  addProxy(proxy) {
29
- return new Promise((resolve, reject) => {
8
+ return new Promise(async (resolve, reject) => {
30
9
  if (!proxy.domain) {
31
10
  return reject(domainNotSpecifiedError);
32
11
  }
33
12
  if (!proxy.target) {
34
13
  return reject(targetNotSpecifiedError);
35
14
  }
36
- this.connectDomainToTarget(proxy);
37
- this.saveProxies();
38
- resolve(null);
15
+ const entry = new ProxyEntry({
16
+ domain: proxy.domain,
17
+ target: proxy.target,
18
+ cors: !!proxy.cors,
19
+ redirect: !!proxy.redirect,
20
+ });
21
+ try {
22
+ await entry.save();
23
+ resolve(entry);
24
+ }
25
+ catch (error) {
26
+ reject(error);
27
+ }
39
28
  });
40
29
  }
41
30
  removeProxy(options) {
42
- return new Promise((resolve, reject) => {
31
+ return new Promise(async (resolve, reject) => {
43
32
  if (!options.domain) {
44
33
  return reject(domainNotSpecifiedError);
45
34
  }
46
35
  if (!options.target) {
47
36
  return reject(targetNotSpecifiedError);
48
37
  }
49
- const proxy = this.proxyList.find((p) => proxyKey(p) === proxyKey(options));
50
- if (!proxy) {
38
+ const proxy = await Resource.find(ProxyEntry, new Query().where('domain').is(options.domain).where('target').is(options.target));
39
+ if (!proxy.length) {
51
40
  return reject(notFoundError);
52
41
  }
53
- this.disconnectDomainAndTarget(proxy);
54
- this.saveProxies();
55
- resolve(true);
42
+ try {
43
+ await proxy[0].remove();
44
+ resolve(true);
45
+ }
46
+ catch (error) {
47
+ reject(error);
48
+ }
56
49
  });
57
50
  }
58
- getDomainList() {
59
- return this.proxyList.map((p) => p.domain);
51
+ async getDomainList() {
52
+ const proxies = await Resource.find(ProxyEntry, new Query());
53
+ return proxies.map((proxy) => proxy.domain);
60
54
  }
61
- getProxyList() {
62
- return this.proxyList.map(proxyKey);
55
+ async getProxyList() {
56
+ return Resource.find(ProxyEntry, new Query());
63
57
  }
64
58
  getProxyListForDomain(options) {
65
- return this.proxyList.filter((p) => p.domain === options.domain);
66
- }
67
- reloadProxies() {
68
- this.closeProxy();
69
- this.proxyList = [];
70
- this.readProxyList().forEach((proxy) => this.connectDomainToTarget(proxy));
71
- }
72
- readProxyList() {
73
- if (!existsSync(configurationFile)) {
74
- return [];
75
- }
76
- const json = readFileSync(configurationFile, 'utf8') || '[]';
77
- return JSON.parse(json);
78
- }
79
- disconnectDomainAndTarget(proxy) {
80
- const { domain, target } = proxy;
81
- this.proxy.unregister(domain, target);
82
- this.proxyList = this.proxyList.filter((p) => proxyKey(p) !== proxyKey(proxy));
83
- }
84
- connectDomainToTarget(proxy) {
85
- const { domain, target } = proxy;
86
- const rootDomain = this.findRootDomain(domain);
87
- const ssl = { redirect: false };
88
- if (rootDomain) {
89
- Object.assign(ssl, {
90
- redirect: true,
91
- cert: join(certificatesFolder, rootDomain, certificateFile),
92
- key: join(certificatesFolder, rootDomain, keyFile),
93
- });
94
- }
95
- this.proxy.register(domain, target, { ssl });
96
- this.proxyList = this.proxyList.filter((p) => proxyKey(p) !== proxyKey(proxy)).concat(proxy);
97
- }
98
- findRootDomain(domain) {
99
- let rootDomain = '';
100
- const parts = domain.split('/')[0].split('.');
101
- while (parts.length) {
102
- rootDomain = parts.join('.');
103
- if (existsSync(join(certificatesFolder, rootDomain, certificateFile))) {
104
- return rootDomain;
105
- }
106
- parts.shift();
107
- }
108
- return '';
109
- }
110
- saveProxies() {
111
- writeFileSync(configurationFile, JSON.stringify(this.proxyList));
112
- }
113
- closeProxy() {
114
- this.proxy.close();
115
- delete this._proxy;
59
+ return Resource.find(ProxyEntry, new Query().where('domain').is(options.domain));
116
60
  }
117
61
  }
@@ -0,0 +1,28 @@
1
+ /// <reference types="node" />
2
+ import { IncomingMessage, ServerResponse } from 'http';
3
+ import { Resource } from '@cloud-cli/store';
4
+ export declare class ProxyEntry extends Resource {
5
+ domain: string;
6
+ target: string;
7
+ redirect: boolean;
8
+ cors: boolean;
9
+ constructor(p: Partial<ProxyEntry>);
10
+ }
11
+ interface Certificates {
12
+ key: string;
13
+ cert: string;
14
+ }
15
+ export declare class ProxyServer {
16
+ protected certs: Record<string, Certificates>;
17
+ protected targets: Record<string, ProxyEntry>;
18
+ protected servers: any;
19
+ start(): void;
20
+ reload(): void;
21
+ protected loadCertificates(): Promise<void>;
22
+ protected loadTargets(): Promise<void>;
23
+ protected serveRequest(req: IncomingMessage, res: ServerResponse): void;
24
+ protected getSslOptions(): {
25
+ SNICallback(domain: string, cb: Function): any;
26
+ };
27
+ }
28
+ export {};
package/dist/proxy.js ADDED
@@ -0,0 +1,162 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { createServer as createHttpServer, request as httpRequest } from 'http';
8
+ import { createServer as createHttpsServer, request as httpsRequest } from 'https';
9
+ import { createSecureContext } from 'tls';
10
+ import { Model, Property, Query, Resource } from '@cloud-cli/store';
11
+ import { readdir, readFile, stat } from 'fs/promises';
12
+ import { existsSync } from 'fs';
13
+ import { join } from 'path';
14
+ const certificatesFolder = process.env.PX_CERTS_FOLDER || '/etc/letsencrypt/live';
15
+ const certificateFile = 'fullchain.pem';
16
+ const keyFile = 'privkey.pem';
17
+ let ProxyEntry = class ProxyEntry extends Resource {
18
+ constructor(p) {
19
+ super(p);
20
+ }
21
+ };
22
+ __decorate([
23
+ Property(String)
24
+ ], ProxyEntry.prototype, "domain", void 0);
25
+ __decorate([
26
+ Property(String)
27
+ ], ProxyEntry.prototype, "target", void 0);
28
+ __decorate([
29
+ Property(Number)
30
+ ], ProxyEntry.prototype, "redirect", void 0);
31
+ __decorate([
32
+ Property(Number)
33
+ ], ProxyEntry.prototype, "cors", void 0);
34
+ ProxyEntry = __decorate([
35
+ Model('proxyentry')
36
+ ], ProxyEntry);
37
+ export { ProxyEntry };
38
+ export class ProxyServer {
39
+ constructor() {
40
+ this.certs = {};
41
+ this.targets = {};
42
+ this.servers = [];
43
+ }
44
+ start() {
45
+ this.reload();
46
+ this.servers.forEach((server) => server.close());
47
+ this.servers = [
48
+ createHttpServer((req, res) => this.serveRequest(req, res)).listen(80),
49
+ createHttpsServer(this.getSslOptions(), (req, res) => this.serveRequest(req, res)).listen(443),
50
+ ];
51
+ }
52
+ reload() {
53
+ this.loadCertificates();
54
+ this.loadTargets();
55
+ }
56
+ async loadCertificates() {
57
+ const certs = (this.certs = {});
58
+ const localCerts = await readdir(certificatesFolder);
59
+ const folders = await Promise.all(localCerts.map(async (dir) => {
60
+ const status = await stat(join(certificatesFolder, dir));
61
+ return status.isDirectory() ? dir : '';
62
+ }));
63
+ folders.filter(Boolean).forEach(async (rootDomain) => {
64
+ console.log(`Loading certificate for ${rootDomain}`);
65
+ certs[rootDomain] = createSecureContext({
66
+ cert: await readFile(join(certificatesFolder, rootDomain, certificateFile), 'utf8'),
67
+ key: await readFile(join(certificatesFolder, rootDomain, keyFile), 'utf8'),
68
+ });
69
+ });
70
+ }
71
+ async loadTargets() {
72
+ const targets = await Resource.find(ProxyEntry, new Query());
73
+ this.targets = {};
74
+ targets.forEach((entry) => (this.targets[entry.domain] = entry));
75
+ }
76
+ serveRequest(req, res) {
77
+ const origin = getOrigin(req);
78
+ const proxyEntry = this.targets[origin?.hostname];
79
+ if (!origin.hostname || !proxyEntry) {
80
+ res.writeHead(404, 'Not found');
81
+ res.end();
82
+ return;
83
+ }
84
+ const target = proxyEntry.target;
85
+ const url = new URL(req.url, target);
86
+ const isCorsPreflight = req.method === 'OPTIONS' && proxyEntry.cors;
87
+ if (isCorsPreflight) {
88
+ res.writeHead(204, 'OK');
89
+ res.setHeader('Access-Control-Allow-Origin', origin.hostname);
90
+ res.end();
91
+ return;
92
+ }
93
+ const proxyRequest = (url.protocol === 'https:' ? httpsRequest : httpRequest)(url, { method: req.method });
94
+ setHeaders(req, proxyRequest);
95
+ proxyRequest.setHeader('host', getHost(target));
96
+ req.on('data', (chunk) => proxyRequest.write(chunk));
97
+ req.on('end', () => proxyRequest.end());
98
+ proxyRequest.on('error', (error) => handleError(error, res));
99
+ proxyRequest.on('response', (proxyRes) => {
100
+ setHeaders(proxyRes, res);
101
+ const isCorsSimple = req.method !== 'OPTIONS' && proxyEntry.cors && req.headers.origin;
102
+ if (isCorsSimple) {
103
+ const corsOrigin = new URL(req.headers.origin);
104
+ res.setHeader('Access-Control-Allow-Origin', corsOrigin.origin);
105
+ }
106
+ res.writeHead(proxyRes.statusCode, proxyRes.statusMessage);
107
+ proxyRes.on('data', (chunk) => res.write(chunk));
108
+ proxyRes.on('end', () => res.end());
109
+ });
110
+ }
111
+ getSslOptions() {
112
+ const server = this;
113
+ return {
114
+ SNICallback(domain, cb) {
115
+ // TODO sanitize string to avoid file navigation!
116
+ const rootDomain = findRootDomain(domain);
117
+ if (rootDomain in server.certs) {
118
+ return cb(null, server.certs[rootDomain]);
119
+ }
120
+ throw new Error('Not found', { cause: 404 });
121
+ },
122
+ };
123
+ }
124
+ }
125
+ function findRootDomain(domain) {
126
+ let rootDomain = '';
127
+ const parts = domain.split('/')[0].split('.');
128
+ while (parts.length) {
129
+ rootDomain = parts.join('.');
130
+ if (existsSync(join(certificatesFolder, rootDomain, certificateFile))) {
131
+ return rootDomain;
132
+ }
133
+ parts.shift();
134
+ }
135
+ return '';
136
+ }
137
+ function setHeaders(from, to) {
138
+ const headers = Object.entries(from.headers);
139
+ for (const header of headers) {
140
+ to.setHeader(header[0], header[1]);
141
+ }
142
+ }
143
+ function handleError(error, res) {
144
+ console.log(error);
145
+ if (error.code === 'ECONNREFUSED') {
146
+ res.writeHead(502);
147
+ res.end();
148
+ return;
149
+ }
150
+ if (!res.headersSent) {
151
+ res.writeHead(500);
152
+ res.end();
153
+ }
154
+ }
155
+ function getOrigin(req) {
156
+ const host = req.headers['x-forwarded-host'] || req.headers.host || '';
157
+ return (host && new URL('http://' + host)) || null;
158
+ }
159
+ function getHost(string) {
160
+ const url = new URL(string);
161
+ return url.hostname + (url.port ? ':' + url.port : '');
162
+ }
@@ -0,0 +1,3 @@
1
+ import config from '@cloud-cli/jest-config';
2
+
3
+ export default { ...config };
package/package.json CHANGED
@@ -1,31 +1,24 @@
1
1
  {
2
2
  "name": "@cloud-cli/px",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Reverse Proxy",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "types": "./dist/index.d.ts",
8
+ "prettier": "@cloud-cli/prettier-config",
8
9
  "scripts": {
9
- "test": "echo true",
10
- "xtest": "jest",
10
+ "test": "echo jest",
11
11
  "tdd": "jest --watchAll",
12
- "build": "rm -rf ./dist/* && tsc --project ./tsconfig.json"
12
+ "build": "rm -rf ./dist/* && tsc"
13
13
  },
14
14
  "keywords": [
15
15
  "cloudy"
16
16
  ],
17
17
  "license": "MIT",
18
18
  "devDependencies": {
19
- "@types/jasmine": "^4.0.3",
20
- "@types/jest": "^27.0.1",
21
- "@types/node": "^14.18.21",
22
- "jasmine": "^4.2.0",
23
- "jest": "^27.1.0",
24
- "nodemon": "^2.0.16",
25
- "nyc": "^15.1.0",
26
- "ts-jest": "^27.0.5",
27
- "ts-jest-resolver": "^1.1.0",
28
- "ts-node": "^9.1.1",
19
+ "@cloud-cli/jest-config": "^1.0.0",
20
+ "@cloud-cli/prettier-config": "^1.0.0",
21
+ "@cloud-cli/typescript-config": "^1.0.1",
29
22
  "typescript": "^4.7.3"
30
23
  },
31
24
  "repository": {
@@ -36,6 +29,8 @@
36
29
  "access": "public"
37
30
  },
38
31
  "dependencies": {
32
+ "@cloud-cli/cli": "^1.2.2",
33
+ "@cloud-cli/store": "^1.1.0",
39
34
  "redbird": "^0.10.0"
40
35
  }
41
36
  }
package/.prettierrc DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "printWidth": 120,
3
- "tabWidth": 2,
4
- "useTabs": false,
5
- "semi": true,
6
- "singleQuote": true,
7
- "trailingComma": "all",
8
- "bracketSpacing": true
9
- }