mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
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:
@@ -1771,8 +1771,21 @@ export module Mock {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -1857,8 +1870,21 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -1866,8 +1892,21 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
|
||||
@@ -219,6 +219,80 @@ export module RR {
|
||||
|
||||
// package
|
||||
|
||||
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 AddDomainRes = 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 = ServerLogsReq & { id: string } // package.logs
|
||||
export type GetPackageLogsRes = LogsRes
|
||||
|
||||
|
||||
@@ -259,4 +259,48 @@ export abstract class ApiService {
|
||||
): Promise<RR.UninstallPackageRes>
|
||||
|
||||
abstract sideloadPackage(): Promise<RR.SideloadPackageRes>
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
@@ -516,6 +516,118 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -19,16 +19,7 @@ import {
|
||||
import { CifsBackupTarget, RR } from './api.types'
|
||||
import { Mock } from './api.fixures'
|
||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||
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,7 @@ import {
|
||||
MarketplacePkg,
|
||||
} from '@start9labs/marketplace'
|
||||
import { WebSocketSubject } from 'rxjs/webSocket'
|
||||
import { toAcmeUrl } from 'src/app/util/acme'
|
||||
|
||||
const PROGRESS: T.FullProgress = {
|
||||
overall: {
|
||||
@@ -1064,6 +1056,283 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Mock } from './api.fixures'
|
||||
import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets'
|
||||
import { knownACME } from 'src/app/util/acme'
|
||||
const version = require('../../../../../../package.json').version
|
||||
|
||||
export const mockPatchData: DataModel = {
|
||||
@@ -35,19 +36,16 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
serverInfo: {
|
||||
arch: 'x86_64',
|
||||
onionAddress: 'myveryownspecialtoraddress',
|
||||
id: 'abcdefgh',
|
||||
version,
|
||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
torAddress: 'https://myveryownspecialtoraddress.onion',
|
||||
networkInterfaces: {
|
||||
eth0: {
|
||||
public: false,
|
||||
ipInfo: {
|
||||
scopeId: 1,
|
||||
deviceType: 'ethernet',
|
||||
subnets: ['10.0.0.1/24'],
|
||||
subnets: ['10.0.0.2/24'],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
@@ -59,14 +57,18 @@ export const mockPatchData: DataModel = {
|
||||
deviceType: 'wireless',
|
||||
subnets: [
|
||||
'10.0.90.12/24',
|
||||
'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64',
|
||||
'fe80::cd00:0000:0cde:1257:0000:211e:72cd/64',
|
||||
],
|
||||
wanIp: null,
|
||||
ntpServers: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
acme: {},
|
||||
acme: {
|
||||
[Object.keys(knownACME)[0]]: {
|
||||
contact: ['mailto:support@start9.com'],
|
||||
},
|
||||
},
|
||||
unreadNotificationCount: 4,
|
||||
// password is asdfasdf
|
||||
passwordHash:
|
||||
@@ -81,6 +83,108 @@ export const mockPatchData: DataModel = {
|
||||
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,
|
||||
@@ -201,8 +305,21 @@ export const mockPatchData: DataModel = {
|
||||
currentDependencies: {},
|
||||
hosts: {
|
||||
abcdefg: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
80: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 443,
|
||||
secure: { ssl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -257,7 +374,7 @@ export const mockPatchData: DataModel = {
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
@@ -269,7 +386,7 @@ 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,
|
||||
@@ -287,8 +404,21 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8332: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8332,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
@@ -296,8 +426,21 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
kind: 'multi',
|
||||
bindings: [],
|
||||
bindings: {
|
||||
8333: {
|
||||
enabled: true,
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
public: false,
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
preferredExternalPort: 8333,
|
||||
secure: { ssl: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
onions: [],
|
||||
domains: {},
|
||||
hostnameInfo: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { PackageDataEntry } from './patch-db/data-model'
|
||||
|
||||
const {
|
||||
@@ -28,8 +28,7 @@ export class ConfigService {
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
isConsulate = (window as any)['platform'] === 'ios'
|
||||
supportsWebSockets = !!window.WebSocket || this.isConsulate
|
||||
supportsWebSockets = !!window.WebSocket
|
||||
|
||||
isTor(): boolean {
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
@@ -41,10 +40,55 @@ export class ConfigService {
|
||||
: this.hostname.endsWith('.local')
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost' || this.hostname === '127.0.0.1'
|
||||
}
|
||||
|
||||
isIpv4(): boolean {
|
||||
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 useMocks
|
||||
? mocks.maskAs === 'ipv6'
|
||||
: new RegExp(utils.Patterns.ipv6.regex).test(this.hostname)
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
|
||||
}
|
||||
|
||||
isClearnet(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'clearnet'
|
||||
: this.isHttps() &&
|
||||
!this.isTor() &&
|
||||
!this.isLocal() &&
|
||||
!this.isLocalhost() &&
|
||||
!this.isLanIpv4() &&
|
||||
!this.isIpv6()
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
@@ -59,48 +103,154 @@ export class ConfigService {
|
||||
/** ${scheme}://${username}@${host}:${externalPort}${suffix} */
|
||||
launchableAddress(
|
||||
interfaces: PackageDataEntry['serviceInterfaces'],
|
||||
hosts: PackageDataEntry['hosts'],
|
||||
hosts: T.Hosts,
|
||||
): string {
|
||||
const ui = Object.values(interfaces).find(
|
||||
i =>
|
||||
i.type === 'ui' &&
|
||||
(i.addressInfo.scheme === 'http' ||
|
||||
i.addressInfo.sslScheme === 'https'),
|
||||
) // TODO: select if multiple
|
||||
)
|
||||
|
||||
if (!ui) return ''
|
||||
|
||||
const hostnameInfo =
|
||||
hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort]
|
||||
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
|
||||
@@ -109,16 +259,6 @@ export class ConfigService {
|
||||
getHost(): string {
|
||||
return this.host
|
||||
}
|
||||
|
||||
private isLocalhost(): boolean {
|
||||
return useMocks
|
||||
? mocks.maskAs === 'localhost'
|
||||
: this.hostname === 'localhost'
|
||||
}
|
||||
|
||||
private isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
}
|
||||
|
||||
export function hasUi(
|
||||
|
||||
@@ -135,7 +135,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))
|
||||
|
||||
Reference in New Issue
Block a user