feat: Add in the features for the utils.networkInterface.get

This commit is contained in:
BluJ
2023-05-12 17:07:09 -06:00
parent 3df957898a
commit f73de12fc8
8 changed files with 482 additions and 35 deletions

View File

@@ -64,8 +64,8 @@ export class Variants<Type, Store, Vault> {
spec: Config<any, Store, Vault> | Config<any, never, never>
}
},
Store,
Vault,
Store = never,
Vault = never,
>(a: VariantValues) {
const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) =>

View File

@@ -42,11 +42,11 @@ export class NetworkInterfaceBuilder {
const { name, description, id, ui, username, path, search } = this.options
const addresses = Array.from(origins).map((o) =>
o.build({ username, path, search }),
o.build({ username, path, search, scheme: null }),
)
await this.options.effects.exportNetworkInterface({
id,
interfaceId: id,
name,
description,
addresses,

View File

@@ -18,11 +18,13 @@ export class Origin<T extends Host> {
options: this.options,
suffix: `${path}${qp}`,
username,
scheme: this.options.scheme,
}
}
}
type BuildOptions = {
scheme: string | null
username: string | null
path: string
search: Record<string, string>

View File

@@ -166,17 +166,21 @@ export type ActionMetadata = {
*/
group?: string
}
export declare const hostName: unique symbol
export type HostName = string & { [hostName]: never }
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
export type Address = {
username: string | null
hostId: string
options: PortOptions
suffix: string
scheme: string | null
}
export type InterfaceId = string
export type NetworkInterface = {
id: string
interfaceId: InterfaceId
/** The title of this field to be displayed */
name: string
/** Human readable description, used as tooltip usually */
@@ -188,19 +192,6 @@ export type NetworkInterface = {
*/
ui?: boolean
}
export type NetworkInterfaceOut = {
id: string
/** The title of this field to be displayed */
name: string
/** Human readable description, used as tooltip usually */
description: string
/** All URIs */
addresses: string[]
/** Defaults to false, but describes if this address can be opened in a browser as an
* ui interface
*/
ui?: boolean
}
/** Used to reach out from the pure js runtime */
export type Effects = {
@@ -240,11 +231,18 @@ export type Effects = {
} & PortOptions,
): Promise<void>
/** Retrieves the current hostname(s) associated with a host id */
getHostNames(options: {
getHostnames(options: {
kind: "static" | "single"
id: string
}): Promise<[string]>
getHostNames(options: { kind: "multi"; id: string }): Promise<string[]>
hostId: string
packageId?: string
callback: () => void
}): Promise<[HostName]>
getHostnames(options: {
kind?: "multi"
packageId?: string
hostId: string
callback: () => void
}): Promise<[HostName, ...HostName[]]>
/** 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.
@@ -310,7 +308,7 @@ export type Effects = {
getIPHostname(): Promise<string[]>
/** Get the address for another service for tor interfaces */
getServiceTorHostname(
interfaceId: string,
interfaceId: InterfaceId,
packageId?: string,
): Promise<string>
/** Get the IP address of the container */
@@ -338,8 +336,20 @@ export type Effects = {
*/
getInterface(options: {
packageId?: PackageId
interfaceId: string
}): Promise<NetworkInterfaceOut>
interfaceId: InterfaceId
callback: () => void
}): Promise<NetworkInterface>
/**
* There are times that we want to see the addresses that where exported
* @param options.addressId If we want to filter the address id
*
* Note: any auth should be filtered out already
*/
listInterface(options: {
packageId?: PackageId
callback: () => void
}): Promise<NetworkInterface[]>
/**
*Remove an address that was exported. Used problably during main or during setConfig.

View File

@@ -0,0 +1,284 @@
import {
Address,
Effects,
EnsureStorePath,
HostName,
NetworkInterface,
hostName,
} from "../types"
import * as regexes from "./regexes"
export type UrlString = string
export type HostId = string
export type Filled = {
hostnames: HostName[]
onionHostnames: HostName[]
localHostnames: HostName[]
ipHostnames: HostName[]
ipv4Hostnames: HostName[]
ipv6Hostnames: HostName[]
nonIpHostnames: HostName[]
allHostnames: HostName[]
urls: UrlString[]
onionUrls: UrlString[]
localUrls: UrlString[]
ipUrls: UrlString[]
ipv4Urls: UrlString[]
ipv6Urls: UrlString[]
nonIpUrls: UrlString[]
allUrls: UrlString[]
}
export type FilledAddress = Address & Filled
export type NetworkInterfaceFilled = {
interfaceId: string
/** The title of this field to be displayed */
name: string
/** Human readable description, used as tooltip usually */
description: string
/** All URIs */
addresses: FilledAddress[]
/** Defaults to false, but describes if this address can be opened in a browser as an
* ui interface
*/
ui?: boolean
} & Filled
const either =
<A>(...args: ((a: A) => boolean)[]) =>
(a: A) =>
args.some((x) => x(a))
const negate =
<A>(fn: (a: A) => boolean) =>
(a: A) =>
!fn(a)
const unique = <A>(values: A[]) => Array.from(new Set(values))
const addressHostToUrl = (
{ scheme, username, suffix }: Address,
host: HostName,
): UrlString =>
`${scheme ? `${scheme}//` : ""}${
username ? `${username}@` : ""
}${host}${suffix}`
export const filledAddress = (
mapHostnames: {
[hostId: string]: HostName[]
},
address: Address,
): FilledAddress => {
const toUrl = addressHostToUrl.bind(null, address)
const hostnames = mapHostnames[address.hostId] ?? []
return {
...address,
hostnames,
get onionHostnames() {
return hostnames.filter(regexes.onionHost.test)
},
get localHostnames() {
return hostnames.filter(regexes.localHost.test)
},
get ipHostnames() {
return hostnames.filter(either(regexes.ipv4.test, regexes.ipv6.test))
},
get ipv4Hostnames() {
return hostnames.filter(regexes.ipv4.test)
},
get ipv6Hostnames() {
return hostnames.filter(regexes.ipv6.test)
},
get nonIpHostnames() {
return hostnames.filter(
negate(either(regexes.ipv4.test, regexes.ipv6.test)),
)
},
allHostnames: hostnames,
get urls() {
return hostnames.map(toUrl)
},
get onionUrls() {
return hostnames.filter(regexes.onionHost.test).map(toUrl)
},
get localUrls() {
return hostnames.filter(regexes.localHost.test).map(toUrl)
},
get ipUrls() {
return hostnames
.filter(either(regexes.ipv4.test, regexes.ipv6.test))
.map(toUrl)
},
get ipv4Urls() {
return hostnames.filter(regexes.ipv4.test).map(toUrl)
},
get ipv6Urls() {
return hostnames.filter(regexes.ipv6.test).map(toUrl)
},
get nonIpUrls() {
return hostnames
.filter(negate(either(regexes.ipv4.test, regexes.ipv6.test)))
.map(toUrl)
},
get allUrls() {
return hostnames.map(toUrl)
},
}
}
export const networkInterfaceFilled = (
interfaceValue: NetworkInterface,
addresses: FilledAddress[],
): NetworkInterfaceFilled => {
return {
...interfaceValue,
addresses,
get hostnames() {
return unique(addresses.flatMap((x) => x.hostnames))
},
get onionHostnames() {
return unique(addresses.flatMap((x) => x.onionHostnames))
},
get localHostnames() {
return unique(addresses.flatMap((x) => x.localHostnames))
},
get ipHostnames() {
return unique(addresses.flatMap((x) => x.ipHostnames))
},
get ipv4Hostnames() {
return unique(addresses.flatMap((x) => x.ipv4Hostnames))
},
get ipv6Hostnames() {
return unique(addresses.flatMap((x) => x.ipv6Hostnames))
},
get nonIpHostnames() {
return unique(addresses.flatMap((x) => x.nonIpHostnames))
},
get allHostnames() {
return unique(addresses.flatMap((x) => x.allHostnames))
},
get urls() {
return unique(addresses.flatMap((x) => x.urls))
},
get onionUrls() {
return unique(addresses.flatMap((x) => x.onionUrls))
},
get localUrls() {
return unique(addresses.flatMap((x) => x.localUrls))
},
get ipUrls() {
return unique(addresses.flatMap((x) => x.ipUrls))
},
get ipv4Urls() {
return unique(addresses.flatMap((x) => x.ipv4Urls))
},
get ipv6Urls() {
return unique(addresses.flatMap((x) => x.ipv6Urls))
},
get nonIpUrls() {
return unique(addresses.flatMap((x) => x.nonIpUrls))
},
get allUrls() {
return unique(addresses.flatMap((x) => x.allUrls))
},
}
}
const makeInterfaceFilled = async ({
effects,
interfaceId,
packageId,
callback,
}: {
effects: Effects
interfaceId: string
packageId: string | undefined
callback: () => void
}) => {
const interfaceValue = await effects.getInterface({
interfaceId,
packageId,
callback,
})
const hostIdsRecord: { [hostId: HostId]: HostName[] } = Object.fromEntries(
await Promise.all(
unique(interfaceValue.addresses.map((x) => x.hostId)).map(
async (hostId) => [
hostId,
effects.getHostnames({
packageId,
hostId,
callback,
}),
],
),
),
)
const fillAddress = filledAddress.bind(null, hostIdsRecord)
const interfaceFilled: NetworkInterfaceFilled = networkInterfaceFilled(
interfaceValue,
interfaceValue.addresses.map(fillAddress),
)
return interfaceFilled
}
export class GetNetworkInterface {
constructor(
readonly effects: Effects,
readonly opts: { interfaceId: string; packageId?: string },
) {}
/**
* Returns the value of Store at the provided path. Restart the service if the value changes
*/
async const() {
const { interfaceId, packageId } = this.opts
const callback = this.effects.restart
const interfaceFilled: NetworkInterfaceFilled = await makeInterfaceFilled({
effects: this.effects,
interfaceId,
packageId,
callback,
})
return interfaceFilled
}
/**
* Returns the value of NetworkInterfacesFilled at the provided path. Does nothing if the value changes
*/
async once() {
const { interfaceId, packageId } = this.opts
const callback = () => {}
const interfaceFilled: NetworkInterfaceFilled = await makeInterfaceFilled({
effects: this.effects,
interfaceId,
packageId,
callback,
})
return interfaceFilled
}
/**
* Watches the value of NetworkInterfacesFilled at the provided path. Takes a custom callback function to run whenever the value changes
*/
async *watch() {
const { interfaceId, packageId } = this.opts
while (true) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
})
yield await makeInterfaceFilled({
effects: this.effects,
interfaceId,
packageId,
callback,
})
await waitForNext
}
}
}
export function getNetworkInterface(
effects: Effects,
opts: { interfaceId: string; packageId?: string },
) {
return new GetNetworkInterface(effects, opts)
}

View File

@@ -0,0 +1,112 @@
import { Address, Effects, EnsureStorePath, HostName, hostName } from "../types"
import * as regexes from "./regexes"
import {
HostId,
NetworkInterfaceFilled,
filledAddress,
networkInterfaceFilled,
} from "./getNetworkInterface"
const makeManyInterfaceFilled = async ({
effects,
packageId,
callback,
}: {
effects: Effects
packageId: string | undefined
callback: () => void
}) => {
const interfaceValues = await effects.listInterface({
packageId,
callback,
})
const hostIdsRecord: { [hostId: HostId]: HostName[] } = Object.fromEntries(
await Promise.all(
Array.from(
new Set(
interfaceValues.flatMap((x) => x.addresses).map((x) => x.hostId),
),
).map(async (hostId) => [
hostId,
effects.getHostnames({
packageId,
hostId,
callback,
}),
]),
),
)
const fillAddress = filledAddress.bind(null, hostIdsRecord)
const interfacesFilled: NetworkInterfaceFilled[] = interfaceValues.map(
(interfaceValue) =>
networkInterfaceFilled(
interfaceValue,
interfaceValue.addresses.map(fillAddress),
),
)
return interfacesFilled
}
export class GetNetworkInterfaces {
constructor(
readonly effects: Effects,
readonly opts: { packageId?: string },
) {}
/**
* Returns the value of Store at the provided path. Restart the service if the value changes
*/
async const() {
const { packageId } = this.opts
const callback = this.effects.restart
const interfaceFilled: NetworkInterfaceFilled[] =
await makeManyInterfaceFilled({
effects: this.effects,
packageId,
callback,
})
return interfaceFilled
}
/**
* Returns the value of NetworkInterfacesFilled at the provided path. Does nothing if the value changes
*/
async once() {
const { packageId } = this.opts
const callback = () => {}
const interfaceFilled: NetworkInterfaceFilled[] =
await makeManyInterfaceFilled({
effects: this.effects,
packageId,
callback,
})
return interfaceFilled
}
/**
* Watches the value of NetworkInterfacesFilled at the provided path. Takes a custom callback function to run whenever the value changes
*/
async *watch() {
const { packageId } = this.opts
while (true) {
let callback: () => void = () => {}
const waitForNext = new Promise<void>((resolve) => {
callback = resolve
})
yield await makeManyInterfaceFilled({
effects: this.effects,
packageId,
callback,
})
await waitForNext
}
}
}
export function getNetworkInterfaces(
effects: Effects,
opts: { packageId?: string },
) {
return new GetNetworkInterfaces(effects, opts)
}

View File

@@ -13,9 +13,12 @@ export const url =
export const local =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
export const localHost = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/
export const onion =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
export const onionHost = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/
// https://ihateregex.io/expr/ascii/
export const ascii = /^[ -~]*$/

View File

@@ -1,4 +1,3 @@
import * as T from "../types"
import FileHelper from "./fileHelper"
import nullIfEmpty from "./nullIfEmpty"
import {
@@ -6,7 +5,13 @@ import {
checkPortListening,
checkWebUrl,
} from "../health/checkFns"
import { ExtractStore } from "../types"
import {
Effects,
EnsureStorePath,
ExtractStore,
InterfaceId,
PackageId,
} from "../types"
import { GetSystemSmtp } from "./GetSystemSmtp"
import { DefaultString } from "../config/configTypes"
import { getDefaultString } from "./getDefaultString"
@@ -24,6 +29,11 @@ import {
} from "../dependency/setupDependencyMounts"
import { Host, MultiHost, SingleHost, StaticHost } from "../interfaces/Host"
import { NetworkInterfaceBuilder } from "../interfaces/NetworkInterfaceBuilder"
import { GetNetworkInterface, getNetworkInterface } from "./getNetworkInterface"
import {
GetNetworkInterfaces,
getNetworkInterfaces,
} from "./getNetworkInterfaces"
export type Utils<Store, Vault, WrapperOverWrite = { const: never }> = {
checkPortListening(
@@ -73,18 +83,29 @@ export type Utils<Store, Vault, WrapperOverWrite = { const: never }> = {
>(
value: In,
) => Promise<MountDependenciesOut<In>>
networkInterface: {
getOwn: (interfaceId: InterfaceId) => GetNetworkInterface & WrapperOverWrite
get: (opts: {
interfaceId: InterfaceId
packageId: PackageId
}) => GetNetworkInterface & WrapperOverWrite
getAllOwn: () => GetNetworkInterfaces & WrapperOverWrite
getAll: (opts: {
packageId: PackageId
}) => GetNetworkInterfaces & WrapperOverWrite
}
nullIfEmpty: typeof nullIfEmpty
readFile: <A>(fileHelper: FileHelper<A>) => ReturnType<FileHelper<A>["read"]>
store: {
get: <Path extends string>(
packageId: string,
path: T.EnsureStorePath<Store, Path>,
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
getOwn: <Path extends string>(
path: T.EnsureStorePath<Store, Path>,
path: EnsureStorePath<Store, Path>,
) => GetStore<Store, Path> & WrapperOverWrite
setOwn: <Path extends string | never>(
path: T.EnsureStorePath<Store, Path>,
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => Promise<void>
}
@@ -102,7 +123,7 @@ export const utils = <
Vault = never,
WrapperOverWrite = { const: never },
>(
effects: T.Effects,
effects: Effects,
): Utils<Store, Vault, WrapperOverWrite> => ({
createOrUpdateVault: async ({
key,
@@ -147,18 +168,33 @@ export const utils = <
writeFile: <A>(fileHelper: FileHelper<A>, data: A) =>
fileHelper.write(data, effects),
nullIfEmpty,
networkInterface: {
getOwn: (interfaceId: InterfaceId) =>
getNetworkInterface(effects, { interfaceId }) as GetNetworkInterface &
WrapperOverWrite,
get: (opts: { interfaceId: InterfaceId; packageId: PackageId }) =>
getNetworkInterface(effects, opts) as GetNetworkInterface &
WrapperOverWrite,
getAllOwn: () =>
getNetworkInterfaces(effects, {}) as GetNetworkInterfaces &
WrapperOverWrite,
getAll: (opts: { packageId: PackageId }) =>
getNetworkInterfaces(effects, opts) as GetNetworkInterfaces &
WrapperOverWrite,
},
store: {
get: <Path extends string = never>(
packageId: string,
path: T.EnsureStorePath<Store, Path>,
path: EnsureStorePath<Store, Path>,
) =>
getStore<Store, Path>(effects, path as any, {
packageId,
}) as any,
getOwn: <Path extends string>(path: T.EnsureStorePath<Store, Path>) =>
getOwn: <Path extends string>(path: EnsureStorePath<Store, Path>) =>
getStore<Store, Path>(effects, path as any) as any,
setOwn: <Path extends string | never>(
path: T.EnsureStorePath<Store, Path>,
path: EnsureStorePath<Store, Path>,
value: ExtractStore<Store, Path>,
) => effects.store.set<Store, Path>({ value, path: path as any }),
},