From 9ff0128fb173a33fe69e3c2bc21cb1f6fc3dfd33 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:58:02 -0600 Subject: [PATCH] support http2 alpn handshake (#2354) * support http2 alpn handshake * fix protocol name * switch to https for tor * update setup wizard and main ui to accommodate https (#2356) * update setup wizard and main ui to accommodate https * update wording in download doc * fix accidential conversion of tor https for services and allow ws still * redirect to https if available * fix replaces to only search at beginning and ignore localhost when checking for https --------- Co-authored-by: Lucy <12953208+elvece@users.noreply.github.com> --- backend/src/db/model.rs | 2 +- backend/src/manager/mod.rs | 11 +- backend/src/net/net_controller.rs | 26 ++-- backend/src/net/vhost.rs | 118 ++++++++++++------ backend/src/setup.rs | 4 +- .../download-doc/download-doc.component.html | 84 +++++++------ .../src/app/pages/success/success.page.ts | 2 +- .../src/app/services/api/mock-api.service.ts | 6 +- frontend/projects/ui/src/app/app.component.ts | 10 +- .../app-interfaces/app-interfaces.page.ts | 6 +- .../apps-routes/app-show/app-show.page.ts | 6 +- .../server-show/server-show.page.ts | 8 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- .../ui/src/app/services/config.service.ts | 1 + 14 files changed, 182 insertions(+), 104 deletions(-) diff --git a/backend/src/db/model.rs b/backend/src/db/model.rs index 7a6fe1f7f..e78ca4bab 100644 --- a/backend/src/db/model.rs +++ b/backend/src/db/model.rs @@ -49,7 +49,7 @@ impl Database { last_wifi_region: None, eos_version_compat: Current::new().compat().clone(), lan_address, - tor_address: format!("http://{}", account.key.tor_address()) + tor_address: format!("https://{}", account.key.tor_address()) .parse() .unwrap(), ip_info: BTreeMap::new(), diff --git a/backend/src/manager/mod.rs b/backend/src/manager/mod.rs index 518fac41d..7a68bdf80 100644 --- a/backend/src/manager/mod.rs +++ b/backend/src/manager/mod.rs @@ -21,6 +21,7 @@ use tracing::instrument; use crate::context::RpcContext; use crate::manager::sync::synchronizer; use crate::net::net_controller::NetService; +use crate::net::vhost::AlpnInfo; use crate::procedure::docker::{DockerContainer, DockerProcedure, LongRunning}; #[cfg(feature = "js_engine")] use crate::procedure::js_scripts::JsProcedure; @@ -573,8 +574,14 @@ async fn add_network_for_main( let mut tx = secrets.begin().await?; for (id, interface) in &seed.manifest.interfaces.0 { for (external, internal) in interface.lan_config.iter().flatten() { - svc.add_lan(&mut tx, id.clone(), external.0, internal.internal, false) - .await?; + svc.add_lan( + &mut tx, + id.clone(), + external.0, + internal.internal, + Err(AlpnInfo::Specified(vec![])), + ) + .await?; } for (external, internal) in interface.tor_config.iter().flat_map(|t| &t.port_mapping) { svc.add_tor(&mut tx, id.clone(), external.0, internal.0) diff --git a/backend/src/net/net_controller.rs b/backend/src/net/net_controller.rs index c04707d37..1ecf49f6b 100644 --- a/backend/src/net/net_controller.rs +++ b/backend/src/net/net_controller.rs @@ -15,7 +15,7 @@ use crate::net::keys::Key; use crate::net::mdns::MdnsController; use crate::net::ssl::{export_cert, export_key, SslManager}; use crate::net::tor::TorController; -use crate::net::vhost::VHostController; +use crate::net::vhost::{AlpnInfo, VHostController}; use crate::s9pk::manifest::PackageId; use crate::volume::cert_dir; use crate::{Error, HOST_IP}; @@ -55,6 +55,8 @@ impl NetController { } async fn add_os_bindings(&mut self, hostname: &Hostname, key: &Key) -> Result<(), Error> { + let alpn = Err(AlpnInfo::Specified(vec!["http/1.1".into(), "h2".into()])); + // Internal DNS self.vhost .add( @@ -62,7 +64,7 @@ impl NetController { Some("embassy".into()), 443, ([127, 0, 0, 1], 80).into(), - false, + alpn.clone(), ) .await?; self.os_bindings @@ -71,7 +73,13 @@ impl NetController { // LAN IP self.os_bindings.push( self.vhost - .add(key.clone(), None, 443, ([127, 0, 0, 1], 80).into(), false) + .add( + key.clone(), + None, + 443, + ([127, 0, 0, 1], 80).into(), + alpn.clone(), + ) .await?, ); @@ -83,7 +91,7 @@ impl NetController { Some("localhost".into()), 443, ([127, 0, 0, 1], 80).into(), - false, + alpn.clone(), ) .await?, ); @@ -94,7 +102,7 @@ impl NetController { Some(hostname.no_dot_host_name()), 443, ([127, 0, 0, 1], 80).into(), - false, + alpn.clone(), ) .await?, ); @@ -107,7 +115,7 @@ impl NetController { Some(hostname.local_domain_name()), 443, ([127, 0, 0, 1], 80).into(), - false, + alpn.clone(), ) .await?, ); @@ -127,7 +135,7 @@ impl NetController { Some(key.tor_address().to_string()), 443, ([127, 0, 0, 1], 80).into(), - false, + alpn.clone(), ) .await?, ); @@ -179,7 +187,7 @@ impl NetController { key: Key, external: u16, target: SocketAddr, - connect_ssl: bool, + connect_ssl: Result<(), AlpnInfo>, ) -> Result>, Error> { let mut rcs = Vec::with_capacity(2); rcs.push( @@ -261,7 +269,7 @@ impl NetService { id: InterfaceId, external: u16, internal: u16, - connect_ssl: bool, + connect_ssl: Result<(), AlpnInfo>, ) -> Result<(), Error> where for<'a> &'a mut Ex: PgExecutor<'a>, diff --git a/backend/src/net/vhost.rs b/backend/src/net/vhost.rs index 840acafb0..c80f17e18 100644 --- a/backend/src/net/vhost.rs +++ b/backend/src/net/vhost.rs @@ -41,7 +41,7 @@ impl VHostController { hostname: Option, external: u16, target: SocketAddr, - connect_ssl: bool, + connect_ssl: Result<(), AlpnInfo>, ) -> Result, Error> { let mut writable = self.servers.lock().await; let server = if let Some(server) = writable.remove(&external) { @@ -77,10 +77,16 @@ impl VHostController { #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] struct TargetInfo { addr: SocketAddr, - connect_ssl: bool, + connect_ssl: Result<(), AlpnInfo>, key: Key, } +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum AlpnInfo { + Reflect, + Specified(Vec>), +} + struct VHostServer { mapping: Weak, BTreeMap>>>>, _thread: NonDetachingJoinHandle<()>, @@ -178,7 +184,7 @@ impl VHostServer { let cfg = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth(); - let cfg = + let mut cfg = if mid.client_hello().signature_schemes().contains( &tokio_rustls::rustls::SignatureScheme::ED25519, ) { @@ -213,48 +219,86 @@ impl VHostServer { .private_key_to_der()?, ), ) - }; - let mut tls_stream = mid - .into_stream(Arc::new( - cfg.with_kind(crate::ErrorKind::OpenSsl)?, - )) - .await?; - tls_stream.get_mut().0.stop_buffering(); - if target.connect_ssl { - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut TlsConnector::from(Arc::new( + } + .with_kind(crate::ErrorKind::OpenSsl)?; + match target.connect_ssl { + Ok(()) => { + let mut client_cfg = tokio_rustls::rustls::ClientConfig::builder() .with_safe_defaults() .with_root_certificates({ let mut store = RootCertStore::empty(); store.add( - &tokio_rustls::rustls::Certificate( - key.root_ca().to_der()?, - ), - ).with_kind(crate::ErrorKind::OpenSsl)?; + &tokio_rustls::rustls::Certificate( + key.root_ca().to_der()?, + ), + ).with_kind(crate::ErrorKind::OpenSsl)?; store }) - .with_no_client_auth(), - )) - .connect( - key.key() - .internal_address() - .as_str() - .try_into() - .with_kind(crate::ErrorKind::OpenSsl)?, - tcp_stream, + .with_no_client_auth(); + client_cfg.alpn_protocols = mid + .client_hello() + .alpn() + .into_iter() + .flatten() + .map(|x| x.to_vec()) + .collect(); + let mut target_stream = + TlsConnector::from(Arc::new(client_cfg)) + .connect_with( + key.key() + .internal_address() + .as_str() + .try_into() + .with_kind( + crate::ErrorKind::OpenSsl, + )?, + tcp_stream, + |conn| { + cfg.alpn_protocols.extend( + conn.alpn_protocol() + .into_iter() + .map(|p| p.to_vec()), + ) + }, + ) + .await + .with_kind(crate::ErrorKind::OpenSsl)?; + let mut tls_stream = + mid.into_stream(Arc::new(cfg)).await?; + tls_stream.get_mut().0.stop_buffering(); + tokio::io::copy_bidirectional( + &mut tls_stream, + &mut target_stream, ) - .await - .with_kind(crate::ErrorKind::OpenSsl)?, - ) - .await?; - } else { - tokio::io::copy_bidirectional( - &mut tls_stream, - &mut tcp_stream, - ) - .await?; + .await?; + } + Err(AlpnInfo::Reflect) => { + for proto in + mid.client_hello().alpn().into_iter().flatten() + { + cfg.alpn_protocols.push(proto.into()); + } + let mut tls_stream = + mid.into_stream(Arc::new(cfg)).await?; + tls_stream.get_mut().0.stop_buffering(); + tokio::io::copy_bidirectional( + &mut tls_stream, + &mut tcp_stream, + ) + .await?; + } + Err(AlpnInfo::Specified(alpn)) => { + cfg.alpn_protocols = alpn; + let mut tls_stream = + mid.into_stream(Arc::new(cfg)).await?; + tls_stream.get_mut().0.stop_buffering(); + tokio::io::copy_bidirectional( + &mut tls_stream, + &mut tcp_stream, + ) + .await?; + } } } else { // 503 diff --git a/backend/src/setup.rs b/backend/src/setup.rs index 1612d579f..ea825cf63 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -144,7 +144,7 @@ pub async fn attach( } let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; *ctx.setup_result.write().await = Some((guid, SetupResult { - tor_address: format!("http://{}", tor_addr), + tor_address: format!("https://{}", tor_addr), lan_address: hostname.lan_address(), root_ca: String::from_utf8(root_ca.to_pem()?)?, })); @@ -281,7 +281,7 @@ pub async fn execute( *ctx.setup_result.write().await = Some(( guid, SetupResult { - tor_address: format!("http://{}", tor_addr), + tor_address: format!("https://{}", tor_addr), lan_address: hostname.lan_address(), root_ca: String::from_utf8( root_ca.to_pem().expect("failed to serialize root ca"), diff --git a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html index 5c5ea38d0..0d659241d 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html +++ b/frontend/projects/setup-wizard/src/app/pages/success/download-doc/download-doc.component.html @@ -27,31 +27,15 @@
-

- Access from home (LAN) -

-

- Visit the address below when you are connected to the same WiFi or - Local Area Network (LAN) as your server: -

-

- -

Important!

- Be sure to + Download your server's Root CA and follow the instructions - to establish a secure connection by installing your server's root - certificate authority. + to establish a secure connection with your server.

- -
+
+

+ Access from home (LAN) +

+

+ Visit the address below when you are connected to the same WiFi or + Local Area Network (LAN) as your server. +

+

+ +

-

Access on the go (Tor)

-

Visit the address below when you are away from home:

+

Visit the address below when you are away from home.

+

+ Note: + This address will only work from a Tor-enabled browser. + + Follow the instructions + + to get setup. +

-
-

Important!

-

- This address will only work from a Tor-enabled browser. - - Follow the instructions - - to get setup. -

-
diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts index 4ea73e619..d9bdedd46 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -49,7 +49,7 @@ export class SuccessPage { const ret = await this.api.complete() if (!this.isKiosk) { this.torAddress = ret['tor-address'] - this.lanAddress = ret['lan-address'].replace('https', 'http') + this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:') this.cert = ret['root-ca'] await this.api.exit() diff --git a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts index e576e9e1d..3977c8824 100644 --- a/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -2,10 +2,10 @@ import { Injectable } from '@angular/core' import { encodeBase64, pauseFor } from '@start9labs/shared' import { ApiService, - CifsRecoverySource, AttachReq, - ExecuteReq, + CifsRecoverySource, CompleteRes, + ExecuteReq, } from './api.service' import * as jose from 'node-jose' @@ -149,7 +149,7 @@ export class MockApiService extends ApiService { async complete(): Promise { await pauseFor(1000) return { - 'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion', + 'tor-address': 'https://asdafsadasdasasdasdfasdfasdf.onion', 'lan-address': 'https://adjective-noun.local', 'root-ca': encodeBase64(rootCA), } diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index af049e130..0b81b506a 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -38,7 +38,15 @@ export class AppComponent implements OnDestroy { readonly themeSwitcher: ThemeSwitcherService, ) {} - ngOnInit() { + async ngOnInit() { + if (location.hostname !== 'localhost' && location.protocol === 'http:') { + // see if site is available securely + const res = await fetch(window.location.href.replace(/^http:/, 'https:')) + if (res && res.status === 200) { + // redirect + window.location.protocol = 'https:' + } + } this.patch .watch$('ui', 'name') .subscribe(name => this.titleService.setTitle(name || 'StartOS')) diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 20c480496..225aa9184 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ModalController, ToastController } from '@ionic/angular' -import { getPkgId, copyToClipboard } from '@start9labs/shared' +import { copyToClipboard, getPkgId } from '@start9labs/shared' import { getUiInterfaceKey } from 'src/app/services/config.service' import { DataModel, @@ -51,6 +51,7 @@ export class AppInterfacesPage { 'lan-address': uiAddresses['lan-address'] ? 'https://' + uiAddresses['lan-address'] : '', + // leave http for services 'tor-address': uiAddresses['tor-address'] ? 'http://' + uiAddresses['tor-address'] : '', @@ -69,7 +70,8 @@ export class AppInterfacesPage { ? 'https://' + addresses['lan-address'] : '', 'tor-address': addresses['tor-address'] - ? 'http://' + addresses['tor-address'] + ? // leave http for services + 'http://' + addresses['tor-address'] : '', }, } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 346be96b3..b7df8da9a 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -65,7 +65,9 @@ export class AppShowPage { } async launchHttps() { - const { 'lan-address': lanAddress } = await getServerInfo(this.patch) - window.open(lanAddress) + const onTor = this.config.isTor() + const { 'lan-address': lanAddress, 'tor-address': torAddress } = + await getServerInfo(this.patch) + onTor ? window.open(torAddress) : window.open(lanAddress) } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index b9e86bfd5..11fc9dfcd 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core' import { AlertController, LoadingController, - NavController, ModalController, + NavController, ToastController, } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' @@ -306,8 +306,10 @@ export class ServerShowPage { } async launchHttps() { - const { 'lan-address': lanAddress } = await getServerInfo(this.patch) - window.open(lanAddress) + const onTor = this.config.isTor() + const { 'lan-address': lanAddress, 'tor-address': torAddress } = + await getServerInfo(this.patch) + onTor ? window.open(torAddress) : window.open(lanAddress) } addClick(title: string) { diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 16ce771aa..1c687c023 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -47,7 +47,7 @@ export const mockPatchData: DataModel = { version: '0.3.4.3', 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), 'lan-address': 'https://adjective-noun.local', - 'tor-address': 'http://myveryownspecialtoraddress.onion', + 'tor-address': 'https://myveryownspecialtoraddress.onion', 'ip-info': { eth0: { ipv4: '10.0.0.1', diff --git a/frontend/projects/ui/src/app/services/config.service.ts b/frontend/projects/ui/src/app/services/config.service.ts index eb8194729..a7740e525 100644 --- a/frontend/projects/ui/src/app/services/config.service.ts +++ b/frontend/projects/ui/src/app/services/config.service.ts @@ -69,6 +69,7 @@ export class ConfigService { if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) { return `https://${lanUiAddress(pkg)}` } else { + // leave http for services return `http://${torUiAddress(pkg)}` } }