diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index d37a74dad..bb2e12454 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -135,11 +135,12 @@ impl BindInfo { } impl InterfaceFilter for NetInfo { fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool { - if info.public() { - self.public_enabled.contains(id) - } else { - !self.private_disabled.contains(id) - } + info.ip_info.is_some() + && if info.public() { + self.public_enabled.contains(id) + } else { + !self.private_disabled.contains(id) + } } } diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index cd74c9b8a..e38ad2828 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -87,7 +87,10 @@ type FormatReturnTy< export type Filled = { hostnames: HostnameInfo[] - toUrl: (h: HostnameInfo) => UrlString[] + toUrls: (h: HostnameInfo) => { + url: UrlString | null + sslUrl: UrlString | null + } filter: ( filter: F, @@ -139,7 +142,7 @@ const unique = (values: A[]) => Array.from(new Set(values)) export const addressHostToUrl = ( { scheme, sslScheme, username, suffix }: AddressInfo, hostname: HostnameInfo, -): UrlString[] => { +): { url: UrlString | null; sslUrl: UrlString | null } => { const res = [] const fmt = (scheme: string | null, host: HostnameInfo, port: number) => { const excludePort = @@ -164,14 +167,16 @@ export const addressHostToUrl = ( username ? `${username}@` : "" }${hostname}${excludePort ? "" : `:${port}`}${suffix}` } + let url = null if (hostname.hostname.sslPort !== null) { - res.push(fmt(sslScheme, hostname, hostname.hostname.sslPort)) + url = fmt(sslScheme, hostname, hostname.hostname.sslPort) } + let sslUrl = null if (hostname.hostname.port !== null) { - res.push(fmt(scheme, hostname, hostname.hostname.port)) + sslUrl = fmt(scheme, hostname, hostname.hostname.port) } - return res + return { url, sslUrl } } function filterRec( @@ -223,13 +228,17 @@ export const filledAddress = ( host: Host, addressInfo: AddressInfo, ): FilledAddressInfo => { - const toUrl = addressHostToUrl.bind(null, addressInfo) + const toUrls = addressHostToUrl.bind(null, addressInfo) + const toUrlArray = (h: HostnameInfo) => { + const u = toUrls(h) + return [u.url, u.sslUrl].filter((u) => u !== null) + } const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? [] return { ...addressInfo, hostnames, - toUrl, + toUrls, filter: ( filter: F, format?: Format, @@ -237,7 +246,7 @@ export const filledAddress = ( const filtered = filterRec(hostnames, filter, false) let res: FormatReturnTy[] = filtered as any if (format === "hostname-info") return res - const urls = filtered.flatMap(toUrl) + const urls = filtered.flatMap(toUrlArray) if (format === "url") res = urls.map((u) => new URL(u)) as any else res = urls as any return res @@ -279,28 +288,28 @@ export const filledAddress = ( ) }, get urls() { - return this.hostnames.flatMap(toUrl) + return this.hostnames.flatMap(toUrlArray) }, get publicUrls() { - return this.publicHostnames.flatMap(toUrl) + return this.publicHostnames.flatMap(toUrlArray) }, get onionUrls() { - return this.onionHostnames.flatMap(toUrl) + return this.onionHostnames.flatMap(toUrlArray) }, get localUrls() { - return this.localHostnames.flatMap(toUrl) + return this.localHostnames.flatMap(toUrlArray) }, get ipUrls() { - return this.ipHostnames.flatMap(toUrl) + return this.ipHostnames.flatMap(toUrlArray) }, get ipv4Urls() { - return this.ipv4Hostnames.flatMap(toUrl) + return this.ipv4Hostnames.flatMap(toUrlArray) }, get ipv6Urls() { - return this.ipv6Hostnames.flatMap(toUrl) + return this.ipv6Hostnames.flatMap(toUrlArray) }, get nonIpUrls() { - return this.nonIpHostnames.flatMap(toUrl) + return this.nonIpHostnames.flatMap(toUrlArray) }, } } diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 720d249de..d88519f4a 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -502,7 +502,7 @@ export default { 531: 'Fehler beim Initialisieren des Servers', 532: 'Abgeschlossen', 533: 'Gateways', - 535: 'Gateway hinzufügen', + 535: 'StartTunnel-Gateway hinzufügen', 536: 'Umbenennen', 537: 'Zugriff', 538: 'Öffentliche Domains', @@ -535,10 +535,8 @@ export default { 569: 'Wählen Sie eine Zertifizierungsstelle aus, um SSL/TLS-Zertifikate für diese Domain auszustellen.', 570: 'Andere', 571: 'Ein Name zur einfachen Identifizierung des Gateways', - 572: 'Wählen Sie diese Option, wenn das Gateway für den privaten Zugriff nur für autorisierte Clients konfiguriert ist. StartTunnel ist ein privates Gateway.', - 573: 'Wählen Sie diese Option, wenn das Gateway für uneingeschränkten öffentlichen Zugriff konfiguriert ist.', 574: 'Datei', - 575: 'Wireguard-Konfigurationsdatei', + 575: 'StartTunnel-Konfigurationsdatei', 576: 'Kopieren/Einfügen', 577: 'Dateiinhalt', 578: 'Öffentlicher Schlüssel', @@ -550,7 +548,7 @@ export default { 584: 'Verbindungen können manchmal langsam oder unzuverlässig sein', 585: 'Öffentlich, wenn Sie die Adresse öffentlich teilen, andernfalls privat', 586: 'Erfordert ein Tor-fähiges Gerät oder einen Browser', - 587: 'Nur nützlich für Clients, die HTTPS erzwingen', + 587: 'Nur nützlich für Clients, die SSL erzwingen', 588: 'Ideal für anonyme, zensurresistente Bereitstellung und Fernzugriff', 589: 'Ideal für lokalen Zugriff', 590: 'Erfordert die Verbindung mit demselben lokalen Netzwerk (LAN) wie Ihr Server, entweder physisch oder über VPN', @@ -589,4 +587,5 @@ export default { 623: 'Alternative Implementierungen', 624: 'Versionen', 625: 'Eine andere Version auswählen', + 626: 'Hochladen', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index 36ebe9e3c..1288fa750 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -501,7 +501,7 @@ export const ENGLISH = { 'Error initializing server': 531, 'Finished': 532, // an in, complete 'Gateways': 533, // as in, a device or software that connects two different networks - 'Add gateway': 535, // as in, add a new network gateway to StartOS + 'Add StartTunnel Gateway': 535, // as in, add a new StartTunnel network gateway to StartOS 'Rename': 536, 'Access': 537, // as in, public or private access, almost "permission" 'Public Domains': 538, // as in, internet domains @@ -534,10 +534,8 @@ export const ENGLISH = { 'Select a Certificate Authority to issue SSL/TLS certificates for this domain': 569, 'Other': 570, // as in, a list option to indicate none of the options listed 'A name to easily identify the gateway': 571, - 'select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.': 572, - 'select this option if the gateway is configured for unfettered public access.': 573, 'File': 574, // as in, a computer file - 'Wireguard Config File': 575, + 'StartTunnel Config File': 575, 'Copy/Paste': 576, 'File Contents': 577, 'Public Key': 578, // as in, a cryptographic public key @@ -549,7 +547,7 @@ export const ENGLISH = { 'Connections can be slow or unreliable at times': 584, 'Public if you share the address publicly, otherwise private': 585, 'Requires using a Tor-enabled device or browser': 586, - 'Only useful for clients that enforce HTTPS': 587, + 'Only useful for clients that require SSL': 587, 'Ideal for anonymous, censorship-resistant hosting and remote access': 588, 'Ideal for local access': 589, 'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN': 590, @@ -588,4 +586,5 @@ export const ENGLISH = { 'Alternative Implementations': 623, 'Versions': 624, 'Select another version': 625, + 'Upload': 626, // as in, upload a file } as const diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index 0ae831ea5..4330775e2 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -502,7 +502,7 @@ export default { 531: 'Error al inicializar el servidor', 532: 'Finalizado', 533: 'Puertas de enlace', - 535: 'Agregar puerta de enlace', + 535: 'Agregar puerta de enlace StartTunnel', 536: 'Renombrar', 537: 'Acceso', 538: 'Dominios públicos', @@ -535,10 +535,8 @@ export default { 569: 'Selecciona una Autoridad Certificadora para emitir certificados SSL/TLS para este dominio.', 570: 'Otro', 571: 'Un nombre para identificar fácilmente la puerta de enlace', - 572: 'Selecciona esta opción si la puerta de enlace está configurada para acceso privado solo a clientes autorizados. StartTunnel es una puerta de enlace privada.', - 573: 'Selecciona esta opción si la puerta de enlace está configurada para acceso público sin restricciones.', 574: 'Archivo', - 575: 'Archivo de configuración de Wireguard', + 575: 'Archivo de configuración de StartTunnel', 576: 'Copiar/Pegar', 577: 'Contenido del archivo', 578: 'Clave pública', @@ -550,7 +548,7 @@ export default { 584: 'Las conexiones pueden ser lentas o poco confiables a veces', 585: 'Público si compartes la dirección públicamente, de lo contrario privado', 586: 'Requiere un dispositivo o navegador habilitado para Tor', - 587: 'Solo útil para clientes que imponen HTTPS', + 587: 'Solo útil para clientes que imponen SSL', 588: 'Ideal para alojamiento y acceso remoto anónimo y resistente a la censura', 589: 'Ideal para acceso local', 590: 'Requiere estar conectado a la misma red de área local (LAN) que tu servidor, ya sea físicamente o mediante VPN', @@ -589,4 +587,5 @@ export default { 623: 'Implementaciones alternativas', 624: 'Versiones', 625: 'Seleccionar otra versión', + 626: 'Subir', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index 37e88705c..9e46ec79d 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -502,7 +502,7 @@ export default { 531: "Erreur lors de l'initialisation du serveur", 532: 'Terminé', 533: 'Passerelles', - 535: 'Ajouter une passerelle', + 535: 'Ajouter une passerelle StartTunnel', 536: 'Renommer', 537: 'Accès', 538: 'Domaines publics', @@ -535,10 +535,8 @@ export default { 569: 'Sélectionnez une Autorité de Certification pour émettre des certificats SSL/TLS pour ce domaine.', 570: 'Autre', 571: 'Un nom pour identifier facilement la passerelle', - 572: 'Sélectionnez cette option si la passerelle est configurée pour un accès privé uniquement aux clients autorisés. StartTunnel est une passerelle privée.', - 573: 'Sélectionnez cette option si la passerelle est configurée pour un accès public illimité.', 574: 'Fichier', - 575: 'Fichier de configuration Wireguard', + 575: 'Fichier de configuration StartTunnel', 576: 'Copier/Coller', 577: 'Contenu du fichier', 578: 'Clé publique', @@ -550,7 +548,7 @@ export default { 584: 'Les connexions peuvent parfois être lentes ou peu fiables', 585: 'Public si vous partagez l’adresse publiquement, sinon privé', 586: 'Nécessite un appareil ou un navigateur compatible Tor', - 587: 'Utile uniquement pour les clients qui imposent HTTPS', + 587: 'Utile uniquement pour les clients qui imposent SSL', 588: 'Idéal pour l’hébergement et l’accès à distance anonymes et résistants à la censure', 589: 'Idéal pour un accès local', 590: 'Nécessite d’être connecté au même réseau local (LAN) que votre serveur, soit physiquement, soit via VPN', @@ -589,4 +587,5 @@ export default { 623: 'Implémentations alternatives', 624: 'Versions', 625: 'Sélectionner une autre version', + 626: 'Téléverser', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 22f50eafd..d5e422fed 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -502,7 +502,7 @@ export default { 531: 'Błąd inicjalizacji serwera', 532: 'Zakończono', 533: 'Bramy sieciowe', - 535: 'Dodaj bramę', + 535: 'Dodaj bramę StartTunnel', 536: 'Zmień nazwę', 537: 'Dostęp', 538: 'Domeny publiczne', @@ -535,10 +535,8 @@ export default { 569: 'Wybierz Urząd Certyfikacji, aby wystawić certyfikaty SSL/TLS dla tej domeny.', 570: 'Inne', 571: 'Nazwa ułatwiająca identyfikację bramy', - 572: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do prywatnego dostępu tylko dla autoryzowanych klientów. StartTunnel to prywatna brama.', - 573: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do nieograniczonego publicznego dostępu.', 574: 'Plik', - 575: 'Plik konfiguracyjny Wireguard', + 575: 'Plik konfiguracyjny StartTunnel', 576: 'Kopiuj/Wklej', 577: 'Zawartość pliku', 578: 'Klucz publiczny', @@ -550,7 +548,7 @@ export default { 584: 'Połączenia mogą być czasami wolne lub niestabilne', 585: 'Publiczne, jeśli udostępniasz adres publicznie, w przeciwnym razie prywatne', 586: 'Wymaga urządzenia lub przeglądarki obsługującej Tor', - 587: 'Przydatne tylko dla klientów wymuszających HTTPS', + 587: 'Przydatne tylko dla klientów wymuszających SSL', 588: 'Idealne do anonimowego, odpornego na cenzurę hostingu i zdalnego dostępu', 589: 'Idealne do dostępu lokalnego', 590: 'Wymaga połączenia z tą samą siecią lokalną (LAN) co serwer, fizycznie lub przez VPN', @@ -589,4 +587,5 @@ export default { 623: 'Alternatywne implementacje', 624: 'Wersje', 625: 'Wybierz inną wersję', + 626: 'Prześlij', } satisfies i18n diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts index 6d66a77a6..8cd7ed512 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts @@ -7,6 +7,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared' type AddressWithInfo = { url: string + ssl: boolean info: T.HostnameInfo gateway?: GatewayPlus } @@ -132,12 +133,26 @@ export class InterfaceService { if (!hostnamesInfos.length) return addresses - const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h => - utils.addressHostToUrl(serviceInterface.addressInfo, h).map(url => ({ - url, - info: h, - gateway: gateways.find(g => h.kind === 'ip' && h.gateway.id === g.id), - })), + const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap( + h => { + const { url, sslUrl } = utils.addressHostToUrl( + serviceInterface.addressInfo, + h, + ) + const info = h + const gateway = + h.kind === 'ip' + ? gateways.find(g => h.gateway.id === g.id) + : undefined + const res = [] + if (url) { + res.push({ url, ssl: false, info, gateway }) + } + if (sslUrl) { + res.push({ url: sslUrl, ssl: true, info, gateway }) + } + return res + }, ) const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor) @@ -311,7 +326,7 @@ export class InterfaceService { } private toDisplayAddress( - { info, url, gateway }: AddressWithInfo, + { info, ssl, url, gateway }: AddressWithInfo, publicDomains: Record, ): DisplayAddress { let access: DisplayAddress['access'] @@ -335,33 +350,29 @@ export class InterfaceService { ), this.i18n.transform('Requires using a Tor-enabled device or browser'), ] - // Tor (HTTPS) - if (url.startsWith('https:')) { - type = `${type} (HTTPS)` + // Tor (SSL) + if (ssl) { + type = `${type} (SSL)` bullets = [ - this.i18n.transform('Only useful for clients that enforce HTTPS'), + this.i18n.transform('Only useful for clients that require SSL'), rootCaRequired, ...bullets, ] - // Tor (HTTP) + // Tor (NON-SSL) } else { bullets.unshift( this.i18n.transform( 'Ideal for anonymous, censorship-resistant hosting and remote access', ), ) - if (url.startsWith('http:')) { - type = `${type} (HTTP)` - } } // ** Not Tor ** } else { const port = info.hostname.sslPort || info.hostname.port - const g = gateway! - gatewayName = g.name + gatewayName = info.gateway.name - const gatewayLanIpv4 = g.lanIpv4[0] - const isWireguard = g.ipInfo.deviceType === 'wireguard' + const gatewayLanIpv4 = gateway?.lanIpv4[0] + const isWireguard = gateway?.ipInfo.deviceType === 'wireguard' const localIdeal = this.i18n.transform('Ideal for local access') const lanRequired = this.i18n.transform( @@ -402,9 +413,9 @@ export class InterfaceService { ), rootCaRequired, ] - if (!g.public) { + if (!info.gateway.public) { bullets.push( - `${portForwarding} "${gatewayName}": ${port} -> ${g.subnets.find(s => s.isIpv4())?.address}:${port}`, + `${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port}`, ) } } else { @@ -436,12 +447,12 @@ export class InterfaceService { if (info.public) { access = 'public' bullets = [ - `${dnsFor} ${info.hostname.value} ${resolvesTo} ${g.ipInfo.wanIp}`, + `${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway?.ipInfo.wanIp}`, ] - if (!g.public) { + if (!info.gateway.public) { bullets.push( - `${portForwarding} "${gatewayName}": ${port} -> ${g.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`, + `${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`, ) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts index 76da277c5..727120f73 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/gateways/gateways.component.ts @@ -83,18 +83,10 @@ export default class GatewaysComponent { ), required: true, default: null, - }), - type: ISB.Value.select({ - name: this.i18n.transform('Type'), - description: `-**${this.i18n.transform('private')}**: ${this.i18n.transform('select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.')}\n-**${this.i18n.transform('public')}**: ${this.i18n.transform('select this option if the gateway is configured for unfettered public access.')}`, - default: 'private', - values: { - private: this.i18n.transform('private'), - public: this.i18n.transform('public'), - }, + placeholder: 'StartTunnel 1', }), config: ISB.Value.union({ - name: this.i18n.transform('Wireguard Config File'), + name: this.i18n.transform('StartTunnel Config File'), default: 'paste', variants: ISB.Variants.of({ paste: { @@ -108,7 +100,7 @@ export default class GatewaysComponent { }), }, select: { - name: this.i18n.transform('Select'), + name: this.i18n.transform('Upload'), spec: ISB.InputSpec.of({ file: ISB.Value.file({ name: this.i18n.transform('File'), @@ -122,7 +114,7 @@ export default class GatewaysComponent { }) this.formDialog.open(FormComponent, { - label: 'Add gateway', + label: 'Add StartTunnel Gateway', data: { spec: await configBuilderToSpec(spec), buttons: [ @@ -138,7 +130,7 @@ export default class GatewaysComponent { input.config.selection === 'paste' ? input.config.value.file : await (input.config.value.file as any as File).text(), - public: input.type === 'public', + public: false, }) return true } catch (e: any) {