From f73de12fc818c61b0f998f2b387c42c76689135f Mon Sep 17 00:00:00 2001 From: BluJ Date: Fri, 12 May 2023 17:07:09 -0600 Subject: [PATCH] feat: Add in the features for the utils.networkInterface.get --- lib/config/builder/variants.ts | 4 +- lib/interfaces/NetworkInterfaceBuilder.ts | 4 +- lib/interfaces/Origin.ts | 2 + lib/types.ts | 54 ++-- lib/util/getNetworkInterface.ts | 284 ++++++++++++++++++++++ lib/util/getNetworkInterfaces.ts | 112 +++++++++ lib/util/regexes.ts | 3 + lib/util/utils.ts | 54 +++- 8 files changed, 482 insertions(+), 35 deletions(-) create mode 100644 lib/util/getNetworkInterface.ts create mode 100644 lib/util/getNetworkInterfaces.ts diff --git a/lib/config/builder/variants.ts b/lib/config/builder/variants.ts index 06ee57c..8404742 100644 --- a/lib/config/builder/variants.ts +++ b/lib/config/builder/variants.ts @@ -64,8 +64,8 @@ export class Variants { spec: Config | Config } }, - Store, - Vault, + Store = never, + Vault = never, >(a: VariantValues) { const validator = anyOf( ...Object.entries(a).map(([name, { spec }]) => diff --git a/lib/interfaces/NetworkInterfaceBuilder.ts b/lib/interfaces/NetworkInterfaceBuilder.ts index 5f64f74..dc1124f 100644 --- a/lib/interfaces/NetworkInterfaceBuilder.ts +++ b/lib/interfaces/NetworkInterfaceBuilder.ts @@ -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, diff --git a/lib/interfaces/Origin.ts b/lib/interfaces/Origin.ts index 7f41473..7feab28 100644 --- a/lib/interfaces/Origin.ts +++ b/lib/interfaces/Origin.ts @@ -18,11 +18,13 @@ export class Origin { options: this.options, suffix: `${path}${qp}`, username, + scheme: this.options.scheme, } } } type BuildOptions = { + scheme: string | null username: string | null path: string search: Record diff --git a/lib/types.ts b/lib/types.ts index 6b99414..73f9a22 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -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 /** 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 + 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 /** Get the address for another service for tor interfaces */ getServiceTorHostname( - interfaceId: string, + interfaceId: InterfaceId, packageId?: string, ): Promise /** Get the IP address of the container */ @@ -338,8 +336,20 @@ export type Effects = { */ getInterface(options: { packageId?: PackageId - interfaceId: string - }): Promise + interfaceId: InterfaceId + callback: () => void + }): Promise + + /** + * 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 /** *Remove an address that was exported. Used problably during main or during setConfig. diff --git a/lib/util/getNetworkInterface.ts b/lib/util/getNetworkInterface.ts new file mode 100644 index 0000000..df1973e --- /dev/null +++ b/lib/util/getNetworkInterface.ts @@ -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 = + (...args: ((a: A) => boolean)[]) => + (a: A) => + args.some((x) => x(a)) +const negate = + (fn: (a: A) => boolean) => + (a: A) => + !fn(a) +const unique = (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((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) +} diff --git a/lib/util/getNetworkInterfaces.ts b/lib/util/getNetworkInterfaces.ts new file mode 100644 index 0000000..5fc4fbf --- /dev/null +++ b/lib/util/getNetworkInterfaces.ts @@ -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((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) +} diff --git a/lib/util/regexes.ts b/lib/util/regexes.ts index ee80f8e..6c1cf37 100644 --- a/lib/util/regexes.ts +++ b/lib/util/regexes.ts @@ -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 = /^[ -~]*$/ diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 51b0ac2..cffb332 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -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 = { checkPortListening( @@ -73,18 +83,29 @@ export type Utils = { >( value: In, ) => Promise> + 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: (fileHelper: FileHelper) => ReturnType["read"]> store: { get: ( packageId: string, - path: T.EnsureStorePath, + path: EnsureStorePath, ) => GetStore & WrapperOverWrite getOwn: ( - path: T.EnsureStorePath, + path: EnsureStorePath, ) => GetStore & WrapperOverWrite setOwn: ( - path: T.EnsureStorePath, + path: EnsureStorePath, value: ExtractStore, ) => Promise } @@ -102,7 +123,7 @@ export const utils = < Vault = never, WrapperOverWrite = { const: never }, >( - effects: T.Effects, + effects: Effects, ): Utils => ({ createOrUpdateVault: async ({ key, @@ -147,18 +168,33 @@ export const utils = < writeFile: (fileHelper: FileHelper, 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: ( packageId: string, - path: T.EnsureStorePath, + path: EnsureStorePath, ) => getStore(effects, path as any, { packageId, }) as any, - getOwn: (path: T.EnsureStorePath) => + getOwn: (path: EnsureStorePath) => getStore(effects, path as any) as any, setOwn: ( - path: T.EnsureStorePath, + path: EnsureStorePath, value: ExtractStore, ) => effects.store.set({ value, path: path as any }), },