Merge pull request #9 from Start9Labs/feature/multi-host

multi-hosts
This commit is contained in:
J H
2023-05-11 13:57:52 -06:00
committed by GitHub
8 changed files with 270 additions and 83 deletions

View File

@@ -4,6 +4,7 @@ import * as D from "./configDependencies"
import { Config, ExtractConfigType } from "./builder/config"
import { Utils, utils } from "../util/utils"
import nullIfEmpty from "../util/nullIfEmpty"
import { InterfaceReceipt } from "../mainFn/interfaceReceipt"
declare const dependencyProof: unique symbol
export type DependenciesReceipt = void & {
@@ -25,6 +26,7 @@ export type Save<
dependencies: D.ConfigDependencies<Manifest>
}) => Promise<{
dependenciesReceipt: DependenciesReceipt
interfaceReceipt: InterfaceReceipt
restart: boolean
}>
export type Read<
@@ -66,6 +68,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,

View File

@@ -60,7 +60,6 @@ export class Daemons<Ids extends string> {
static of(config: {
effects: Effects
started: (onTerm: () => void) => null
interfaceReceipt: InterfaceReceipt
healthReceipts: HealthReceipt[]
}) {
return new Daemons<never>(config.effects, config.started)

223
lib/mainFn/Host.ts Normal file
View File

@@ -0,0 +1,223 @@
import { object, string } from "ts-matches"
import { Effects } from "../types"
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 Scheme = string | null
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 KnownProtocols = typeof knownProtocols
type ProtocolsWithSslVariants = {
[K in keyof KnownProtocols]: KnownProtocols[K] extends {
withSsl: string
}
? K
: never
}[keyof KnownProtocols]
type NotProtocolsWithSslVariants = Exclude<
keyof KnownProtocols,
ProtocolsWithSslVariants
>
type PortOptionsByKnownProtocol =
| ({
protocol: ProtocolsWithSslVariants
preferredExternalPort?: number
scheme?: Scheme
} & ({ noAddSsl: true } | { addSsl?: Partial<AddSslOptions> }))
| {
protocol: NotProtocolsWithSslVariants
preferredExternalPort?: number
scheme?: Scheme
addSsl?: AddSslOptions | null
}
type PortOptionsByProtocol = PortOptionsByKnownProtocol | PortOptions
const hasStringProtocal = object({
protocol: string,
}).test
export class Host {
constructor(
readonly kind: "static" | "single" | "multi",
readonly options: {
effects: Effects
id: string
},
) {}
async bindPort(
internalPort: number,
options: PortOptionsByProtocol,
): Promise<Origin<this>> {
if (hasStringProtocal(options)) {
return await this.bindPortForKnown(options, internalPort)
} else {
return await this.bindPortForUnknown(internalPort, options)
}
}
private async bindPortForUnknown(
internalPort: number,
options:
| ({
scheme: Scheme
preferredExternalPort: number
addSsl: AddSslOptions | null
} & { secure: false; ssl: false })
| ({
scheme: Scheme
preferredExternalPort: number
addSsl: AddSslOptions | null
} & { secure: true; ssl: boolean }),
) {
await this.options.effects.bind({
kind: this.kind,
id: this.options.id,
internalPort: internalPort,
...options,
})
return new Origin(this, options)
}
private async bindPortForKnown(
options: PortOptionsByKnownProtocol,
internalPort: number,
) {
const scheme =
options.scheme === undefined ? options.protocol : options.scheme
const protoInfo = knownProtocols[options.protocol]
const preferredExternalPort =
options.preferredExternalPort ||
knownProtocols[options.protocol].defaultPort
const addSsl = this.getAddSsl(options, protoInfo)
const security: Security = !protoInfo.secure
? {
secure: protoInfo.secure,
ssl: protoInfo.ssl,
}
: { secure: false, ssl: false }
const newOptions = {
scheme,
preferredExternalPort,
addSsl,
...security,
}
await this.options.effects.bind({
kind: this.kind,
id: this.options.id,
internalPort,
...newOptions,
})
return new Origin(this, newOptions)
}
private getAddSsl(
options: PortOptionsByKnownProtocol,
protoInfo: KnownProtocols[keyof KnownProtocols],
): AddSslOptions | null {
if ("noAddSsl" in options && options.noAddSsl) return null
if ("withSsl" in protoInfo && protoInfo.withSsl)
return {
preferredExternalPort: knownProtocols[protoInfo.withSsl].defaultPort,
scheme: protoInfo.withSsl,
...("addSsl" in options ? options.addSsl : null),
}
return null
}
}
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])
}

View File

@@ -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))
},
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<Origin>) {
async export(origins: Iterable<Origin<Host>>) {
const { name, description, id, ui, username, path, search } = this.options
const addresses = Array.from(origins).map((o) =>

View File

@@ -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<T extends Host> {
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,
}
}
}

View File

@@ -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
@@ -166,14 +167,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
*/
@@ -206,16 +215,23 @@ export type Effects = {
/** Check that a file exists or not */
exists(input: { volumeId: string; path: string }): Promise<boolean>
/** Declaring that we are opening a interface on some protocal for local network
* Returns the port exposed
*/
bindLan(options: { internalPort: number }): Promise<number>
/** Declaring that we are opening a interface on some protocal for tor network */
bindTor(options: {
internalPort: number
name: string
externalPort: number
}): Promise<string>
/** Removes all network bindings */
clearBindings(): Promise<void>
/** Creates a host connected to the specified port with the provided options */
bind(
options: {
kind: "static" | "single" | "multi"
id: string
internalPort: number
} & PortOptions,
): Promise<void>
/** 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<string[]>
/** 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.
@@ -294,6 +310,8 @@ export type Effects = {
packageId?: string,
): Promise<number>
/** Removes all network interfaces */
clearNetworkInterfaces(): Promise<void>
/** When we want to create a link in the front end interfaces, and example is
* exposing a url to view a web service
*/