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>
This commit is contained in:
Aiden McClelland
2023-07-14 14:58:02 -06:00
committed by GitHub
parent 36c3617204
commit 9ff0128fb1
14 changed files with 182 additions and 104 deletions

View File

@@ -49,7 +49,7 @@ impl Database {
last_wifi_region: None, last_wifi_region: None,
eos_version_compat: Current::new().compat().clone(), eos_version_compat: Current::new().compat().clone(),
lan_address, lan_address,
tor_address: format!("http://{}", account.key.tor_address()) tor_address: format!("https://{}", account.key.tor_address())
.parse() .parse()
.unwrap(), .unwrap(),
ip_info: BTreeMap::new(), ip_info: BTreeMap::new(),

View File

@@ -21,6 +21,7 @@ use tracing::instrument;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::manager::sync::synchronizer; use crate::manager::sync::synchronizer;
use crate::net::net_controller::NetService; use crate::net::net_controller::NetService;
use crate::net::vhost::AlpnInfo;
use crate::procedure::docker::{DockerContainer, DockerProcedure, LongRunning}; use crate::procedure::docker::{DockerContainer, DockerProcedure, LongRunning};
#[cfg(feature = "js_engine")] #[cfg(feature = "js_engine")]
use crate::procedure::js_scripts::JsProcedure; use crate::procedure::js_scripts::JsProcedure;
@@ -573,8 +574,14 @@ async fn add_network_for_main(
let mut tx = secrets.begin().await?; let mut tx = secrets.begin().await?;
for (id, interface) in &seed.manifest.interfaces.0 { for (id, interface) in &seed.manifest.interfaces.0 {
for (external, internal) in interface.lan_config.iter().flatten() { for (external, internal) in interface.lan_config.iter().flatten() {
svc.add_lan(&mut tx, id.clone(), external.0, internal.internal, false) svc.add_lan(
.await?; &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) { 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) svc.add_tor(&mut tx, id.clone(), external.0, internal.0)

View File

@@ -15,7 +15,7 @@ use crate::net::keys::Key;
use crate::net::mdns::MdnsController; use crate::net::mdns::MdnsController;
use crate::net::ssl::{export_cert, export_key, SslManager}; use crate::net::ssl::{export_cert, export_key, SslManager};
use crate::net::tor::TorController; use crate::net::tor::TorController;
use crate::net::vhost::VHostController; use crate::net::vhost::{AlpnInfo, VHostController};
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::volume::cert_dir; use crate::volume::cert_dir;
use crate::{Error, HOST_IP}; use crate::{Error, HOST_IP};
@@ -55,6 +55,8 @@ impl NetController {
} }
async fn add_os_bindings(&mut self, hostname: &Hostname, key: &Key) -> Result<(), Error> { 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 // Internal DNS
self.vhost self.vhost
.add( .add(
@@ -62,7 +64,7 @@ impl NetController {
Some("embassy".into()), Some("embassy".into()),
443, 443,
([127, 0, 0, 1], 80).into(), ([127, 0, 0, 1], 80).into(),
false, alpn.clone(),
) )
.await?; .await?;
self.os_bindings self.os_bindings
@@ -71,7 +73,13 @@ impl NetController {
// LAN IP // LAN IP
self.os_bindings.push( self.os_bindings.push(
self.vhost 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?, .await?,
); );
@@ -83,7 +91,7 @@ impl NetController {
Some("localhost".into()), Some("localhost".into()),
443, 443,
([127, 0, 0, 1], 80).into(), ([127, 0, 0, 1], 80).into(),
false, alpn.clone(),
) )
.await?, .await?,
); );
@@ -94,7 +102,7 @@ impl NetController {
Some(hostname.no_dot_host_name()), Some(hostname.no_dot_host_name()),
443, 443,
([127, 0, 0, 1], 80).into(), ([127, 0, 0, 1], 80).into(),
false, alpn.clone(),
) )
.await?, .await?,
); );
@@ -107,7 +115,7 @@ impl NetController {
Some(hostname.local_domain_name()), Some(hostname.local_domain_name()),
443, 443,
([127, 0, 0, 1], 80).into(), ([127, 0, 0, 1], 80).into(),
false, alpn.clone(),
) )
.await?, .await?,
); );
@@ -127,7 +135,7 @@ impl NetController {
Some(key.tor_address().to_string()), Some(key.tor_address().to_string()),
443, 443,
([127, 0, 0, 1], 80).into(), ([127, 0, 0, 1], 80).into(),
false, alpn.clone(),
) )
.await?, .await?,
); );
@@ -179,7 +187,7 @@ impl NetController {
key: Key, key: Key,
external: u16, external: u16,
target: SocketAddr, target: SocketAddr,
connect_ssl: bool, connect_ssl: Result<(), AlpnInfo>,
) -> Result<Vec<Arc<()>>, Error> { ) -> Result<Vec<Arc<()>>, Error> {
let mut rcs = Vec::with_capacity(2); let mut rcs = Vec::with_capacity(2);
rcs.push( rcs.push(
@@ -261,7 +269,7 @@ impl NetService {
id: InterfaceId, id: InterfaceId,
external: u16, external: u16,
internal: u16, internal: u16,
connect_ssl: bool, connect_ssl: Result<(), AlpnInfo>,
) -> Result<(), Error> ) -> Result<(), Error>
where where
for<'a> &'a mut Ex: PgExecutor<'a>, for<'a> &'a mut Ex: PgExecutor<'a>,

View File

@@ -41,7 +41,7 @@ impl VHostController {
hostname: Option<String>, hostname: Option<String>,
external: u16, external: u16,
target: SocketAddr, target: SocketAddr,
connect_ssl: bool, connect_ssl: Result<(), AlpnInfo>,
) -> Result<Arc<()>, Error> { ) -> Result<Arc<()>, Error> {
let mut writable = self.servers.lock().await; let mut writable = self.servers.lock().await;
let server = if let Some(server) = writable.remove(&external) { let server = if let Some(server) = writable.remove(&external) {
@@ -77,10 +77,16 @@ impl VHostController {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct TargetInfo { struct TargetInfo {
addr: SocketAddr, addr: SocketAddr,
connect_ssl: bool, connect_ssl: Result<(), AlpnInfo>,
key: Key, key: Key,
} }
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlpnInfo {
Reflect,
Specified(Vec<Vec<u8>>),
}
struct VHostServer { struct VHostServer {
mapping: Weak<RwLock<BTreeMap<Option<String>, BTreeMap<TargetInfo, Weak<()>>>>>, mapping: Weak<RwLock<BTreeMap<Option<String>, BTreeMap<TargetInfo, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>, _thread: NonDetachingJoinHandle<()>,
@@ -178,7 +184,7 @@ impl VHostServer {
let cfg = ServerConfig::builder() let cfg = ServerConfig::builder()
.with_safe_defaults() .with_safe_defaults()
.with_no_client_auth(); .with_no_client_auth();
let cfg = let mut cfg =
if mid.client_hello().signature_schemes().contains( if mid.client_hello().signature_schemes().contains(
&tokio_rustls::rustls::SignatureScheme::ED25519, &tokio_rustls::rustls::SignatureScheme::ED25519,
) { ) {
@@ -213,48 +219,86 @@ impl VHostServer {
.private_key_to_der()?, .private_key_to_der()?,
), ),
) )
}; }
let mut tls_stream = mid .with_kind(crate::ErrorKind::OpenSsl)?;
.into_stream(Arc::new( match target.connect_ssl {
cfg.with_kind(crate::ErrorKind::OpenSsl)?, Ok(()) => {
)) let mut client_cfg =
.await?;
tls_stream.get_mut().0.stop_buffering();
if target.connect_ssl {
tokio::io::copy_bidirectional(
&mut tls_stream,
&mut TlsConnector::from(Arc::new(
tokio_rustls::rustls::ClientConfig::builder() tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults() .with_safe_defaults()
.with_root_certificates({ .with_root_certificates({
let mut store = RootCertStore::empty(); let mut store = RootCertStore::empty();
store.add( store.add(
&tokio_rustls::rustls::Certificate( &tokio_rustls::rustls::Certificate(
key.root_ca().to_der()?, key.root_ca().to_der()?,
), ),
).with_kind(crate::ErrorKind::OpenSsl)?; ).with_kind(crate::ErrorKind::OpenSsl)?;
store store
}) })
.with_no_client_auth(), .with_no_client_auth();
)) client_cfg.alpn_protocols = mid
.connect( .client_hello()
key.key() .alpn()
.internal_address() .into_iter()
.as_str() .flatten()
.try_into() .map(|x| x.to_vec())
.with_kind(crate::ErrorKind::OpenSsl)?, .collect();
tcp_stream, 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 .await?;
.with_kind(crate::ErrorKind::OpenSsl)?, }
) Err(AlpnInfo::Reflect) => {
.await?; for proto in
} else { mid.client_hello().alpn().into_iter().flatten()
tokio::io::copy_bidirectional( {
&mut tls_stream, cfg.alpn_protocols.push(proto.into());
&mut tcp_stream, }
) let mut tls_stream =
.await?; 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 { } else {
// 503 // 503

View File

@@ -144,7 +144,7 @@ pub async fn attach(
} }
let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?;
*ctx.setup_result.write().await = Some((guid, SetupResult { *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(), lan_address: hostname.lan_address(),
root_ca: String::from_utf8(root_ca.to_pem()?)?, root_ca: String::from_utf8(root_ca.to_pem()?)?,
})); }));
@@ -281,7 +281,7 @@ pub async fn execute(
*ctx.setup_result.write().await = Some(( *ctx.setup_result.write().await = Some((
guid, guid,
SetupResult { SetupResult {
tor_address: format!("http://{}", tor_addr), tor_address: format!("https://{}", tor_addr),
lan_address: hostname.lan_address(), lan_address: hostname.lan_address(),
root_ca: String::from_utf8( root_ca: String::from_utf8(
root_ca.to_pem().expect("failed to serialize root ca"), root_ca.to_pem().expect("failed to serialize root ca"),

View File

@@ -27,31 +27,15 @@
<section <section
style=" style="
padding: 1rem 3rem 2rem 3rem; padding: 1rem 3rem 2rem 3rem;
border: solid #c4c4c5 3px;
margin-bottom: 24px; margin-bottom: 24px;
border: solid #c4c4c5 3px;
border-radius: 20px;
" "
> >
<h2 style="font-variant-caps: all-small-caps">
Access from home (LAN)
</h2>
<p>
Visit the address below when you are connected to the same WiFi or
Local Area Network (LAN) as your server:
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="lan-addr"></code>
</p>
<div> <div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3> <h3 style="color: #f8546a; font-weight: bold">Important!</h3>
<p> <p>
Be sure to Download your server's Root CA and
<a <a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan" href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
target="_blank" target="_blank"
@@ -60,12 +44,10 @@
> >
follow the instructions follow the instructions
</a> </a>
to establish a secure connection by installing your server's root to establish a secure connection with your server.
certificate authority.
</p> </p>
</div> </div>
<div style="text-align: center">
<div style="padding: 2rem; text-align: center">
<a <a
id="cert" id="cert"
[download]="crtName" [download]="crtName"
@@ -88,12 +70,49 @@
</a> </a>
</div> </div>
</section> </section>
<section
style="
padding: 1rem 3rem 2rem 3rem;
border: solid #c4c4c5 3px;
border-radius: 20px;
margin-bottom: 24px;
"
>
<h2 style="font-variant-caps: all-small-caps">
Access from home (LAN)
</h2>
<p>
Visit the address below when you are connected to the same WiFi or
Local Area Network (LAN) as your server.
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="lan-addr"></code>
</p>
<section style="padding: 1rem 3rem 2rem 3rem; border: solid #c4c4c5 3px">
<h2 style="font-variant-caps: all-small-caps"> <h2 style="font-variant-caps: all-small-caps">
Access on the go (Tor) Access on the go (Tor)
</h2> </h2>
<p>Visit the address below when you are away from home:</p> <p>Visit the address below when you are away from home.</p>
<p>
<span style="font-weight: bold">Note:</span>
This address will only work from a Tor-enabled browser.
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
<p <p
style=" style="
padding: 16px; padding: 16px;
@@ -104,21 +123,6 @@
> >
<code id="tor-addr"></code> <code id="tor-addr"></code>
</p> </p>
<div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3>
<p>
This address will only work from a Tor-enabled browser.
<a
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
target="_blank"
rel="noreferrer"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
</div>
</section> </section>
</div> </div>
</body> </body>

View File

@@ -49,7 +49,7 @@ export class SuccessPage {
const ret = await this.api.complete() const ret = await this.api.complete()
if (!this.isKiosk) { if (!this.isKiosk) {
this.torAddress = ret['tor-address'] 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'] this.cert = ret['root-ca']
await this.api.exit() await this.api.exit()

View File

@@ -2,10 +2,10 @@ import { Injectable } from '@angular/core'
import { encodeBase64, pauseFor } from '@start9labs/shared' import { encodeBase64, pauseFor } from '@start9labs/shared'
import { import {
ApiService, ApiService,
CifsRecoverySource,
AttachReq, AttachReq,
ExecuteReq, CifsRecoverySource,
CompleteRes, CompleteRes,
ExecuteReq,
} from './api.service' } from './api.service'
import * as jose from 'node-jose' import * as jose from 'node-jose'
@@ -149,7 +149,7 @@ export class MockApiService extends ApiService {
async complete(): Promise<CompleteRes> { async complete(): Promise<CompleteRes> {
await pauseFor(1000) await pauseFor(1000)
return { return {
'tor-address': 'http://asdafsadasdasasdasdfasdfasdf.onion', 'tor-address': 'https://asdafsadasdasasdasdfasdfasdf.onion',
'lan-address': 'https://adjective-noun.local', 'lan-address': 'https://adjective-noun.local',
'root-ca': encodeBase64(rootCA), 'root-ca': encodeBase64(rootCA),
} }

View File

@@ -38,7 +38,15 @@ export class AppComponent implements OnDestroy {
readonly themeSwitcher: ThemeSwitcherService, 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 this.patch
.watch$('ui', 'name') .watch$('ui', 'name')
.subscribe(name => this.titleService.setTitle(name || 'StartOS')) .subscribe(name => this.titleService.setTitle(name || 'StartOS'))

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { ModalController, ToastController } from '@ionic/angular' 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 { getUiInterfaceKey } from 'src/app/services/config.service'
import { import {
DataModel, DataModel,
@@ -51,6 +51,7 @@ export class AppInterfacesPage {
'lan-address': uiAddresses['lan-address'] 'lan-address': uiAddresses['lan-address']
? 'https://' + uiAddresses['lan-address'] ? 'https://' + uiAddresses['lan-address']
: '', : '',
// leave http for services
'tor-address': uiAddresses['tor-address'] 'tor-address': uiAddresses['tor-address']
? 'http://' + uiAddresses['tor-address'] ? 'http://' + uiAddresses['tor-address']
: '', : '',
@@ -69,7 +70,8 @@ export class AppInterfacesPage {
? 'https://' + addresses['lan-address'] ? 'https://' + addresses['lan-address']
: '', : '',
'tor-address': addresses['tor-address'] 'tor-address': addresses['tor-address']
? 'http://' + addresses['tor-address'] ? // leave http for services
'http://' + addresses['tor-address']
: '', : '',
}, },
} }

View File

@@ -65,7 +65,9 @@ export class AppShowPage {
} }
async launchHttps() { async launchHttps() {
const { 'lan-address': lanAddress } = await getServerInfo(this.patch) const onTor = this.config.isTor()
window.open(lanAddress) const { 'lan-address': lanAddress, 'tor-address': torAddress } =
await getServerInfo(this.patch)
onTor ? window.open(torAddress) : window.open(lanAddress)
} }
} }

View File

@@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core'
import { import {
AlertController, AlertController,
LoadingController, LoadingController,
NavController,
ModalController, ModalController,
NavController,
ToastController, ToastController,
} from '@ionic/angular' } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -306,8 +306,10 @@ export class ServerShowPage {
} }
async launchHttps() { async launchHttps() {
const { 'lan-address': lanAddress } = await getServerInfo(this.patch) const onTor = this.config.isTor()
window.open(lanAddress) const { 'lan-address': lanAddress, 'tor-address': torAddress } =
await getServerInfo(this.patch)
onTor ? window.open(torAddress) : window.open(lanAddress)
} }
addClick(title: string) { addClick(title: string) {

View File

@@ -47,7 +47,7 @@ export const mockPatchData: DataModel = {
version: '0.3.4.3', version: '0.3.4.3',
'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(),
'lan-address': 'https://adjective-noun.local', 'lan-address': 'https://adjective-noun.local',
'tor-address': 'http://myveryownspecialtoraddress.onion', 'tor-address': 'https://myveryownspecialtoraddress.onion',
'ip-info': { 'ip-info': {
eth0: { eth0: {
ipv4: '10.0.0.1', ipv4: '10.0.0.1',

View File

@@ -69,6 +69,7 @@ export class ConfigService {
if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) { if (this.isLan() && hasLanUi(pkg.manifest.interfaces)) {
return `https://${lanUiAddress(pkg)}` return `https://${lanUiAddress(pkg)}`
} else { } else {
// leave http for services
return `http://${torUiAddress(pkg)}` return `http://${torUiAddress(pkg)}`
} }
} }