Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major

This commit is contained in:
Matt Hill
2025-02-08 19:19:35 -07:00
parent 95cad7bdd9
commit 95722802dc
206 changed files with 11364 additions and 4104 deletions

View File

@@ -1615,7 +1615,43 @@ export module Mock {
},
internal: {
name: 'Internal',
spec: ISB.InputSpec.of({}),
spec: ISB.InputSpec.of({
listitems: ISB.Value.list(
ISB.List.text(
{
name: 'RPC Allowed IPs',
minLength: 1,
maxLength: 10,
default: ['192.168.1.1'],
description:
'external ip addresses that are authorized to access your Bitcoin node',
warning:
'Any IP you allow here will have RPC access to your Bitcoin node.',
},
{
patterns: [
{
regex:
'((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}:){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]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
description:
'must be a valid ipv4, ipv6, or domain name',
},
],
},
),
),
name: ISB.Value.text({
name: 'Name',
required: false,
default: null,
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'Must contain only letters.',
},
],
}),
}),
},
external: {
name: 'External',
@@ -1763,6 +1799,10 @@ export module Mock {
},
'bitcoin-node': {
selection: 'internal',
value: {
listitems: ['192.168.1.1', '192.1681.23'],
name: 'Matt',
},
},
port: 20,
rpcallowip: undefined,
@@ -1797,6 +1837,15 @@ export module Mock {
hasInput: true,
group: null,
},
rpc: {
name: 'Set RPC',
description: 'Create RPC Credentials',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: {
name: 'View Properties',
description: 'view important information about Bitcoin',
@@ -1820,7 +1869,6 @@ export module Mock {
serviceInterfaces: {
ui: {
id: 'ui',
hasPrimary: false,
masked: false,
name: 'Web UI',
description:
@@ -1837,7 +1885,6 @@ export module Mock {
},
rpc: {
id: 'rpc',
hasPrimary: false,
masked: false,
name: 'RPC',
description:
@@ -1854,7 +1901,6 @@ export module Mock {
},
p2p: {
id: 'p2p',
hasPrimary: true,
masked: false,
name: 'P2P',
description:
@@ -1873,9 +1919,23 @@ export module Mock {
currentDependencies: {},
hosts: {
abcdefg: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
80: {
enabled: true,
net: {
assignedPort: 80,
assignedSslPort: 443,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
80: [
{
@@ -1928,7 +1988,8 @@ export module Mock {
public: false,
hostname: {
kind: 'ipv6',
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
scopeId: 2,
port: null,
sslPort: 1234,
},
@@ -1939,7 +2000,8 @@ export module Mock {
public: false,
hostname: {
kind: 'ipv6',
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]',
scopeId: 3,
port: null,
sslPort: 1234,
},
@@ -1956,17 +2018,45 @@ export module Mock {
},
},
bcdefgh: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
8332: {
enabled: true,
net: {
assignedPort: 8332,
assignedSslPort: null,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 8332,
secure: { ssl: false },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
8332: [],
},
},
cdefghi: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
8333: {
enabled: true,
net: {
assignedPort: 8333,
assignedSslPort: null,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 8333,
secure: { ssl: false },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
8333: [],
},
@@ -2016,7 +2106,6 @@ export module Mock {
serviceInterfaces: {
ui: {
id: 'ui',
hasPrimary: false,
masked: false,
name: 'Web UI',
description: 'A launchable web app for Bitcoin Proxy',
@@ -2061,11 +2150,29 @@ export module Mock {
status: {
main: 'stopped',
},
actions: {},
actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
},
serviceInterfaces: {
grpc: {
id: 'grpc',
hasPrimary: false,
masked: false,
name: 'GRPC',
description:
@@ -2082,7 +2189,6 @@ export module Mock {
},
lndconnect: {
id: 'lndconnect',
hasPrimary: false,
masked: true,
name: 'LND Connect',
description:
@@ -2099,7 +2205,6 @@ export module Mock {
},
p2p: {
id: 'p2p',
hasPrimary: true,
masked: false,
name: 'P2P',
description:
@@ -2136,6 +2241,24 @@ export module Mock {
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
config: {
active: true,
request: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
request: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': {
active: true,
request: {
@@ -2147,10 +2270,24 @@ export module Mock {
kind: 'partial',
value: {
color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: {
rpcuser: 'lnd',
},
testnet: false,
},
},
},

View File

@@ -2,7 +2,7 @@ import {
DomainInfo,
NetworkStrategy,
} from 'src/app/services/patch-db/data-model'
import { FetchLogsReq, FetchLogsRes, Log } from '@start9labs/shared'
import { FetchLogsReq, FetchLogsRes } from '@start9labs/shared'
import { Dump } from 'patch-db-client'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo } from '@start9labs/shared'
@@ -118,6 +118,17 @@ export module RR {
} // server.proxy.set-outbound
export type SetOsOutboundProxyRes = null
// smtp
export type SetSMTPReq = T.SmtpValue // server.set-smtp
export type SetSMTPRes = null
export type ClearSMTPReq = {} // server.clear-smtp
export type ClearSMTPRes = null
export type TestSMTPReq = SetSMTPReq & { to: string } // server.test-smtp
export type TestSMTPRes = null
// sessions
export type GetSessionsReq = {} // sessions.list
@@ -326,18 +337,83 @@ export module RR {
export type CreateBackupRes = null
// package
// @TODO Matt I just copy-pasted those types from minor
export type GetPackageLogsReq = {
id: string
before: boolean
cursor?: string
limit?: number
} // package.logs
export type GetPackageLogsRes = {
entries: Log[]
startCursor?: string
endCursor?: string
export type InitAcmeReq = {
provider: 'letsencrypt' | 'letsencrypt-staging' | string
contact: string[]
}
export type InitAcmeRes = null
export type RemoveAcmeReq = {
provider: string
}
export type RemoveAcmeRes = null
export type AddTorKeyReq = {
// net.tor.key.add
key: string
}
export type GenerateTorKeyReq = {} // net.tor.key.generate
export type AddTorKeyRes = string // onion address without .onion suffix
export type ServerBindingSetPublicReq = {
// server.host.binding.set-public
internalPort: number
public: boolean | null // default true
}
export type BindingSetPublicRes = null
export type ServerAddOnionReq = {
// server.host.address.onion.add
onion: string // address *with* .onion suffix
}
export type AddOnionRes = null
export type ServerRemoveOnionReq = ServerAddOnionReq // server.host.address.onion.remove
export type RemoveOnionRes = null
export type ServerAddDomainReq = {
// server.host.address.domain.add
domain: string // FQDN
private: boolean
acme: string | null // "letsencrypt" | "letsencrypt-staging" | Url | null
}
export type ServerAddDomainRes = null
export type ServerRemoveDomainReq = {
// server.host.address.domain.remove
domain: string // FQDN
}
export type RemoveDomainRes = null
export type PkgBindingSetPublicReq = ServerBindingSetPublicReq & {
// package.host.binding.set-public
package: T.PackageId // string
host: T.HostId // string
}
export type PkgAddOnionReq = ServerAddOnionReq & {
// package.host.address.onion.add
package: T.PackageId // string
host: T.HostId // string
}
export type PkgRemoveOnionReq = PkgAddOnionReq // package.host.address.onion.remove
export type PkgAddDomainReq = ServerAddDomainReq & {
// package.host.address.domain.add
package: T.PackageId // string
host: T.HostId // string
}
export type PkgRemoveDomainReq = ServerRemoveDomainReq & {
// package.host.address.domain.remove
package: T.PackageId // string
host: T.HostId // string
}
export type GetPackageLogsReq = GetServerLogsReq & { id: string } // package.logs
export type GetPackageLogsRes = GetServerLogsRes
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
export type FollowPackageLogsRes = FollowServerLogsRes
@@ -601,10 +677,10 @@ export type ServerNotification<T extends number> = {
export type NotificationData<T> = T extends 0
? null
: T extends 1
? BackupReport
: T extends 2
? string
: any
? BackupReport
: T extends 2
? string
: any
export type BackupReport = {
server: {

View File

@@ -7,6 +7,7 @@ import { RPCOptions } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { Observable } from 'rxjs'
import { BackupTargetType, RR } from './api.types'
import { WebSocketSubject } from 'rxjs/webSocket'
export abstract class ApiService {
// http
@@ -32,7 +33,7 @@ export abstract class ApiService {
abstract openWebsocket$<T>(
guid: string,
config?: RR.WebsocketConfig<T>,
): Observable<T>
): WebSocketSubject<T>
// state
@@ -137,6 +138,14 @@ export abstract class ApiService {
params: RR.SetOsOutboundProxyReq,
): Promise<RR.SetOsOutboundProxyRes>
// smtp
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
abstract clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes>
abstract testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes>
// marketplace URLs
abstract registryRequest<T>(
@@ -221,14 +230,6 @@ export abstract class ApiService {
abstract deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes>
// email
abstract testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes>
abstract configureEmail(
params: RR.ConfigureEmailReq,
): Promise<RR.ConfigureEmailRes>
// ssh
abstract getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes>
@@ -342,4 +343,48 @@ export abstract class ApiService {
abstract setServiceOutboundProxy(
params: RR.SetServiceOutboundProxyReq,
): Promise<RR.SetServiceOutboundProxyRes>
abstract initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes>
abstract removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes>
abstract addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes>
abstract generateTorKey(
params: RR.GenerateTorKeyReq,
): Promise<RR.AddTorKeyRes>
abstract serverBindingSetPubic(
params: RR.ServerBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes>
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
abstract serverRemoveOnion(
params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes>
abstract serverAddDomain(
params: RR.ServerAddDomainReq,
): Promise<RR.AddDomainRes>
abstract serverRemoveDomain(
params: RR.ServerRemoveDomainReq,
): Promise<RR.RemoveDomainRes>
abstract pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes>
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
abstract pkgRemoveOnion(
params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes>
abstract pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes>
abstract pkgRemoveDomain(
params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes>
}

View File

@@ -11,7 +11,7 @@ import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source'
import { ApiService } from './embassy-api.service'
import { BackupTargetType, RR } from './api.types'
import { ConfigService } from '../config.service'
import { webSocket } from 'rxjs/webSocket'
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'
import { Observable, filter, firstValueFrom } from 'rxjs'
import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
@@ -95,7 +95,7 @@ export class LiveApiService extends ApiService {
openWebsocket$<T>(
guid: string,
config: RR.WebsocketConfig<T> = {},
): Observable<T> {
): WebSocketSubject<T> {
const { location } = this.document.defaultView!
const protocol = location.protocol === 'http:' ? 'ws' : 'wss'
const host = location.host
@@ -458,16 +458,18 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'wifi.delete', params })
}
// email
// smtp
async testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes> {
return this.rpcRequest({ method: 'email.test', params })
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
return this.rpcRequest({ method: 'server.set-smtp', params })
}
async configureEmail(
params: RR.ConfigureEmailReq,
): Promise<RR.ConfigureEmailRes> {
return this.rpcRequest({ method: 'email.configure', params })
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
return this.rpcRequest({ method: 'server.clear-smtp', params })
}
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
return this.rpcRequest({ method: 'server.test-smtp', params })
}
// ssh
@@ -640,6 +642,118 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.proxy.set-outbound', params })
}
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
return this.rpcRequest({
method: 'net.acme.delete',
params,
})
}
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
return this.rpcRequest({
method: 'net.acme.init',
params,
})
}
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
return this.rpcRequest({
method: 'net.tor.key.add',
params,
})
}
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
return this.rpcRequest({
method: 'net.tor.key.generate',
params,
})
}
async serverBindingSetPubic(
params: RR.ServerBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
return this.rpcRequest({
method: 'server.host.binding.set-public',
params,
})
}
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
return this.rpcRequest({
method: 'server.host.address.onion.add',
params,
})
}
async serverRemoveOnion(
params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
return this.rpcRequest({
method: 'server.host.address.onion.remove',
params,
})
}
async serverAddDomain(
params: RR.ServerAddDomainReq,
): Promise<RR.AddDomainRes> {
return this.rpcRequest({
method: 'server.host.address.domain.add',
params,
})
}
async serverRemoveDomain(
params: RR.ServerRemoveDomainReq,
): Promise<RR.RemoveDomainRes> {
return this.rpcRequest({
method: 'server.host.address.domain.remove',
params,
})
}
async pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
return this.rpcRequest({
method: 'package.host.binding.set-public',
params,
})
}
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
return this.rpcRequest({
method: 'package.host.address.onion.add',
params,
})
}
async pkgRemoveOnion(
params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
return this.rpcRequest({
method: 'package.host.address.onion.remove',
params,
})
}
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
return this.rpcRequest({
method: 'package.host.address.domain.add',
params,
})
}
async pkgRemoveDomain(
params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> {
return this.rpcRequest({
method: 'package.host.address.domain.remove',
params,
})
}
private async rpcRequest<T>(
options: RPCOptions,
urlOverride?: string,

View File

@@ -19,16 +19,7 @@ import {
} from 'src/app/services/patch-db/data-model'
import { BackupTargetType, RR } from './api.types'
import { Mock } from './api.fixures'
import {
from,
interval,
map,
Observable,
shareReplay,
startWith,
Subject,
tap,
} from 'rxjs'
import { from, interval, map, shareReplay, startWith, Subject, tap } from 'rxjs'
import { mockPatchData } from './mock-patch'
import { AuthService } from '../auth.service'
import { T } from '@start9labs/start-sdk'
@@ -38,6 +29,8 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
import { WebSocketSubject } from 'rxjs/webSocket'
import { toAcmeUrl } from 'src/app/utils/acme'
const PROGRESS: T.FullProgress = {
overall: {
@@ -113,11 +106,11 @@ export class MockApiService extends ApiService {
openWebsocket$<T>(
guid: string,
config: RR.WebsocketConfig<T> = {},
): Observable<T> {
): WebSocketSubject<T> {
if (guid === 'db-guid') {
return this.mockWsSource$.pipe<any>(
shareReplay({ bufferSize: 1, refCount: true }),
)
) as WebSocketSubject<T>
} else if (guid === 'logs-guid') {
return interval(50).pipe<any>(
map((_, index) => {
@@ -126,16 +119,16 @@ export class MockApiService extends ApiService {
if (index === 100) throw new Error('HAAHHA')
return Mock.ServerLogs[0]
}),
)
) as WebSocketSubject<T>
} else if (guid === 'init-progress-guid') {
return from(this.initProgress()).pipe(
startWith(PROGRESS),
) as Observable<T>
) as WebSocketSubject<T>
} else if (guid === 'sideload-progress-guid') {
config.openObserver?.next(new Event(''))
return from(this.initProgress()).pipe(
startWith(PROGRESS),
) as Observable<T>
) as WebSocketSubject<T>
} else {
throw new Error('invalid guid type')
}
@@ -768,16 +761,9 @@ export class MockApiService extends ApiService {
return null
}
// email
// smtp
async testEmail(params: RR.TestEmailReq): Promise<RR.TestEmailRes> {
await pauseFor(2000)
return null
}
async configureEmail(
params: RR.ConfigureEmailReq,
): Promise<RR.ConfigureEmailRes> {
async setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes> {
await pauseFor(2000)
const patch = [
{
@@ -791,6 +777,25 @@ export class MockApiService extends ApiService {
return null
}
async clearSmtp(params: RR.ClearSMTPReq): Promise<RR.ClearSMTPRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/serverInfo/smtp',
value: null,
},
]
this.mockRevision(patch)
return null
}
async testSmtp(params: RR.TestSMTPReq): Promise<RR.TestSMTPRes> {
await pauseFor(2000)
return null
}
// ssh
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
@@ -1344,6 +1349,283 @@ export class MockApiService extends ApiService {
return null
}
async initAcme(params: RR.InitAcmeReq): Promise<RR.InitAcmeRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.ADD,
path: `/serverInfo/acme`,
value: {
[toAcmeUrl(params.provider)]: { contact: params.contact },
},
},
]
this.mockRevision(patch)
return null
}
async removeAcme(params: RR.RemoveAcmeReq): Promise<RR.RemoveAcmeRes> {
await pauseFor(2000)
const regex = new RegExp('/', 'g')
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/serverInfo/acme/${params.provider.replace(regex, '~1')}`,
},
]
this.mockRevision(patch)
return null
}
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
await pauseFor(2000)
return 'vanityabcdefghijklmnop'
}
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
await pauseFor(2000)
return 'abcdefghijklmnopqrstuv'
}
async serverBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: `/serverInfo/host/bindings/${params.internalPort}/net/public`,
value: params.public,
},
]
this.mockRevision(patch)
return null
}
async serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
op: PatchOp.ADD,
path: `/serverInfo/host/onions/0`,
value: params.onion,
},
{
op: PatchOp.ADD,
path: `/serverInfo/host/hostnameInfo/80/0`,
value: {
kind: 'onion',
hostname: {
port: 80,
sslPort: 443,
value: params.onion,
},
},
},
]
this.mockRevision(patch)
return null
}
async serverRemoveOnion(
params: RR.ServerRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/serverInfo/host/onions/0`,
},
{
op: PatchOp.REMOVE,
path: `/serverInfo/host/hostnameInfo/80/-1`,
},
]
this.mockRevision(patch)
return null
}
async serverAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
op: PatchOp.ADD,
path: `/serverInfo/host/domains`,
value: {
[params.domain]: { public: !params.private, acme: params.acme },
},
},
{
op: PatchOp.ADD,
path: `/serverInfo/host/hostnameInfo/80/0`,
value: {
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'domain',
domain: params.domain,
subdomain: null,
port: null,
sslPort: 443,
},
},
},
]
this.mockRevision(patch)
return null
}
async serverRemoveDomain(
params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/serverInfo/host/domains/${params.domain}`,
},
{
op: PatchOp.REMOVE,
path: `/serverInfo/host/hostnameInfo/80/0`,
},
]
this.mockRevision(patch)
return null
}
async pkgBindingSetPubic(
params: RR.PkgBindingSetPublicReq,
): Promise<RR.BindingSetPublicRes> {
await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/public`,
value: params.public,
},
]
this.mockRevision(patch)
return null
}
async pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
value: params.onion,
},
{
op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
value: {
kind: 'onion',
hostname: {
port: 80,
sslPort: 443,
value: params.onion,
},
},
},
]
this.mockRevision(patch)
return null
}
async pkgRemoveOnion(
params: RR.PkgRemoveOnionReq,
): Promise<RR.RemoveOnionRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/onions/0`,
},
{
op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
},
]
this.mockRevision(patch)
return null
}
async pkgAddDomain(params: RR.PkgAddDomainReq): Promise<RR.AddDomainRes> {
await pauseFor(2000)
const patch: Operation<any>[] = [
{
op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/domains`,
value: {
[params.domain]: { public: !params.private, acme: params.acme },
},
},
{
op: PatchOp.ADD,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
value: {
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'domain',
domain: params.domain,
subdomain: null,
port: null,
sslPort: 443,
},
},
},
]
this.mockRevision(patch)
return null
}
async pkgRemoveDomain(
params: RR.PkgRemoveDomainReq,
): Promise<RR.RemoveDomainRes> {
await pauseFor(2000)
const patch: RemoveOperation[] = [
{
op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/domains/${params.domain}`,
},
{
op: PatchOp.REMOVE,
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
},
]
this.mockRevision(patch)
return null
}
private async initProgress(): Promise<T.FullProgress> {
const progress = JSON.parse(JSON.stringify(PROGRESS))

View File

@@ -1,5 +1,7 @@
import { DataModel } from 'src/app/services/patch-db/data-model'
import { Mock } from './api.fixures'
import { knownACME } from 'src/app/utils/acme'
const version = require('../../../../../../package.json').version
export const mockPatchData: DataModel = {
ui: {
@@ -26,94 +28,49 @@ export const mockPatchData: DataModel = {
ackInstructions: {},
},
serverInfo: {
arch: 'x86_64',
id: 'abcdefgh',
version: '0.3.5.1',
country: 'us',
ui: [
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 1111,
},
},
{
kind: 'onion',
hostname: {
value: 'myveryownspecialtoraddress.onion',
port: 80,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'ipv4',
value: '192.168.1.5',
port: null,
sslPort: 1111,
},
},
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'ipv6',
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
port: null,
sslPort: 1111,
},
},
],
network: {
domains: [],
start9ToSubdomain: null,
wifi: {
enabled: false,
lastRegion: null,
interface: 'test',
ssids: [],
selected: null,
},
wanConfig: {
upnp: false,
forwards: [
{
assigned: 443,
override: null,
target: 443,
error: null,
},
{
assigned: 80,
override: null,
target: 80,
error: null,
},
{
assigned: 8332,
override: null,
target: 8332,
error: null,
},
],
},
proxies: [],
outboundProxy: null,
},
version,
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
networkInterfaces: {
eth0: {
public: false,
ipInfo: {
scopeId: 1,
deviceType: 'ethernet',
subnets: ['10.0.0.2/24'],
wanIp: null,
ntpServers: [],
},
},
wlan0: {
public: false,
ipInfo: {
scopeId: 2,
deviceType: 'wireless',
subnets: [
'10.0.90.12/24',
'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64',
],
wanIp: null,
ntpServers: [],
},
},
},
acme: {
[knownACME[0].url]: {
contact: ['mailto:support@start9.com'],
},
},
unreadNotifications: {
count: 4,
recent: Mock.Notifications,
},
eosVersionCompat: '>=0.3.0 <=0.3.0.1',
// password is asdfasdf
passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
packageVersionCompat: '>=0.3.0 <=0.3.6',
postInitMigrationTodos: [],
statusInfo: {
currentBackup: null,
updated: false,
@@ -121,6 +78,109 @@ export const mockPatchData: DataModel = {
restarting: false,
shuttingDown: false,
},
hostname: 'random-words',
host: {
bindings: {
80: {
enabled: true,
net: {
assignedPort: null,
assignedSslPort: 443,
public: false,
},
options: {
preferredExternalPort: 80,
addSsl: {
preferredExternalPort: 443,
alpn: { specified: ['http/1.1', 'h2'] },
},
secure: null,
},
},
},
domains: {},
onions: ['myveryownspecialtoraddress'],
hostnameInfo: {
80: [
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.1',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.2',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
scopeId: 2,
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
scopeId: 3,
port: null,
sslPort: 443,
},
},
{
kind: 'onion',
hostname: {
value: 'myveryownspecialtoraddress.onion',
port: 80,
sslPort: 443,
},
},
],
},
},
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
ntpSynced: false,
@@ -171,6 +231,15 @@ export const mockPatchData: DataModel = {
hasInput: true,
group: null,
},
rpc: {
name: 'Set RPC',
description: 'Create RPC Credentials',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: {
name: 'View Properties',
description: 'view important information about Bitcoin',
@@ -194,7 +263,6 @@ export const mockPatchData: DataModel = {
serviceInterfaces: {
ui: {
id: 'ui',
hasPrimary: false,
masked: false,
name: 'Web UI',
description:
@@ -211,7 +279,6 @@ export const mockPatchData: DataModel = {
},
rpc: {
id: 'rpc',
hasPrimary: false,
masked: false,
name: 'RPC',
description:
@@ -228,7 +295,6 @@ export const mockPatchData: DataModel = {
},
p2p: {
id: 'p2p',
hasPrimary: true,
masked: false,
name: 'P2P',
description:
@@ -247,9 +313,23 @@ export const mockPatchData: DataModel = {
currentDependencies: {},
hosts: {
abcdefg: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
80: {
enabled: true,
net: {
assignedPort: 80,
assignedSslPort: 443,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 443,
secure: { ssl: true },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
80: [
{
@@ -302,7 +382,8 @@ export const mockPatchData: DataModel = {
public: false,
hostname: {
kind: 'ipv6',
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
scopeId: 2,
port: null,
sslPort: 1234,
},
@@ -313,7 +394,8 @@ export const mockPatchData: DataModel = {
public: false,
hostname: {
kind: 'ipv6',
value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
scopeId: 3,
port: null,
sslPort: 1234,
},
@@ -330,17 +412,45 @@ export const mockPatchData: DataModel = {
},
},
bcdefgh: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
8332: {
enabled: true,
net: {
assignedPort: 8332,
assignedSslPort: null,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 8332,
secure: { ssl: false },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
8332: [],
},
},
cdefghi: {
kind: 'multi',
bindings: [],
addresses: [],
bindings: {
8333: {
enabled: true,
net: {
assignedPort: 8333,
assignedSslPort: null,
public: false,
},
options: {
addSsl: null,
preferredExternalPort: 8333,
secure: { ssl: false },
},
},
},
onions: [],
domains: {},
hostnameInfo: {
8333: [],
},
@@ -388,11 +498,29 @@ export const mockPatchData: DataModel = {
status: {
main: 'stopped',
},
actions: {},
actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
},
serviceInterfaces: {
grpc: {
id: 'grpc',
hasPrimary: false,
masked: false,
name: 'GRPC',
description:
@@ -409,7 +537,6 @@ export const mockPatchData: DataModel = {
},
lndconnect: {
id: 'lndconnect',
hasPrimary: false,
masked: true,
name: 'LND Connect',
description:
@@ -426,7 +553,6 @@ export const mockPatchData: DataModel = {
},
p2p: {
id: 'p2p',
hasPrimary: true,
masked: false,
name: 'P2P',
description:
@@ -464,6 +590,24 @@ export const mockPatchData: DataModel = {
developerKey: 'developer-key',
outboundProxy: null,
requestedActions: {
config: {
active: true,
request: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
request: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': {
active: true,
request: {
@@ -475,10 +619,24 @@ export const mockPatchData: DataModel = {
kind: 'partial',
value: {
color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: {
rpcuser: 'lnd',
},
testnet: false,
},
},
},

View File

@@ -1,8 +1,8 @@
import { DOCUMENT } from '@angular/common'
import { Inject, Injectable } from '@angular/core'
import { WorkspaceConfig } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { T, utils } from '@start9labs/start-sdk'
import { PackageDataEntry } from './patch-db/data-model'
const {
gitHash,
@@ -28,6 +28,7 @@ export class ConfigService {
api = api
marketplace = marketplace
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
supportsWebSockets = !!window.WebSocket
isTor(): boolean {
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
@@ -40,35 +41,54 @@ export class ConfigService {
}
isLocalhost(): boolean {
return (
this.hostname === 'localhost' ||
(useMocks && mocks.maskAs === 'localhost')
)
return useMocks
? mocks.maskAs === 'localhost'
: this.hostname === 'localhost' || this.hostname === '127.0.0.1'
}
isIpv4(): boolean {
return isValidIpv4(this.hostname) || (useMocks && mocks.maskAs === 'ipv4')
return useMocks
? mocks.maskAs === 'ipv4'
: new RegExp(utils.Patterns.ipv4.regex).test(this.hostname)
}
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]].filter(
n => n >= 16 && n < 32,
).length))
}
isIpv6(): boolean {
return isValidIpv6(this.hostname) || (useMocks && mocks.maskAs === 'ipv6')
return useMocks
? mocks.maskAs === 'ipv6'
: new RegExp(utils.Patterns.ipv6.regex).test(this.hostname)
}
isClearnet(): boolean {
return (
(useMocks && mocks.maskAs === 'clearnet') ||
(!this.isTor() &&
!this.isLocal() &&
!this.isLocalhost() &&
!this.isIpv4() &&
!this.isIpv6())
)
return useMocks
? mocks.maskAs === 'clearnet'
: this.isHttps() &&
!this.isTor() &&
!this.isLocal() &&
!this.isLocalhost() &&
!this.isLanIpv4() &&
!this.isIpv6()
}
isLanHttp(): boolean {
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
}
isHttps(): boolean {
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
}
isSecure(): boolean {
return window.isSecureContext || this.isTor()
}
@@ -82,46 +102,155 @@ export class ConfigService {
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
launchableAddress(
ui: T.ServiceInterface,
hosts: PackageDataEntry['hosts'],
interfaces: PackageDataEntry['serviceInterfaces'],
hosts: T.Hosts,
): string {
if (
ui.type !== 'ui' ||
(ui.addressInfo.scheme !== 'http' && ui.addressInfo.sslScheme !== 'https')
const ui = Object.values(interfaces).find(
i =>
i.type === 'ui' &&
(i.addressInfo.scheme === 'http' ||
i.addressInfo.sslScheme === 'https'),
)
return ''
const hostnameInfo =
hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort]
if (!ui) return ''
const host = hosts[ui.addressInfo.hostId]
if (!host) return ''
let hostnameInfo = host.hostnameInfo[ui.addressInfo.internalPort]
hostnameInfo = hostnameInfo.filter(
h =>
this.isLocalhost() ||
h.kind !== 'ip' ||
h.hostname.kind !== 'ipv6' ||
!h.hostname.value.startsWith('fe80::'),
)
if (this.isLocalhost()) {
const local = hostnameInfo.find(
h => h.kind === 'ip' && h.hostname.kind === 'local',
)
if (local) {
hostnameInfo.unshift({
kind: 'ip',
networkInterfaceId: 'lo',
public: false,
hostname: {
kind: 'local',
port: local.hostname.port,
sslPort: local.hostname.sslPort,
value: 'localhost',
},
})
}
}
if (!hostnameInfo) return ''
const addressInfo = ui.addressInfo
const scheme = this.isHttps()
? ui.addressInfo.sslScheme === 'https'
? 'https'
: 'http'
: ui.addressInfo.scheme === 'http'
? 'http'
: 'https'
const username = addressInfo.username ? addressInfo.username + '@' : ''
const suffix = addressInfo.suffix || ''
const url = new URL(`${scheme}://${username}placeholder${suffix}`)
const onionHostname = hostnameInfo.find(h => h.kind === 'onion')
?.hostname as T.OnionHostname | undefined
if (this.isTor() && onionHostname) {
url.hostname = onionHostname.value
} else {
const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as
| T.IpHostname
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.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
}
if (!ipHostname) return ''
const ipHostnames = hostnameInfo
.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.domain,
sslPort: h.sslPort,
port: h.port,
}))[0]
const wanIpHostname = hostnameInfo
.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 = hostnameInfo
.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]
url.hostname = this.hostname
url.port = String(ipHostname.sslPort || ipHostname.port)
if (this.isClearnet()) {
if (
!useFirst([domainHostname, wanIpHostname, onionHostname, localHostname])
) {
return ''
}
} else if (this.isTor()) {
if (
!useFirst([onionHostname, domainHostname, wanIpHostname, localHostname])
) {
return ''
}
} else if (this.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.hostname,
port: localHostname.port,
sslPort: localHostname.sslPort,
})
}
return url.href
@@ -130,32 +259,6 @@ export class ConfigService {
getHost(): string {
return this.host
}
private isHttps(): boolean {
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
}
}
export function isValidIpv4(address: string): boolean {
const regexExp =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
return regexExp.test(address)
}
export function isValidIpv6(address: string): boolean {
const regexExp =
/(([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]))/gi
return regexExp.test(address)
}
export function removeProtocol(str: string): string {
if (str.startsWith('http://')) return str.slice(7)
if (str.startsWith('https://')) return str.slice(8)
return str
}
export function removePort(str: string): string {
return str.split(':')[0]
}
export function hasUi(

View File

@@ -124,11 +124,6 @@ export class FormService {
),
listValidators(spec),
)
case 'file':
return this.formBuilder.control(
currentValue || null,
fileValidators(spec),
)
case 'union':
return this.getUnionObject(spec, currentValue)
case 'toggle':
@@ -136,7 +131,7 @@ export class FormService {
return this.formBuilder.control(value)
case 'select':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value)
return this.formBuilder.control(value, [Validators.required])
case 'multiselect':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, multiselectValidators(spec))