feat(linux): make deep link auth work (#4102)

Right now it only works on my dev VM, not on my test VMs, due to #4053
and #4103, but it passes tests and should be safe to merge.

There's one doc fix and one script fix which are unrelated and could be
their own PRs, but they'd be tiny, so I left them in here.

Ref #4106 and #3713 for the plan to fix all this by splitting the tunnel
process off so that the GUI runs as a normal user.
This commit is contained in:
Reactor Scram
2024-03-13 13:11:04 -05:00
committed by GitHub
parent 32d18abb09
commit 52cde610e1
13 changed files with 281 additions and 122 deletions

View File

@@ -1,18 +1,15 @@
#!/bin/sh
#!/usr/bin/env bash
# The Windows client obviously doesn't build for *nix, but this
# script is helpful for doing UI work on those platforms for the
# Windows client.
set -e
set -euo pipefail
# Copy frontend dependencies
cp node_modules/flowbite/dist/flowbite.min.js src/
# Compile TypeScript
tsc
pnpm tsc
# Compile CSS
tailwindcss -i src/input.css -o src/output.css
pnpm tailwindcss -i src/input.css -o src/output.css
# Compile Rust and bundle
tauri build
pnpm tauri build

View File

@@ -80,7 +80,9 @@ If the client stops running while signed in, then the token may be stored in Win
# Resetting state
This is a list of all the on-disk state that you need to delete / reset to test a first-time install / first-time run of the Firezone client.
This is a list of all the on-disk state that you need to delete / reset to test a first-time install / first-time run of the Firezone GUI client.
## Windows
- Dir `%LOCALAPPDATA%/dev.firezone.client/` (Config, logs, webview cache, wintun.dll etc.)
- Dir `%PROGRAMDATA%/dev.firezone.client/` (Device ID file)
@@ -89,6 +91,10 @@ This is a list of all the on-disk state that you need to delete / reset to test
- Registry key `Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{e9245bc1-b8c1-44ca-ab1d-c6aad4f13b9c}` (IP address and DNS server for our tunnel interface)
- Windows Credential Manager, "Windows Credentials", "Generic Credentials", `dev.firezone.client/token`
## Linux
- Dir `$HOME/.local/share/applications` (.desktop file for deep links. This dir may not even exist by default on distros like Debian)
# Token storage
([#2740](https://github.com/firezone/firezone/issues/2740))

View File

@@ -78,7 +78,9 @@ pub(crate) fn run() -> Result<()> {
Some(Cmd::Elevated) => run_gui(cli),
Some(Cmd::OpenDeepLink(deep_link)) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(deep_link::open(&deep_link.url))?;
if let Err(error) = rt.block_on(deep_link::open(&deep_link.url)) {
tracing::error!(?error, "Error in `OpenDeepLink`");
}
Ok(())
}
Some(Cmd::SmokeTest) => {
@@ -161,6 +163,9 @@ struct Cli {
/// If true, show a fake update notification that opens the Firezone release page when clicked
#[arg(long, hide = true)]
test_update_notification: bool,
/// Disable deep link registration and handling, for headless CI environments
#[arg(long, hide = true)]
no_deep_links: bool,
}
impl Cli {
@@ -212,6 +217,7 @@ pub enum Cmd {
#[derive(Args)]
pub struct DeepLink {
// TODO: Should be `Secret`?
pub url: url::Url,
}

View File

@@ -1,8 +1,8 @@
//! A module for registering, catching, and parsing deep links that are sent over to the app's already-running instance
use crate::client::auth::Response as AuthResponse;
use anyhow::{bail, Context, Result};
use secrecy::{ExposeSecret, SecretString};
use std::io;
use url::Url;
pub(crate) const FZ_SCHEME: &str = "firezone-fd0020211111";
@@ -26,42 +26,22 @@ mod imp;
pub enum Error {
#[error("named pipe server couldn't start listening, we are probably the second instance")]
CantListen,
/// Error from client's POV
#[error(transparent)]
ClientCommunications(io::Error),
/// Error while connecting to the server
#[error(transparent)]
Connect(io::Error),
/// Something went wrong finding the path to our own exe
#[error(transparent)]
CurrentExe(io::Error),
/// We got some data but it's not UTF-8
#[error(transparent)]
LinkNotUtf8(std::str::Utf8Error),
#[cfg(target_os = "windows")]
#[error("Couldn't set up security descriptor for deep link server")]
SecurityDescriptor,
/// Error from server's POV
#[error(transparent)]
ServerCommunications(io::Error),
#[error(transparent)]
UrlParse(#[from] url::ParseError),
/// Something went wrong setting up the registry
#[cfg(target_os = "windows")]
#[error(transparent)]
WindowsRegistry(io::Error),
Other(#[from] anyhow::Error),
}
pub(crate) use imp::{open, register, Server};
pub(crate) fn parse_auth_callback(url: &SecretString) -> Option<AuthResponse> {
let url = Url::parse(url.expose_secret()).ok()?;
match url.host() {
Some(url::Host::Domain("handle_client_sign_in_callback")) => {}
_ => return None,
pub(crate) fn parse_auth_callback(url_secret: &SecretString) -> Result<AuthResponse> {
let url = Url::parse(url_secret.expose_secret())?;
if Some(url::Host::Domain("handle_client_sign_in_callback")) != url.host() {
bail!("URL host should be `handle_client_sign_in_callback`");
}
if url.path() != "/" {
return None;
// Sometimes I get an empty path, might be a glitch in Firefox Linux aarch64?
match url.path() {
"/" => {}
"" => {}
_ => bail!("URL path should be `/` or empty"),
}
let mut actor_name = None;
@@ -72,22 +52,19 @@ pub(crate) fn parse_auth_callback(url: &SecretString) -> Option<AuthResponse> {
match key.as_ref() {
"actor_name" => {
if actor_name.is_some() {
// actor_name must appear exactly once
return None;
bail!("`actor_name` should appear exactly once");
}
actor_name = Some(value.to_string());
}
"fragment" => {
if fragment.is_some() {
// must appear exactly once
return None;
bail!("`fragment` should appear exactly once");
}
fragment = Some(SecretString::new(value.to_string()));
}
"state" => {
if state.is_some() {
// must appear exactly once
return None;
bail!("`state` should appear exactly once");
}
state = Some(SecretString::new(value.to_string()));
}
@@ -95,23 +72,30 @@ pub(crate) fn parse_auth_callback(url: &SecretString) -> Option<AuthResponse> {
}
}
Some(AuthResponse {
actor_name: actor_name?,
fragment: fragment?,
state: state?,
Ok(AuthResponse {
actor_name: actor_name.context("URL should have `actor_name`")?,
fragment: fragment.context("URL should have `fragment`")?,
state: state.context("URL should have `state`")?,
})
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use anyhow::{Context, Result};
use secrecy::{ExposeSecret, SecretString};
#[test]
fn parse_auth_callback() -> Result<()> {
// Positive cases
let input = "firezone://handle_client_sign_in_callback/?actor_name=Reactor+Scram&fragment=a_very_secret_string&state=a_less_secret_string&identity_provider_identifier=12345";
let actual = parse_callback_wrapper(input).unwrap();
let actual = parse_callback_wrapper(input)?;
assert_eq!(actual.actor_name, "Reactor Scram");
assert_eq!(actual.fragment.expose_secret(), "a_very_secret_string");
assert_eq!(actual.state.expose_secret(), "a_less_secret_string");
let input = "firezone-fd0020211111://handle_client_sign_in_callback?account_name=Firezone&account_slug=firezone&actor_name=Reactor+Scram&fragment=a_very_secret_string&identity_provider_identifier=1234&state=a_less_secret_string";
let actual = parse_callback_wrapper(input)?;
assert_eq!(actual.actor_name, "Reactor Scram");
assert_eq!(actual.fragment.expose_secret(), "a_very_secret_string");
@@ -119,7 +103,7 @@ mod tests {
// Empty string "" `actor_name` is fine
let input = "firezone://handle_client_sign_in_callback/?actor_name=&fragment=&state=&identity_provider_identifier=12345";
let actual = parse_callback_wrapper(input).unwrap();
let actual = parse_callback_wrapper(input)?;
assert_eq!(actual.actor_name, "");
assert_eq!(actual.fragment.expose_secret(), "");
@@ -130,22 +114,44 @@ mod tests {
// URL host is wrong
let input = "firezone://not_handle_client_sign_in_callback/?actor_name=Reactor+Scram&fragment=a_very_secret_string&state=a_less_secret_string&identity_provider_identifier=12345";
let actual = parse_callback_wrapper(input);
assert!(actual.is_none());
assert!(actual.is_err());
// `actor_name` is not just blank but totally missing
let input = "firezone://handle_client_sign_in_callback/?fragment=&state=&identity_provider_identifier=12345";
let actual = parse_callback_wrapper(input);
assert!(actual.is_none());
assert!(actual.is_err());
// URL is nonsense
let input = "?????????";
let actual_result = parse_callback_wrapper(input);
assert!(actual_result.is_none());
assert!(actual_result.is_err());
Ok(())
}
fn parse_callback_wrapper(s: &str) -> Option<super::AuthResponse> {
fn parse_callback_wrapper(s: &str) -> Result<super::AuthResponse> {
super::parse_auth_callback(&SecretString::new(s.to_owned()))
}
/// Tests the named pipe or Unix domain socket, doesn't test the URI scheme itself
///
/// Will fail if any other Firezone Client instance is running
/// Will fail with permission error if Firezone already ran as sudo
#[tokio::test]
async fn socket_smoke_test() -> Result<()> {
let server = super::Server::new().context("Couldn't start Server")?;
let server_task = tokio::spawn(async move {
let bytes = server.accept().await?;
Ok::<_, anyhow::Error>(bytes)
});
let id = uuid::Uuid::new_v4().to_string();
let expected_url = url::Url::parse(&format!("bogus-test-schema://{id}"))?;
super::open(&expected_url).await?;
let bytes = server_task.await??;
let s = std::str::from_utf8(bytes.expose_secret())?;
let url = url::Url::parse(s)?;
assert_eq!(url, expected_url);
Ok(())
}
}

View File

@@ -1,29 +1,121 @@
//! TODO: Not implemented for Linux yet
use crate::client::known_dirs;
use anyhow::{bail, Context, Result};
use secrecy::{ExposeSecret, Secret};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{UnixListener, UnixStream},
};
use super::Error;
use secrecy::SecretString;
const SOCK_NAME: &str = "deep_link.sock";
pub(crate) struct Server {}
pub(crate) struct Server {
listener: UnixListener,
}
impl Server {
pub(crate) fn new() -> Result<Self, Error> {
tracing::warn!("Not implemented yet");
tracing::trace!(scheme = super::FZ_SCHEME, "prevents dead code warning");
Ok(Self {})
/// Create a new deep link server to make sure we're the only instance
///
/// Still uses `thiserror` so we can catch the deep_link `CantListen` error
pub(crate) fn new() -> Result<Self, super::Error> {
let dir = known_dirs::runtime().context("couldn't find runtime dir")?;
let path = dir.join(SOCK_NAME);
// TODO: This breaks single instance. Can we enforce it some other way?
std::fs::remove_file(&path).ok();
std::fs::create_dir_all(&dir).context("Can't create dir for deep link socket")?;
let listener = UnixListener::bind(&path).context("Couldn't bind listener Unix socket")?;
// Figure out who we were before `sudo`, if using sudo
if let Ok(username) = std::env::var("SUDO_USER") {
// chown so that when the non-privileged browser launches us,
// we can send a message to our privileged main process
std::process::Command::new("chown")
.arg(username)
.arg(&path)
.status()
.context("couldn't chown Unix domain socket")?;
}
Ok(Self { listener })
}
pub(crate) async fn accept(self) -> Result<SecretString, Error> {
tracing::warn!("Deep links not implemented yet on Linux");
futures::future::pending().await
/// Await one incoming deep link
///
/// To match the Windows API, this consumes the `Server`.
pub(crate) async fn accept(self) -> Result<Secret<Vec<u8>>> {
tracing::debug!("deep_link::accept");
let (mut stream, _) = self.listener.accept().await?;
tracing::debug!("Accepted Unix domain socket connection");
// TODO: Limit reads to 4,096 bytes. Partial reads will probably never happen
// since it's a local socket transferring very small data.
let mut bytes = vec![];
stream
.read_to_end(&mut bytes)
.await
.context("failed to read incoming deep link over Unix socket stream")?;
let bytes = Secret::new(bytes);
tracing::debug!(
len = bytes.expose_secret().len(),
"Got data from Unix domain socket"
);
Ok(bytes)
}
}
pub(crate) async fn open(_url: &url::Url) -> Result<(), Error> {
tracing::warn!("Not implemented yet");
pub(crate) async fn open(url: &url::Url) -> Result<()> {
crate::client::logging::debug_command_setup()?;
let dir = known_dirs::runtime().context("deep_link::open couldn't find runtime dir")?;
let path = dir.join(SOCK_NAME);
let mut stream = UnixStream::connect(&path).await?;
stream.write_all(url.to_string().as_bytes()).await?;
Ok(())
}
pub(crate) fn register() -> Result<(), Error> {
tracing::warn!("Not implemented yet");
/// Register a URI scheme so that browser can deep link into our app for auth
///
/// Performs blocking I/O (Waits on `xdg-desktop-menu` subprocess)
pub(crate) fn register() -> Result<()> {
// Write `$HOME/.local/share/applications/firezone-client.desktop`
// According to <https://wiki.archlinux.org/title/Desktop_entries>, that's the place to put
// per-user desktop entries.
let dir = dirs::data_local_dir()
.context("can't figure out where to put our desktop entry")?
.join("applications");
std::fs::create_dir_all(&dir)?;
// Don't use atomic writes here - If we lose power, we'll just rewrite this file on
// the next boot anyway.
let path = dir.join("firezone-client.desktop");
let exe = std::env::current_exe().context("failed to find our own exe path")?;
let content = format!(
"[Desktop Entry]
Version=1.0
Name=Firezone
Comment=Firezone GUI Client
Exec={} open-deep-link %U
Terminal=false
Type=Application
MimeType=x-scheme-handler/{}
Categories=Network;
",
exe.display(),
super::FZ_SCHEME
);
std::fs::write(&path, content).context("failed to write desktop entry file")?;
// Run `xdg-desktop-menu install` with that desktop file
let xdg_desktop_menu = "xdg-desktop-menu";
let status = std::process::Command::new(xdg_desktop_menu)
.arg("install")
.arg(&path)
.status()
.with_context(|| format!("failed to run `{xdg_desktop_menu}`"))?;
if !status.success() {
bail!("failed to register our deep link scheme")
}
Ok(())
}

View File

@@ -1,26 +1,26 @@
//! Placeholder
use super::Error;
use secrecy::{Secret, SecretString};
use anyhow::Result;
use secrecy::Secret;
pub(crate) struct Server {}
impl Server {
pub(crate) fn new() -> Result<Self, Error> {
pub(crate) fn new() -> Result<Self> {
tracing::warn!("This is not the actual Mac client");
tracing::trace!(scheme = super::FZ_SCHEME, "prevents dead code warning");
Ok(Self {})
}
pub(crate) async fn accept(self) -> Result<SecretString, Error> {
pub(crate) async fn accept(self) -> Result<Secret<Vec<u8>>> {
futures::future::pending().await
}
}
pub(crate) async fn open(_url: &url::Url) -> Result<(), Error> {
pub(crate) async fn open(_url: &url::Url) -> Result<()> {
Ok(())
}
pub(crate) fn register() -> Result<(), Error> {
pub(crate) fn register() -> Result<()> {
Ok(())
}

View File

@@ -1,10 +1,11 @@
//! A module for registering, catching, and parsing deep links that are sent over to the app's already-running instance
//! Based on reading some of the Windows code from <https://github.com/FabianLars/tauri-plugin-deep-link>, which is licensed "MIT OR Apache-2.0"
use super::{Error, FZ_SCHEME};
use super::FZ_SCHEME;
use anyhow::{Context, Result};
use connlib_shared::BUNDLE_ID;
use secrecy::{ExposeSecret, Secret, SecretString};
use std::{ffi::c_void, io, path::Path, str::FromStr};
use secrecy::Secret;
use std::{ffi::c_void, io, path::Path};
use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::windows::named_pipe};
use windows::Win32::Security as WinSec;
@@ -18,7 +19,8 @@ impl Server {
/// Construct a server, but don't await client connections yet
///
/// Panics if there is no Tokio runtime
pub(crate) fn new() -> Result<Self, Error> {
/// Still uses `thiserror` so we can catch the deep_link `CantListen` error
pub(crate) fn new() -> Result<Self, super::Error> {
// This isn't air-tight - We recreate the whole server on each loop,
// rather than binding 1 socket and accepting many streams like a normal socket API.
// I can only assume Tokio is following Windows' underlying API.
@@ -42,9 +44,9 @@ impl Server {
psd,
windows::Win32::System::SystemServices::SECURITY_DESCRIPTOR_REVISION,
)
.map_err(|_| Error::SecurityDescriptor)?;
.context("InitializeSecurityDescriptor failed")?;
WinSec::SetSecurityDescriptorDacl(psd, true, None, false)
.map_err(|_| Error::SecurityDescriptor)?;
.context("SetSecurityDescriptorDacl failed")?;
}
let mut sa = WinSec::SECURITY_ATTRIBUTES {
@@ -64,7 +66,7 @@ impl Server {
// or lifetime problems because we only pass pointers to our local vars to
// Win32, and Win32 shouldn't save them anywhere.
let server = unsafe { server_options.create_with_security_attributes_raw(path, sa_ptr) }
.map_err(|_| Error::CantListen)?;
.map_err(|_| super::Error::CantListen)?;
tracing::debug!("server is bound");
Ok(Server { inner: server })
@@ -75,11 +77,11 @@ impl Server {
/// I assume this is based on the underlying Windows API.
/// I tried re-using the server and it acted strange. The official Tokio
/// examples are not clear on this.
pub(crate) async fn accept(mut self) -> Result<SecretString, Error> {
pub(crate) async fn accept(mut self) -> Result<Secret<Vec<u8>>> {
self.inner
.connect()
.await
.map_err(Error::ServerCommunications)?;
.context("Couldn't accept connection from named pipe client")?;
tracing::debug!("server got connection");
// TODO: Limit the read size here. Our typical callback is 350 bytes, so 4,096 bytes should be more than enough.
@@ -89,31 +91,24 @@ impl Server {
self.inner
.read_to_end(&mut bytes)
.await
.map_err(Error::ServerCommunications)?;
.context("Couldn't read bytes from named pipe client")?;
let bytes = Secret::new(bytes);
self.inner.disconnect().ok();
tracing::debug!("Server read");
let s = SecretString::from_str(
std::str::from_utf8(bytes.expose_secret()).map_err(Error::LinkNotUtf8)?,
)
.expect("Infallible");
Ok(s)
Ok(bytes)
}
}
/// Open a deep link by sending it to the already-running instance of the app
pub async fn open(url: &url::Url) -> Result<(), Error> {
pub async fn open(url: &url::Url) -> Result<()> {
let path = named_pipe_path(BUNDLE_ID);
let mut client = named_pipe::ClientOptions::new()
.open(path)
.map_err(Error::Connect)?;
.context("Couldn't connect to named pipe server")?;
client
.write_all(url.as_str().as_bytes())
.await
.map_err(Error::ClientCommunications)?;
.context("Couldn't write bytes to named pipe server")?;
Ok(())
}
@@ -121,14 +116,14 @@ pub async fn open(url: &url::Url) -> Result<(), Error> {
///
/// This is copied almost verbatim from tauri-plugin-deep-link's `register` fn, with an improvement
/// that we send the deep link to a subcommand so the URL won't confuse `clap`
pub fn register() -> Result<(), Error> {
pub fn register() -> Result<()> {
let exe = tauri_utils::platform::current_exe()
.map_err(Error::CurrentExe)?
.context("Can't find our own exe path")?
.display()
.to_string()
.replace("\\\\?\\", "");
set_registry_values(BUNDLE_ID, &exe).map_err(Error::WindowsRegistry)?;
set_registry_values(BUNDLE_ID, &exe).context("Can't set Windows Registry values")?;
Ok(())
}

View File

@@ -87,9 +87,13 @@ pub(crate) enum Error {
// `client.rs` provides a more user-friendly message when showing the error dialog box
#[error("WebViewNotInstalled")]
WebViewNotInstalled,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// Runs the Tauri GUI and returns on exit or unrecoverable error
///
/// Still uses `thiserror` so we can catch the deep_link `CantListen` error
pub(crate) fn run(cli: &client::Cli) -> Result<(), Error> {
let advanced_settings = settings::load_advanced_settings().unwrap_or_default();
@@ -150,6 +154,11 @@ pub(crate) fn run(cli: &client::Cli) -> Result<(), Error> {
}
});
// Make sure we're single-instance
// We register our deep links to call the `open-deep-link` subcommand,
// so if we're at this point, we know we've been launched manually
let server = deep_link::Server::new()?;
if let Some(client::Cmd::SmokeTest) = &cli.command {
let ctlr_tx = ctlr_tx.clone();
tokio::spawn(async move {
@@ -160,15 +169,13 @@ pub(crate) fn run(cli: &client::Cli) -> Result<(), Error> {
});
}
// Make sure we're single-instance
// We register our deep links to call the `open-deep-link` subcommand,
// so if we're at this point, we know we've been launched manually
let server = deep_link::Server::new()?;
// We know now we're the only instance on the computer, so register our exe
// to handle deep links
deep_link::register()?;
tokio::spawn(accept_deep_links(server, ctlr_tx.clone()));
tracing::debug!(cli.no_deep_links);
if !cli.no_deep_links {
// The single-instance check is done, so register our exe
// to handle deep links
deep_link::register().context("Failed to register deep link handler")?;
tokio::spawn(accept_deep_links(server, ctlr_tx.clone()));
}
let managed = Managed {
ctlr_tx: ctlr_tx.clone(),
@@ -393,11 +400,20 @@ async fn check_for_updates(ctlr_tx: CtlrTx, always_show_update_notification: boo
/// * `server` An initial named pipe server to consume before making new servers. This lets us also use the named pipe to enforce single-instance
async fn accept_deep_links(mut server: deep_link::Server, ctlr_tx: CtlrTx) -> Result<()> {
loop {
if let Ok(url) = server.accept().await {
ctlr_tx
.send(ControllerRequest::SchemeRequest(url))
.await
.ok();
match server.accept().await {
Ok(bytes) => {
let url = SecretString::from_str(
std::str::from_utf8(bytes.expose_secret())
.context("Incoming deep link was not valid UTF-8")?,
)
.context("Impossible: can't wrap String into SecretString")?;
// Ignore errors from this, it would only happen if the app is shutting down, otherwise we would wait
ctlr_tx
.send(ControllerRequest::SchemeRequest(url))
.await
.ok();
}
Err(error) => tracing::error!(?error, "error while accepting deep link"),
}
// We re-create the named pipe server every time we get a link, because of an oddity in the Windows API.
server = deep_link::Server::new()?;
@@ -645,7 +661,7 @@ impl Controller {
if let Some(req) = self.auth.start_sign_in()? {
let url = req.to_url(&self.advanced_settings.auth_base_url);
self.refresh_system_tray_menu()?;
tauri::api::shell::open(&self.app.shell_scope(), url.expose_secret(), None)?;
os::open_url(&self.app, &url)?;
}
}
Req::SystemTrayMenu(TrayMenuEvent::SignOut) => {

View File

@@ -1,4 +1,29 @@
use super::{ControllerRequest, CtlrTx, Error};
use anyhow::Result;
use secrecy::{ExposeSecret, SecretString};
/// Open a URL in the user's default browser
pub(crate) fn open_url(_app: &tauri::AppHandle, url: &SecretString) -> Result<()> {
// This is silly but it might work.
// Once <https://github.com/firezone/firezone/issues/3713> closes,
// just go back to using Tauri's shell open like we do on Windows.
//
// Figure out who we were before `sudo`
let username = std::env::var("SUDO_USER")?;
std::process::Command::new("sudo")
// Make sure `XDG_RUNTIME_DIR` is preserved so we can find the Unix domain socket again
.arg("--preserve-env")
// Sudo back to our original username
.arg("--user")
.arg(username)
// Use the XDG wrapper for the user's default web browser (this will leak an `xdg-open` process if the user's first browser tab is the Firezone login. That process will end when the browser closes
.arg("xdg-open")
.arg(url.expose_secret())
// Detach, since web browsers, and therefore xdg-open, typically block if there are no windows open and you're opening the first tab from CLI
.spawn()?;
Ok(())
}
/// Show a notification in the bottom right of the screen
pub(crate) fn show_notification(_title: &str, _body: &str) -> Result<(), Error> {

View File

@@ -1,5 +1,11 @@
//! This file is a stub only to do Tauri UI dev natively on a Mac.
use super::{ControllerRequest, CtlrTx, Error};
use anyhow::Result;
use secrecy::SecretString;
pub(crate) fn open_url(_app: &tauri::AppHandle, _url: &SecretString) -> Result<()> {
unimplemented!()
}
/// Show a notification in the bottom right of the screen
pub(crate) fn show_notification(_title: &str, _body: &str) -> Result<(), Error> {

View File

@@ -1,5 +1,14 @@
use super::{ControllerRequest, CtlrTx, Error};
use anyhow::Result;
use connlib_shared::BUNDLE_ID;
use secrecy::{ExposeSecret, SecretString};
use tauri::Manager;
/// Open a URL in the user's default browser
pub(crate) fn open_url(app: &tauri::AppHandle, url: &SecretString) -> Result<()> {
tauri::api::shell::open(&app.shell_scope(), url.expose_secret(), None)?;
Ok(())
}
/// Show a notification in the bottom right of the screen
///

View File

@@ -11,7 +11,8 @@ pub enum Error {
#[cfg(target_os = "linux")]
pub fn get() -> Result<Vec<IpAddr>, Error> {
todo!()
tracing::error!("Resolvers module not yet implemented for Linux, returning empty Vec");
Ok(Vec::default())
}
#[cfg(target_os = "macos")]

View File

@@ -23,7 +23,7 @@ function smoke_test() {
sudo stat "$DEVICE_ID_PATH" && exit 1
# Run the smoke test normally
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" smoke-test
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" --no-deep-links smoke-test
# Note the device ID
DEVICE_ID_1=$(cat "$DEVICE_ID_PATH")
@@ -36,7 +36,7 @@ function smoke_test() {
sudo stat "$DEVICE_ID_PATH"
# Run the test again and make sure the device ID is not changed
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" smoke-test
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" --no-deep-links smoke-test
DEVICE_ID_2=$(cat "$DEVICE_ID_PATH")
if [ "$DEVICE_ID_1" != "$DEVICE_ID_2" ]
@@ -56,7 +56,7 @@ function crash_test() {
sudo rm -f "$DUMP_PATH"
# Fail if it returns success, this is supposed to crash
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" --crash && exit 1
sudo --preserve-env xvfb-run --auto-servernum ../target/debug/"$PACKAGE" --crash --no-deep-links && exit 1
# Fail if the crash file wasn't written
sudo stat "$DUMP_PATH"