mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
refactor OpenUI
This commit is contained in:
@@ -5,6 +5,7 @@ import { Effects } from "../Effects"
|
||||
import { DropGenerator, DropPromise } from "./Drop"
|
||||
import { IpAddress, IPV6_LINK_LOCAL } from "./ip"
|
||||
import { deepEqual } from "./deepEqual"
|
||||
import { once } from "./once"
|
||||
|
||||
export type UrlString = string
|
||||
export type HostId = string
|
||||
@@ -255,6 +256,21 @@ export const filledAddress = (
|
||||
function filledAddressFromHostnames<F extends Filter>(
|
||||
hostnames: HostnameInfo[],
|
||||
): Filled<F> & AddressInfo {
|
||||
const getNonLocal = once(() =>
|
||||
filledAddressFromHostnames<typeof nonLocalFilter & F>(
|
||||
filterRec(hostnames, nonLocalFilter, false),
|
||||
),
|
||||
)
|
||||
const getPublic = once(() =>
|
||||
filledAddressFromHostnames<typeof publicFilter & F>(
|
||||
filterRec(hostnames, publicFilter, false),
|
||||
),
|
||||
)
|
||||
const getOnion = once(() =>
|
||||
filledAddressFromHostnames<typeof onionFilter & F>(
|
||||
filterRec(hostnames, onionFilter, false),
|
||||
),
|
||||
)
|
||||
return {
|
||||
...addressInfo,
|
||||
hostnames,
|
||||
@@ -273,19 +289,13 @@ export const filledAddress = (
|
||||
)
|
||||
},
|
||||
get nonLocal(): Filled<typeof nonLocalFilter & F> {
|
||||
return filledAddressFromHostnames<typeof nonLocalFilter & F>(
|
||||
filterRec(hostnames, nonLocalFilter, false),
|
||||
)
|
||||
return getNonLocal()
|
||||
},
|
||||
get public(): Filled<typeof publicFilter & F> {
|
||||
return filledAddressFromHostnames<typeof publicFilter & F>(
|
||||
filterRec(hostnames, publicFilter, false),
|
||||
)
|
||||
return getPublic()
|
||||
},
|
||||
get onion(): Filled<typeof onionFilter & F> {
|
||||
return filledAddressFromHostnames<typeof onionFilter & F>(
|
||||
filterRec(hostnames, onionFilter, false),
|
||||
)
|
||||
return getOnion()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ export { getDefaultString } from "./getDefaultString"
|
||||
export * from "./ip"
|
||||
|
||||
/// Not being used, but known to be browser compatible
|
||||
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
|
||||
export {
|
||||
GetServiceInterface,
|
||||
getServiceInterface,
|
||||
filledAddress,
|
||||
} from "./getServiceInterface"
|
||||
export { getServiceInterfaces } from "./getServiceInterfaces"
|
||||
export { once } from "./once"
|
||||
export { asError } from "./asError"
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export type AccessType =
|
||||
| 'tor'
|
||||
| 'mdns'
|
||||
| 'localhost'
|
||||
| 'ipv4'
|
||||
| 'ipv6'
|
||||
| 'domain'
|
||||
| 'wan-ipv4'
|
||||
|
||||
export type WorkspaceConfig = {
|
||||
gitHash: string
|
||||
useMocks: boolean
|
||||
@@ -8,7 +17,7 @@ export type WorkspaceConfig = {
|
||||
version: string
|
||||
}
|
||||
mocks: {
|
||||
maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet'
|
||||
maskAs: AccessType
|
||||
maskAsHttps: boolean
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
|
||||
@@ -206,119 +206,67 @@ export class InterfaceService {
|
||||
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(ui: T.ServiceInterface, host: T.Host): string {
|
||||
const hostnameInfos = this.hostnameInfo(ui, host)
|
||||
const addresses = utils.filledAddress(host, ui.addressInfo)
|
||||
|
||||
if (!hostnameInfos.length) return ''
|
||||
if (!addresses.hostnames.length) return ''
|
||||
|
||||
const addressInfo = ui.addressInfo
|
||||
const username = addressInfo.username ? addressInfo.username + '@' : ''
|
||||
const suffix = addressInfo.suffix || ''
|
||||
const url = new URL(`https://${username}placeholder${suffix}`)
|
||||
const use = (hostname: {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}) => {
|
||||
url.hostname = hostname.value
|
||||
const useSsl =
|
||||
hostname.port && hostname.sslPort
|
||||
? this.config.isHttps()
|
||||
: !!hostname.sslPort
|
||||
url.protocol = useSsl
|
||||
? `${addressInfo.sslScheme || 'https'}:`
|
||||
: `${addressInfo.scheme || 'http'}:`
|
||||
const port = useSsl ? hostname.sslPort : hostname.port
|
||||
const omitPort = useSsl
|
||||
? ui.addressInfo.sslScheme === 'https' && port === 443
|
||||
: ui.addressInfo.scheme === 'http' && port === 80
|
||||
if (!omitPort && port) url.port = String(port)
|
||||
}
|
||||
const useFirst = (
|
||||
hostnames: (
|
||||
| {
|
||||
value: string
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
| undefined
|
||||
)[],
|
||||
) => {
|
||||
const first = hostnames.find(h => h)
|
||||
if (first) {
|
||||
use(first)
|
||||
}
|
||||
return !!first
|
||||
const publicDomains = addresses.filter({
|
||||
kind: 'domain',
|
||||
visibility: 'public',
|
||||
})
|
||||
const tor = addresses.filter({ kind: 'onion' })
|
||||
const wanIp = addresses.filter({ kind: 'ipv4', visibility: 'public' })
|
||||
const bestPublic = [publicDomains, tor, wanIp].flatMap(h =>
|
||||
h.format('urlstring'),
|
||||
)[0]
|
||||
const privateDomains = addresses.filter({
|
||||
kind: 'domain',
|
||||
visibility: 'private',
|
||||
})
|
||||
const mdns = addresses.filter({ kind: 'mdns' })
|
||||
const bestPrivate = [privateDomains, mdns].flatMap(h =>
|
||||
h.format('urlstring'),
|
||||
)[0]
|
||||
|
||||
let matching
|
||||
let onLan = false
|
||||
switch (this.config.accessType) {
|
||||
case 'ipv4':
|
||||
matching = addresses.nonLocal
|
||||
.filter({
|
||||
kind: 'ipv4',
|
||||
predicate: h => h.hostname.value === this.config.hostname,
|
||||
})
|
||||
.format('urlstring')[0]
|
||||
onLan = true
|
||||
break
|
||||
case 'ipv6':
|
||||
matching = addresses.nonLocal
|
||||
.filter({
|
||||
kind: 'ipv6',
|
||||
predicate: h => h.hostname.value === this.config.hostname,
|
||||
})
|
||||
.format('urlstring')[0]
|
||||
break
|
||||
case 'localhost':
|
||||
matching = addresses
|
||||
.filter({ kind: 'localhost' })
|
||||
.format('urlstring')[0]
|
||||
onLan = true
|
||||
break
|
||||
case 'tor':
|
||||
matching = tor.format('urlstring')[0]
|
||||
break
|
||||
case 'mdns':
|
||||
matching = mdns.format('urlstring')[0]
|
||||
onLan = true
|
||||
break
|
||||
}
|
||||
|
||||
const ipHostnames = hostnameInfos
|
||||
.filter(h => h.kind === 'ip')
|
||||
.map(h => h.hostname) as T.IpHostname[]
|
||||
const domainHostname = ipHostnames
|
||||
.filter(h => h.kind === 'domain')
|
||||
.map(h => h as T.IpHostname & { kind: 'domain' })
|
||||
.map(h => ({
|
||||
value: h.value,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const wanIpHostname = hostnameInfos
|
||||
.filter(h => h.kind === 'ip' && h.public && h.hostname.kind !== 'domain')
|
||||
.map(h => h.hostname as Exclude<T.IpHostname, { kind: 'domain' }>)
|
||||
.map(h => ({
|
||||
value: h.value,
|
||||
sslPort: h.sslPort,
|
||||
port: h.port,
|
||||
}))[0]
|
||||
const onionHostname = hostnameInfos
|
||||
.filter(h => h.kind === 'onion')
|
||||
.map(h => h as T.HostnameInfo & { kind: 'onion' })
|
||||
.map(h => ({
|
||||
value: h.hostname.value,
|
||||
sslPort: h.hostname.sslPort,
|
||||
port: h.hostname.port,
|
||||
}))[0]
|
||||
const localHostname = ipHostnames
|
||||
.filter(h => h.kind === 'local')
|
||||
.map(h => h as T.IpHostname & { kind: 'local' })
|
||||
.map(h => ({ value: h.value, sslPort: h.sslPort, port: h.port }))[0]
|
||||
|
||||
if (this.config.isClearnet()) {
|
||||
if (
|
||||
!useFirst([domainHostname, wanIpHostname, onionHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.config.isTor()) {
|
||||
if (
|
||||
!useFirst([onionHostname, domainHostname, wanIpHostname, localHostname])
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
} else if (this.config.isIpv6()) {
|
||||
const ipv6Hostname = ipHostnames.find(h => h.kind === 'ipv6') as {
|
||||
kind: 'ipv6'
|
||||
value: string
|
||||
scopeId: number
|
||||
port: number | null
|
||||
sslPort: number | null
|
||||
}
|
||||
|
||||
if (!useFirst([ipv6Hostname, localHostname])) {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// ipv4 or .local or localhost
|
||||
|
||||
if (!localHostname) return ''
|
||||
|
||||
use({
|
||||
value: this.config.hostname,
|
||||
port: localHostname.port,
|
||||
sslPort: localHostname.sslPort,
|
||||
})
|
||||
}
|
||||
|
||||
return url.href
|
||||
if (matching) return matching
|
||||
if (onLan && bestPrivate) return bestPrivate
|
||||
if (bestPublic) return bestPublic
|
||||
return ''
|
||||
}
|
||||
|
||||
private hostnameInfo(
|
||||
@@ -330,7 +278,7 @@ export class InterfaceService {
|
||||
return (
|
||||
hostnameInfo?.filter(
|
||||
h =>
|
||||
this.config.isLocalhost() ||
|
||||
this.config.accessType === 'localhost' ||
|
||||
!(
|
||||
h.kind === 'ip' &&
|
||||
((h.hostname.kind === 'ipv6' &&
|
||||
|
||||
@@ -92,7 +92,7 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
],
|
||||
})
|
||||
export default class SideloadComponent {
|
||||
readonly isTor = inject(ConfigService).isTor()
|
||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
|
||||
file: File | null = null
|
||||
readonly package = signal<MarketplacePkgSideload | null>(null)
|
||||
|
||||
@@ -245,7 +245,7 @@ export default class SystemGeneralComponent {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly isTor = inject(ConfigService).isTor()
|
||||
private readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly injector = inject(INJECTOR)
|
||||
|
||||
@@ -31,6 +31,6 @@ import { i18nPipe } from '@start9labs/shared'
|
||||
imports: [TuiLabel, FormsModule, TuiCheckbox, i18nPipe],
|
||||
})
|
||||
export class SystemWipeComponent {
|
||||
readonly isTor = inject(ConfigService).isTor()
|
||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
readonly component = inject(SystemGeneralComponent)
|
||||
}
|
||||
|
||||
@@ -242,6 +242,7 @@ export namespace Mock {
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
hardwareAcceleration: false,
|
||||
}
|
||||
|
||||
export const MockManifestLnd: T.Manifest = {
|
||||
@@ -300,6 +301,7 @@ export namespace Mock {
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
hardwareAcceleration: false,
|
||||
}
|
||||
|
||||
export const MockManifestBitcoinProxy: T.Manifest = {
|
||||
@@ -351,6 +353,7 @@ export namespace Mock {
|
||||
arch: null,
|
||||
ram: null,
|
||||
},
|
||||
hardwareAcceleration: false,
|
||||
}
|
||||
|
||||
export const BitcoinDep: T.DependencyMetadata = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable, DOCUMENT } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { AccessType, WorkspaceConfig } from '@start9labs/shared'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
|
||||
const {
|
||||
@@ -29,53 +29,29 @@ export class ConfigService {
|
||||
supportsWebSockets = !!window.WebSocket
|
||||
defaultRegistry = defaultRegistry
|
||||
|
||||
isTor(): boolean {
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost' || this.hostname === '127.0.0.1'
|
||||
private getAccessType = utils.once(() => {
|
||||
if (useMocks) return mocks.maskAs
|
||||
if (this.hostname === 'localhost') return 'localhost'
|
||||
if (this.hostname.endsWith('.onion')) return 'tor'
|
||||
if (this.hostname.endsWith('.local')) return 'mdns'
|
||||
let ip = null
|
||||
try {
|
||||
ip = utils.IpAddress.parse(this.hostname.replace(/[\[\]]/g, ''))
|
||||
} catch {}
|
||||
if (ip) {
|
||||
if (utils.IPV4_LOOPBACK.contains(ip) || utils.IPV6_LOOPBACK.contains(ip))
|
||||
return 'localhost'
|
||||
if (ip.isIpv4()) return ip.isPublic() ? 'wan-ipv4' : 'ipv4'
|
||||
return 'ipv6'
|
||||
}
|
||||
return 'domain'
|
||||
})
|
||||
get accessType(): AccessType {
|
||||
return this.getAccessType()
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
|
||||
}
|
||||
|
||||
private isLocal(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'local'
|
||||
: this.hostname.endsWith('.local')
|
||||
}
|
||||
|
||||
private isLanIpv4(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv4'
|
||||
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname) &&
|
||||
(this.hostname.startsWith('192.168.') ||
|
||||
this.hostname.startsWith('10.') ||
|
||||
(this.hostname.startsWith('172.') &&
|
||||
!![this.hostname.split('.').map(Number)[1] || NaN].filter(
|
||||
n => n >= 16 && n < 32,
|
||||
).length))
|
||||
}
|
||||
|
||||
isIpv6(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'ipv6'
|
||||
: new RegExp(utils.Patterns.ipv6.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isClearnet(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'clearnet'
|
||||
: this.isHttps() &&
|
||||
!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isLanIpv4() &&
|
||||
!this.isIpv6()
|
||||
return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType)
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user