diff --git a/lib/config/setupConfig.ts b/lib/config/setupConfig.ts index 07d0057..a41c505 100644 --- a/lib/config/setupConfig.ts +++ b/lib/config/setupConfig.ts @@ -4,6 +4,7 @@ import * as D from "./dependencies" import { Config, ExtractConfigType } from "./builder/config" import { Utils, utils } from "../util" import nullIfEmpty from "../util/nullIfEmpty" +import { InterfaceReceipt } from "../mainFn/interfaceReceipt" declare const dependencyProof: unique symbol export type DependenciesReceipt = void & { @@ -24,6 +25,7 @@ export type Save< dependencies: D.Dependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt + interfaceReceipt: InterfaceReceipt restart: boolean }> export type Read< @@ -63,6 +65,8 @@ export function setupConfig< await console.error(String(validator.errorMessage(input))) return { error: "Set config type error for config" } } + await effects.clearBindings() + await effects.clearNetworkInterfaces() const { restart } = await write({ input: JSON.parse(JSON.stringify(input)), effects, diff --git a/lib/mainFn/Daemons.ts b/lib/mainFn/Daemons.ts index 247a83e..8767d15 100644 --- a/lib/mainFn/Daemons.ts +++ b/lib/mainFn/Daemons.ts @@ -60,7 +60,6 @@ export class Daemons { static of(config: { effects: Effects started: (onTerm: () => void) => null - interfaceReceipt: InterfaceReceipt healthReceipts: HealthReceipt[] }) { return new Daemons(config.effects, config.started) diff --git a/lib/mainFn/Host.ts b/lib/mainFn/Host.ts new file mode 100644 index 0000000..5bccabb --- /dev/null +++ b/lib/mainFn/Host.ts @@ -0,0 +1,185 @@ +import { Effects } from "../types" +import { AddressReceipt } from "./AddressReceipt" +import { NetworkInterfaceBuilder } from "./NetworkInterfaceBuilder" +import { Origin } from "./Origin" + +const knownProtocols = { + http: { + secure: false, + ssl: false, + defaultPort: 80, + withSsl: "https", + }, + https: { + secure: true, + ssl: true, + defaultPort: 443, + }, + ws: { + secure: false, + ssl: false, + defaultPort: 80, + withSsl: "wss", + }, + wss: { + secure: true, + ssl: true, + defaultPort: 443, + }, + ssh: { + secure: true, + ssl: false, + defaultPort: 22, + }, + bitcoin: { + secure: true, + ssl: false, + defaultPort: 8333, + }, + grpc: { + secure: true, + ssl: true, + defaultPort: 50051, + }, + dns: { + secure: true, + ssl: false, + defaultPort: 53, + }, +} as const + +type KnownProtocol = keyof typeof knownProtocols + +type Scheme = string | null + +type BasePortOptions = { + protocol: T + preferredExternalPort?: number + scheme?: Scheme +} +type AddSslOptions = { + preferredExternalPort: number + scheme: Scheme + addXForwardedHeaders?: boolean /** default: false */ +} +type Security = { secure: false; ssl: false } | { secure: true; ssl: boolean } +export type PortOptions = { + scheme: Scheme + preferredExternalPort: number + addSsl: AddSslOptions | null +} & Security +type PortOptionsByKnownProtocol = + (typeof knownProtocols)[T] extends { withSsl: KnownProtocol } + ? BasePortOptions & + ({ noAddSsl: true } | { addSsl?: Partial }) + : BasePortOptions & { addSsl?: AddSslOptions | null } +type PortOptionsByProtocol = T extends KnownProtocol + ? PortOptionsByKnownProtocol + : PortOptions + +function isForKnownProtocol( + options: PortOptionsByProtocol | PortOptionsByProtocol, +): options is PortOptionsByProtocol { + return "protocol" in options && (options.protocol as string) in knownProtocols +} + +export class Host { + constructor( + readonly kind: "static" | "single" | "multi", + readonly options: { + effects: Effects + id: string + }, + ) {} + + async bindPort( + internalPort: number, + options: PortOptionsByProtocol, + ): Promise> { + if (isForKnownProtocol(options)) { + const scheme = + options.scheme === undefined ? options.protocol : options.scheme + const protoInfo = knownProtocols[options.protocol] + const preferredExternalPort = + options.preferredExternalPort || + knownProtocols[options.protocol].defaultPort + const defaultAddSsl = + "noAddSsl" in options && options.noAddSsl + ? null + : "withSsl" in protoInfo + ? { + preferredExternalPort: + knownProtocols[protoInfo.withSsl].defaultPort, + scheme: protoInfo.withSsl, + } + : null + const addSsl = options.addSsl + ? { ...defaultAddSsl, ...options.addSsl } + : defaultAddSsl + const security = { + secure: protoInfo.secure, + ssl: protoInfo.ssl, + } as Security + + const newOptions = { + scheme, + preferredExternalPort, + addSsl, + ...security, + } + + await this.options.effects.bind({ + kind: this.kind, + id: this.options.id, + internalPort: internalPort, + ...newOptions, + }) + + return new Origin(this, newOptions) + } else { + await this.options.effects.bind({ + kind: this.kind, + id: this.options.id, + internalPort: internalPort, + ...options, + }) + + return new Origin(this, options) + } + } +} + +export class StaticHost extends Host { + constructor(options: { effects: Effects; id: string }) { + super("static", options) + } +} + +export class SingleHost extends Host { + constructor(options: { effects: Effects; id: string }) { + super("single", options) + } +} + +export class MultiHost extends Host { + constructor(options: { effects: Effects; id: string }) { + super("multi", options) + } +} + +async function test(effects: Effects) { + const foo = new MultiHost({ effects, id: "foo" }) + const fooOrigin = await foo.bindPort(80, { protocol: "http" as const }) + const fooInterface = new NetworkInterfaceBuilder({ + effects, + name: "Foo", + id: "foo", + description: "A Foo", + ui: true, + username: "bar", + path: "/baz", + search: { qux: "yes" }, + }) + + await fooInterface.export([fooOrigin]) +} diff --git a/lib/mainFn/LocalBinding.ts b/lib/mainFn/LocalBinding.ts deleted file mode 100644 index 50c0cd3..0000000 --- a/lib/mainFn/LocalBinding.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { once } from "../util/once" -import { Origin } from "./Origin" - -/** - * Pulled from https://www.oreilly.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html - * to test ipv4 addresses - */ -export const regexToTestIp4 = once(() => /(?:[0-9]{1,3}\.){3}[0-9]{1,3}/) -/** - * Pulled from https://ihateregex.io/expr/ipv6/ - * to test ipv6 addresses - */ -export const ipv6 = once( - () => - /(([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]))/, -) -/** - * Some addresses are local - */ -export const local = once(() => /\.local/) -export class LocalBinding { - constructor(readonly localHost: string, readonly ipHosts: string[]) {} - createOrigins(protocol: string | null) { - const ipHosts = this.ipHosts - return { - local: new Origin(protocol, this.localHost), - get all() { - return [this.local, ...this.ip] - }, - get ip() { - return ipHosts - .filter((x) => !local().test(x)) - .map((x) => new Origin(protocol, x)) - }, - get ipv4() { - return ipHosts - .filter(regexToTestIp4().test) - .map((x) => new Origin(protocol, x)) - }, - get ipv6() { - return ipHosts.filter(ipv6().test).map((x) => new Origin(protocol, x)) - }, - } - } -} diff --git a/lib/mainFn/LocalPort.ts b/lib/mainFn/LocalPort.ts deleted file mode 100644 index a70762c..0000000 --- a/lib/mainFn/LocalPort.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Effects } from "../types" -import { LocalBinding } from "./LocalBinding" - -export class LocalPort { - constructor(readonly effects: Effects) {} - static async bindLan(effects: Effects, internalPort: number) { - const port = await effects.bindLan({ - internalPort, - }) - const localAddress = `${await effects.getLocalHostname()}:${port}` - const ipAddress = await ( - await effects.getIPHostname() - ).map((x) => `${x}:${port}`) - return new LocalBinding(localAddress, ipAddress) - } -} diff --git a/lib/mainFn/NetworkInterfaceBuilder.ts b/lib/mainFn/NetworkInterfaceBuilder.ts index 1d8d475..66b6de9 100644 --- a/lib/mainFn/NetworkInterfaceBuilder.ts +++ b/lib/mainFn/NetworkInterfaceBuilder.ts @@ -1,5 +1,6 @@ import { Effects } from "../types" import { AddressReceipt } from "./AddressReceipt" +import { Host } from "./Host" import { Origin } from "./Origin" /** @@ -35,7 +36,7 @@ export class NetworkInterfaceBuilder { * @param addresses * @returns */ - async export(origins: Iterable) { + async export(origins: Iterable>) { const { name, description, id, ui, username, path, search } = this.options const addresses = Array.from(origins).map((o) => diff --git a/lib/mainFn/Origin.ts b/lib/mainFn/Origin.ts index 5bb0daa..7f41473 100644 --- a/lib/mainFn/Origin.ts +++ b/lib/mainFn/Origin.ts @@ -1,12 +1,10 @@ -export class Origin { - constructor(readonly protocol: string | null, readonly host: string) {} +import { Address } from "../types" +import { Host, PortOptions } from "./Host" - build({ username, path, search }: BuildOptions) { - // prettier-ignore - const urlAuth = !!(username) ? `${username}@` : - ''; - const protocolSection = this.protocol != null ? `${this.protocol}://` : "" +export class Origin { + constructor(readonly host: T, readonly options: PortOptions) {} + build({ username, path, search }: BuildOptions): Address { const qpEntries = Object.entries(search) .map( ([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`, @@ -15,7 +13,12 @@ export class Origin { const qp = qpEntries.length ? `?${qpEntries}` : "" - return `${protocolSection}${urlAuth}${this.host}${path}${qp}` + return { + hostId: this.host.options.id, + options: this.options, + suffix: `${path}${qp}`, + username, + } } } diff --git a/lib/types.ts b/lib/types.ts index 1d7744a..96c4e75 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,6 +1,7 @@ export * as configTypes from "./config/configTypes" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" +import { PortOptions } from "./mainFn/Host" export type ExportedAction = (options: { effects: Effects @@ -161,14 +162,22 @@ export type ActionMetaData = { group?: string } +/** ${scheme}://${username}@${host}:${externalPort}${suffix} */ +export type Address = { + username: string | null + hostId: string + options: PortOptions + suffix: string +} + export type NetworkInterface = { id: string - /** The title of this field to be dsimplayed */ + /** The title of this field to be displayed */ name: string /** Human readable description, used as tooltip usually */ description: string /** All URIs */ - addresses: string[] + addresses: Address[] /** Defaults to false, but describes if this address can be opened in a browser as an * ui interface */ @@ -201,16 +210,23 @@ export type Effects = { /** Check that a file exists or not */ exists(input: { volumeId: string; path: string }): Promise - /** Declaring that we are opening a interface on some protocal for local network - * Returns the port exposed - */ - bindLan(options: { internalPort: number }): Promise - /** Declaring that we are opening a interface on some protocal for tor network */ - bindTor(options: { - internalPort: number - name: string - externalPort: number - }): Promise + + /** Removes all network bindings */ + clearBindings(): Promise + /** Creates a host connected to the specified port with the provided options */ + bind( + options: { + kind: "static" | "single" | "multi" + id: string + internalPort: number + } & PortOptions, + ): Promise + /** Retrieves the current hostname(s) associated with a host id */ + getHostNames(options: { + kind: "static" | "single" + id: string + }): Promise<[string]> + getHostNames(options: { kind: "multi"; id: string }): Promise /** Similar to the fetch api via the mdn, this is simplified but the point is * to get something from some website, and return the response. @@ -288,6 +304,8 @@ export type Effects = { packageId?: string, ): Promise + /** Removes all network interfaces */ + clearNetworkInterfaces(): Promise /** When we want to create a link in the front end interfaces, and example is * exposing a url to view a web service */