mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-26 02:11:56 +00:00
multi-hosts
This commit is contained in:
@@ -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<Manifest>
|
||||
}) => 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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
185
lib/mainFn/Host.ts
Normal file
185
lib/mainFn/Host.ts
Normal file
@@ -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<T extends KnownProtocol> = {
|
||||
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<T extends KnownProtocol> =
|
||||
(typeof knownProtocols)[T] extends { withSsl: KnownProtocol }
|
||||
? BasePortOptions<T> &
|
||||
({ noAddSsl: true } | { addSsl?: Partial<AddSslOptions> })
|
||||
: BasePortOptions<T> & { addSsl?: AddSslOptions | null }
|
||||
type PortOptionsByProtocol<T extends string> = T extends KnownProtocol
|
||||
? PortOptionsByKnownProtocol<T>
|
||||
: PortOptions
|
||||
|
||||
function isForKnownProtocol(
|
||||
options: PortOptionsByProtocol<string> | PortOptionsByProtocol<KnownProtocol>,
|
||||
): options is PortOptionsByProtocol<KnownProtocol> {
|
||||
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<T extends string>(
|
||||
internalPort: number,
|
||||
options: PortOptionsByProtocol<T>,
|
||||
): Promise<Origin<this>> {
|
||||
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])
|
||||
}
|
||||
@@ -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))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
lib/types.ts
42
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<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.
|
||||
@@ -288,6 +304,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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user