mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-02 05:23:14 +00:00
Merge branch 'feat/preferred-port-design' of github.com:Start9Labs/start-os into claude
This commit is contained in:
8487
web/package-lock.json
generated
8487
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.19",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -679,4 +679,17 @@ export default {
|
||||
714: 'Installation abgeschlossen!',
|
||||
715: 'StartOS wurde erfolgreich installiert.',
|
||||
716: 'Weiter zur Einrichtung',
|
||||
717: '',
|
||||
718: '',
|
||||
719: '',
|
||||
720: '',
|
||||
721: '',
|
||||
722: '',
|
||||
723: '',
|
||||
724: '',
|
||||
725: '',
|
||||
726: '',
|
||||
727: '',
|
||||
728: '',
|
||||
729: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -563,7 +563,7 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Requires setting a static IP address for': 591, // this is a partial sentence. An IP address will be added after "for" to complete the sentence.
|
||||
'Ideal for VPN access via': 592, // this is a partial sentence. A connection medium will be added after "via" to complete the sentence.
|
||||
'in your gateway': 593, // this is a partial sentence. It is preceded by an instruction: e.g. "do something" in your gateway. Gateway refers to a router or VPN server.
|
||||
"your router's Wireguard server": 594, // this is a partial sentence. It is preceded by "ideal for access via"
|
||||
"your router's WireGuard server": 594, // this is a partial sentence. It is preceded by "ideal for access via"
|
||||
'Requires port forwarding in gateway': 595,
|
||||
'Requires a DNS record for': 596, // this is a partial sentence. A domain name will be added after "for" to complete the sentence.
|
||||
'that resolves to': 597, // this is a partial sentence. It is preceded by "requires a DNS record for [domain] "
|
||||
@@ -679,4 +679,17 @@ export const ENGLISH: Record<string, number> = {
|
||||
'Installation Complete!': 714,
|
||||
'StartOS has been installed successfully.': 715,
|
||||
'Continue to Setup': 716,
|
||||
'Set Outbound Gateway': 717,
|
||||
'Current': 718,
|
||||
'System default': 719,
|
||||
'Outbound Gateway': 720,
|
||||
'Select the gateway for outbound traffic': 721,
|
||||
'The type of gateway': 722,
|
||||
'Outbound Only': 723,
|
||||
'Set as default outbound': 724,
|
||||
'Route all outbound traffic through this gateway': 725,
|
||||
'WireGuard Config File': 726,
|
||||
'Inbound/Outbound': 727,
|
||||
'StartTunnel (Inbound/Outbound)': 728,
|
||||
'Ethernet': 729
|
||||
}
|
||||
|
||||
@@ -679,4 +679,17 @@ export default {
|
||||
714: '¡Instalación completada!',
|
||||
715: 'StartOS se ha instalado correctamente.',
|
||||
716: 'Continuar con la configuración',
|
||||
717: '',
|
||||
718: '',
|
||||
719: '',
|
||||
720: '',
|
||||
721: '',
|
||||
722: '',
|
||||
723: '',
|
||||
724: '',
|
||||
725: '',
|
||||
726: '',
|
||||
727: '',
|
||||
728: '',
|
||||
729: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -679,4 +679,17 @@ export default {
|
||||
714: 'Installation terminée !',
|
||||
715: 'StartOS a été installé avec succès.',
|
||||
716: 'Continuer vers la configuration',
|
||||
717: '',
|
||||
718: '',
|
||||
719: '',
|
||||
720: '',
|
||||
721: '',
|
||||
722: '',
|
||||
723: '',
|
||||
724: '',
|
||||
725: '',
|
||||
726: '',
|
||||
727: '',
|
||||
728: '',
|
||||
729: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -679,4 +679,17 @@ export default {
|
||||
714: 'Instalacja zakończona!',
|
||||
715: 'StartOS został pomyślnie zainstalowany.',
|
||||
716: 'Przejdź do konfiguracji',
|
||||
717: '',
|
||||
718: '',
|
||||
719: '',
|
||||
720: '',
|
||||
721: '',
|
||||
722: '',
|
||||
723: '',
|
||||
724: '',
|
||||
725: '',
|
||||
726: '',
|
||||
727: '',
|
||||
728: '',
|
||||
729: '',
|
||||
} satisfies i18n
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type AccessType =
|
||||
| 'tor'
|
||||
| 'mdns'
|
||||
| 'localhost'
|
||||
| 'ipv4'
|
||||
|
||||
@@ -45,8 +45,8 @@ export const mockTunnelData: TunnelData = {
|
||||
gateways: {
|
||||
eth0: {
|
||||
name: null,
|
||||
public: null,
|
||||
secure: null,
|
||||
type: null,
|
||||
ipInfo: {
|
||||
name: 'Wired Connection 1',
|
||||
scopeId: 1,
|
||||
|
||||
@@ -83,32 +83,8 @@ export class InterfaceGatewaysComponent {
|
||||
|
||||
readonly gateways = input.required<InterfaceGateway[] | undefined>()
|
||||
|
||||
async onToggle(gateway: InterfaceGateway) {
|
||||
const addressInfo = this.interface.value()!.addressInfo
|
||||
const pkgId = this.interface.packageId()
|
||||
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
try {
|
||||
if (pkgId) {
|
||||
await this.api.pkgBindingToggleGateway({
|
||||
gateway: gateway.id,
|
||||
enabled: !gateway.enabled,
|
||||
internalPort: addressInfo.internalPort,
|
||||
host: addressInfo.hostId,
|
||||
package: pkgId,
|
||||
})
|
||||
} else {
|
||||
await this.api.serverBindingToggleGateway({
|
||||
gateway: gateway.id,
|
||||
enabled: !gateway.enabled,
|
||||
internalPort: 80,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
async onToggle(_gateway: InterfaceGateway) {
|
||||
// TODO: Replace with per-address toggle UI (Section 6 frontend overhaul).
|
||||
// Gateway-level toggle replaced by set-address-enabled RPC.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { tuiButtonOptionsProvider } from '@taiga-ui/core'
|
||||
import { MappedServiceInterface } from './interface.service'
|
||||
import { InterfaceGatewaysComponent } from './gateways.component'
|
||||
import { InterfaceTorDomainsComponent } from './tor-domains.component'
|
||||
import { PublicDomainsComponent } from './public-domains/pd.component'
|
||||
import { InterfacePrivateDomainsComponent } from './private-domains.component'
|
||||
import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
@@ -16,7 +15,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
[publicDomains]="value()?.publicDomains"
|
||||
[addSsl]="value()?.addSsl || false"
|
||||
></section>
|
||||
<section [torDomains]="value()?.torDomains"></section>
|
||||
<section [privateDomains]="value()?.privateDomains"></section>
|
||||
</div>
|
||||
<hr [style.width.rem]="10" />
|
||||
@@ -52,7 +50,6 @@ import { InterfaceAddressesComponent } from './addresses/addresses.component'
|
||||
providers: [tuiButtonOptionsProvider({ size: 'xs' })],
|
||||
imports: [
|
||||
InterfaceGatewaysComponent,
|
||||
InterfaceTorDomainsComponent,
|
||||
PublicDomainsComponent,
|
||||
InterfacePrivateDomainsComponent,
|
||||
InterfaceAddressesComponent,
|
||||
|
||||
@@ -27,17 +27,9 @@ function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
||||
return 0
|
||||
}
|
||||
|
||||
type TorAddress = AddressWithInfo & { info: { kind: 'onion' } }
|
||||
function filterTor(a: AddressWithInfo): a is TorAddress {
|
||||
return a.info.kind === 'onion'
|
||||
}
|
||||
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
|
||||
return cmpWithRankedPredicates(a, b, [x => !x.showSsl])
|
||||
}
|
||||
|
||||
type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } }
|
||||
type LanAddress = AddressWithInfo & { info: { public: false } }
|
||||
function filterLan(a: AddressWithInfo): a is LanAddress {
|
||||
return a.info.kind === 'ip' && !a.info.public
|
||||
return !a.info.public
|
||||
}
|
||||
function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
||||
return cmpWithRankedPredicates(a, b, [
|
||||
@@ -53,15 +45,12 @@ function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
||||
|
||||
type VpnAddress = AddressWithInfo & {
|
||||
info: {
|
||||
kind: 'ip'
|
||||
public: false
|
||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||
}
|
||||
}
|
||||
function filterVpn(a: AddressWithInfo): a is VpnAddress {
|
||||
return (
|
||||
a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local'
|
||||
)
|
||||
return !a.info.public && a.info.hostname.kind !== 'local'
|
||||
}
|
||||
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||
return cmpWithRankedPredicates(a, b, [
|
||||
@@ -76,13 +65,12 @@ function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||
|
||||
type ClearnetAddress = AddressWithInfo & {
|
||||
info: {
|
||||
kind: 'ip'
|
||||
public: true
|
||||
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||
}
|
||||
}
|
||||
function filterClearnet(a: AddressWithInfo): a is ClearnetAddress {
|
||||
return a.info.kind === 'ip' && a.info.public
|
||||
return a.info.public
|
||||
}
|
||||
function cmpClearnet(
|
||||
host: T.Host,
|
||||
@@ -142,10 +130,7 @@ export class InterfaceService {
|
||||
h,
|
||||
)
|
||||
const info = h
|
||||
const gateway =
|
||||
h.kind === 'ip'
|
||||
? gateways.find(g => h.gateway.id === g.id)
|
||||
: undefined
|
||||
const gateway = gateways.find(g => h.gateway.id === g.id)
|
||||
const res = []
|
||||
if (url) {
|
||||
res.push({
|
||||
@@ -171,7 +156,6 @@ export class InterfaceService {
|
||||
},
|
||||
)
|
||||
|
||||
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
||||
const lanAddrs = allAddressesWithInfo
|
||||
.filter(filterLan)
|
||||
.sort((a, b) => cmpLan(host, a, b))
|
||||
@@ -188,7 +172,6 @@ export class InterfaceService {
|
||||
clearnetAddrs[0],
|
||||
lanAddrs[0],
|
||||
vpnAddrs[0],
|
||||
torAddrs[0],
|
||||
]
|
||||
.filter(a => !!a)
|
||||
.reduce((acc, x) => {
|
||||
@@ -214,9 +197,8 @@ export class InterfaceService {
|
||||
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 =>
|
||||
const bestPublic = [publicDomains, wanIp].flatMap(h =>
|
||||
h.format('urlstring'),
|
||||
)[0]
|
||||
const privateDomains = addresses.filter({
|
||||
@@ -254,9 +236,6 @@ export class InterfaceService {
|
||||
.format('urlstring')[0]
|
||||
onLan = true
|
||||
break
|
||||
case 'tor':
|
||||
matching = tor.format('urlstring')[0]
|
||||
break
|
||||
case 'mdns':
|
||||
matching = mdns.format('urlstring')[0]
|
||||
onLan = true
|
||||
@@ -273,19 +252,23 @@ export class InterfaceService {
|
||||
serviceInterface: T.ServiceInterface,
|
||||
host: T.Host,
|
||||
): T.HostnameInfo[] {
|
||||
let hostnameInfo =
|
||||
host.hostnameInfo[serviceInterface.addressInfo.internalPort]
|
||||
return (
|
||||
hostnameInfo?.filter(
|
||||
h =>
|
||||
this.config.accessType === 'localhost' ||
|
||||
!(
|
||||
h.kind === 'ip' &&
|
||||
((h.hostname.kind === 'ipv6' &&
|
||||
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||
h.gateway.id === 'lo')
|
||||
),
|
||||
) || []
|
||||
const binding =
|
||||
host.bindings[serviceInterface.addressInfo.internalPort]
|
||||
if (!binding) return []
|
||||
const addr = binding.addresses
|
||||
const enabled = addr.possible.filter(h =>
|
||||
addr.enabled.some(e => utils.deepEqual(e, h)) ||
|
||||
(!addr.disabled.some(d => utils.deepEqual(d, h)) &&
|
||||
!(h.public && (h.hostname.kind === 'ipv4' || h.hostname.kind === 'ipv6'))),
|
||||
)
|
||||
return enabled.filter(
|
||||
h =>
|
||||
this.config.accessType === 'localhost' ||
|
||||
!(
|
||||
(h.hostname.kind === 'ipv6' &&
|
||||
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||
h.gateway.id === 'lo'
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,31 +285,7 @@ export class InterfaceService {
|
||||
"Requires trusting your server's Root CA",
|
||||
)
|
||||
|
||||
// ** Tor **
|
||||
if (info.kind === 'onion') {
|
||||
access = null
|
||||
gatewayName = null
|
||||
type = 'Tor'
|
||||
bullets = [
|
||||
this.i18n.transform('Connections can be slow or unreliable at times'),
|
||||
this.i18n.transform(
|
||||
'Public if you share the address publicly, otherwise private',
|
||||
),
|
||||
this.i18n.transform('Requires using a Tor-enabled device or browser'),
|
||||
]
|
||||
// Tor (SSL)
|
||||
if (showSsl) {
|
||||
bullets = [rootCaRequired, ...bullets]
|
||||
// Tor (NON-SSL)
|
||||
} else {
|
||||
bullets.unshift(
|
||||
this.i18n.transform(
|
||||
'Ideal for anonymous, censorship-resistant hosting and remote access',
|
||||
),
|
||||
)
|
||||
}
|
||||
// ** Not Tor **
|
||||
} else {
|
||||
{
|
||||
const port = info.hostname.sslPort || info.hostname.port
|
||||
gatewayName = info.gateway.name
|
||||
|
||||
@@ -479,7 +438,6 @@ export class InterfaceService {
|
||||
|
||||
export type MappedServiceInterface = T.ServiceInterface & {
|
||||
gateways: InterfaceGateway[]
|
||||
torDomains: string[]
|
||||
publicDomains: PublicDomain[]
|
||||
privateDomains: string[]
|
||||
addresses: {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
DialogService,
|
||||
DocsLinkDirective,
|
||||
ErrorService,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, utils } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiSkeleton } from '@taiga-ui/kit'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
FormComponent,
|
||||
FormContext,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
import { InterfaceComponent } from './interface.component'
|
||||
|
||||
type OnionForm = {
|
||||
key: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'section[torDomains]',
|
||||
template: `
|
||||
<header>
|
||||
{{ 'Tor Domains' | i18n }}
|
||||
<a
|
||||
tuiIconButton
|
||||
docsLink
|
||||
path="/user-manual/connecting-remotely/tor.html"
|
||||
appearance="icon"
|
||||
iconStart="@tui.external-link"
|
||||
>
|
||||
{{ 'Documentation' | i18n }}
|
||||
</a>
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.plus"
|
||||
[style.margin-inline-start]="'auto'"
|
||||
(click)="add()"
|
||||
[disabled]="!torDomains()"
|
||||
>
|
||||
{{ 'Add' | i18n }}
|
||||
</button>
|
||||
</header>
|
||||
@for (domain of torDomains(); track domain) {
|
||||
<div tuiCell="s">
|
||||
<span tuiTitle>{{ domain }}</span>
|
||||
<button
|
||||
tuiIconButton
|
||||
iconStart="@tui.trash"
|
||||
appearance="action-destructive"
|
||||
(click)="remove(domain)"
|
||||
>
|
||||
{{ 'Delete' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
} @empty {
|
||||
@if (torDomains()) {
|
||||
<app-placeholder icon="@tui.target">
|
||||
{{ 'No Tor domains' | i18n }}
|
||||
</app-placeholder>
|
||||
} @else {
|
||||
@for (_ of [0, 1]; track $index) {
|
||||
<label tuiCell="s">
|
||||
<span tuiTitle [tuiSkeleton]="true">{{ 'Loading' | i18n }}</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
grid-column: span 6;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`,
|
||||
host: { class: 'g-card' },
|
||||
imports: [
|
||||
TuiCell,
|
||||
TuiTitle,
|
||||
TuiButton,
|
||||
PlaceholderComponent,
|
||||
i18nPipe,
|
||||
DocsLinkDirective,
|
||||
TuiSkeleton,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InterfaceTorDomainsComponent {
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly interface = inject(InterfaceComponent)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
readonly torDomains = input.required<readonly string[] | undefined>()
|
||||
|
||||
async remove(onion: string) {
|
||||
this.dialog
|
||||
.openConfirm({ label: 'Are you sure?', size: 's' })
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(async () => {
|
||||
const loader = this.loader.open('Removing').subscribe()
|
||||
const params = { onion }
|
||||
|
||||
try {
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgRemoveOnion({
|
||||
...params,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.serverRemoveOnion(params)
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async add() {
|
||||
this.formDialog.open<FormContext<OnionForm>>(FormComponent, {
|
||||
label: 'New Tor domain',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(
|
||||
ISB.InputSpec.of({
|
||||
key: ISB.Value.text({
|
||||
name: this.i18n.transform('Private Key (optional)')!,
|
||||
description: this.i18n.transform(
|
||||
'Optionally provide a base64-encoded ed25519 private key for generating the Tor V3 (.onion) domain. If not provided, a random key will be generated.',
|
||||
),
|
||||
required: false,
|
||||
default: null,
|
||||
patterns: [utils.Patterns.base64],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save')!,
|
||||
handler: async value => this.save(value.key),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private async save(key?: string): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
const onion = key
|
||||
? await this.api.addTorKey({ key })
|
||||
: await this.api.generateTorKey({})
|
||||
|
||||
if (this.interface.packageId()) {
|
||||
await this.api.pkgAddOnion({
|
||||
onion,
|
||||
package: this.interface.packageId(),
|
||||
host: this.interface.value()?.addressInfo.hostId || '',
|
||||
})
|
||||
} else {
|
||||
await this.api.serverAddOnion({ onion })
|
||||
}
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,6 @@ export const ROUTES: Routes = [
|
||||
path: 'os',
|
||||
loadComponent: () => import('./routes/os.component'),
|
||||
},
|
||||
{
|
||||
path: 'tor',
|
||||
loadComponent: () => import('./routes/tor.component'),
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
|
||||
@@ -79,12 +79,6 @@ export default class SystemLogsComponent {
|
||||
subtitle: 'Raw, unfiltered operating system logs',
|
||||
icon: '@tui.square-dashed-bottom-code',
|
||||
},
|
||||
{
|
||||
link: 'tor',
|
||||
title: 'Tor Logs',
|
||||
subtitle: 'Diagnostics for the Tor daemon on this server',
|
||||
icon: '@tui.target',
|
||||
},
|
||||
{
|
||||
link: 'kernel',
|
||||
title: 'Kernel Logs',
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
import { LogsComponent } from 'src/app/routes/portal/components/logs/logs.component'
|
||||
import { LogsHeaderComponent } from 'src/app/routes/portal/routes/logs/components/header.component'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<logs-header [title]="'Tor Logs' | i18n">
|
||||
{{ 'Diagnostics for the Tor daemon on this server' | i18n }}
|
||||
</logs-header>
|
||||
<logs context="tor" [followLogs]="follow" [fetchLogs]="fetch" />
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
padding: 1rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [LogsComponent, LogsHeaderComponent, i18nPipe],
|
||||
host: { class: 'g-page' },
|
||||
})
|
||||
export default class SystemTorComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
protected readonly follow = (params: RR.FollowServerLogsReq) =>
|
||||
this.api.followTorLogs(params)
|
||||
|
||||
protected readonly fetch = (params: RR.GetServerLogsReq) =>
|
||||
this.api.getTorLogs(params)
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import { TuiBadge } from '@taiga-ui/kit'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
clip-path: inset(0 round 0.75rem);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -6,12 +6,18 @@ import {
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
import { getPkgId, i18nPipe } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
ErrorService,
|
||||
getPkgId,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { ISB, T } from '@start9labs/start-sdk'
|
||||
import { TuiCell } from '@taiga-ui/layout'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { firstValueFrom, map } from 'rxjs'
|
||||
import { ActionService } from 'src/app/services/action.service'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
@@ -20,6 +26,9 @@ import {
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { FormComponent } from 'src/app/routes/portal/components/form.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
|
||||
const INACTIVE: PrimaryStatus[] = [
|
||||
'installing',
|
||||
@@ -65,6 +74,12 @@ const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
||||
|
||||
<section class="g-card">
|
||||
<header>StartOS</header>
|
||||
<button
|
||||
tuiCell
|
||||
[action]="outboundGatewayAction()"
|
||||
[inactive]="inactive"
|
||||
(click)="openOutboundGatewayModal()"
|
||||
></button>
|
||||
<button
|
||||
tuiCell
|
||||
[action]="rebuild"
|
||||
@@ -95,66 +110,78 @@ const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
||||
export default class ServiceActionsRoute {
|
||||
private readonly actions = inject(ActionService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
|
||||
ungrouped: 'General' | 'Other' = 'General'
|
||||
|
||||
readonly service = inject(StandardActionsService)
|
||||
readonly package = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData', getPkgId())
|
||||
.pipe(
|
||||
map(pkg => {
|
||||
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
||||
? 'Other'
|
||||
: 'General'
|
||||
const status = renderPkgStatus(pkg).primary
|
||||
return {
|
||||
status,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.entries(pkg.actions)
|
||||
.filter(([_, action]) => action.visibility !== 'hidden')
|
||||
.map(([id, action]) => ({
|
||||
...action,
|
||||
id,
|
||||
group: action.group || specialGroup,
|
||||
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(
|
||||
status,
|
||||
)
|
||||
? action.visibility
|
||||
: ({
|
||||
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
|
||||
} as T.ActionVisibility),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.group === specialGroup && b.group !== specialGroup)
|
||||
return 1
|
||||
if (b.group === specialGroup && a.group !== specialGroup)
|
||||
return -1
|
||||
this.patch.watch$('packageData', getPkgId()).pipe(
|
||||
map(pkg => {
|
||||
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
||||
? 'Other'
|
||||
: 'General'
|
||||
const status = renderPkgStatus(pkg).primary
|
||||
return {
|
||||
status,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
outboundGateway: pkg.outboundGateway,
|
||||
actions: Object.entries(pkg.actions)
|
||||
.filter(([_, action]) => action.visibility !== 'hidden')
|
||||
.map(([id, action]) => ({
|
||||
...action,
|
||||
id,
|
||||
group: action.group || specialGroup,
|
||||
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(status)
|
||||
? action.visibility
|
||||
: ({
|
||||
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
|
||||
} as T.ActionVisibility),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.group === specialGroup && b.group !== specialGroup) return 1
|
||||
if (b.group === specialGroup && a.group !== specialGroup)
|
||||
return -1
|
||||
|
||||
const groupCompare = a.group.localeCompare(b.group) // sort groups lexicographically
|
||||
if (groupCompare !== 0) return groupCompare
|
||||
const groupCompare = a.group.localeCompare(b.group) // sort groups lexicographically
|
||||
if (groupCompare !== 0) return groupCompare
|
||||
|
||||
return a.id.localeCompare(b.id) // sort actions within groups lexicographically
|
||||
})
|
||||
.reduce<
|
||||
Record<
|
||||
string,
|
||||
Array<T.ActionMetadata & { id: string; group: string }>
|
||||
>
|
||||
>((acc, action) => {
|
||||
const key = action.group
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(action)
|
||||
return acc
|
||||
}, {}),
|
||||
}
|
||||
}),
|
||||
),
|
||||
return a.id.localeCompare(b.id) // sort actions within groups lexicographically
|
||||
})
|
||||
.reduce<
|
||||
Record<
|
||||
string,
|
||||
Array<T.ActionMetadata & { id: string; group: string }>
|
||||
>
|
||||
>((acc, action) => {
|
||||
const key = action.group
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(action)
|
||||
return acc
|
||||
}, {}),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
readonly outboundGatewayAction = computed(() => {
|
||||
const pkg = this.package()
|
||||
const gateway = pkg?.outboundGateway
|
||||
return {
|
||||
name: this.i18n.transform('Set Outbound Gateway')!,
|
||||
description: gateway
|
||||
? `${this.i18n.transform('Current')}: ${gateway}`
|
||||
: `${this.i18n.transform('Current')}: ${this.i18n.transform('System')}`,
|
||||
}
|
||||
})
|
||||
|
||||
readonly rebuild = {
|
||||
name: this.i18n.transform('Rebuild Service')!,
|
||||
description: this.i18n.transform(
|
||||
@@ -181,6 +208,71 @@ export default class ServiceActionsRoute {
|
||||
})
|
||||
}
|
||||
|
||||
async openOutboundGatewayModal() {
|
||||
const pkg = this.package()
|
||||
if (!pkg) return
|
||||
|
||||
const gateways = await firstValueFrom(
|
||||
this.patch.watch$('serverInfo', 'network', 'gateways'),
|
||||
)
|
||||
|
||||
const SYSTEM_KEY = 'system'
|
||||
|
||||
const options: Record<string, string> = {
|
||||
[SYSTEM_KEY]: this.i18n.transform('System default')!,
|
||||
}
|
||||
|
||||
Object.entries(gateways)
|
||||
.filter(
|
||||
([_, g]) =>
|
||||
!!g.ipInfo &&
|
||||
g.ipInfo.deviceType !== 'bridge' &&
|
||||
g.ipInfo.deviceType !== 'loopback',
|
||||
)
|
||||
.forEach(([id, g]) => {
|
||||
options[id] = g.name ?? g.ipInfo?.name ?? id
|
||||
})
|
||||
|
||||
const spec = ISB.InputSpec.of({
|
||||
gateway: ISB.Value.select({
|
||||
name: this.i18n.transform('Outbound Gateway'),
|
||||
description: this.i18n.transform(
|
||||
'Select the gateway for outbound traffic',
|
||||
),
|
||||
default: pkg.outboundGateway ?? SYSTEM_KEY,
|
||||
values: options,
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Set Outbound Gateway',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
{
|
||||
text: this.i18n.transform('Save'),
|
||||
handler: async (input: typeof spec._TYPE) => {
|
||||
const loader = this.loader.open('Saving').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setServiceOutbound({
|
||||
packageId: pkg.manifest.id,
|
||||
gateway: input.gateway === SYSTEM_KEY ? null : input.gateway,
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
protected readonly isInactive = computed(
|
||||
(pkg = this.package()) => !pkg || INACTIVE.includes(pkg.status),
|
||||
)
|
||||
|
||||
@@ -134,12 +134,14 @@ export default class ServiceInterfaceRoute {
|
||||
gateways:
|
||||
gateways.map(g => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
(binding?.addresses.enabled.some(a => a.gateway.id === g.id) ||
|
||||
(!binding?.addresses.disabled.some(a => a.gateway.id === g.id) &&
|
||||
binding?.addresses.possible.some(a =>
|
||||
a.gateway.id === g.id &&
|
||||
!(a.public && (a.hostname.kind === 'ipv4' || a.hostname.kind === 'ipv6'))
|
||||
))) ?? false,
|
||||
...g,
|
||||
})) || [],
|
||||
torDomains: host.onions,
|
||||
publicDomains: getPublicDomains(host.publicDomains, gateways),
|
||||
privateDomains: host.privateDomains,
|
||||
addSsl: !!binding?.options.addSsl,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
TuiFiles,
|
||||
tuiInputFilesOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
|
||||
import { SideloadPackageComponent } from './package.component'
|
||||
@@ -55,11 +54,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
<div>
|
||||
<tui-avatar appearance="secondary" src="@tui.upload" />
|
||||
<p>{{ 'Upload .s9pk package file' | i18n }}</p>
|
||||
@if (isTor) {
|
||||
<p class="g-warning">
|
||||
{{ 'Warning: package upload will be slow over Tor.' | i18n }}
|
||||
</p>
|
||||
}
|
||||
<button tuiButton>{{ 'Select' | i18n }}</button>
|
||||
</div>
|
||||
}
|
||||
@@ -92,8 +86,6 @@ import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
],
|
||||
})
|
||||
export default class SideloadComponent {
|
||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
|
||||
file: File | null = null
|
||||
readonly package = signal<MarketplacePkgSideload | null>(null)
|
||||
readonly error = signal<i18nKey | null>(null)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { GatewaysTableComponent } from './table.component'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { ISB } from '@start9labs/start-sdk'
|
||||
import { RR } from 'src/app/services/api/api.types'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -51,11 +52,6 @@ import { ISB } from '@start9labs/start-sdk'
|
||||
<gateways-table />
|
||||
</section>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 64rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -85,8 +81,19 @@ export default class GatewaysComponent {
|
||||
default: null,
|
||||
placeholder: 'StartTunnel 1',
|
||||
}),
|
||||
type: ISB.Value.select({
|
||||
name: this.i18n.transform('Type'),
|
||||
description: this.i18n.transform('The type of gateway'),
|
||||
default: 'inbound-outbound',
|
||||
values: {
|
||||
'inbound-outbound': this.i18n.transform(
|
||||
'StartTunnel (Inbound/Outbound)',
|
||||
),
|
||||
'outbound-only': this.i18n.transform('Outbound Only'),
|
||||
},
|
||||
}),
|
||||
config: ISB.Value.union({
|
||||
name: this.i18n.transform('StartTunnel Config File'),
|
||||
name: this.i18n.transform('WireGuard Config File'),
|
||||
default: 'paste',
|
||||
variants: ISB.Variants.of({
|
||||
paste: {
|
||||
@@ -113,10 +120,17 @@ export default class GatewaysComponent {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
setAsDefaultOutbound: ISB.Value.toggle({
|
||||
name: this.i18n.transform('Set as default outbound'),
|
||||
description: this.i18n.transform(
|
||||
'Route all outbound traffic through this gateway',
|
||||
),
|
||||
default: false,
|
||||
}),
|
||||
})
|
||||
|
||||
this.formDialog.open(FormComponent, {
|
||||
label: 'Add StartTunnel Gateway',
|
||||
label: 'Add Wireguard Gateway',
|
||||
data: {
|
||||
spec: await configBuilderToSpec(spec),
|
||||
buttons: [
|
||||
@@ -132,7 +146,8 @@ export default class GatewaysComponent {
|
||||
input.config.selection === 'paste'
|
||||
? input.config.value.file
|
||||
: await (input.config.value.file as any as File).text(),
|
||||
public: false,
|
||||
type: input.type as RR.GatewayType,
|
||||
setAsDefaultOutbound: input.setAsDefaultOutbound,
|
||||
})
|
||||
return true
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TuiButton,
|
||||
TuiDataList,
|
||||
TuiDropdown,
|
||||
TuiIcon,
|
||||
TuiOptGroup,
|
||||
TuiTextfield,
|
||||
} from '@taiga-ui/core'
|
||||
@@ -24,32 +25,55 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[gateway]',
|
||||
template: `
|
||||
@if (gateway(); as gateway) {
|
||||
<td class="name">
|
||||
<td>
|
||||
{{ gateway.name }}
|
||||
</td>
|
||||
<td class="type">
|
||||
@if (gateway.ipInfo.deviceType; as type) {
|
||||
{{ type }} ({{
|
||||
gateway.public ? ('public' | i18n) : ('private' | i18n)
|
||||
}})
|
||||
} @else {
|
||||
-
|
||||
@if (gateway.isDefaultOutbound) {
|
||||
<span tuiBadge tuiStatus appearance="positive">Default outbound</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@switch (gateway.ipInfo.deviceType) {
|
||||
@case ('ethernet') {
|
||||
<tui-icon icon="@tui.cable" />
|
||||
{{ 'Ethernet' | i18n }}
|
||||
}
|
||||
@case ('wireless') {
|
||||
<tui-icon icon="@tui.wifi" />
|
||||
{{ 'WiFi' | i18n }}
|
||||
}
|
||||
@case ('wireguard') {
|
||||
<tui-icon icon="@tui.shield" />
|
||||
WireGuard'
|
||||
}
|
||||
@default {
|
||||
{{ gateway.ipInfo.deviceType }}
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (gateway.type === 'outbound-only') {
|
||||
<tui-icon icon="@tui.arrow-up-right" />
|
||||
{{ 'Outbound Only' | i18n }}
|
||||
} @else {
|
||||
<tui-icon icon="@tui.arrow-left-right" />
|
||||
{{ 'Inbound/Outbound' | i18n }}
|
||||
}
|
||||
</td>
|
||||
<td class="lan">{{ gateway.lanIpv4.join(', ') }}</td>
|
||||
<td
|
||||
class="wan"
|
||||
[style.color]="
|
||||
gateway.ipInfo.wanIp ? 'var(--tui-text-warning)' : undefined
|
||||
gateway.ipInfo.wanIp ? undefined : 'var(--tui-text-warning)'
|
||||
"
|
||||
>
|
||||
{{ gateway.ipInfo.wanIp || ('Error' | i18n) }}
|
||||
</td>
|
||||
<td class="lan">{{ gateway.lanIpv4.join(', ') || '-' }}</td>
|
||||
<td>
|
||||
<button
|
||||
tuiIconButton
|
||||
@@ -67,6 +91,18 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
{{ 'Rename' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
@if (!gateway.isDefaultOutbound) {
|
||||
<tui-opt-group>
|
||||
<button
|
||||
tuiOption
|
||||
new
|
||||
iconStart="@tui.arrow-up-right"
|
||||
(click)="setDefaultOutbound()"
|
||||
>
|
||||
{{ 'Set as default outbound' | i18n }}
|
||||
</button>
|
||||
</tui-opt-group>
|
||||
}
|
||||
@if (gateway.ipInfo.deviceType === 'wireguard') {
|
||||
<tui-opt-group>
|
||||
<button
|
||||
@@ -87,19 +123,11 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
`,
|
||||
styles: `
|
||||
td:last-child {
|
||||
grid-area: 1 / 3 / 5;
|
||||
grid-area: 1 / 3 / 7;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.type {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
|
||||
@@ -107,11 +135,15 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.type {
|
||||
.connection {
|
||||
grid-column: span 2;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.type {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.lan,
|
||||
.wan {
|
||||
grid-column: span 2;
|
||||
@@ -132,9 +164,11 @@ import { GatewayPlus } from 'src/app/services/gateway.service'
|
||||
TuiButton,
|
||||
TuiDropdown,
|
||||
TuiDataList,
|
||||
TuiIcon,
|
||||
TuiOptGroup,
|
||||
TuiTextfield,
|
||||
i18nPipe,
|
||||
TuiBadge,
|
||||
],
|
||||
})
|
||||
export class GatewaysItemComponent {
|
||||
@@ -166,6 +200,18 @@ export class GatewaysItemComponent {
|
||||
})
|
||||
}
|
||||
|
||||
async setDefaultOutbound() {
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.setDefaultOutbound({ gateway: this.gateway().id })
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async rename() {
|
||||
const { id, name } = this.gateway()
|
||||
const renameSpec = ISB.InputSpec.of({
|
||||
|
||||
@@ -8,12 +8,21 @@ import { GatewayService } from 'src/app/services/gateway.service'
|
||||
@Component({
|
||||
selector: 'gateways-table',
|
||||
template: `
|
||||
<table [appTable]="['Name', 'Type', $any('LAN IP'), $any('WAN IP'), null]">
|
||||
<table
|
||||
[appTable]="[
|
||||
'Name',
|
||||
'Connection',
|
||||
'Type',
|
||||
$any('WAN IP'),
|
||||
$any('LAN IP'),
|
||||
null,
|
||||
]"
|
||||
>
|
||||
@for (gateway of gatewayService.gateways(); track $index) {
|
||||
<tr [gateway]="gateway"></tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<td colspan="7">
|
||||
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -47,13 +47,11 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { OSService } from 'src/app/services/os.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SnekDirective } from './snek.directive'
|
||||
import { UPDATE } from './update.component'
|
||||
import { SystemWipeComponent } from './wipe.component'
|
||||
import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
|
||||
@Component({
|
||||
@@ -66,7 +64,7 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
</ng-container>
|
||||
@if (server(); as server) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.zap" />
|
||||
<tui-icon icon="@tui.zap" (click)="count = count + 1" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Software Update' | i18n }}</strong>
|
||||
<span tuiSubtitle [style.flex-wrap]="'wrap'">
|
||||
@@ -178,18 +176,6 @@ import { KeyboardSelectComponent } from './keyboard-select.component'
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div tuiCell tuiAppearance="outline-grayscale">
|
||||
<tui-icon icon="@tui.rotate-cw" (click)="count = count + 1" />
|
||||
<span tuiTitle>
|
||||
<strong>{{ 'Restart Tor' | i18n }}</strong>
|
||||
<span tuiSubtitle>
|
||||
{{ 'Restart the Tor daemon on your server' | i18n }}
|
||||
</span>
|
||||
</span>
|
||||
<button tuiButton appearance="glass" (click)="onTorRestart()">
|
||||
{{ 'Restart' | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
@if (count > 4) {
|
||||
<div tuiCell tuiAppearance="outline-grayscale" tuiAnimated>
|
||||
<tui-icon icon="@tui.briefcase-medical" />
|
||||
@@ -283,12 +269,10 @@ 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).accessType === 'tor'
|
||||
private readonly dialog = inject(DialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
private readonly injector = inject(INJECTOR)
|
||||
|
||||
wipe = false
|
||||
count = 0
|
||||
|
||||
readonly server = toSignal(this.patch.watch$('serverInfo'))
|
||||
@@ -392,24 +376,6 @@ export default class SystemGeneralComponent {
|
||||
})
|
||||
}
|
||||
|
||||
onTorRestart() {
|
||||
this.wipe = false
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
label: this.isTor ? 'Warning' : 'Confirm',
|
||||
data: {
|
||||
content: new PolymorpheusComponent(
|
||||
SystemWipeComponent,
|
||||
this.injector,
|
||||
),
|
||||
yes: 'Restart',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => this.resetTor(this.wipe))
|
||||
}
|
||||
|
||||
async onRepair() {
|
||||
this.dialog
|
||||
.openConfirm({
|
||||
@@ -532,19 +498,6 @@ export default class SystemGeneralComponent {
|
||||
.subscribe(() => this.restart())
|
||||
}
|
||||
|
||||
private async resetTor(wipeState: boolean) {
|
||||
const loader = this.loader.open().subscribe()
|
||||
|
||||
try {
|
||||
await this.api.resetTor({ wipeState, reason: 'User triggered' })
|
||||
this.dialog.openAlert('Tor restart in progress').subscribe()
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.dialogs
|
||||
.open(UPDATE, {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiLabel } from '@taiga-ui/core'
|
||||
import { TuiCheckbox } from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import SystemGeneralComponent from './general.component'
|
||||
import { i18nPipe } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (isTor) {
|
||||
<p>
|
||||
{{
|
||||
'You are currently connected over Tor. If you restart the Tor daemon, you will lose connectivity until it comes back online.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
<p>
|
||||
{{
|
||||
'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.'
|
||||
| i18n
|
||||
}}
|
||||
</p>
|
||||
<label tuiLabel>
|
||||
<input type="checkbox" tuiCheckbox [(ngModel)]="component.wipe" />
|
||||
{{ 'Wipe state' | i18n }}
|
||||
</label>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiLabel, FormsModule, TuiCheckbox, i18nPipe],
|
||||
})
|
||||
export class SystemWipeComponent {
|
||||
readonly isTor = inject(ConfigService).accessType === 'tor'
|
||||
readonly component = inject(SystemGeneralComponent)
|
||||
}
|
||||
@@ -95,12 +95,14 @@ export default class StartOsUiComponent {
|
||||
),
|
||||
gateways: gateways.map(g => ({
|
||||
enabled:
|
||||
(g.public
|
||||
? binding?.net.publicEnabled.includes(g.id)
|
||||
: !binding?.net.privateDisabled.includes(g.id)) ?? false,
|
||||
(binding?.addresses.enabled.some(a => a.gateway.id === g.id) ||
|
||||
(!binding?.addresses.disabled.some(a => a.gateway.id === g.id) &&
|
||||
binding?.addresses.possible.some(a =>
|
||||
a.gateway.id === g.id &&
|
||||
!(a.public && (a.hostname.kind === 'ipv4' || a.hostname.kind === 'ipv6'))
|
||||
))) ?? false,
|
||||
...g,
|
||||
})),
|
||||
torDomains: network.host.onions,
|
||||
publicDomains: getPublicDomains(network.host.publicDomains, gateways),
|
||||
privateDomains: network.host.privateDomains,
|
||||
addSsl: true,
|
||||
|
||||
@@ -2126,8 +2126,74 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.10.11',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2138,87 +2204,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '192.168.10.11',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:72cd]',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: '[fe80:cd00:0000:0cde:1257:0000:211e:1234]',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-p2p.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
bindings: {
|
||||
@@ -2227,8 +2212,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2239,10 +2227,6 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -2251,8 +2235,11 @@ export namespace Mock {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -2263,13 +2250,10 @@ export namespace Mock {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
@@ -2338,6 +2322,7 @@ export namespace Mock {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {},
|
||||
@@ -2444,6 +2429,7 @@ export namespace Mock {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
|
||||
@@ -79,14 +79,14 @@ export namespace RR {
|
||||
uptime: number // seconds
|
||||
}
|
||||
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs & net.tor.logs
|
||||
export type GetServerLogsReq = FetchLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsRes = FetchLogsRes
|
||||
|
||||
export type FollowServerLogsReq = {
|
||||
limit?: number // (optional) default is 50. Ignored if cursor provided
|
||||
boot?: number | string | null // (optional) number is offset (0: current, -1 prev, +1 first), string is a specific boot id, null is all. Default is undefined
|
||||
cursor?: string // the last known log. Websocket will return all logs since this log
|
||||
} // server.logs.follow & server.kernel-logs.follow & net.tor.follow-logs
|
||||
} // server.logs.follow & server.kernel-logs.follow
|
||||
export type FollowServerLogsRes = {
|
||||
startCursor: string
|
||||
guid: string
|
||||
@@ -120,12 +120,6 @@ export namespace RR {
|
||||
} // net.dns.query
|
||||
export type QueryDnsRes = string | null
|
||||
|
||||
export type ResetTorReq = {
|
||||
wipeState: boolean
|
||||
reason: string
|
||||
} // net.tor.reset
|
||||
export type ResetTorRes = null
|
||||
|
||||
export type SetKeyboardReq = FullKeyboard // server.set-keyboard
|
||||
export type SetKeyboardRes = null
|
||||
|
||||
@@ -258,10 +252,13 @@ export namespace RR {
|
||||
|
||||
// network
|
||||
|
||||
export type GatewayType = 'inbound-outbound' | 'outbound-only'
|
||||
|
||||
export type AddTunnelReq = {
|
||||
name: string
|
||||
config: string // file contents
|
||||
public: boolean
|
||||
type: GatewayType
|
||||
setAsDefaultOutbound?: boolean
|
||||
} // net.tunnel.add
|
||||
export type AddTunnelRes = {
|
||||
id: string
|
||||
@@ -276,6 +273,17 @@ export namespace RR {
|
||||
export type RemoveTunnelReq = { id: string } // net.tunnel.remove
|
||||
export type RemoveTunnelRes = null
|
||||
|
||||
// Set default outbound gateway
|
||||
export type SetDefaultOutboundReq = { gateway: string | null } // net.gateway.set-default-outbound
|
||||
export type SetDefaultOutboundRes = null
|
||||
|
||||
// Set service outbound gateway
|
||||
export type SetServiceOutboundReq = {
|
||||
packageId: string
|
||||
gateway: string | null
|
||||
} // package.set-outbound-gateway
|
||||
export type SetServiceOutboundRes = null
|
||||
|
||||
export type InitAcmeReq = {
|
||||
provider: string
|
||||
contact: string[]
|
||||
@@ -287,29 +295,13 @@ export namespace RR {
|
||||
}
|
||||
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 *with* .onion suffix
|
||||
|
||||
export type ServerBindingToggleGatewayReq = {
|
||||
// server.host.binding.set-gateway-enabled
|
||||
gateway: T.GatewayId
|
||||
export type ServerBindingSetAddressEnabledReq = {
|
||||
// server.host.binding.set-address-enabled
|
||||
internalPort: 80
|
||||
enabled: boolean
|
||||
address: string // JSON-serialized HostnameInfo
|
||||
enabled: boolean | null // null = reset to default
|
||||
}
|
||||
export type ServerBindingToggleGatewayRes = 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 ServerBindingSetAddressEnabledRes = null
|
||||
|
||||
export type OsUiAddPublicDomainReq = {
|
||||
// server.host.address.domain.public.add
|
||||
@@ -337,23 +329,16 @@ export namespace RR {
|
||||
}
|
||||
export type OsUiRemovePrivateDomainRes = null
|
||||
|
||||
export type PkgBindingToggleGatewayReq = Omit<
|
||||
ServerBindingToggleGatewayReq,
|
||||
export type PkgBindingSetAddressEnabledReq = Omit<
|
||||
ServerBindingSetAddressEnabledReq,
|
||||
'internalPort'
|
||||
> & {
|
||||
// package.host.binding.set-gateway-enabled
|
||||
// package.host.binding.set-address-enabled
|
||||
internalPort: number
|
||||
package: T.PackageId // string
|
||||
host: T.HostId // string
|
||||
}
|
||||
export type PkgBindingToggleGatewayRes = null
|
||||
|
||||
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 PkgBindingSetAddressEnabledRes = null
|
||||
|
||||
export type PkgAddPublicDomainReq = OsUiAddPublicDomainReq & {
|
||||
// package.host.address.domain.public.add
|
||||
|
||||
@@ -81,8 +81,6 @@ export abstract class ApiService {
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes>
|
||||
|
||||
abstract getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes>
|
||||
@@ -91,10 +89,6 @@ export abstract class ApiService {
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
|
||||
abstract followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes>
|
||||
@@ -125,8 +119,6 @@ export abstract class ApiService {
|
||||
|
||||
abstract queryDns(params: RR.QueryDnsReq): Promise<RR.QueryDnsRes>
|
||||
|
||||
abstract resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes>
|
||||
|
||||
// smtp
|
||||
|
||||
abstract setSmtp(params: RR.SetSMTPReq): Promise<RR.SetSMTPRes>
|
||||
@@ -183,6 +175,14 @@ export abstract class ApiService {
|
||||
|
||||
abstract removeTunnel(params: RR.RemoveTunnelReq): Promise<RR.RemoveTunnelRes>
|
||||
|
||||
abstract setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes>
|
||||
|
||||
abstract setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes>
|
||||
|
||||
// ** domains **
|
||||
|
||||
// wifi
|
||||
@@ -344,21 +344,9 @@ export abstract class ApiService {
|
||||
|
||||
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 serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes>
|
||||
|
||||
abstract serverAddOnion(params: RR.ServerAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract serverRemoveOnion(
|
||||
params: RR.ServerRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
abstract serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes>
|
||||
|
||||
abstract osUiAddPublicDomain(
|
||||
params: RR.OsUiAddPublicDomainReq,
|
||||
@@ -376,15 +364,9 @@ export abstract class ApiService {
|
||||
params: RR.OsUiRemovePrivateDomainReq,
|
||||
): Promise<RR.OsUiRemovePrivateDomainRes>
|
||||
|
||||
abstract pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes>
|
||||
|
||||
abstract pkgAddOnion(params: RR.PkgAddOnionReq): Promise<RR.AddOnionRes>
|
||||
|
||||
abstract pkgRemoveOnion(
|
||||
params: RR.PkgRemoveOnionReq,
|
||||
): Promise<RR.RemoveOnionRes>
|
||||
abstract pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes>
|
||||
|
||||
abstract pkgAddPublicDomain(
|
||||
params: RR.PkgAddPublicDomainReq,
|
||||
|
||||
@@ -49,7 +49,7 @@ export class LiveApiService extends ApiService {
|
||||
urls: string[],
|
||||
params: Record<string, string | number>,
|
||||
): Promise<string> {
|
||||
for (let url in urls) {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const res = await this.httpRequest<string>({
|
||||
method: 'GET',
|
||||
@@ -195,10 +195,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.logs', params })
|
||||
}
|
||||
|
||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.logs', params })
|
||||
}
|
||||
|
||||
async getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -211,12 +207,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'server.logs.follow', params })
|
||||
}
|
||||
|
||||
async followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.logs.follow', params })
|
||||
}
|
||||
|
||||
async followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
@@ -278,10 +268,6 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
return this.rpcRequest({ method: 'net.tor.reset', params })
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async checkOSUpdate(
|
||||
@@ -369,6 +355,18 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'net.tunnel.remove', params })
|
||||
}
|
||||
|
||||
async setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes> {
|
||||
return this.rpcRequest({ method: 'net.gateway.set-default-outbound', params })
|
||||
}
|
||||
|
||||
async setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes> {
|
||||
return this.rpcRequest({ method: 'package.set-outbound-gateway', params })
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
@@ -621,41 +619,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
async serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||
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 serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'server.host.binding.set-gateway-enabled',
|
||||
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',
|
||||
method: 'server.host.binding.set-address-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
@@ -696,27 +664,11 @@ export class LiveApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
return this.rpcRequest({
|
||||
method: 'package.host.binding.set-gateway-enabled',
|
||||
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',
|
||||
method: 'package.host.binding.set-address-enabled',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -281,17 +281,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async getTorLogs(params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
const entries = this.randomLogs(params.limit)
|
||||
|
||||
return {
|
||||
entries,
|
||||
startCursor: 'start-cursor',
|
||||
endCursor: 'end-cursor',
|
||||
}
|
||||
}
|
||||
|
||||
async getKernelLogs(
|
||||
params: RR.GetServerLogsReq,
|
||||
): Promise<RR.GetServerLogsRes> {
|
||||
@@ -315,16 +304,6 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async followTorLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
startCursor: 'start-cursor',
|
||||
guid: 'logs-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async followKernelLogs(
|
||||
params: RR.FollowServerLogsReq,
|
||||
): Promise<RR.FollowServerLogsRes> {
|
||||
@@ -504,11 +483,6 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async resetTor(params: RR.ResetTorReq): Promise<RR.ResetTorRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
|
||||
async checkOSUpdate(
|
||||
@@ -592,13 +566,12 @@ export class MockApiService extends ApiService {
|
||||
|
||||
const id = `wg${this.proxyId++}`
|
||||
|
||||
const patch: AddOperation<T.NetworkInterfaceInfo>[] = [
|
||||
const patch: AddOperation<any>[] = [
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/network/gateways/${id}`,
|
||||
value: {
|
||||
name: params.name,
|
||||
public: params.public,
|
||||
secure: false,
|
||||
ipInfo: {
|
||||
name: id,
|
||||
@@ -610,9 +583,19 @@ export class MockApiService extends ApiService {
|
||||
lanIp: ['192.168.1.10'],
|
||||
dnsServers: [],
|
||||
},
|
||||
type: params.type,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (params.setAsDefaultOutbound) {
|
||||
;(patch as any[]).push({
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/defaultOutbound',
|
||||
value: id,
|
||||
})
|
||||
}
|
||||
|
||||
this.mockRevision(patch)
|
||||
|
||||
return { id }
|
||||
@@ -646,6 +629,38 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async setDefaultOutbound(
|
||||
params: RR.SetDefaultOutboundReq,
|
||||
): Promise<RR.SetDefaultOutboundRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/serverInfo/network/defaultOutbound',
|
||||
value: params.gateway,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setServiceOutbound(
|
||||
params: RR.SetServiceOutboundReq,
|
||||
): Promise<RR.SetServiceOutboundRes> {
|
||||
await pauseFor(2000)
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.packageId}/outboundGateway`,
|
||||
value: params.gateway,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// wifi
|
||||
|
||||
async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
|
||||
@@ -1374,77 +1389,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async addTorKey(params: RR.AddTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'vanityabcdefghijklmnop.onion'
|
||||
}
|
||||
|
||||
async generateTorKey(params: RR.GenerateTorKeyReq): Promise<RR.AddTorKeyRes> {
|
||||
await pauseFor(2000)
|
||||
return 'abcdefghijklmnopqrstuv.onion'
|
||||
}
|
||||
|
||||
async serverBindingToggleGateway(
|
||||
params: RR.ServerBindingToggleGatewayReq,
|
||||
): Promise<RR.ServerBindingToggleGatewayRes> {
|
||||
async serverBindingSetAddressEnabled(
|
||||
params: RR.ServerBindingSetAddressEnabledReq,
|
||||
): Promise<RR.ServerBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/serverInfo/network/host/bindings/${params.internalPort}/net/publicEnabled`,
|
||||
value: params.enabled ? [params.gateway] : [],
|
||||
},
|
||||
]
|
||||
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)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1463,10 +1413,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1495,7 +1444,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1516,10 +1465,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1549,7 +1497,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/serverInfo/host/hostnameInfo/80/0`,
|
||||
path: `/serverInfo/network/host/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1557,67 +1505,12 @@ export class MockApiService extends ApiService {
|
||||
return null
|
||||
}
|
||||
|
||||
async pkgBindingToggleGateway(
|
||||
params: RR.PkgBindingToggleGatewayReq,
|
||||
): Promise<RR.PkgBindingToggleGatewayRes> {
|
||||
async pkgBindingSetAddressEnabled(
|
||||
params: RR.PkgBindingSetAddressEnabledReq,
|
||||
): Promise<RR.PkgBindingSetAddressEnabledRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/${params.internalPort}/net/privateDisabled`,
|
||||
value: params.enabled ? [] : [params.gateway],
|
||||
},
|
||||
]
|
||||
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)
|
||||
|
||||
// Mock: no-op since address enable/disable modifies DerivedAddressInfo sets
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1636,10 +1529,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: true,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1668,7 +1560,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
@@ -1689,10 +1581,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.ADD,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
|
||||
value: {
|
||||
kind: 'ip',
|
||||
gatewayId: 'eth0',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'domain',
|
||||
@@ -1722,7 +1613,7 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
{
|
||||
op: PatchOp.REMOVE,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/hostnameInfo/80/0`,
|
||||
path: `/packageData/${params.package}/hosts/${params.host}/bindings/80/addresses/possible/0`,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
@@ -38,8 +38,74 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: null,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
preferredExternalPort: 80,
|
||||
@@ -54,93 +120,12 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: ['myveryownspecialtoraddress'],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
gateways: {
|
||||
eth0: {
|
||||
name: null,
|
||||
public: null,
|
||||
secure: null,
|
||||
type: null,
|
||||
ipInfo: {
|
||||
name: 'Wired Connection 1',
|
||||
scopeId: 1,
|
||||
@@ -154,8 +139,8 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
wlan0: {
|
||||
name: null,
|
||||
public: null,
|
||||
secure: null,
|
||||
type: null,
|
||||
ipInfo: {
|
||||
name: 'Wireless Connection 1',
|
||||
scopeId: 2,
|
||||
@@ -172,8 +157,8 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
wireguard1: {
|
||||
name: 'StartTunnel',
|
||||
public: null,
|
||||
secure: null,
|
||||
type: 'inbound-outbound',
|
||||
ipInfo: {
|
||||
name: 'wireguard1',
|
||||
scopeId: 2,
|
||||
@@ -188,7 +173,23 @@ export const mockPatchData: DataModel = {
|
||||
dnsServers: ['1.1.1.1'],
|
||||
},
|
||||
},
|
||||
wireguard2: {
|
||||
name: 'Mullvad VPN',
|
||||
secure: null,
|
||||
type: 'outbound-only',
|
||||
ipInfo: {
|
||||
name: 'wireguard2',
|
||||
scopeId: 4,
|
||||
deviceType: 'wireguard',
|
||||
subnets: [],
|
||||
wanIp: '198.51.100.77',
|
||||
ntpServers: [],
|
||||
lanIp: [],
|
||||
dnsServers: ['10.64.0.1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultOutbound: 'eth0',
|
||||
dns: {
|
||||
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
||||
staticServers: null,
|
||||
@@ -335,6 +336,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
hosts: {},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
@@ -512,8 +514,74 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 80,
|
||||
assignedSslPort: 443,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -524,87 +592,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
80: [
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'local',
|
||||
value: 'adjective-noun.local',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.1',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv4',
|
||||
value: '10.0.0.2',
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'eth0', name: 'Ethernet', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
|
||||
scopeId: 2,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'ip',
|
||||
gateway: { id: 'wlan0', name: 'Wireless', public: false },
|
||||
public: false,
|
||||
hostname: {
|
||||
kind: 'ipv6',
|
||||
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
|
||||
scopeId: 3,
|
||||
port: null,
|
||||
sslPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'onion',
|
||||
hostname: {
|
||||
value: 'bitcoin-p2p.onion',
|
||||
port: 80,
|
||||
sslPort: 443,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bcdefgh: {
|
||||
bindings: {
|
||||
@@ -613,8 +600,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8332,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -625,10 +615,6 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8332: [],
|
||||
},
|
||||
},
|
||||
cdefghi: {
|
||||
bindings: {
|
||||
@@ -637,8 +623,11 @@ export const mockPatchData: DataModel = {
|
||||
net: {
|
||||
assignedPort: 8333,
|
||||
assignedSslPort: null,
|
||||
publicEnabled: [],
|
||||
privateDisabled: [],
|
||||
},
|
||||
addresses: {
|
||||
enabled: [],
|
||||
disabled: [],
|
||||
possible: [],
|
||||
},
|
||||
options: {
|
||||
addSsl: null,
|
||||
@@ -649,13 +638,10 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
publicDomains: {},
|
||||
privateDomains: [],
|
||||
onions: [],
|
||||
hostnameInfo: {
|
||||
8333: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
storeExposedDependents: [],
|
||||
outboundGateway: null,
|
||||
registry: 'https://registry.start9.com/',
|
||||
developerKey: 'developer-key',
|
||||
tasks: {
|
||||
|
||||
@@ -32,7 +32,6 @@ export class ConfigService {
|
||||
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 {
|
||||
@@ -51,7 +50,7 @@ export class ConfigService {
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isHttps() && !['localhost', 'tor'].includes(this.accessType)
|
||||
return !this.isHttps() && this.accessType !== 'localhost'
|
||||
}
|
||||
|
||||
isHttps(): boolean {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { T, utils } from '@start9labs/start-sdk'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { map } from 'rxjs'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { toSignal } from '@angular/core/rxjs-interop'
|
||||
|
||||
@@ -12,39 +12,47 @@ export type GatewayPlus = T.NetworkInterfaceInfo & {
|
||||
subnets: utils.IpNet[]
|
||||
lanIpv4: string[]
|
||||
wanIp?: utils.IpAddress
|
||||
public: boolean
|
||||
isDefaultOutbound: boolean
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GatewayService {
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
private readonly network$ = this.patch.watch$('serverInfo', 'network')
|
||||
|
||||
readonly defaultOutbound = toSignal(
|
||||
this.network$.pipe(map(n => n.defaultOutbound)),
|
||||
)
|
||||
|
||||
readonly gateways = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('serverInfo', 'network', 'gateways')
|
||||
.pipe(
|
||||
map(gateways =>
|
||||
Object.entries(gateways)
|
||||
.filter(([_, val]) => !!val?.ipInfo)
|
||||
.filter(
|
||||
([_, val]) =>
|
||||
val?.ipInfo?.deviceType !== 'bridge' &&
|
||||
val?.ipInfo?.deviceType !== 'loopback',
|
||||
)
|
||||
.map(([id, val]) => {
|
||||
const subnets =
|
||||
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
||||
const name = val.name ?? val.ipInfo!.name
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
name,
|
||||
subnets,
|
||||
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
||||
public: val.public ?? subnets.some(s => s.isPublic()),
|
||||
wanIp:
|
||||
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
||||
} as GatewayPlus
|
||||
}),
|
||||
),
|
||||
),
|
||||
this.network$.pipe(
|
||||
map(network => {
|
||||
const gateways = network.gateways
|
||||
const defaultOutbound = network.defaultOutbound
|
||||
return Object.entries(gateways)
|
||||
.filter(([_, val]) => !!val?.ipInfo)
|
||||
.filter(
|
||||
([_, val]) =>
|
||||
val?.ipInfo?.deviceType !== 'bridge' &&
|
||||
val?.ipInfo?.deviceType !== 'loopback',
|
||||
)
|
||||
.map(([id, val]) => {
|
||||
const subnets =
|
||||
val.ipInfo?.subnets.map(s => utils.IpNet.parse(s)) ?? []
|
||||
const name = val.name ?? val.ipInfo!.name
|
||||
return {
|
||||
...val,
|
||||
id,
|
||||
name,
|
||||
subnets,
|
||||
lanIpv4: subnets.filter(s => s.isIpv4()).map(s => s.address),
|
||||
wanIp:
|
||||
val.ipInfo?.wanIp && utils.IpAddress.parse(val.ipInfo?.wanIp),
|
||||
isDefaultOutbound: id === defaultOutbound,
|
||||
} as GatewayPlus
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user