mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-26 02:11:56 +00:00
@@ -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,
|
||||
|
||||
@@ -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
223
lib/mainFn/Host.ts
Normal 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])
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user