mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
feat: inline domain health checks and improve address UX
- addPublicDomain returns DNS query + port check results (AddPublicDomainRes) so frontend skips separate API calls after adding a domain - addPrivateDomain returns check_dns result for the gateway - Support multiple ports per domain in validation modal (deduplicated) - Run port checks concurrently via futures::future::join_all - Add note to add-domain dialog showing other interfaces on same host - Add addXForwardedHeaders to knownProtocols in SDK Host.ts - Add plugin filter kind, pluginId filter, matchesAny, and docs to getServiceInterface.ts - Add PassthroughInfo type and passthroughs field to NetworkInfo - Pluralize "port forwarding rules" in i18n dictionaries
This commit is contained in:
@@ -174,11 +174,11 @@ async fn set_name(
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
struct CheckPortParams {
|
pub struct CheckPortParams {
|
||||||
#[arg(help = "help.arg.port")]
|
#[arg(help = "help.arg.port")]
|
||||||
port: u16,
|
pub port: u16,
|
||||||
#[arg(help = "help.arg.gateway-id")]
|
#[arg(help = "help.arg.gateway-id")]
|
||||||
gateway: GatewayId,
|
pub gateway: GatewayId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
@@ -200,7 +200,7 @@ pub struct IfconfigPortRes {
|
|||||||
pub reachable: bool,
|
pub reachable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_port(
|
pub async fn check_port(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
CheckPortParams { port, gateway }: CheckPortParams,
|
CheckPortParams { port, gateway }: CheckPortParams,
|
||||||
) -> Result<CheckPortRes, Error> {
|
) -> Result<CheckPortRes, Error> {
|
||||||
@@ -276,12 +276,12 @@ async fn check_port(
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
struct CheckDnsParams {
|
pub struct CheckDnsParams {
|
||||||
#[arg(help = "help.arg.gateway-id")]
|
#[arg(help = "help.arg.gateway-id")]
|
||||||
gateway: GatewayId,
|
pub gateway: GatewayId,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_dns(
|
pub async fn check_dns(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
CheckDnsParams { gateway }: CheckDnsParams,
|
CheckDnsParams { gateway }: CheckDnsParams,
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::context::{CliContext, RpcContext};
|
|||||||
use crate::db::model::DatabaseModel;
|
use crate::db::model::DatabaseModel;
|
||||||
use crate::hostname::ServerHostname;
|
use crate::hostname::ServerHostname;
|
||||||
use crate::net::acme::AcmeProvider;
|
use crate::net::acme::AcmeProvider;
|
||||||
|
use crate::net::gateway::{CheckDnsParams, CheckPortParams, CheckPortRes, check_dns, check_port};
|
||||||
use crate::net::host::{HostApiKind, all_hosts};
|
use crate::net::host::{HostApiKind, all_hosts};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
use crate::util::serde::{HandlerExtSerde, display_serializable};
|
||||||
@@ -170,6 +171,15 @@ pub struct AddPublicDomainParams {
|
|||||||
pub gateway: GatewayId,
|
pub gateway: GatewayId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct AddPublicDomainRes {
|
||||||
|
#[ts(type = "string | null")]
|
||||||
|
pub dns: Option<Ipv4Addr>,
|
||||||
|
pub port: Vec<CheckPortRes>,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn add_public_domain<Kind: HostApiKind>(
|
pub async fn add_public_domain<Kind: HostApiKind>(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
AddPublicDomainParams {
|
AddPublicDomainParams {
|
||||||
@@ -178,8 +188,9 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
|||||||
gateway,
|
gateway,
|
||||||
}: AddPublicDomainParams,
|
}: AddPublicDomainParams,
|
||||||
inheritance: Kind::Inheritance,
|
inheritance: Kind::Inheritance,
|
||||||
) -> Result<Option<Ipv4Addr>, Error> {
|
) -> Result<AddPublicDomainRes, Error> {
|
||||||
ctx.db
|
let ports = ctx
|
||||||
|
.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
if let Some(acme) = &acme {
|
if let Some(acme) = &acme {
|
||||||
if !db
|
if !db
|
||||||
@@ -195,21 +206,62 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
|||||||
|
|
||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_public_domains_mut()
|
.as_public_domains_mut()
|
||||||
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
|
.insert(
|
||||||
|
&fqdn,
|
||||||
|
&PublicDomainConfig {
|
||||||
|
acme,
|
||||||
|
gateway: gateway.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
handle_duplicates(db)?;
|
handle_duplicates(db)?;
|
||||||
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
||||||
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
|
let gateways = db
|
||||||
let ports = db.as_private().as_available_ports().de()?;
|
.as_public()
|
||||||
Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
|
.as_server_info()
|
||||||
|
.as_network()
|
||||||
|
.as_gateways()
|
||||||
|
.de()?;
|
||||||
|
let available_ports = db.as_private().as_available_ports().de()?;
|
||||||
|
let host = Kind::host_for(&inheritance, db)?;
|
||||||
|
host.update_addresses(&hostname, &gateways, &available_ports)?;
|
||||||
|
let bindings = host.as_bindings().de()?;
|
||||||
|
let ports: BTreeSet<u16> = bindings
|
||||||
|
.values()
|
||||||
|
.flat_map(|b| &b.addresses.available)
|
||||||
|
.filter(|a| a.public && a.hostname == fqdn)
|
||||||
|
.filter_map(|a| a.port)
|
||||||
|
.collect();
|
||||||
|
Ok(ports)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
tokio::task::spawn_blocking(|| {
|
let ctx2 = ctx.clone();
|
||||||
crate::net::dns::query_dns(ctx, crate::net::dns::QueryDnsParams { fqdn })
|
let fqdn2 = fqdn.clone();
|
||||||
|
|
||||||
|
let (dns_result, port_results) = tokio::join!(
|
||||||
|
async {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
crate::net::dns::query_dns(ctx2, crate::net::dns::QueryDnsParams { fqdn: fqdn2 })
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_kind(ErrorKind::Unknown)?
|
||||||
|
},
|
||||||
|
futures::future::join_all(ports.into_iter().map(|port| {
|
||||||
|
check_port(
|
||||||
|
ctx.clone(),
|
||||||
|
CheckPortParams {
|
||||||
|
port,
|
||||||
|
gateway: gateway.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(AddPublicDomainRes {
|
||||||
|
dns: dns_result?,
|
||||||
|
port: port_results.into_iter().collect::<Result<Vec<_>, _>>()?,
|
||||||
})
|
})
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Unknown)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
@@ -257,13 +309,13 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
|||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
AddPrivateDomainParams { fqdn, gateway }: AddPrivateDomainParams,
|
AddPrivateDomainParams { fqdn, gateway }: AddPrivateDomainParams,
|
||||||
inheritance: Kind::Inheritance,
|
inheritance: Kind::Inheritance,
|
||||||
) -> Result<(), Error> {
|
) -> Result<bool, Error> {
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
Kind::host_for(&inheritance, db)?
|
Kind::host_for(&inheritance, db)?
|
||||||
.as_private_domains_mut()
|
.as_private_domains_mut()
|
||||||
.upsert(&fqdn, || Ok(BTreeSet::new()))?
|
.upsert(&fqdn, || Ok(BTreeSet::new()))?
|
||||||
.mutate(|d| Ok(d.insert(gateway)))?;
|
.mutate(|d| Ok(d.insert(gateway.clone())))?;
|
||||||
handle_duplicates(db)?;
|
handle_duplicates(db)?;
|
||||||
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
let hostname = ServerHostname::load(db.as_public().as_server_info())?;
|
||||||
let gateways = db
|
let gateways = db
|
||||||
@@ -278,7 +330,7 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
|||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
Ok(())
|
check_dns(ctx, CheckDnsParams { gateway }).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_private_domain<Kind: HostApiKind>(
|
pub async fn remove_private_domain<Kind: HostApiKind>(
|
||||||
|
|||||||
@@ -14,28 +14,34 @@ export const knownProtocols = {
|
|||||||
defaultPort: 80,
|
defaultPort: 80,
|
||||||
withSsl: 'https',
|
withSsl: 'https',
|
||||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||||
|
addXForwardedHeaders: true,
|
||||||
},
|
},
|
||||||
https: {
|
https: {
|
||||||
secure: { ssl: true },
|
secure: { ssl: true },
|
||||||
defaultPort: 443,
|
defaultPort: 443,
|
||||||
|
addXForwardedHeaders: true,
|
||||||
},
|
},
|
||||||
ws: {
|
ws: {
|
||||||
secure: null,
|
secure: null,
|
||||||
defaultPort: 80,
|
defaultPort: 80,
|
||||||
withSsl: 'wss',
|
withSsl: 'wss',
|
||||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||||
|
addXForwardedHeaders: true,
|
||||||
},
|
},
|
||||||
wss: {
|
wss: {
|
||||||
secure: { ssl: true },
|
secure: { ssl: true },
|
||||||
defaultPort: 443,
|
defaultPort: 443,
|
||||||
|
addXForwardedHeaders: true,
|
||||||
},
|
},
|
||||||
ssh: {
|
ssh: {
|
||||||
secure: { ssl: false },
|
secure: { ssl: false },
|
||||||
defaultPort: 22,
|
defaultPort: 22,
|
||||||
|
addXForwardedHeaders: false,
|
||||||
},
|
},
|
||||||
dns: {
|
dns: {
|
||||||
secure: { ssl: false },
|
secure: { ssl: false },
|
||||||
defaultPort: 53,
|
defaultPort: 53,
|
||||||
|
addXForwardedHeaders: false,
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -136,7 +142,7 @@ export class MultiHost {
|
|||||||
const sslProto = this.getSslProto(options)
|
const sslProto = this.getSslProto(options)
|
||||||
const addSsl = sslProto
|
const addSsl = sslProto
|
||||||
? {
|
? {
|
||||||
addXForwardedHeaders: false,
|
addXForwardedHeaders: knownProtocols[sslProto].addXForwardedHeaders,
|
||||||
preferredExternalPort: knownProtocols[sslProto].defaultPort,
|
preferredExternalPort: knownProtocols[sslProto].defaultPort,
|
||||||
scheme: sslProto,
|
scheme: sslProto,
|
||||||
alpn: 'alpn' in protoInfo ? protoInfo.alpn : null,
|
alpn: 'alpn' in protoInfo ? protoInfo.alpn : null,
|
||||||
@@ -148,7 +154,7 @@ export class MultiHost {
|
|||||||
preferredExternalPort: 443,
|
preferredExternalPort: 443,
|
||||||
scheme: sslProto,
|
scheme: sslProto,
|
||||||
alpn: null,
|
alpn: null,
|
||||||
...('addSsl' in options ? options.addSsl : null),
|
...options.addSsl,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
|||||||
7
sdk/base/lib/osBindings/AddPublicDomainRes.ts
Normal file
7
sdk/base/lib/osBindings/AddPublicDomainRes.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { CheckPortRes } from './CheckPortRes'
|
||||||
|
|
||||||
|
export type AddPublicDomainRes = {
|
||||||
|
dns: string | null
|
||||||
|
port: Array<CheckPortRes>
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import type { DnsSettings } from './DnsSettings'
|
|||||||
import type { GatewayId } from './GatewayId'
|
import type { GatewayId } from './GatewayId'
|
||||||
import type { Host } from './Host'
|
import type { Host } from './Host'
|
||||||
import type { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
import type { NetworkInterfaceInfo } from './NetworkInterfaceInfo'
|
||||||
|
import type { PassthroughInfo } from './PassthroughInfo'
|
||||||
import type { WifiInfo } from './WifiInfo'
|
import type { WifiInfo } from './WifiInfo'
|
||||||
|
|
||||||
export type NetworkInfo = {
|
export type NetworkInfo = {
|
||||||
@@ -14,4 +15,5 @@ export type NetworkInfo = {
|
|||||||
acme: { [key: AcmeProvider]: AcmeSettings }
|
acme: { [key: AcmeProvider]: AcmeSettings }
|
||||||
dns: DnsSettings
|
dns: DnsSettings
|
||||||
defaultOutbound: string | null
|
defaultOutbound: string | null
|
||||||
|
passthroughs: Array<PassthroughInfo>
|
||||||
}
|
}
|
||||||
|
|||||||
9
sdk/base/lib/osBindings/PassthroughInfo.ts
Normal file
9
sdk/base/lib/osBindings/PassthroughInfo.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type PassthroughInfo = {
|
||||||
|
hostname: string
|
||||||
|
listenPort: number
|
||||||
|
backend: string
|
||||||
|
publicGateways: string[]
|
||||||
|
privateIps: string[]
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ export { AddPackageSignerParams } from './AddPackageSignerParams'
|
|||||||
export { AddPackageToCategoryParams } from './AddPackageToCategoryParams'
|
export { AddPackageToCategoryParams } from './AddPackageToCategoryParams'
|
||||||
export { AddPrivateDomainParams } from './AddPrivateDomainParams'
|
export { AddPrivateDomainParams } from './AddPrivateDomainParams'
|
||||||
export { AddPublicDomainParams } from './AddPublicDomainParams'
|
export { AddPublicDomainParams } from './AddPublicDomainParams'
|
||||||
|
export { AddPublicDomainRes } from './AddPublicDomainRes'
|
||||||
export { AddressInfo } from './AddressInfo'
|
export { AddressInfo } from './AddressInfo'
|
||||||
export { AddSslOptions } from './AddSslOptions'
|
export { AddSslOptions } from './AddSslOptions'
|
||||||
export { AddTunnelParams } from './AddTunnelParams'
|
export { AddTunnelParams } from './AddTunnelParams'
|
||||||
@@ -201,6 +202,7 @@ export { PackagePlugin } from './PackagePlugin'
|
|||||||
export { PackageState } from './PackageState'
|
export { PackageState } from './PackageState'
|
||||||
export { PackageVersionInfo } from './PackageVersionInfo'
|
export { PackageVersionInfo } from './PackageVersionInfo'
|
||||||
export { PartitionInfo } from './PartitionInfo'
|
export { PartitionInfo } from './PartitionInfo'
|
||||||
|
export { PassthroughInfo } from './PassthroughInfo'
|
||||||
export { PasswordType } from './PasswordType'
|
export { PasswordType } from './PasswordType'
|
||||||
export { PathOrUrl } from './PathOrUrl'
|
export { PathOrUrl } from './PathOrUrl'
|
||||||
export { Pem } from './Pem'
|
export { Pem } from './Pem'
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export const getHostname = (url: string): Hostname | null => {
|
|||||||
return last
|
return last
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The kinds of hostnames that can be filtered on.
|
||||||
|
*
|
||||||
|
* - `'mdns'` — mDNS / Bonjour `.local` hostnames
|
||||||
|
* - `'domain'` — any os-managed domain name (matches both `'private-domain'` and `'public-domain'` metadata kinds)
|
||||||
|
* - `'ip'` — shorthand for both `'ipv4'` and `'ipv6'`
|
||||||
|
* - `'ipv4'` — IPv4 addresses only
|
||||||
|
* - `'ipv6'` — IPv6 addresses only
|
||||||
|
* - `'localhost'` — loopback addresses (`localhost`, `127.0.0.1`, `::1`)
|
||||||
|
* - `'link-local'` — IPv6 link-local addresses (fe80::/10)
|
||||||
|
* - `'plugin'` — hostnames provided by a plugin package
|
||||||
|
*/
|
||||||
type FilterKinds =
|
type FilterKinds =
|
||||||
| 'mdns'
|
| 'mdns'
|
||||||
| 'domain'
|
| 'domain'
|
||||||
@@ -34,10 +46,25 @@ type FilterKinds =
|
|||||||
| 'ipv6'
|
| 'ipv6'
|
||||||
| 'localhost'
|
| 'localhost'
|
||||||
| 'link-local'
|
| 'link-local'
|
||||||
|
| 'plugin'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes which hostnames to include (or exclude) when filtering a `Filled` address.
|
||||||
|
*
|
||||||
|
* Every field is optional — omitted fields impose no constraint.
|
||||||
|
* Filters are composable: the `.filter()` method intersects successive filters,
|
||||||
|
* and the `exclude` field inverts a nested filter.
|
||||||
|
*/
|
||||||
export type Filter = {
|
export type Filter = {
|
||||||
|
/** Keep only hostnames with the given visibility. `'public'` = externally reachable, `'private'` = LAN-only. */
|
||||||
visibility?: 'public' | 'private'
|
visibility?: 'public' | 'private'
|
||||||
|
/** Keep only hostnames whose metadata kind matches. A single kind or array of kinds. `'ip'` expands to `['ipv4','ipv6']`, `'domain'` matches both `'private-domain'` and `'public-domain'`. */
|
||||||
kind?: FilterKinds | FilterKinds[]
|
kind?: FilterKinds | FilterKinds[]
|
||||||
|
/** Arbitrary predicate — hostnames for which this returns `false` are excluded. */
|
||||||
predicate?: (h: HostnameInfo) => boolean
|
predicate?: (h: HostnameInfo) => boolean
|
||||||
|
/** Keep only plugin hostnames provided by this package. Implies `kind: 'plugin'`. */
|
||||||
|
pluginId?: PackageId
|
||||||
|
/** A nested filter whose matches are *removed* from the result (logical NOT). */
|
||||||
exclude?: Filter
|
exclude?: Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +92,13 @@ type KindFilter<K extends FilterKinds> = K extends 'mdns'
|
|||||||
?
|
?
|
||||||
| (HostnameInfo & { metadata: { kind: 'ipv6' } })
|
| (HostnameInfo & { metadata: { kind: 'ipv6' } })
|
||||||
| KindFilter<Exclude<K, 'ipv6'>>
|
| KindFilter<Exclude<K, 'ipv6'>>
|
||||||
: K extends 'ip'
|
: K extends 'plugin'
|
||||||
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
?
|
||||||
: never
|
| (HostnameInfo & { metadata: { kind: 'plugin' } })
|
||||||
|
| KindFilter<Exclude<K, 'plugin'>>
|
||||||
|
: K extends 'ip'
|
||||||
|
? KindFilter<Exclude<K, 'ip'> | 'ipv4' | 'ipv6'>
|
||||||
|
: never
|
||||||
|
|
||||||
type FilterReturnTy<F extends Filter> = F extends {
|
type FilterReturnTy<F extends Filter> = F extends {
|
||||||
visibility: infer V extends 'public' | 'private'
|
visibility: infer V extends 'public' | 'private'
|
||||||
@@ -107,20 +138,62 @@ type FormatReturnTy<
|
|||||||
? UrlString | FormatReturnTy<F, Exclude<Format, 'urlstring'>>
|
? UrlString | FormatReturnTy<F, Exclude<Format, 'urlstring'>>
|
||||||
: never
|
: never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resolved address with its hostnames already populated, plus helpers
|
||||||
|
* for filtering, formatting, and converting hostnames to URLs.
|
||||||
|
*
|
||||||
|
* Filters are chainable and each call returns a new `Filled` narrowed to the
|
||||||
|
* matching subset of hostnames:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* addresses.nonLocal // exclude localhost & link-local
|
||||||
|
* addresses.public // only publicly-reachable hostnames
|
||||||
|
* addresses.filter({ kind: 'domain' }) // only domain-name hostnames
|
||||||
|
* addresses.filter({ visibility: 'private' }) // only LAN-reachable hostnames
|
||||||
|
* addresses.nonLocal.filter({ kind: 'ip' }) // chainable — non-local IPs only
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
export type Filled<F extends Filter = {}> = {
|
export type Filled<F extends Filter = {}> = {
|
||||||
|
/** The hostnames that survived all applied filters. */
|
||||||
hostnames: HostnameInfo[]
|
hostnames: HostnameInfo[]
|
||||||
|
|
||||||
|
/** Convert a single hostname into a fully-formed URL string, applying the address's scheme, username, and suffix. */
|
||||||
toUrl: (h: HostnameInfo) => UrlString
|
toUrl: (h: HostnameInfo) => UrlString
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return every hostname in the requested format.
|
||||||
|
*
|
||||||
|
* - `'urlstring'` (default) — formatted URL strings
|
||||||
|
* - `'url'` — `URL` objects
|
||||||
|
* - `'hostname-info'` — raw `HostnameInfo` objects
|
||||||
|
*/
|
||||||
format: <Format extends Formats = 'urlstring'>(
|
format: <Format extends Formats = 'urlstring'>(
|
||||||
format?: Format,
|
format?: Format,
|
||||||
) => FormatReturnTy<{}, Format>[]
|
) => FormatReturnTy<{}, Format>[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an arbitrary {@link Filter} and return a new `Filled` containing only
|
||||||
|
* the hostnames that match. Filters compose: calling `.filter()` on an
|
||||||
|
* already-filtered `Filled` intersects the constraints.
|
||||||
|
*/
|
||||||
filter: <NewFilter extends Filter>(
|
filter: <NewFilter extends Filter>(
|
||||||
filter: NewFilter,
|
filter: NewFilter,
|
||||||
) => Filled<NewFilter & Filter>
|
) => Filled<NewFilter & Filter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply multiple filters and return hostnames that match **any** of them (union / OR).
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* addresses.matchesAny([{ kind: 'domain' }, { kind: 'mdns' }])
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
matchesAny: <NewFilters extends Filter[]>(
|
||||||
|
filters: [...NewFilters],
|
||||||
|
) => Filled<NewFilters[number] & F>
|
||||||
|
|
||||||
|
/** Shorthand filter that excludes `localhost` and IPv6 link-local addresses — keeps only network-reachable hostnames. */
|
||||||
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
||||||
|
/** Shorthand filter that keeps only publicly-reachable hostnames (those with `public: true`). */
|
||||||
public: Filled<typeof publicFilter & Filter>
|
public: Filled<typeof publicFilter & Filter>
|
||||||
}
|
}
|
||||||
export type FilledAddressInfo = AddressInfo & Filled
|
export type FilledAddressInfo = AddressInfo & Filled
|
||||||
@@ -210,7 +283,16 @@ function filterRec(
|
|||||||
['localhost', '127.0.0.1', '::1'].includes(h.hostname)) ||
|
['localhost', '127.0.0.1', '::1'].includes(h.hostname)) ||
|
||||||
(kind.has('link-local') &&
|
(kind.has('link-local') &&
|
||||||
h.metadata.kind === 'ipv6' &&
|
h.metadata.kind === 'ipv6' &&
|
||||||
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname)))),
|
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname))) ||
|
||||||
|
(kind.has('plugin') && h.metadata.kind === 'plugin')),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (filter.pluginId) {
|
||||||
|
const id = filter.pluginId
|
||||||
|
hostnames = hostnames.filter(
|
||||||
|
(h) =>
|
||||||
|
invert !==
|
||||||
|
(h.metadata.kind === 'plugin' && h.metadata.packageId === id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +362,19 @@ export const filledAddress = (
|
|||||||
filterRec(hostnames, filter, false),
|
filterRec(hostnames, filter, false),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
matchesAny: <NewFilters extends Filter[]>(filters: [...NewFilters]) => {
|
||||||
|
const seen = new Set<HostnameInfo>()
|
||||||
|
const union: HostnameInfo[] = []
|
||||||
|
for (const f of filters) {
|
||||||
|
for (const h of filterRec(hostnames, f, false)) {
|
||||||
|
if (!seen.has(h)) {
|
||||||
|
seen.add(h)
|
||||||
|
union.push(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filledAddressFromHostnames<NewFilters[number] & F>(union)
|
||||||
|
},
|
||||||
get nonLocal(): Filled<typeof nonLocalFilter & F> {
|
get nonLocal(): Filled<typeof nonLocalFilter & F> {
|
||||||
return getNonLocal()
|
return getNonLocal()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export default {
|
|||||||
741: 'In Ihrem Domain-Registrar für',
|
741: 'In Ihrem Domain-Registrar für',
|
||||||
742: 'diesen DNS-Eintrag erstellen',
|
742: 'diesen DNS-Eintrag erstellen',
|
||||||
743: 'In Ihrem Gateway',
|
743: 'In Ihrem Gateway',
|
||||||
744: 'diese Portweiterleitungsregel erstellen',
|
744: 'diese Portweiterleitungsregeln erstellen',
|
||||||
745: 'Externer Port',
|
745: 'Externer Port',
|
||||||
747: 'Interner Port',
|
747: 'Interner Port',
|
||||||
749: 'DNS-Server-Konfiguration',
|
749: 'DNS-Server-Konfiguration',
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export const ENGLISH: Record<string, number> = {
|
|||||||
'In your domain registrar for': 741, // partial sentence, followed by a domain name
|
'In your domain registrar for': 741, // partial sentence, followed by a domain name
|
||||||
'create this DNS record': 742,
|
'create this DNS record': 742,
|
||||||
'In your gateway': 743, // partial sentence, followed by a gateway name
|
'In your gateway': 743, // partial sentence, followed by a gateway name
|
||||||
'create this port forwarding rule': 744,
|
'create these port forwarding rules': 744,
|
||||||
'External Port': 745,
|
'External Port': 745,
|
||||||
'Internal Port': 747,
|
'Internal Port': 747,
|
||||||
'DNS Server Config': 749,
|
'DNS Server Config': 749,
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export default {
|
|||||||
741: 'En su registrador de dominios para',
|
741: 'En su registrador de dominios para',
|
||||||
742: 'cree este registro DNS',
|
742: 'cree este registro DNS',
|
||||||
743: 'En su puerta de enlace',
|
743: 'En su puerta de enlace',
|
||||||
744: 'cree esta regla de reenvío de puertos',
|
744: 'cree estas reglas de reenvío de puertos',
|
||||||
745: 'Puerto externo',
|
745: 'Puerto externo',
|
||||||
747: 'Puerto interno',
|
747: 'Puerto interno',
|
||||||
749: 'Configuración del servidor DNS',
|
749: 'Configuración del servidor DNS',
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export default {
|
|||||||
741: 'Dans votre registraire de domaine pour',
|
741: 'Dans votre registraire de domaine pour',
|
||||||
742: 'créez cet enregistrement DNS',
|
742: 'créez cet enregistrement DNS',
|
||||||
743: 'Dans votre passerelle',
|
743: 'Dans votre passerelle',
|
||||||
744: 'créez cette règle de redirection de port',
|
744: 'créez ces règles de redirection de port',
|
||||||
745: 'Port externe',
|
745: 'Port externe',
|
||||||
747: 'Port interne',
|
747: 'Port interne',
|
||||||
749: 'Configuration du serveur DNS',
|
749: 'Configuration du serveur DNS',
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ export default {
|
|||||||
741: 'W rejestratorze domeny dla',
|
741: 'W rejestratorze domeny dla',
|
||||||
742: 'utwórz ten rekord DNS',
|
742: 'utwórz ten rekord DNS',
|
||||||
743: 'W bramie',
|
743: 'W bramie',
|
||||||
744: 'utwórz tę regułę przekierowania portów',
|
744: 'utwórz te reguły przekierowania portów',
|
||||||
745: 'Port zewnętrzny',
|
745: 'Port zewnętrzny',
|
||||||
747: 'Port wewnętrzny',
|
747: 'Port wewnętrzny',
|
||||||
749: 'Konfiguracja serwera DNS',
|
749: 'Konfiguracja serwera DNS',
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface FormContext<T> {
|
|||||||
buttons: ActionButton<T>[]
|
buttons: ActionButton<T>[]
|
||||||
value?: T
|
value?: T
|
||||||
operations?: Operation[]
|
operations?: Operation[]
|
||||||
|
note?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -43,6 +44,9 @@ export interface FormContext<T> {
|
|||||||
(tuiValueChanges)="markAsDirty()"
|
(tuiValueChanges)="markAsDirty()"
|
||||||
>
|
>
|
||||||
<form-group [spec]="spec" />
|
<form-group [spec]="spec" />
|
||||||
|
@if (note) {
|
||||||
|
<p class="note">{{ note }}</p>
|
||||||
|
}
|
||||||
<footer>
|
<footer>
|
||||||
<ng-content />
|
<ng-content />
|
||||||
@for (button of buttons; track $index) {
|
@for (button of buttons; track $index) {
|
||||||
@@ -70,6 +74,12 @@ export interface FormContext<T> {
|
|||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
styles: `
|
styles: `
|
||||||
|
.note {
|
||||||
|
color: var(--tui-text-secondary);
|
||||||
|
font: var(--tui-font-text-s);
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -106,6 +116,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
|||||||
@Input() buttons = this.context?.data.buttons || []
|
@Input() buttons = this.context?.data.buttons || []
|
||||||
@Input() operations = this.context?.data.operations || []
|
@Input() operations = this.context?.data.operations || []
|
||||||
@Input() value?: T = this.context?.data.value
|
@Input() value?: T = this.context?.data.value
|
||||||
|
@Input() note = this.context?.data.note || ''
|
||||||
|
|
||||||
form = new FormGroup({})
|
form = new FormGroup({})
|
||||||
|
|
||||||
|
|||||||
@@ -185,11 +185,32 @@ export class InterfaceAddressesComponent {
|
|||||||
: {}),
|
: {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let note = ''
|
||||||
|
const pkgId = this.packageId()
|
||||||
|
if (pkgId) {
|
||||||
|
const pkg = await firstValueFrom(
|
||||||
|
this.patch.watch$('packageData', pkgId),
|
||||||
|
)
|
||||||
|
if (pkg) {
|
||||||
|
const hostId = iface.addressInfo.hostId
|
||||||
|
const otherNames = Object.values(pkg.serviceInterfaces)
|
||||||
|
.filter(
|
||||||
|
si =>
|
||||||
|
si.addressInfo.hostId === hostId && si.id !== iface.id,
|
||||||
|
)
|
||||||
|
.map(si => si.name)
|
||||||
|
if (otherNames.length) {
|
||||||
|
note = `This domain also applies to ${otherNames.join(', ')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.formDialog.open(FormComponent, {
|
this.formDialog.open(FormComponent, {
|
||||||
label: 'Add public domain',
|
label: 'Add public domain',
|
||||||
size: 's',
|
size: 's',
|
||||||
data: {
|
data: {
|
||||||
spec: await configBuilderToSpec(addSpec),
|
spec: await configBuilderToSpec(addSpec),
|
||||||
|
note,
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
text: this.i18n.transform('Save')!,
|
text: this.i18n.transform('Save')!,
|
||||||
@@ -207,18 +228,22 @@ export class InterfaceAddressesComponent {
|
|||||||
const loader = this.loader.open('Saving').subscribe()
|
const loader = this.loader.open('Saving').subscribe()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let configured: boolean
|
||||||
if (this.packageId()) {
|
if (this.packageId()) {
|
||||||
await this.api.pkgAddPrivateDomain({
|
configured = await this.api.pkgAddPrivateDomain({
|
||||||
fqdn,
|
fqdn,
|
||||||
gateway: gatewayId,
|
gateway: gatewayId,
|
||||||
package: this.packageId(),
|
package: this.packageId(),
|
||||||
host: iface?.addressInfo.hostId || '',
|
host: iface?.addressInfo.hostId || '',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.api.osUiAddPrivateDomain({ fqdn, gateway: gatewayId })
|
configured = await this.api.osUiAddPrivateDomain({
|
||||||
|
fqdn,
|
||||||
|
gateway: gatewayId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.domainHealth.checkPrivateDomain(gatewayId)
|
await this.domainHealth.checkPrivateDomain(gatewayId, configured)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -244,23 +269,18 @@ export class InterfaceAddressesComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let res
|
||||||
if (this.packageId()) {
|
if (this.packageId()) {
|
||||||
await this.api.pkgAddPublicDomain({
|
res = await this.api.pkgAddPublicDomain({
|
||||||
...params,
|
...params,
|
||||||
package: this.packageId(),
|
package: this.packageId(),
|
||||||
host: iface?.addressInfo.hostId || '',
|
host: iface?.addressInfo.hostId || '',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await this.api.osUiAddPublicDomain(params)
|
res = await this.api.osUiAddPublicDomain(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = this.gatewayGroup().addresses.find(
|
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, res)
|
||||||
a => a.access === 'public' && a.hostnameInfo.port !== null,
|
|
||||||
)?.hostnameInfo.port
|
|
||||||
|
|
||||||
if (port !== undefined && port !== null) {
|
|
||||||
await this.domainHealth.checkPublicDomain(fqdn, gatewayId, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from '@taiga-ui/kit'
|
} from '@taiga-ui/kit'
|
||||||
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||||
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
|
import { PortCheckIconComponent } from 'src/app/routes/portal/components/port-check-icon.component'
|
||||||
import { PortCheckWarningsComponent } from 'src/app/routes/portal/components/port-check-warnings.component'
|
|
||||||
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
import { TableComponent } from 'src/app/routes/portal/components/table.component'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
@@ -29,8 +28,11 @@ export type DnsGateway = T.NetworkInterfaceInfo & {
|
|||||||
export type DomainValidationData = {
|
export type DomainValidationData = {
|
||||||
fqdn: string
|
fqdn: string
|
||||||
gateway: DnsGateway
|
gateway: DnsGateway
|
||||||
port: number
|
ports: number[]
|
||||||
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null }
|
initialResults?: {
|
||||||
|
dnsPass: boolean
|
||||||
|
portResults: (T.CheckPortRes | null)[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -92,32 +94,50 @@ export type DomainValidationData = {
|
|||||||
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
||||||
{{ 'create this port forwarding rule' | i18n }}
|
{{ 'create these port forwarding rules' | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@let portRes = portResult();
|
|
||||||
|
|
||||||
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
<table [appTable]="[null, 'External Port', 'Internal Port', null]">
|
||||||
<tr>
|
@for (port of context.data.ports; track port; let i = $index) {
|
||||||
<td class="status">
|
<tr>
|
||||||
<port-check-icon [result]="portRes" [loading]="portLoading()" />
|
<td class="status">
|
||||||
</td>
|
<port-check-icon
|
||||||
<td>{{ context.data.port }}</td>
|
[result]="portResults()[i]"
|
||||||
<td>{{ context.data.port }}</td>
|
[loading]="!!portLoadings()[i]"
|
||||||
<td>
|
/>
|
||||||
<button
|
</td>
|
||||||
tuiButton
|
<td>{{ port }}</td>
|
||||||
size="s"
|
<td>{{ port }}</td>
|
||||||
[loading]="portLoading()"
|
<td>
|
||||||
(click)="testPort()"
|
<button
|
||||||
>
|
tuiButton
|
||||||
{{ 'Test' | i18n }}
|
size="s"
|
||||||
</button>
|
[loading]="!!portLoadings()[i]"
|
||||||
</td>
|
(click)="testPort(i)"
|
||||||
</tr>
|
>
|
||||||
|
{{ 'Test' | i18n }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<port-check-warnings [result]="portRes" />
|
@if (anyNotRunning()) {
|
||||||
|
<p class="g-warning">
|
||||||
|
{{
|
||||||
|
'Port status cannot be determined while service is not running'
|
||||||
|
| i18n
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
@if (anyNoHairpinning()) {
|
||||||
|
<p class="g-warning">
|
||||||
|
{{
|
||||||
|
'This address will not work from your local network due to a router hairpinning limitation'
|
||||||
|
| i18n
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
@if (!isManualMode) {
|
@if (!isManualMode) {
|
||||||
<footer class="g-buttons padding-top">
|
<footer class="g-buttons padding-top">
|
||||||
@@ -216,7 +236,6 @@ export type DomainValidationData = {
|
|||||||
TuiIcon,
|
TuiIcon,
|
||||||
TuiLoader,
|
TuiLoader,
|
||||||
PortCheckIconComponent,
|
PortCheckIconComponent,
|
||||||
PortCheckWarningsComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DomainValidationComponent {
|
export class DomainValidationComponent {
|
||||||
@@ -232,16 +251,28 @@ export class DomainValidationComponent {
|
|||||||
parse(this.context.data.fqdn).domain || this.context.data.fqdn
|
parse(this.context.data.fqdn).domain || this.context.data.fqdn
|
||||||
|
|
||||||
readonly dnsLoading = signal(false)
|
readonly dnsLoading = signal(false)
|
||||||
readonly portLoading = signal(false)
|
readonly portLoadings = signal<boolean[]>(
|
||||||
|
this.context.data.ports.map(() => false),
|
||||||
|
)
|
||||||
readonly dnsPass = signal<boolean | undefined>(undefined)
|
readonly dnsPass = signal<boolean | undefined>(undefined)
|
||||||
readonly portResult = signal<T.CheckPortRes | undefined>(undefined)
|
readonly portResults = signal<(T.CheckPortRes | undefined)[]>(
|
||||||
|
this.context.data.ports.map(() => undefined),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly anyNotRunning = computed(() =>
|
||||||
|
this.portResults().some(r => r && !r.openInternally),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly anyNoHairpinning = computed(() =>
|
||||||
|
this.portResults().some(r => r && r.openExternally && !r.hairpinning),
|
||||||
|
)
|
||||||
|
|
||||||
readonly allPass = computed(() => {
|
readonly allPass = computed(() => {
|
||||||
const result = this.portResult()
|
const results = this.portResults()
|
||||||
return (
|
return (
|
||||||
this.dnsPass() === true &&
|
this.dnsPass() === true &&
|
||||||
!!result?.openInternally &&
|
results.length > 0 &&
|
||||||
!!result?.openExternally
|
results.every(r => !!r?.openInternally && !!r?.openExternally)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -251,7 +282,9 @@ export class DomainValidationComponent {
|
|||||||
const initial = this.context.data.initialResults
|
const initial = this.context.data.initialResults
|
||||||
if (initial) {
|
if (initial) {
|
||||||
this.dnsPass.set(initial.dnsPass)
|
this.dnsPass.set(initial.dnsPass)
|
||||||
if (initial.portResult) this.portResult.set(initial.portResult)
|
this.portResults.set(
|
||||||
|
initial.portResults.map(r => r ?? undefined),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,20 +304,32 @@ export class DomainValidationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async testPort() {
|
async testPort(index: number) {
|
||||||
this.portLoading.set(true)
|
this.portLoadings.update(l => {
|
||||||
|
const copy = [...l]
|
||||||
|
copy[index] = true
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.api.checkPort({
|
const result = await this.api.checkPort({
|
||||||
gateway: this.context.data.gateway.id,
|
gateway: this.context.data.gateway.id,
|
||||||
port: this.context.data.port,
|
port: this.context.data.ports[index]!,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.portResult.set(result)
|
this.portResults.update(r => {
|
||||||
|
const copy = [...r]
|
||||||
|
copy[index] = result
|
||||||
|
return copy
|
||||||
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
} finally {
|
} finally {
|
||||||
this.portLoading.set(false)
|
this.portLoadings.update(l => {
|
||||||
|
const copy = [...l]
|
||||||
|
copy[index] = false
|
||||||
|
return copy
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,33 +19,45 @@ export class DomainHealthService {
|
|||||||
async checkPublicDomain(
|
async checkPublicDomain(
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
gatewayId: string,
|
gatewayId: string,
|
||||||
port: number,
|
portOrRes: number | T.AddPublicDomainRes,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const gateway = await this.getGatewayData(gatewayId)
|
const gateway = await this.getGatewayData(gatewayId)
|
||||||
if (!gateway) return
|
if (!gateway) return
|
||||||
|
|
||||||
const [dnsPass, portResult] = await Promise.all([
|
let dnsPass: boolean
|
||||||
this.api
|
let ports: number[]
|
||||||
.queryDns({ fqdn })
|
let portResults: (T.CheckPortRes | null)[]
|
||||||
.then(ip => ip === gateway.ipInfo.wanIp)
|
|
||||||
.catch(() => false),
|
|
||||||
this.api
|
|
||||||
.checkPort({ gateway: gatewayId, port })
|
|
||||||
.catch((): null => null),
|
|
||||||
])
|
|
||||||
|
|
||||||
const portOk =
|
if (typeof portOrRes === 'number') {
|
||||||
!!portResult?.openInternally &&
|
ports = [portOrRes]
|
||||||
!!portResult?.openExternally &&
|
const [dns, portResult] = await Promise.all([
|
||||||
!!portResult?.hairpinning
|
this.api
|
||||||
|
.queryDns({ fqdn })
|
||||||
|
.then(ip => ip === gateway.ipInfo.wanIp)
|
||||||
|
.catch(() => false),
|
||||||
|
this.api
|
||||||
|
.checkPort({ gateway: gatewayId, port: portOrRes })
|
||||||
|
.catch((): null => null),
|
||||||
|
])
|
||||||
|
dnsPass = dns
|
||||||
|
portResults = [portResult]
|
||||||
|
} else {
|
||||||
|
dnsPass = portOrRes.dns === gateway.ipInfo.wanIp
|
||||||
|
ports = portOrRes.port.map(r => r.port)
|
||||||
|
portResults = portOrRes.port
|
||||||
|
}
|
||||||
|
|
||||||
if (!dnsPass || !portOk) {
|
const allPortsOk = portResults.every(
|
||||||
|
r => !!r?.openInternally && !!r?.openExternally && !!r?.hairpinning,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!dnsPass || !allPortsOk) {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
this.openPublicDomainModal(fqdn, gateway, port, {
|
this.openPublicDomainModal(fqdn, gateway, ports, {
|
||||||
dnsPass,
|
dnsPass,
|
||||||
portResult,
|
portResults,
|
||||||
}),
|
}),
|
||||||
250,
|
250,
|
||||||
)
|
)
|
||||||
@@ -55,14 +67,17 @@ export class DomainHealthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPrivateDomain(gatewayId: string): Promise<void> {
|
async checkPrivateDomain(
|
||||||
|
gatewayId: string,
|
||||||
|
prefetchedConfigured?: boolean,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const gateway = await this.getGatewayData(gatewayId)
|
const gateway = await this.getGatewayData(gatewayId)
|
||||||
if (!gateway) return
|
if (!gateway) return
|
||||||
|
|
||||||
const configured = await this.api
|
const configured =
|
||||||
.checkDns({ gateway: gatewayId })
|
prefetchedConfigured ??
|
||||||
.catch(() => false)
|
(await this.api.checkDns({ gateway: gatewayId }).catch(() => false))
|
||||||
|
|
||||||
if (!configured) {
|
if (!configured) {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
@@ -84,7 +99,7 @@ export class DomainHealthService {
|
|||||||
const gateway = await this.getGatewayData(gatewayId)
|
const gateway = await this.getGatewayData(gatewayId)
|
||||||
if (!gateway) return
|
if (!gateway) return
|
||||||
|
|
||||||
this.openPublicDomainModal(fqdn, gateway, port)
|
this.openPublicDomainModal(fqdn, gateway, [port])
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.errorService.handleError(e)
|
this.errorService.handleError(e)
|
||||||
}
|
}
|
||||||
@@ -149,14 +164,17 @@ export class DomainHealthService {
|
|||||||
private openPublicDomainModal(
|
private openPublicDomainModal(
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
gateway: DnsGateway,
|
gateway: DnsGateway,
|
||||||
port: number,
|
ports: number[],
|
||||||
initialResults?: { dnsPass: boolean; portResult: T.CheckPortRes | null },
|
initialResults?: {
|
||||||
|
dnsPass: boolean
|
||||||
|
portResults: (T.CheckPortRes | null)[]
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
this.dialog
|
this.dialog
|
||||||
.openComponent(DOMAIN_VALIDATION, {
|
.openComponent(DOMAIN_VALIDATION, {
|
||||||
label: 'Address Requirements',
|
label: 'Address Requirements',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
data: { fqdn, gateway, port, initialResults },
|
data: { fqdn, gateway, ports, initialResults },
|
||||||
})
|
})
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export type PortForwardValidationData = {
|
|||||||
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
<h2>{{ 'Port Forwarding' | i18n }}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
{{ 'In your gateway' | i18n }} "{{ gatewayName }}",
|
||||||
{{ 'create this port forwarding rule' | i18n }}
|
{{ 'create these port forwarding rules' | i18n }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@let portRes = portResult();
|
@let portRes = portResult();
|
||||||
|
|||||||
@@ -340,11 +340,13 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract osUiAddPublicDomain(
|
abstract osUiAddPublicDomain(
|
||||||
params: T.AddPublicDomainParams,
|
params: T.AddPublicDomainParams,
|
||||||
): Promise<string | null>
|
): Promise<T.AddPublicDomainRes>
|
||||||
|
|
||||||
abstract osUiRemovePublicDomain(params: T.RemoveDomainParams): Promise<null>
|
abstract osUiRemovePublicDomain(params: T.RemoveDomainParams): Promise<null>
|
||||||
|
|
||||||
abstract osUiAddPrivateDomain(params: T.AddPrivateDomainParams): Promise<null>
|
abstract osUiAddPrivateDomain(
|
||||||
|
params: T.AddPrivateDomainParams,
|
||||||
|
): Promise<boolean>
|
||||||
|
|
||||||
abstract osUiRemovePrivateDomain(params: T.RemoveDomainParams): Promise<null>
|
abstract osUiRemovePrivateDomain(params: T.RemoveDomainParams): Promise<null>
|
||||||
|
|
||||||
@@ -354,13 +356,15 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract pkgAddPublicDomain(
|
abstract pkgAddPublicDomain(
|
||||||
params: PkgAddPublicDomainReq,
|
params: PkgAddPublicDomainReq,
|
||||||
): Promise<string | null>
|
): Promise<T.AddPublicDomainRes>
|
||||||
|
|
||||||
abstract pkgRemovePublicDomain(
|
abstract pkgRemovePublicDomain(
|
||||||
params: PkgRemovePublicDomainReq,
|
params: PkgRemovePublicDomainReq,
|
||||||
): Promise<null>
|
): Promise<null>
|
||||||
|
|
||||||
abstract pkgAddPrivateDomain(params: PkgAddPrivateDomainReq): Promise<null>
|
abstract pkgAddPrivateDomain(
|
||||||
|
params: PkgAddPrivateDomainReq,
|
||||||
|
): Promise<boolean>
|
||||||
|
|
||||||
abstract pkgRemovePrivateDomain(
|
abstract pkgRemovePrivateDomain(
|
||||||
params: PkgRemovePrivateDomainReq,
|
params: PkgRemovePrivateDomainReq,
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
async osUiAddPublicDomain(
|
async osUiAddPublicDomain(
|
||||||
params: T.AddPublicDomainParams,
|
params: T.AddPublicDomainParams,
|
||||||
): Promise<string | null> {
|
): Promise<T.AddPublicDomainRes> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'server.host.address.domain.public.add',
|
method: 'server.host.address.domain.public.add',
|
||||||
params,
|
params,
|
||||||
@@ -644,7 +644,9 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async osUiAddPrivateDomain(params: T.AddPrivateDomainParams): Promise<null> {
|
async osUiAddPrivateDomain(
|
||||||
|
params: T.AddPrivateDomainParams,
|
||||||
|
): Promise<boolean> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'server.host.address.domain.private.add',
|
method: 'server.host.address.domain.private.add',
|
||||||
params,
|
params,
|
||||||
@@ -669,7 +671,7 @@ export class LiveApiService extends ApiService {
|
|||||||
|
|
||||||
async pkgAddPublicDomain(
|
async pkgAddPublicDomain(
|
||||||
params: PkgAddPublicDomainReq,
|
params: PkgAddPublicDomainReq,
|
||||||
): Promise<string | null> {
|
): Promise<T.AddPublicDomainRes> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.host.address.domain.public.add',
|
method: 'package.host.address.domain.public.add',
|
||||||
params,
|
params,
|
||||||
@@ -683,7 +685,9 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgAddPrivateDomain(params: PkgAddPrivateDomainReq): Promise<null> {
|
async pkgAddPrivateDomain(
|
||||||
|
params: PkgAddPrivateDomainReq,
|
||||||
|
): Promise<boolean> {
|
||||||
return this.rpcRequest({
|
return this.rpcRequest({
|
||||||
method: 'package.host.address.domain.private.add',
|
method: 'package.host.address.domain.private.add',
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -1440,7 +1440,7 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async osUiAddPublicDomain(
|
async osUiAddPublicDomain(
|
||||||
params: T.AddPublicDomainParams,
|
params: T.AddPublicDomainParams,
|
||||||
): Promise<string | null> {
|
): Promise<T.AddPublicDomainRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
const patch: Operation<any>[] = [
|
||||||
@@ -1465,7 +1465,18 @@ export class MockApiService extends ApiService {
|
|||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|
||||||
return null
|
return {
|
||||||
|
dns: null,
|
||||||
|
port: [
|
||||||
|
{
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: 443,
|
||||||
|
openExternally: false,
|
||||||
|
openInternally: false,
|
||||||
|
hairpinning: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async osUiRemovePublicDomain(params: T.RemoveDomainParams): Promise<null> {
|
async osUiRemovePublicDomain(params: T.RemoveDomainParams): Promise<null> {
|
||||||
@@ -1482,7 +1493,9 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async osUiAddPrivateDomain(params: T.AddPrivateDomainParams): Promise<null> {
|
async osUiAddPrivateDomain(
|
||||||
|
params: T.AddPrivateDomainParams,
|
||||||
|
): Promise<boolean> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
const patch: Operation<any>[] = [
|
||||||
@@ -1505,7 +1518,7 @@ export class MockApiService extends ApiService {
|
|||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|
||||||
return null
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async osUiRemovePrivateDomain(params: T.RemoveDomainParams): Promise<null> {
|
async osUiRemovePrivateDomain(params: T.RemoveDomainParams): Promise<null> {
|
||||||
@@ -1535,7 +1548,7 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async pkgAddPublicDomain(
|
async pkgAddPublicDomain(
|
||||||
params: PkgAddPublicDomainReq,
|
params: PkgAddPublicDomainReq,
|
||||||
): Promise<string | null> {
|
): Promise<T.AddPublicDomainRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
const patch: Operation<any>[] = [
|
||||||
@@ -1560,7 +1573,18 @@ export class MockApiService extends ApiService {
|
|||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|
||||||
return null
|
return {
|
||||||
|
dns: null,
|
||||||
|
port: [
|
||||||
|
{
|
||||||
|
ip: '0.0.0.0',
|
||||||
|
port: 443,
|
||||||
|
openExternally: false,
|
||||||
|
openInternally: false,
|
||||||
|
hairpinning: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgRemovePublicDomain(params: PkgRemovePublicDomainReq): Promise<null> {
|
async pkgRemovePublicDomain(params: PkgRemovePublicDomainReq): Promise<null> {
|
||||||
@@ -1577,7 +1601,9 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgAddPrivateDomain(params: PkgAddPrivateDomainReq): Promise<null> {
|
async pkgAddPrivateDomain(
|
||||||
|
params: PkgAddPrivateDomainReq,
|
||||||
|
): Promise<boolean> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
const patch: Operation<any>[] = [
|
const patch: Operation<any>[] = [
|
||||||
@@ -1600,7 +1626,7 @@ export class MockApiService extends ApiService {
|
|||||||
]
|
]
|
||||||
this.mockRevision(patch)
|
this.mockRevision(patch)
|
||||||
|
|
||||||
return null
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async pkgRemovePrivateDomain(
|
async pkgRemovePrivateDomain(
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export const mockPatchData: DataModel = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
passthroughs: [],
|
||||||
defaultOutbound: 'eth0',
|
defaultOutbound: 'eth0',
|
||||||
dns: {
|
dns: {
|
||||||
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
dhcpServers: ['1.1.1.1', '8.8.8.8'],
|
||||||
|
|||||||
Reference in New Issue
Block a user