diff options
Diffstat (limited to 'browser/app/nmhproxy')
-rw-r--r-- | browser/app/nmhproxy/Cargo.toml | 17 | ||||
-rw-r--r-- | browser/app/nmhproxy/moz.build | 15 | ||||
-rw-r--r-- | browser/app/nmhproxy/src/commands.rs | 350 | ||||
-rw-r--r-- | browser/app/nmhproxy/src/main.rs | 55 |
4 files changed, 437 insertions, 0 deletions
diff --git a/browser/app/nmhproxy/Cargo.toml b/browser/app/nmhproxy/Cargo.toml new file mode 100644 index 0000000000..14746d51b6 --- /dev/null +++ b/browser/app/nmhproxy/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nmhproxy" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +description = "A lightweight native messaging listener executable for the Firefox Bridge extension which launches Firefox in regular or private modes, avoiding the need to convert Firefox itself into a listener." + +[[bin]] +name = "nmhproxy" +path = "src/main.rs" + +[dependencies] +mozbuild = "0.1" +mozilla-central-workspace-hack = { version = "0.1", features = ["nmhproxy"], optional = true } +serde = { version = "1", features = ["derive", "rc"] } +serde_json = "1.0" +url = "2.4" diff --git a/browser/app/nmhproxy/moz.build b/browser/app/nmhproxy/moz.build new file mode 100644 index 0000000000..1f12e2880d --- /dev/null +++ b/browser/app/nmhproxy/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +RUST_PROGRAMS += [ + "nmhproxy", +] + +# Ideally, the build system would set @rpath to be @executable_path as +# a default for this executable so that this addition to LDFLAGS would not be +# needed here. Bug 1772575 is filed to implement that. +if CONFIG["OS_ARCH"] == "Darwin": + LDFLAGS += ["-Wl,-rpath,@executable_path"] diff --git a/browser/app/nmhproxy/src/commands.rs b/browser/app/nmhproxy/src/commands.rs new file mode 100644 index 0000000000..29c86a0dd7 --- /dev/null +++ b/browser/app/nmhproxy/src/commands.rs @@ -0,0 +1,350 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::{Deserialize, Serialize}; +use std::io::{self, Read, Write}; +use std::process::Command; +use url::Url; + +#[cfg(target_os = "windows")] +const OS_NAME: &str = "windows"; + +#[cfg(target_os = "macos")] +const OS_NAME: &str = "macos"; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "command", content = "data")] +// { +// "command": "LaunchFirefox", +// "data": {"url": "https://example.com"}, +// } +pub enum FirefoxCommand { + LaunchFirefox { url: String }, + LaunchFirefoxPrivate { url: String }, + GetVersion {}, +} +#[derive(Serialize, Deserialize)] +// { +// "message": "Successful launch", +// "result_code": 1, +// } +pub struct Response { + pub message: String, + pub result_code: u32, +} + +#[repr(u32)] +pub enum ResultCode { + Success = 0, + Error = 1, +} +impl From<ResultCode> for u32 { + fn from(m: ResultCode) -> u32 { + m as u32 + } +} + +trait CommandRunner { + fn new() -> Self + where + Self: Sized; + fn arg(&mut self, arg: &str) -> &mut Self; + fn args(&mut self, args: &[&str]) -> &mut Self; + fn spawn(&mut self) -> std::io::Result<()>; + fn to_string(&mut self) -> std::io::Result<String>; +} + +impl CommandRunner for Command { + fn new() -> Self { + #[cfg(target_os = "macos")] + { + Command::new("open") + } + #[cfg(target_os = "windows")] + { + use mozbuild::config::MOZ_APP_NAME; + use std::env; + use std::path::Path; + // Get the current executable's path, we know Firefox is in the + // same folder is nmhproxy.exe so we can use that. + let nmh_exe_path = env::current_exe().unwrap(); + let nmh_exe_folder = nmh_exe_path.parent().unwrap_or_else(|| Path::new("")); + let moz_exe_path = nmh_exe_folder.join(format!("{}.exe", MOZ_APP_NAME)); + Command::new(moz_exe_path) + } + } + fn arg(&mut self, arg: &str) -> &mut Self { + self.arg(arg) + } + fn args(&mut self, args: &[&str]) -> &mut Self { + self.args(args) + } + fn spawn(&mut self) -> std::io::Result<()> { + self.spawn().map(|_| ()) + } + fn to_string(&mut self) -> std::io::Result<String> { + Ok("".to_string()) + } +} + +struct MockCommand { + command_line: String, +} + +impl CommandRunner for MockCommand { + fn new() -> Self { + MockCommand { + command_line: String::new(), + } + } + fn arg(&mut self, arg: &str) -> &mut Self { + self.command_line.push_str(arg); + self.command_line.push(' '); + self + } + fn args(&mut self, args: &[&str]) -> &mut Self { + for arg in args { + self.command_line.push_str(arg); + self.command_line.push(' '); + } + self + } + fn spawn(&mut self) -> std::io::Result<()> { + Ok(()) + } + fn to_string(&mut self) -> std::io::Result<String> { + Ok(self.command_line.clone()) + } +} + +// The message length is a 32-bit integer in native byte order +pub fn read_message_length<R: Read>(mut reader: R) -> std::io::Result<u32> { + let mut buffer = [0u8; 4]; + reader.read_exact(&mut buffer)?; + let length: u32 = u32::from_ne_bytes(buffer); + if (length > 0) && (length < 100 * 1024) { + Ok(length) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid message length", + )) + } +} + +pub fn read_message_string<R: Read>(mut reader: R, length: u32) -> io::Result<String> { + let mut buffer = vec![0u8; length.try_into().unwrap()]; + reader.read_exact(&mut buffer)?; + let message = + String::from_utf8(buffer).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok(message) +} + +pub fn process_command(command: &FirefoxCommand) -> std::io::Result<bool> { + match &command { + FirefoxCommand::LaunchFirefox { url } => { + launch_firefox::<Command>(url.to_owned(), false, OS_NAME)?; + Ok(true) + } + FirefoxCommand::LaunchFirefoxPrivate { url } => { + launch_firefox::<Command>(url.to_owned(), true, OS_NAME)?; + Ok(true) + } + FirefoxCommand::GetVersion {} => generate_response("1", ResultCode::Success.into()), + } +} + +pub fn generate_response(message: &str, result_code: u32) -> std::io::Result<bool> { + let response_struct = Response { + message: message.to_string(), + result_code, + }; + let response_str = serde_json::to_string(&response_struct) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + let response_len_bytes: [u8; 4] = (response_str.len() as u32).to_ne_bytes(); + std::io::stdout().write_all(&response_len_bytes)?; + std::io::stdout().write_all(response_str.as_bytes())?; + std::io::stdout().flush()?; + Ok(true) +} + +fn validate_url(url: String) -> std::io::Result<String> { + let parsed_url = Url::parse(url.as_str()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + match parsed_url.scheme() { + "http" | "https" | "file" => Ok(parsed_url.to_string()), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Invalid URL scheme", + )), + } +} + +fn launch_firefox<C: CommandRunner>( + url: String, + private: bool, + os: &str, +) -> std::io::Result<String> { + let validated_url: String = validate_url(url)?; + let mut command = C::new(); + if os == "macos" { + use mozbuild::config::MOZ_MACBUNDLE_ID; + let mut args: [&str; 2] = ["--args", "-url"]; + if private { + args[1] = "-private-window"; + } + command + .arg("-n") + .arg("-b") + .arg(MOZ_MACBUNDLE_ID) + .args(&args) + .arg(validated_url.as_str()); + } else if os == "windows" { + let mut args: [&str; 2] = ["-osint", "-url"]; + if private { + args[1] = "-private-window"; + } + command.args(&args).arg(validated_url.as_str()); + } + match command.spawn() { + Ok(_) => generate_response( + if private { + "Successful private launch" + } else { + "Sucessful launch" + }, + ResultCode::Success.into(), + )?, + Err(_) => generate_response( + if private { + "Failed private launch" + } else { + "Failed launch" + }, + ResultCode::Error.into(), + )?, + }; + command.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + #[test] + fn test_validate_url() { + let valid_test_cases = vec![ + "https://example.com/".to_string(), + "http://example.com/".to_string(), + "file:///path/to/file".to_string(), + "https://test.example.com/".to_string(), + ]; + + for input in valid_test_cases { + let result = validate_url(input.clone()); + assert!(result.is_ok(), "Expected Ok, got Err"); + // Safe to unwrap because we know the result is Ok + let ok_value = result.unwrap(); + assert_eq!(ok_value, input); + } + + assert!(matches!( + validate_url("fakeprotocol://test.example.com/".to_string()).map_err(|e| e.kind()), + Err(std::io::ErrorKind::InvalidInput) + )); + + assert!(matches!( + validate_url("invalidURL".to_string()).map_err(|e| e.kind()), + Err(std::io::ErrorKind::InvalidData) + )); + } + + #[test] + fn test_read_message_length_valid() { + let input: [u8; 4] = 256u32.to_ne_bytes(); + let mut cursor = Cursor::new(input); + let length = read_message_length(&mut cursor); + assert!(length.is_ok(), "Expected Ok, got Err"); + assert_eq!(length.unwrap(), 256); + } + + #[test] + fn test_read_message_length_invalid_too_large() { + let input: [u8; 4] = 1_000_000u32.to_ne_bytes(); + let mut cursor = Cursor::new(input); + let result = read_message_length(&mut cursor); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn test_read_message_length_invalid_zero() { + let input: [u8; 4] = 0u32.to_ne_bytes(); + let mut cursor = Cursor::new(input); + let result = read_message_length(&mut cursor); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn test_read_message_string_valid() { + let input_data = b"Valid UTF8 string!"; + let input_length = input_data.len() as u32; + let message = read_message_string(&input_data[..], input_length); + assert!(message.is_ok(), "Expected Ok, got Err"); + assert_eq!(message.unwrap(), "Valid UTF8 string!"); + } + + #[test] + fn test_read_message_string_invalid() { + let input_data: [u8; 3] = [0xff, 0xfe, 0xfd]; + let input_length = input_data.len() as u32; + let result = read_message_string(&input_data[..], input_length); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn test_launch_regular_command_macos() { + let url = "https://example.com"; + let result = launch_firefox::<MockCommand>(url.to_string(), false, "macos"); + assert!(result.is_ok()); + let command_line = result.unwrap(); + let correct_url_format = format!("-url {}", url); + assert!(command_line.contains(correct_url_format.as_str())); + } + + #[test] + fn test_launch_regular_command_windows() { + let url = "https://example.com"; + let result = launch_firefox::<MockCommand>(url.to_string(), false, "windows"); + assert!(result.is_ok()); + let command_line = result.unwrap(); + let correct_url_format = format!("-osint -url {}", url); + assert!(command_line.contains(correct_url_format.as_str())); + } + + #[test] + fn test_launch_private_command_macos() { + let url = "https://example.com"; + let result = launch_firefox::<MockCommand>(url.to_string(), true, "macos"); + assert!(result.is_ok()); + let command_line = result.unwrap(); + let correct_url_format = format!("-private-window {}", url); + assert!(command_line.contains(correct_url_format.as_str())); + } + + #[test] + fn test_launch_private_command_windows() { + let url = "https://example.com"; + let result = launch_firefox::<MockCommand>(url.to_string(), true, "windows"); + assert!(result.is_ok()); + let command_line = result.unwrap(); + let correct_url_format = format!("-osint -private-window {}", url); + assert!(command_line.contains(correct_url_format.as_str())); + } +} diff --git a/browser/app/nmhproxy/src/main.rs b/browser/app/nmhproxy/src/main.rs new file mode 100644 index 0000000000..de9cd8c2a3 --- /dev/null +++ b/browser/app/nmhproxy/src/main.rs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +mod commands; +use commands::ResultCode; +use std::io::Error; +use std::io::ErrorKind; + +fn main() -> Result<(), Error> { + // The general structure of these functions is to print error cases to + // stdout so that the extension can read them and then do error-handling + // on that end. + let message_length: u32 = + commands::read_message_length(std::io::stdin()).or_else(|_| -> Result<u32, _> { + commands::generate_response("Failed to read message length", ResultCode::Error.into()) + .expect("JSON error"); + return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to read message length", + )); + })?; + let message: String = commands::read_message_string(std::io::stdin(), message_length).or_else( + |_| -> Result<String, _> { + commands::generate_response("Failed to read message", ResultCode::Error.into()) + .expect("JSON error"); + return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to read message", + )); + }, + )?; + // Deserialize the message with the following expected format + let native_messaging_json: commands::FirefoxCommand = + serde_json::from_str(&message).or_else(|_| -> Result<commands::FirefoxCommand, _> { + commands::generate_response( + "Failed to deserialize message JSON", + ResultCode::Error.into(), + ) + .expect("JSON error"); + return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to deserialize message JSON", + )); + })?; + commands::process_command(&native_messaging_json).or_else(|_| -> Result<bool, _> { + commands::generate_response("Failed to process command", ResultCode::Error.into()) + .expect("JSON error"); + return Err(Error::new( + ErrorKind::InvalidInput, + "Failed to process command", + )); + })?; + Ok(()) +} |