mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
best address logic
This commit is contained in:
@@ -194,7 +194,12 @@ pub async fn add_domain<Kind: HostApiKind>(
|
|||||||
.as_domains()
|
.as_domains()
|
||||||
.keys()?
|
.keys()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|root| domain.ends_with(&**root))
|
.find(|root| {
|
||||||
|
domain == root
|
||||||
|
|| domain
|
||||||
|
.strip_suffix(&**root)
|
||||||
|
.map_or(false, |d| d.ends_with("."))
|
||||||
|
})
|
||||||
.or_not_found(lazy_format!("root domain for {domain}"))?;
|
.or_not_found(lazy_format!("root domain for {domain}"))?;
|
||||||
|
|
||||||
if let Some(acme) = &acme {
|
if let Some(acme) = &acme {
|
||||||
@@ -209,7 +214,6 @@ pub async fn add_domain<Kind: HostApiKind>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Kind::host_for(&inheritance, db)?.as_domains_mut().insert(
|
Kind::host_for(&inheritance, db)?.as_domains_mut().insert(
|
||||||
domain,
|
domain,
|
||||||
&DomainConfig {
|
&DomainConfig {
|
||||||
|
|||||||
@@ -385,8 +385,7 @@ impl NetServiceData {
|
|||||||
gateway_id: interface.clone(),
|
gateway_id: interface.clone(),
|
||||||
public, // TODO: check if port forward is active
|
public, // TODO: check if port forward is active
|
||||||
hostname: IpHostname::Domain {
|
hostname: IpHostname::Domain {
|
||||||
domain: address.clone(),
|
value: address.clone(),
|
||||||
subdomain: None,
|
|
||||||
port: None,
|
port: None,
|
||||||
ssl_port: Some(443),
|
ssl_port: Some(443),
|
||||||
},
|
},
|
||||||
@@ -396,8 +395,7 @@ impl NetServiceData {
|
|||||||
gateway_id: interface.clone(),
|
gateway_id: interface.clone(),
|
||||||
public,
|
public,
|
||||||
hostname: IpHostname::Domain {
|
hostname: IpHostname::Domain {
|
||||||
domain: address.clone(),
|
value: address.clone(),
|
||||||
subdomain: None,
|
|
||||||
port: bind.net.assigned_port,
|
port: bind.net.assigned_port,
|
||||||
ssl_port: bind.net.assigned_ssl_port,
|
ssl_port: bind.net.assigned_ssl_port,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ pub enum IpHostname {
|
|||||||
},
|
},
|
||||||
Domain {
|
Domain {
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
domain: InternedString,
|
value: InternedString,
|
||||||
#[ts(type = "string | null")]
|
|
||||||
subdomain: Option<InternedString>,
|
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
ssl_port: Option<u16>,
|
ssl_port: Option<u16>,
|
||||||
},
|
},
|
||||||
@@ -85,15 +83,7 @@ impl IpHostname {
|
|||||||
Self::Ipv4 { value, .. } => InternedString::from_display(value),
|
Self::Ipv4 { value, .. } => InternedString::from_display(value),
|
||||||
Self::Ipv6 { value, .. } => InternedString::from_display(value),
|
Self::Ipv6 { value, .. } => InternedString::from_display(value),
|
||||||
Self::Local { value, .. } => value.clone(),
|
Self::Local { value, .. } => value.clone(),
|
||||||
Self::Domain {
|
Self::Domain { value, .. } => value.clone(),
|
||||||
domain, subdomain, ..
|
|
||||||
} => {
|
|
||||||
if let Some(subdomain) = subdomain {
|
|
||||||
InternedString::from_display(&lazy_format!("{subdomain}.{domain}"))
|
|
||||||
} else {
|
|
||||||
domain.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export type IpHostname =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
kind: "domain"
|
kind: "domain"
|
||||||
domain: string
|
value: string
|
||||||
subdomain: string | null
|
|
||||||
port: number | null
|
port: number | null
|
||||||
sslPort: number | null
|
sslPort: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { HostnameInfo } from "../types"
|
|
||||||
|
|
||||||
export function hostnameInfoToAddress(hostInfo: HostnameInfo): string {
|
|
||||||
if (hostInfo.kind === "onion") {
|
|
||||||
return `${hostInfo.hostname.value}`
|
|
||||||
}
|
|
||||||
if (hostInfo.kind !== "ip") {
|
|
||||||
throw Error("Expecting that the kind is ip.")
|
|
||||||
}
|
|
||||||
const hostname = hostInfo.hostname
|
|
||||||
if (hostname.kind === "domain") {
|
|
||||||
return `${hostname.subdomain ? `${hostname.subdomain}.` : ""}${hostname.domain}`
|
|
||||||
}
|
|
||||||
const port = hostname.sslPort || hostname.port
|
|
||||||
const portString = port ? `:${port}` : ""
|
|
||||||
if ("ipv4" === hostname.kind || "ipv6" === hostname.kind) {
|
|
||||||
return `${hostname.value}${portString}`
|
|
||||||
}
|
|
||||||
if ("local" === hostname.kind) {
|
|
||||||
return `${hostname.value}${portString}`
|
|
||||||
}
|
|
||||||
throw Error(
|
|
||||||
"Expecting to have a valid hostname kind." + JSON.stringify(hostname),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import { knownProtocols } from "../interfaces/Host"
|
|||||||
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
|
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
|
||||||
import { Effects } from "../Effects"
|
import { Effects } from "../Effects"
|
||||||
import { DropGenerator, DropPromise } from "./Drop"
|
import { DropGenerator, DropPromise } from "./Drop"
|
||||||
|
import { IPV6_LINK_LOCAL } from "./ip"
|
||||||
|
|
||||||
export type UrlString = string
|
export type UrlString = string
|
||||||
export type HostId = string
|
export type HostId = string
|
||||||
@@ -95,9 +96,9 @@ export const addressHostToUrl = (
|
|||||||
hostname = host.hostname.value
|
hostname = host.hostname.value
|
||||||
} else if (host.kind === "ip") {
|
} else if (host.kind === "ip") {
|
||||||
if (host.hostname.kind === "domain") {
|
if (host.hostname.kind === "domain") {
|
||||||
hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}`
|
hostname = host.hostname.value
|
||||||
} else if (host.hostname.kind === "ipv6") {
|
} else if (host.hostname.kind === "ipv6") {
|
||||||
hostname = host.hostname.value.startsWith("fe80::")
|
hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
|
||||||
? `[${host.hostname.value}%${host.hostname.scopeId}]`
|
? `[${host.hostname.value}%${host.hostname.scopeId}]`
|
||||||
: `[${host.hostname.value}]`
|
: `[${host.hostname.value}]`
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/// Currently being used
|
/// Currently being used
|
||||||
export { addressHostToUrl } from "./getServiceInterface"
|
export { addressHostToUrl } from "./getServiceInterface"
|
||||||
export { getDefaultString } from "./getDefaultString"
|
export { getDefaultString } from "./getDefaultString"
|
||||||
|
export * from "./ip"
|
||||||
|
|
||||||
/// Not being used, but known to be browser compatible
|
/// Not being used, but known to be browser compatible
|
||||||
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
|
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
|
||||||
@@ -16,6 +17,5 @@ export { splitCommand } from "./splitCommand"
|
|||||||
export { nullIfEmpty } from "./nullIfEmpty"
|
export { nullIfEmpty } from "./nullIfEmpty"
|
||||||
export { deepMerge, partialDiff } from "./deepMerge"
|
export { deepMerge, partialDiff } from "./deepMerge"
|
||||||
export { deepEqual } from "./deepEqual"
|
export { deepEqual } from "./deepEqual"
|
||||||
export { hostnameInfoToAddress } from "./Hostname"
|
|
||||||
export * as regexes from "./regexes"
|
export * as regexes from "./regexes"
|
||||||
export { stringFromStdErrOut } from "./stringFromStdErrOut"
|
export { stringFromStdErrOut } from "./stringFromStdErrOut"
|
||||||
|
|||||||
@@ -73,3 +73,5 @@ export const PRIVATE_IPV4_RANGES = [
|
|||||||
new IpNet("172.16.0.0/12"),
|
new IpNet("172.16.0.0/12"),
|
||||||
new IpNet("192.168.0.0/16"),
|
new IpNet("192.168.0.0/16"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const IPV6_LINK_LOCAL = new IpNet("fe80::/10")
|
||||||
|
|||||||
@@ -7,12 +7,24 @@ type AddressWithInfo = {
|
|||||||
info: T.HostnameInfo
|
info: T.HostnameInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterTor(a: AddressWithInfo): a is (AddressWithInfo & { info: { kind: 'onion' } }) {
|
function cmpWithRankedPredicates<T extends AddressWithInfo>(
|
||||||
return a.info.kind === 'onion'
|
a: T,
|
||||||
|
b: T,
|
||||||
|
preds: ((x: T) => boolean)[],
|
||||||
|
): -1 | 0 | 1 {
|
||||||
|
for (const pred of preds) {
|
||||||
|
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
||||||
|
if (pred(x) && !pred(y)) return sign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmpTor(a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 {
|
type TorAddress = AddressWithInfo & { info: { kind: 'onion' } }
|
||||||
if (!filterTor(a) || !filterTor(b)) return 0
|
function filterTor(a: AddressWithInfo): a is TorAddress {
|
||||||
|
return a.info.kind === 'onion'
|
||||||
|
}
|
||||||
|
function cmpTor(a: TorAddress, b: TorAddress): -1 | 0 | 1 {
|
||||||
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
||||||
if (x.address.protocol === 'http:' && y.address.protocol === 'https:')
|
if (x.address.protocol === 'http:' && y.address.protocol === 'https:')
|
||||||
return sign
|
return sign
|
||||||
@@ -20,17 +32,68 @@ function cmpTor(a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterLan(a: AddressWithInfo): a is (AddressWithInfo & { info: { kind: 'ip', public: false } }) {
|
type LanAddress = AddressWithInfo & { info: { kind: 'ip'; public: false } }
|
||||||
|
function filterLan(a: AddressWithInfo): a is LanAddress {
|
||||||
return a.info.kind === 'ip' && !a.info.public
|
return a.info.kind === 'ip' && !a.info.public
|
||||||
}
|
}
|
||||||
|
function cmpLan(host: T.Host, a: LanAddress, b: LanAddress): -1 | 0 | 1 {
|
||||||
|
return cmpWithRankedPredicates(a, b, [
|
||||||
|
x =>
|
||||||
|
x.info.hostname.kind === 'domain' &&
|
||||||
|
!host.domains[x.info.hostname.value]?.public, // private domain
|
||||||
|
x => x.info.hostname.kind === 'local', // .local
|
||||||
|
x => x.info.hostname.kind === 'ipv4', // ipv4
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
function cmpLan(host: T.Host, a: AddressWithInfo, b: AddressWithInfo): -1 | 0 | 1 {
|
type VpnAddress = AddressWithInfo & {
|
||||||
if (!filterLan(a) || !filterLan(b)) return 0
|
info: {
|
||||||
for (let [x, y, sign] of [[a, b, 1] as const, [b, a, -1] as const]) {
|
kind: 'ip'
|
||||||
if (x.info.kind === 'domain' && host.domains.)
|
public: false
|
||||||
return sign
|
hostname: { kind: 'ipv4' | 'ipv6' | 'domain' }
|
||||||
}
|
}
|
||||||
return 0
|
}
|
||||||
|
function filterVpn(a: AddressWithInfo): a is VpnAddress {
|
||||||
|
return (
|
||||||
|
a.info.kind === 'ip' && !a.info.public && a.info.hostname.kind !== 'local'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function cmpVpn(host: T.Host, a: VpnAddress, b: VpnAddress): -1 | 0 | 1 {
|
||||||
|
return cmpWithRankedPredicates(a, b, [
|
||||||
|
x =>
|
||||||
|
x.info.hostname.kind === 'domain' &&
|
||||||
|
!host.domains[x.info.hostname.value]?.public, // private domain
|
||||||
|
x => x.info.hostname.kind === 'ipv4', // ipv4
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
function cmpClearnet(
|
||||||
|
domains: Record<string, T.DomainSettings>,
|
||||||
|
host: T.Host,
|
||||||
|
a: ClearnetAddress,
|
||||||
|
b: ClearnetAddress,
|
||||||
|
): -1 | 0 | 1 {
|
||||||
|
return cmpWithRankedPredicates(a, b, [
|
||||||
|
x =>
|
||||||
|
x.info.hostname.kind === 'domain' &&
|
||||||
|
x.info.gatewayId ===
|
||||||
|
domains[host.domains[x.info.hostname.value]?.root!]?.gateway, // public domain for this gateway
|
||||||
|
x => x.info.hostname.kind === 'ipv4', // ipv4
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDisplayAddress(a: AddressWithInfo): Address {
|
||||||
|
throw new Error('@TODO: MattHill')
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -40,6 +103,7 @@ export class InterfaceService {
|
|||||||
private readonly config = inject(ConfigService)
|
private readonly config = inject(ConfigService)
|
||||||
|
|
||||||
getAddresses(
|
getAddresses(
|
||||||
|
serverDomains: Record<string, T.DomainSettings>,
|
||||||
serviceInterface: T.ServiceInterface,
|
serviceInterface: T.ServiceInterface,
|
||||||
host: T.Host,
|
host: T.Host,
|
||||||
): MappedServiceInterface['addresses'] {
|
): MappedServiceInterface['addresses'] {
|
||||||
@@ -52,56 +116,35 @@ export class InterfaceService {
|
|||||||
|
|
||||||
if (!hostnamesInfos.length) return addresses
|
if (!hostnamesInfos.length) return addresses
|
||||||
|
|
||||||
hostnamesInfos.forEach(h => {
|
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
|
||||||
const addresses = utils.addressHostToUrl(serviceInterface.addressInfo, h)
|
utils
|
||||||
|
.addressHostToUrl(serviceInterface.addressInfo, h)
|
||||||
|
.map(a => ({ address: new URL(a), info: h })),
|
||||||
|
)
|
||||||
|
|
||||||
addresses.forEach(url => {
|
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
|
||||||
if (h.kind === 'onion') {
|
const lanAddrs = allAddressesWithInfo
|
||||||
tor.push({
|
.filter(filterLan)
|
||||||
protocol: /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)
|
.sort((a, b) => cmpLan(host, a, b))
|
||||||
? new URL(url).protocol.replace(':', '').toUpperCase()
|
const vpnAddrs = allAddressesWithInfo
|
||||||
: null,
|
.filter(filterVpn)
|
||||||
url,
|
.sort((a, b) => cmpVpn(host, a, b))
|
||||||
})
|
const clearnetAddrs = allAddressesWithInfo
|
||||||
} else {
|
.filter(filterClearnet)
|
||||||
const hostnameKind = h.hostname.kind
|
.sort((a, b) => cmpClearnet(serverDomains, host, a, b))
|
||||||
|
|
||||||
if (
|
let bestAddrs = [clearnetAddrs[0], lanAddrs[0], vpnAddrs[0], torAddrs[0]]
|
||||||
h.public ||
|
.filter(a => !!a)
|
||||||
(hostnameKind === 'domain' &&
|
.reduce((acc, x) => {
|
||||||
host.domains[h.hostname.domain]?.public)
|
if (!acc.includes(x)) acc.push(x)
|
||||||
) {
|
return acc
|
||||||
clearnet.push({
|
}, [] as AddressWithInfo[])
|
||||||
url,
|
|
||||||
disabled: !h.public,
|
|
||||||
isDomain: hostnameKind == 'domain',
|
|
||||||
authority:
|
|
||||||
hostnameKind == 'domain'
|
|
||||||
? host.domains[h.hostname.domain]?.acme || null
|
|
||||||
: null,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
local.push({
|
|
||||||
nid:
|
|
||||||
hostnameKind === 'local'
|
|
||||||
? 'Local'
|
|
||||||
: `${h.gatewayId} (${hostnameKind})`,
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
common: common.filter(
|
common: bestAddrs.map(toDisplayAddress),
|
||||||
(value, index, self) =>
|
uncommon: allAddressesWithInfo
|
||||||
index === self.findIndex(t => t.url === value.url),
|
.filter(a => !bestAddrs.includes(a))
|
||||||
),
|
.map(toDisplayAddress),
|
||||||
uncommon: uncommon.filter(
|
|
||||||
(value, index, self) =>
|
|
||||||
index === self.findIndex(t => t.url === value.url),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +201,7 @@ export class InterfaceService {
|
|||||||
.filter(h => h.kind === 'domain')
|
.filter(h => h.kind === 'domain')
|
||||||
.map(h => h as T.IpHostname & { kind: 'domain' })
|
.map(h => h as T.IpHostname & { kind: 'domain' })
|
||||||
.map(h => ({
|
.map(h => ({
|
||||||
value: h.domain,
|
value: h.value,
|
||||||
sslPort: h.sslPort,
|
sslPort: h.sslPort,
|
||||||
port: h.port,
|
port: h.port,
|
||||||
}))[0]
|
}))[0]
|
||||||
@@ -235,7 +278,7 @@ export class InterfaceService {
|
|||||||
!(
|
!(
|
||||||
h.kind === 'ip' &&
|
h.kind === 'ip' &&
|
||||||
((h.hostname.kind === 'ipv6' &&
|
((h.hostname.kind === 'ipv6' &&
|
||||||
h.hostname.value.startsWith('fe80::')) ||
|
utils.IPV6_LINK_LOCAL.contains(h.hostname.value)) ||
|
||||||
h.gatewayId === 'lo')
|
h.gatewayId === 'lo')
|
||||||
),
|
),
|
||||||
) || []
|
) || []
|
||||||
|
|||||||
Reference in New Issue
Block a user