diff --git a/Cargo.lock b/Cargo.lock
index 41bab16..60799b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -132,6 +132,17 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "caps"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c088f2dddef283f86b023ab1ebe2301c653326834996458b2f48d29b804e9540"
+dependencies = [
+ "errno",
+ "libc",
+ "thiserror",
+]
+
[[package]]
name = "cc"
version = "1.0.66"
@@ -412,11 +423,33 @@ dependencies = [
"syn 1.0.67",
]
+[[package]]
+name = "errno"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14ca354e36190500e1e1fb267c647932382b54053c50b14970856c0b00a35067"
+dependencies = [
+ "gcc",
+ "libc",
+]
+
[[package]]
name = "espanso"
version = "2.0.0"
dependencies = [
"anyhow",
+ "caps",
"clap",
"colored",
"crossbeam",
@@ -774,6 +807,12 @@ dependencies = [
"new_debug_unreachable",
]
+[[package]]
+name = "gcc"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
+
[[package]]
name = "getrandom"
version = "0.1.16"
diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml
index 30d123a..751243b 100644
--- a/espanso/Cargo.toml
+++ b/espanso/Cargo.toml
@@ -66,4 +66,7 @@ widestring = "0.4.3"
libc = "0.2.98"
[target.'cfg(target_os="macos")'.dependencies]
-espanso-mac-utils = { path = "../espanso-mac-utils" }
\ No newline at end of file
+espanso-mac-utils = { path = "../espanso-mac-utils" }
+
+[target.'cfg(target_os="linux")'.dependencies]
+caps = "0.5.2"
\ No newline at end of file
diff --git a/espanso/src/capabilities/fallback.rs b/espanso/src/capabilities/fallback.rs
new file mode 100644
index 0000000..10b08c9
--- /dev/null
+++ b/espanso/src/capabilities/fallback.rs
@@ -0,0 +1,32 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use anyhow::Result;
+
+pub fn can_use_capabilities() -> bool {
+ false
+}
+
+pub fn grant_capabilities() -> Result<()> {
+ Ok(())
+}
+
+pub fn clear_capabilities() -> Result<()> {
+ Ok(())
+}
diff --git a/espanso/src/capabilities/linux.rs b/espanso/src/capabilities/linux.rs
new file mode 100644
index 0000000..8a7a1db
--- /dev/null
+++ b/espanso/src/capabilities/linux.rs
@@ -0,0 +1,42 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+use anyhow::Result;
+use caps::{CapSet, Capability};
+use log::error;
+
+pub fn can_use_capabilities() -> bool {
+ match caps::has_cap(None, CapSet::Permitted, Capability::CAP_DAC_OVERRIDE) {
+ Ok(has_cap) => has_cap,
+ Err(err) => {
+ error!("error while checking if capabilities are enabled: {}", err);
+ false
+ },
+ }
+}
+
+pub fn grant_capabilities() -> Result<()> {
+ caps::raise(None, CapSet::Effective, Capability::CAP_DAC_OVERRIDE)?;
+ Ok(())
+}
+
+pub fn clear_capabilities() -> Result<()> {
+ caps::clear(None, CapSet::Effective)?;
+ caps::clear(None, CapSet::Permitted)?;
+ Ok(())
+}
\ No newline at end of file
diff --git a/espanso/src/capabilities/mod.rs b/espanso/src/capabilities/mod.rs
new file mode 100644
index 0000000..5204961
--- /dev/null
+++ b/espanso/src/capabilities/mod.rs
@@ -0,0 +1,29 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "linux")]
+pub use linux::*;
+
+#[cfg(not(target_os = "linux"))]
+mod fallback;
+#[cfg(not(target_os = "linux"))]
+pub use fallback::*;
+
diff --git a/espanso/src/cli/mod.rs b/espanso/src/cli/mod.rs
index a0ce61d..7ac0e32 100644
--- a/espanso/src/cli/mod.rs
+++ b/espanso/src/cli/mod.rs
@@ -42,6 +42,7 @@ pub struct CliModule {
pub requires_config: bool,
pub subcommand: String,
pub show_in_dock: bool,
+ pub requires_linux_capabilities: bool,
pub entry: fn(CliModuleArgs)->i32,
}
@@ -55,6 +56,7 @@ impl Default for CliModule {
requires_config: false,
subcommand: "".to_string(),
show_in_dock: false,
+ requires_linux_capabilities: false,
entry: |_| {0},
}
}
diff --git a/espanso/src/cli/worker/engine/funnel/mod.rs b/espanso/src/cli/worker/engine/funnel/mod.rs
index f912c79..2276a30 100644
--- a/espanso/src/cli/worker/engine/funnel/mod.rs
+++ b/espanso/src/cli/worker/engine/funnel/mod.rs
@@ -18,7 +18,7 @@
*/
use anyhow::Result;
-use espanso_detect::event::{InputEvent, KeyboardEvent, Status};
+use espanso_detect::{SourceCreationOptions, event::{InputEvent, KeyboardEvent, Status}};
use log::{error};
use thiserror::Error;
@@ -33,8 +33,7 @@ pub mod secure_input;
pub mod sequencer;
pub mod ui;
-// TODO: pass options
-pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)> {
+pub fn init_and_spawn(source_options: SourceCreationOptions) -> Result<(DetectSource, ModifierStateStore, Sequencer)> {
let (sender, receiver) = crossbeam::channel::unbounded();
let (init_tx, init_rx) = crossbeam::channel::unbounded();
@@ -46,7 +45,7 @@ pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)>
if let Err(error) = std::thread::Builder::new()
.name("detect thread".to_string())
.spawn(
- move || match espanso_detect::get_source(Default::default()) {
+ move || match espanso_detect::get_source(source_options) {
Ok(mut source) => {
if source.initialize().is_err() {
init_tx
diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs
index fa5fd1e..294ead4 100644
--- a/espanso/src/cli/worker/engine/mod.rs
+++ b/espanso/src/cli/worker/engine/mod.rs
@@ -22,18 +22,39 @@ use std::thread::JoinHandle;
use anyhow::Result;
use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore};
+use espanso_detect::SourceCreationOptions;
+use espanso_inject::InjectorCreationOptions;
use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote};
-use log::info;
+use log::{debug, error, info, warn};
-use crate::{cli::worker::{engine::{dispatch::executor::{
+use crate::{
+ cli::worker::{
+ engine::{
+ dispatch::executor::{
clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter,
event_injector::EventInjectorAdapter, icon::IconHandlerAdapter,
key_injector::KeyInjectorAdapter,
- }, process::middleware::{image_resolve::PathProviderAdapter, match_select::MatchSelectorAdapter, matcher::{convert::MatchConverter, regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions}, rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions}}, multiplex::MultiplexAdapter, render::{
+ },
+ process::middleware::{
+ image_resolve::PathProviderAdapter,
+ match_select::MatchSelectorAdapter,
+ matcher::{
+ convert::MatchConverter,
+ regex::{RegexMatcherAdapter, RegexMatcherAdapterOptions},
+ rolling::{RollingMatcherAdapter, RollingMatcherAdapterOptions},
+ },
+ multiplex::MultiplexAdapter,
+ render::{
extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter},
RendererAdapter,
- }}}, match_cache::MatchCache}, engine::event::ExitMode};
+ },
+ },
+ },
+ match_cache::MatchCache,
+ },
+ engine::event::ExitMode,
+};
use super::secure_input::SecureInputEvent;
@@ -41,6 +62,7 @@ pub mod dispatch;
pub mod funnel;
pub mod process;
+#[allow(clippy::too_many_arguments)]
pub fn initialize_and_spawn(
paths: Paths,
config_store: Box,
@@ -49,6 +71,7 @@ pub fn initialize_and_spawn(
exit_signal: Receiver,
ui_event_receiver: Receiver,
secure_input_receiver: Receiver,
+ use_evdev_backend: bool,
) -> Result> {
let handle = std::thread::Builder::new()
.name("engine thread".to_string())
@@ -66,18 +89,35 @@ pub fn initialize_and_spawn(
let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager);
let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager);
+ let has_granted_capabilities = grant_linux_capabilities(use_evdev_backend);
+
+ // TODO: pass all the options
let (detect_source, modifier_state_store, sequencer) =
- super::engine::funnel::init_and_spawn().expect("failed to initialize detector module");
+ super::engine::funnel::init_and_spawn(SourceCreationOptions {
+ use_evdev: use_evdev_backend,
+ ..Default::default()
+ })
+ .expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
- let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(secure_input_receiver, &sequencer);
- let sources: Vec<&dyn crate::engine::funnel::Source> =
- vec![&detect_source, &exit_source, &ui_source, &secure_input_source];
+ let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
+ secure_input_receiver,
+ &sequencer,
+ );
+ let sources: Vec<&dyn crate::engine::funnel::Source> = vec![
+ &detect_source,
+ &exit_source,
+ &ui_source,
+ &secure_input_source,
+ ];
let funnel = crate::engine::funnel::default(&sources);
- let rolling_matcher = RollingMatcherAdapter::new(&match_converter.get_rolling_matches(), RollingMatcherAdapterOptions {
- char_word_separators: config_manager.default().word_separators(),
- });
+ let rolling_matcher = RollingMatcherAdapter::new(
+ &match_converter.get_rolling_matches(),
+ RollingMatcherAdapterOptions {
+ char_word_separators: config_manager.default().word_separators(),
+ },
+ );
let regex_matcher = RegexMatcherAdapter::new(
&match_converter.get_regex_matches(),
&RegexMatcherAdapterOptions {
@@ -92,8 +132,11 @@ pub fn initialize_and_spawn(
let selector = MatchSelectorAdapter::new(&modulo_search_ui, &match_cache);
let multiplexer = MultiplexAdapter::new(&match_cache);
- let injector = espanso_inject::get_injector(Default::default())
- .expect("failed to initialize injector module"); // TODO: handle the options
+ let injector = espanso_inject::get_injector(InjectorCreationOptions {
+ use_evdev: use_evdev_backend,
+ ..Default::default()
+ })
+ .expect("failed to initialize injector module"); // TODO: handle the options
let clipboard = espanso_clipboard::get_clipboard(Default::default())
.expect("failed to initialize clipboard module"); // TODO: handle options
@@ -161,6 +204,13 @@ pub fn initialize_and_spawn(
&icon_adapter,
);
+ // Disable previously granted linux capabilities if not needed anymore
+ if has_granted_capabilities {
+ if let Err(err) = crate::capabilities::clear_capabilities() {
+ error!("unable to revoke linux capabilities: {}", err);
+ }
+ }
+
let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher);
let exit_mode = engine.run();
@@ -172,3 +222,30 @@ pub fn initialize_and_spawn(
Ok(handle)
}
+
+fn grant_linux_capabilities(use_evdev_backend: bool) -> bool {
+ if use_evdev_backend {
+ if crate::capabilities::can_use_capabilities() {
+ debug!("using linux capabilities to grant permissions needed by EVDEV backend");
+ if let Err(err) = crate::capabilities::grant_capabilities() {
+ error!("unable to grant CAP_DAC_OVERRIDE capability: {}", err);
+ false
+ } else {
+ debug!("successfully granted permissions using capabilities");
+ true
+ }
+ } else {
+ warn!("EVDEV backend is being used, but without enabling linux capabilities.");
+ warn!(" Although you CAN run espanso EVDEV backend as root, it's not recommended due");
+ warn!(
+ " to security reasons. Espanso supports linux capabilities to limit the attack surface"
+ );
+ warn!(" area by only leveraging on the CAP_DAC_OVERRIDE capability (needed to work with");
+ warn!(" /dev/input/* devices to detect and inject text) and disabling it as soon as the");
+ warn!(" initial setup is completed.");
+ false
+ }
+ } else {
+ false
+ }
+}
diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs
index f56bb24..b411638 100644
--- a/espanso/src/cli/worker/mod.rs
+++ b/espanso/src/cli/worker/mod.rs
@@ -46,6 +46,7 @@ pub fn new() -> CliModule {
CliModule {
requires_paths: true,
requires_config: true,
+ requires_linux_capabilities: true,
enable_logs: true,
log_mode: super::LogMode::AppendOnly,
subcommand: "worker".to_string(),
@@ -81,6 +82,12 @@ fn worker_main(args: CliModuleArgs) -> i32 {
// TODO: show config loading errors in a GUI, if any
+ let use_evdev_backend = if cfg!(feature = "wayland") {
+ true
+ } else {
+ std::env::var("USE_EVDEV").unwrap_or_else(|_| "false".to_string()) == "true"
+ };
+
let icon_paths =
crate::icon::load_icon_paths(&paths.runtime).expect("unable to initialize icons");
@@ -112,6 +119,7 @@ fn worker_main(args: CliModuleArgs) -> i32 {
engine_exit_receiver,
engine_ui_event_receiver,
engine_secure_input_receiver,
+ use_evdev_backend,
)
.expect("unable to initialize engine");
diff --git a/espanso/src/main.rs b/espanso/src/main.rs
index 93d28c4..eda2df2 100644
--- a/espanso/src/main.rs
+++ b/espanso/src/main.rs
@@ -35,6 +35,7 @@ use simplelog::{
use crate::cli::{LogMode, PathsOverrides};
+mod capabilities;
mod cli;
mod config;
mod engine;
@@ -401,6 +402,13 @@ fn main() {
log_panics::init();
}
+ // If the process doesn't require linux capabilities, disable them
+ if !handler.requires_linux_capabilities {
+ if let Err(err) = crate::capabilities::clear_capabilities() {
+ error!("unable to clear linux capabilities: {}", err);
+ }
+ }
+
// If explicitly requested, we show the Dock icon on macOS
// We need to enable this selectively, otherwise we would end up with multiple
// dock icons due to the multi-process nature of espanso.