add clearnet functionality to frontend (#2814)

* add clearnet functionality to frontend

* add pattern and add sync db on rpcs

* add domain pattern

* show acme name instead of url if known

* dont blow up if domain not present after delete

* use common name for letsencrypt

* normalize urls

* refactor start-os ui net service

* backend migration and rpcs for serverInfo.host

* fix cors

* implement clearnet for main startos ui

* ability to add and remove tor addresses, including vanity

* add guard to prevent duplicate addresses

* misc bugfixes

* better heuristics for launching UIs

* fix ipv6 mocks

* fix ipv6 display bug

* rewrite url selection for launch ui

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2025-01-21 20:46:36 -07:00
committed by GitHub
parent 0a9f1d2a27
commit 479797361e
90 changed files with 2838 additions and 1203 deletions

View File

@@ -77,19 +77,18 @@ type BindOptionsByKnownProtocol =
preferredExternalPort?: number
addSsl?: AddSslOptions
}
export type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions
export type HostKind = BindParams["kind"]
export type BindOptionsByProtocol =
| BindOptionsByKnownProtocol
| (BindOptions & { protocol: null })
const hasStringProtocol = object({
protocol: string,
}).test
export class Host {
export class MultiHost {
constructor(
readonly options: {
effects: Effects
kind: HostKind
id: string
},
) {}
@@ -113,7 +112,7 @@ export class Host {
async bindPort(
internalPort: number,
options: BindOptionsByProtocol,
): Promise<Origin<this>> {
): Promise<Origin> {
if (hasStringProtocol(options)) {
return await this.bindPortForKnown(options, internalPort)
} else {
@@ -130,7 +129,6 @@ export class Host {
},
) {
const binderOptions = {
kind: this.options.kind,
id: this.options.id,
internalPort,
...options,
@@ -163,7 +161,6 @@ export class Host {
const secure: Security | null = !protoInfo.secure ? null : { ssl: false }
await this.options.effects.bind({
kind: this.options.kind,
id: this.options.id,
internalPort,
preferredExternalPort,
@@ -190,21 +187,3 @@ function inObject<Key extends string>(
): obj is { [K in Key]: unknown } {
return key in obj
}
// export class StaticHost extends Host {
// constructor(options: { effects: Effects; id: string }) {
// super({ ...options, kind: "static" })
// }
// }
// export class SingleHost extends Host {
// constructor(options: { effects: Effects; id: string }) {
// super({ ...options, kind: "single" })
// }
// }
export class MultiHost extends Host {
constructor(options: { effects: Effects; id: string }) {
super({ ...options, kind: "multi" })
}
}

View File

@@ -1,11 +1,11 @@
import { AddressInfo } from "../types"
import { AddressReceipt } from "./AddressReceipt"
import { Host, Scheme } from "./Host"
import { MultiHost, Scheme } from "./Host"
import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder"
export class Origin<T extends Host> {
export class Origin {
constructor(
readonly host: T,
readonly host: MultiHost,
readonly internalPort: number,
readonly scheme: string | null,
readonly sslScheme: string | null,

View File

@@ -1,11 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AddSslOptions } from "./AddSslOptions"
import type { HostId } from "./HostId"
import type { HostKind } from "./HostKind"
import type { Security } from "./Security"
export type BindParams = {
kind: HostKind
id: HostId
internalPort: number
preferredExternalPort: number

View File

@@ -1,11 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindInfo } from "./BindInfo"
import type { DomainConfig } from "./DomainConfig"
import type { HostKind } from "./HostKind"
import type { HostnameInfo } from "./HostnameInfo"
export type Host = {
kind: HostKind
bindings: { [key: number]: BindInfo }
onions: string[]
domains: { [key: string]: DomainConfig }

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HostKind = "multi"

View File

@@ -2,6 +2,7 @@
import type { AcmeProvider } from "./AcmeProvider"
import type { AcmeSettings } from "./AcmeSettings"
import type { Governor } from "./Governor"
import type { Host } from "./Host"
import type { LshwDevice } from "./LshwDevice"
import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
import type { ServerStatus } from "./ServerStatus"
@@ -13,16 +14,11 @@ export type ServerInfo = {
platform: string
id: string
hostname: string
host: Host
version: string
packageVersionCompat: string
postInitMigrationTodos: string[]
lastBackup: string | null
lanAddress: string
onionAddress: string
/**
* for backwards compatibility
*/
torAddress: string
networkInterfaces: { [key: string]: NetworkInterfaceInfo }
acme: { [key: AcmeProvider]: AcmeSettings }
statusInfo: ServerStatus

View File

@@ -1,7 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetupResult = {
torAddress: string
torAddresses: Array<string>
hostname: string
lanAddress: string
rootCa: string

View File

@@ -4,13 +4,13 @@ export { AcmeSettings } from "./AcmeSettings"
export { ActionId } from "./ActionId"
export { ActionInput } from "./ActionInput"
export { ActionMetadata } from "./ActionMetadata"
export { ActionRequest } from "./ActionRequest"
export { ActionRequestCondition } from "./ActionRequestCondition"
export { ActionRequestEntry } from "./ActionRequestEntry"
export { ActionRequestInput } from "./ActionRequestInput"
export { ActionRequestTrigger } from "./ActionRequestTrigger"
export { ActionRequest } from "./ActionRequest"
export { ActionResultMember } from "./ActionResultMember"
export { ActionResult } from "./ActionResult"
export { ActionResultMember } from "./ActionResultMember"
export { ActionResultV0 } from "./ActionResultV0"
export { ActionResultV1 } from "./ActionResultV1"
export { ActionResultValue } from "./ActionResultValue"
@@ -20,13 +20,13 @@ export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams"
export { AddCategoryParams } from "./AddCategoryParams"
export { AddPackageParams } from "./AddPackageParams"
export { AddressInfo } from "./AddressInfo"
export { AddSslOptions } from "./AddSslOptions"
export { AddVersionParams } from "./AddVersionParams"
export { AddressInfo } from "./AddressInfo"
export { Alerts } from "./Alerts"
export { Algorithm } from "./Algorithm"
export { AllowedStatuses } from "./AllowedStatuses"
export { AllPackageData } from "./AllPackageData"
export { AllowedStatuses } from "./AllowedStatuses"
export { AlpnInfo } from "./AlpnInfo"
export { AnySignature } from "./AnySignature"
export { AnySigningKey } from "./AnySigningKey"
@@ -38,9 +38,9 @@ export { BackupTargetFS } from "./BackupTargetFS"
export { Base64 } from "./Base64"
export { BindId } from "./BindId"
export { BindInfo } from "./BindInfo"
export { BindingSetPublicParams } from "./BindingSetPublicParams"
export { BindOptions } from "./BindOptions"
export { BindParams } from "./BindParams"
export { BindingSetPublicParams } from "./BindingSetPublicParams"
export { Blake3Commitment } from "./Blake3Commitment"
export { BlockDev } from "./BlockDev"
export { BuildArg } from "./BuildArg"
@@ -60,11 +60,11 @@ export { CreateSubcontainerFsParams } from "./CreateSubcontainerFsParams"
export { CurrentDependencies } from "./CurrentDependencies"
export { CurrentDependencyInfo } from "./CurrentDependencyInfo"
export { DataUrl } from "./DataUrl"
export { DepInfo } from "./DepInfo"
export { Dependencies } from "./Dependencies"
export { DependencyKind } from "./DependencyKind"
export { DependencyMetadata } from "./DependencyMetadata"
export { DependencyRequirement } from "./DependencyRequirement"
export { DepInfo } from "./DepInfo"
export { Description } from "./Description"
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
export { DeviceFilter } from "./DeviceFilter"
@@ -84,8 +84,8 @@ export { GetHostInfoParams } from "./GetHostInfoParams"
export { GetOsAssetParams } from "./GetOsAssetParams"
export { GetOsVersionParams } from "./GetOsVersionParams"
export { GetPackageParams } from "./GetPackageParams"
export { GetPackageResponseFull } from "./GetPackageResponseFull"
export { GetPackageResponse } from "./GetPackageResponse"
export { GetPackageResponseFull } from "./GetPackageResponseFull"
export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams"
export { GetServicePortForwardParams } from "./GetServicePortForwardParams"
export { GetSslCertificateParams } from "./GetSslCertificateParams"
@@ -98,22 +98,21 @@ export { Governor } from "./Governor"
export { Guid } from "./Guid"
export { HardwareRequirements } from "./HardwareRequirements"
export { HealthCheckId } from "./HealthCheckId"
export { Host } from "./Host"
export { HostAddress } from "./HostAddress"
export { HostId } from "./HostId"
export { HostKind } from "./HostKind"
export { HostnameInfo } from "./HostnameInfo"
export { Hosts } from "./Hosts"
export { Host } from "./Host"
export { ImageConfig } from "./ImageConfig"
export { ImageId } from "./ImageId"
export { ImageMetadata } from "./ImageMetadata"
export { ImageSource } from "./ImageSource"
export { InitProgressRes } from "./InitProgressRes"
export { InstallParams } from "./InstallParams"
export { InstalledState } from "./InstalledState"
export { InstalledVersionParams } from "./InstalledVersionParams"
export { InstallingInfo } from "./InstallingInfo"
export { InstallingState } from "./InstallingState"
export { InstallParams } from "./InstallParams"
export { IpHostname } from "./IpHostname"
export { IpInfo } from "./IpInfo"
export { ListPackageSignersParams } from "./ListPackageSignersParams"
@@ -137,14 +136,14 @@ export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicPara
export { NetworkInterfaceType } from "./NetworkInterfaceType"
export { OnionHostname } from "./OnionHostname"
export { OsIndex } from "./OsIndex"
export { OsVersionInfoMap } from "./OsVersionInfoMap"
export { OsVersionInfo } from "./OsVersionInfo"
export { OsVersionInfoMap } from "./OsVersionInfoMap"
export { PackageDataEntry } from "./PackageDataEntry"
export { PackageDetailLevel } from "./PackageDetailLevel"
export { PackageId } from "./PackageId"
export { PackageIndex } from "./PackageIndex"
export { PackageInfoShort } from "./PackageInfoShort"
export { PackageInfo } from "./PackageInfo"
export { PackageInfoShort } from "./PackageInfoShort"
export { PackageSignerParams } from "./PackageSignerParams"
export { PackageState } from "./PackageState"
export { PackageVersionInfo } from "./PackageVersionInfo"
@@ -166,18 +165,18 @@ export { Security } from "./Security"
export { ServerInfo } from "./ServerInfo"
export { ServerSpecs } from "./ServerSpecs"
export { ServerStatus } from "./ServerStatus"
export { ServiceInterfaceId } from "./ServiceInterfaceId"
export { ServiceInterface } from "./ServiceInterface"
export { ServiceInterfaceId } from "./ServiceInterfaceId"
export { ServiceInterfaceType } from "./ServiceInterfaceType"
export { Session } from "./Session"
export { SessionList } from "./SessionList"
export { Sessions } from "./Sessions"
export { Session } from "./Session"
export { SetDataVersionParams } from "./SetDataVersionParams"
export { SetDependenciesParams } from "./SetDependenciesParams"
export { SetHealth } from "./SetHealth"
export { SetIconParams } from "./SetIconParams"
export { SetMainStatusStatus } from "./SetMainStatusStatus"
export { SetMainStatus } from "./SetMainStatus"
export { SetMainStatusStatus } from "./SetMainStatusStatus"
export { SetNameParams } from "./SetNameParams"
export { SetStoreParams } from "./SetStoreParams"
export { SetupExecuteParams } from "./SetupExecuteParams"
@@ -192,7 +191,7 @@ export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetPublicParams } from "./UnsetPublicParams"
export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams"
export { VersionSignerParams } from "./VersionSignerParams"
export { Version } from "./Version"
export { VersionSignerParams } from "./VersionSignerParams"
export { VolumeId } from "./VolumeId"
export { WifiInfo } from "./WifiInfo"

View File

@@ -2,63 +2,68 @@ import { Pattern } from "../actions/input/inputSpecTypes"
import * as regexes from "./regexes"
export const ipv6: Pattern = {
regex: regexes.ipv6.source,
regex: regexes.ipv6.matches(),
description: "Must be a valid IPv6 address",
}
export const ipv4: Pattern = {
regex: regexes.ipv4.source,
regex: regexes.ipv4.matches(),
description: "Must be a valid IPv4 address",
}
export const hostname: Pattern = {
regex: regexes.hostname.source,
regex: regexes.hostname.matches(),
description: "Must be a valid hostname",
}
export const localHostname: Pattern = {
regex: regexes.localHostname.source,
regex: regexes.localHostname.matches(),
description: 'Must be a valid ".local" hostname',
}
export const torHostname: Pattern = {
regex: regexes.torHostname.source,
regex: regexes.torHostname.matches(),
description: 'Must be a valid Tor (".onion") hostname',
}
export const url: Pattern = {
regex: regexes.url.source,
regex: regexes.url.matches(),
description: "Must be a valid URL",
}
export const localUrl: Pattern = {
regex: regexes.localUrl.source,
regex: regexes.localUrl.matches(),
description: 'Must be a valid ".local" URL',
}
export const torUrl: Pattern = {
regex: regexes.torUrl.source,
regex: regexes.torUrl.matches(),
description: 'Must be a valid Tor (".onion") URL',
}
export const ascii: Pattern = {
regex: regexes.ascii.source,
regex: regexes.ascii.matches(),
description:
"May only contain ASCII characters. See https://www.w3schools.com/charsets/ref_html_ascii.asp",
}
export const domain: Pattern = {
regex: regexes.domain.matches(),
description: "Must be a valid Fully Qualified Domain Name",
}
export const email: Pattern = {
regex: regexes.email.source,
regex: regexes.email.matches(),
description: "Must be a valid email address",
}
export const emailWithName: Pattern = {
regex: regexes.emailWithName.source,
regex: regexes.emailWithName.matches(),
description: "Must be a valid email address, optionally with a name",
}
export const base64: Pattern = {
regex: regexes.base64.source,
regex: regexes.base64.matches(),
description:
"May only contain base64 characters. See https://base64.guru/learn/base64-characters",
}

View File

@@ -1,38 +1,71 @@
export class ComposableRegex {
readonly regex: RegExp
constructor(regex: RegExp | string) {
if (regex instanceof RegExp) {
this.regex = regex
} else {
this.regex = new RegExp(regex)
}
}
asExpr(): string {
return `(${this.regex.source})`
}
matches(): string {
return `^${this.regex.source}$`
}
contains(): string {
return this.regex.source
}
}
// https://ihateregex.io/expr/ipv6/
export const ipv6 =
/(([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]))/
export const ipv6 = new ComposableRegex(
/(([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]))/,
)
// https://ihateregex.io/expr/ipv4/
export const ipv4 =
/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/
export const ipv4 = new ComposableRegex(
/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
)
export const hostname =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/
export const hostname = new ComposableRegex(
/(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])/,
)
export const localHostname = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/
export const localHostname = new ComposableRegex(
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local/,
)
export const torHostname = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/
export const torHostname = new ComposableRegex(
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion/,
)
// https://ihateregex.io/expr/url/
export const url =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
export const url = new ComposableRegex(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
)
export const localUrl =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
export const localUrl = new ComposableRegex(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.local\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
)
export const torUrl =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
export const torUrl = new ComposableRegex(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.onion\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/,
)
// https://ihateregex.io/expr/ascii/
export const ascii = /^[ -~]*$/
export const ascii = new ComposableRegex(/[ -~]*/)
export const domain = new ComposableRegex(/[A-Za-z0-9.-]+\.[A-Za-z]{2,}/)
// https://www.regular-expressions.info/email.html
export const email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/
export const email = new ComposableRegex(`[A-Za-z0-9._%+-]+@${domain.asExpr()}`)
export const emailWithName = new RegExp(
`(${email.source})|([^<]*<(${email.source})>)`,
export const emailWithName = new ComposableRegex(
`${email.asExpr()}|([^<]*<${email.asExpr()}>)`,
)
//https://rgxdb.com/r/1NUN74O6
export const base64 =
/^(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))$/
export const base64 = new ComposableRegex(
/(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/,
)