@cloud-cli/px
Version:
Reverse Proxy
180 lines (179 loc) • 5.87 kB
JavaScript
import { getStorage, getConfig } from '@cloud-cli/cli';
import { ProxyEntry, ProxyServer, ProxySettings } from '@cloud-cli/proxy';
import { exec } from '@cloud-cli/exec';
const defaultOptions = {
httpPort: 80,
httpsPort: 443,
certsFolder: process.env.PX_CERTS_FOLDER || '/etc/letsencrypt/live',
};
const domainNotSpecifiedError = new Error('Domain not specified');
const targetNotSpecifiedError = new Error('Target not specified');
const moduleConfig = await getConfig('px', defaultOptions);
const { set, get, has, remove, getAll } = getStorage('px');
const settings = new ProxySettings({
certificatesFolder: moduleConfig.certsFolder,
certificateFile: 'fullchain.pem',
keyFile: 'privkey.pem',
httpPort: moduleConfig.httpPort,
httpsPort: moduleConfig.httpsPort,
});
const px = new ProxyServer(settings);
const emptyProxy = {
domain: '',
target: '',
cors: false,
preserveHost: false,
redirect: false,
redirectUrl: '',
headers: '',
authorization: '',
};
const readDomain = (options) => (options.domain = options.domain || options.host || options._[0]);
const numberRe = /^[0-9]+$/;
const readOption = value => {
switch (true) {
case value === 'true':
return true;
case value === 'false':
return false;
case numberRe.test(value):
return Number(value);
default:
return value;
}
};
function applyProperties(proxy, options) {
const properties = ['target', 'cors', 'redirect', 'redirectUrl', 'headers', 'authorization', 'preserveHost'];
for (const p of properties) {
if (p in options) {
proxy[p] = readOption(options[p]);
}
}
}
export class ProxyManager {
constructor() {
this.server = px;
}
async addProxy(properties) {
readDomain(properties);
if (!properties.domain) {
throw domainNotSpecifiedError;
}
if (!properties.target && !properties.redirectUrl) {
throw targetNotSpecifiedError;
}
const proxy = { ...emptyProxy, domain: properties.domain };
applyProperties(proxy, properties);
set(properties.domain, proxy);
await this.reload();
return proxy;
}
async updateProxy(options) {
readDomain(options);
const domain = options.domain;
let proxy = get(domain);
if (!domain) {
throw domainNotSpecifiedError;
}
if (!proxy) {
proxy = { ...emptyProxy, domain };
}
applyProperties(proxy, options);
set(proxy.domain, proxy);
await this.reload();
return proxy;
}
async removeProxy(options) {
readDomain(options);
const host = options.domain;
if (!host) {
throw domainNotSpecifiedError;
}
if (has(host)) {
remove(host);
await this.reload();
return true;
}
return false;
}
async getDomainList() {
const proxies = await this.getProxyList();
return proxies.map((proxy) => proxy.domain);
}
async getProxyList(filters = {}) {
const staticRoutes = getAll();
const containers = await this.getRunningContainers();
const list = [...staticRoutes, ...containers.map(readProxyFromContainer)];
const keys = Object.keys(filters);
if (!keys.length) {
return list;
}
return keys.reduce((list, key) => {
const filter = String(filters[key]).toLowerCase();
return list.filter((p) => String(p[key]).toLowerCase().includes(filter));
}, list);
}
async getProxyListForDomain(options) {
readDomain(options);
const all = await this.getProxyList();
return all.filter((p) => p.domain === options.domain);
}
async reload() {
const targets = await getAll();
const containers = await this.getRunningContainers();
const all = [...targets, ...containers.map(readProxyFromContainer)];
px.reset();
for (const t of all) {
const [domain, path = ""] = t.domain.split("/");
px.add(new ProxyEntry({
domain: domain,
path: path,
target: t.target,
redirectToHttps: t.redirect,
redirectToUrl: t.redirectUrl,
cors: t.cors,
headers: t.headers,
authorization: t.authorization,
preserveHost: t.preserveHost,
}));
}
px.start();
}
async getRunningContainers() {
const ps = await exec("docker", ["ps", "-aq"]);
const ids = ps.stdout.trim().split("\n");
const state = await exec("docker", ["inspect", ...ids]);
const json = JSON.parse(state.stdout);
return json
.map(readDockerContainer)
.filter(d => d.ports.length && d.labels.host);
}
}
function readProxyFromContainer(c) {
return {
domain: [c.labels.host, c.labels.path].join("/"),
target: `http://localhost:${c.ports[0].host}`,
redirect: true,
redirectUrl: "",
cors: true,
authorization: "",
preserveHost: false,
headers: "",
};
}
function readDockerContainer(d) {
const labels = Object.fromEntries(Object.entries(d.Config.Labels)
.filter(([key]) => key.startsWith("px:"))
.map(([key, value]) => [key.replace("px:", ""), value]));
return {
id: d.Id,
image: d.Image,
state: d.State.Status,
name: d.Name,
labels: labels,
ports: Object.entries(d.HostConfig.PortBindings).map(([port, pb]) => ({
container: Number(port.replace(/\D+/, "")),
host: Number(pb[0].HostPort),
})),
};
}