feat(core): add support for linux capabilities
This commit is contained in:
parent
c381da94f9
commit
72d7c19e9f
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
espanso-mac-utils = { path = "../espanso-mac-utils" }
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
caps = "0.5.2"
|
32
espanso/src/capabilities/fallback.rs
Normal file
32
espanso/src/capabilities/fallback.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn can_use_capabilities() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn grant_capabilities() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clear_capabilities() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
42
espanso/src/capabilities/linux.rs
Normal file
42
espanso/src/capabilities/linux.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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(())
|
||||
}
|
29
espanso/src/capabilities/mod.rs
Normal file
29
espanso/src/capabilities/mod.rs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#[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::*;
|
||||
|
|
@ -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},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<dyn ConfigStore>,
|
||||
|
@ -49,6 +71,7 @@ pub fn initialize_and_spawn(
|
|||
exit_signal: Receiver<ExitMode>,
|
||||
ui_event_receiver: Receiver<UIEvent>,
|
||||
secure_input_receiver: Receiver<SecureInputEvent>,
|
||||
use_evdev_backend: bool,
|
||||
) -> Result<JoinHandle<ExitMode>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user