diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-04-18 14:49:47 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-04-18 16:12:14 +0200 |
| commit | d28e918c183509666254c2e4cafceb86f43c048d (patch) | |
| tree | 755830134785f992cc82eaed6528cb53a88065e2 | |
| parent | ea6daf2b5539a9c9cfee69a464ba56bb7871541b (diff) | |
| download | mullvadvpn-d28e918c183509666254c2e4cafceb86f43c048d.tar.xz mullvadvpn-d28e918c183509666254c2e4cafceb86f43c048d.zip | |
Add windows-service crate
| -rw-r--r-- | Cargo.lock | 10 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | windows-service/Cargo.toml | 10 | ||||
| -rw-r--r-- | windows-service/src/lib.rs | 18 | ||||
| -rw-r--r-- | windows-service/src/service.rs | 390 | ||||
| -rw-r--r-- | windows-service/src/service_control_handler.rs | 144 | ||||
| -rw-r--r-- | windows-service/src/service_dispatcher.rs | 90 | ||||
| -rw-r--r-- | windows-service/src/service_manager.rs | 196 | ||||
| -rw-r--r-- | windows-service/src/shell_escape.rs | 120 |
9 files changed, 979 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock index bf3c9f6102..527ef532a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1582,6 +1582,16 @@ dependencies = [ ] [[package]] +name = "windows-service" +version = "0.1.0" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "widestring 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "ws" version = "0.7.5" source = "git+https://github.com/tomusdrw/ws-rs#368ce39e2aa8700d568ca29dbacaecdf1bf749d1" diff --git a/Cargo.toml b/Cargo.toml index 1bed44a013..68e56d35aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "mullvad-daemon", + "windows-service", "mullvad-cli", "mullvad-types", "mullvad-rpc", diff --git a/windows-service/Cargo.toml b/windows-service/Cargo.toml new file mode 100644 index 0000000000..536dbca559 --- /dev/null +++ b/windows-service/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "windows-service" +version = "0.1.0" +authors = ["Mullvad VPN <admin@mullvad.net>", "Andrej Mihajlov <and@mullvad.net>"] + +[target.'cfg(windows)'.dependencies] +bitflags = "1.0.1" +error-chain = "0.11" +winapi = { version = "0.3", features = ["std", "winsvc", "winerror"] } +widestring = "0.3.0" diff --git a/windows-service/src/lib.rs b/windows-service/src/lib.rs new file mode 100644 index 0000000000..bae17eb41d --- /dev/null +++ b/windows-service/src/lib.rs @@ -0,0 +1,18 @@ +#![cfg(windows)] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate error_chain; +extern crate widestring; +extern crate winapi; + +pub use error_chain::ChainedError; + +pub mod service; +pub mod service_control_handler; +pub mod service_manager; +#[macro_use] +pub mod service_dispatcher; + +mod shell_escape; diff --git a/windows-service/src/service.rs b/windows-service/src/service.rs new file mode 100644 index 0000000000..f609221b8c --- /dev/null +++ b/windows-service/src/service.rs @@ -0,0 +1,390 @@ +use std::ffi::OsString; +use std::path::PathBuf; +use std::time::Duration; +use std::{io, mem}; + +use winapi::shared::winerror::ERROR_SERVICE_SPECIFIC_ERROR; +use winapi::um::{winnt, winsvc}; + +mod errors { + error_chain! { + errors { + InvalidServiceType(raw_value: u32) { + description("Invalid service type value") + display("Invalid service type value: {}", raw_value) + } + InvalidServiceState(raw_value: u32) { + description("Invalid service state") + display("Invalid service state value: {}", raw_value) + } + InvalidServiceControl(raw_value: u32) { + description("Invalid service control") + display("Invalid service control value: {}", raw_value) + } + } + foreign_links { + System(::std::io::Error); + } + } +} +pub use self::errors::*; + +/// Enum describing types of windows services +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ServiceType { + /// Service that runs in its own process. + OwnProcess = winnt::SERVICE_WIN32_OWN_PROCESS, +} + +impl ServiceType { + pub fn from_raw(raw_value: u32) -> Result<Self> { + let service_type = match raw_value { + x if x == ServiceType::OwnProcess.to_raw() => ServiceType::OwnProcess, + _ => Err(ErrorKind::InvalidServiceType(raw_value))?, + }; + Ok(service_type) + } + + pub fn to_raw(&self) -> u32 { + *self as u32 + } +} + +/// Flags describing the access permissions when working with services +bitflags! { + pub struct ServiceAccess: u32 { + /// Can query the service status + const QUERY_STATUS = winsvc::SERVICE_QUERY_STATUS; + + /// Can start the service + const START = winsvc::SERVICE_START; + + // Can stop the service + const STOP = winsvc::SERVICE_STOP; + + /// Can pause or continue the service execution + const PAUSE_CONTINUE = winsvc::SERVICE_PAUSE_CONTINUE; + + /// Can ask the service to report its status + const INTERROGATE = winsvc::SERVICE_INTERROGATE; + + /// Can delete the service + const DELETE = winnt::DELETE; + } +} + +/// Enum describing the start options for windows services +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ServiceStartType { + /// Autostart on system startup + AutoStart = winnt::SERVICE_AUTO_START, + /// Service is enabled, can be started manually + OnDemand = winnt::SERVICE_DEMAND_START, + /// Disabled service + Disabled = winnt::SERVICE_DISABLED, +} + +impl ServiceStartType { + pub fn to_raw(&self) -> u32 { + *self as u32 + } +} + +/// Error handling strategy for service failures. +/// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682450(v=vs.85).aspx +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ServiceErrorControl { + Critical = winnt::SERVICE_ERROR_CRITICAL, + Ignore = winnt::SERVICE_ERROR_IGNORE, + Normal = winnt::SERVICE_ERROR_NORMAL, + Severe = winnt::SERVICE_ERROR_SEVERE, +} + +impl ServiceErrorControl { + pub fn to_raw(&self) -> u32 { + *self as u32 + } +} + +/// A struct that describes the service +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ServiceInfo { + /// Service name + pub name: OsString, + + /// Friendly service name + pub display_name: OsString, + + pub service_type: ServiceType, + pub start_type: ServiceStartType, + pub error_control: ServiceErrorControl, + + /// Path to the service binary. + pub executable_path: PathBuf, + + /// Launch arguments passed to `main` when system starts the service. + /// This is not the same as arguments passed to `service_main`. + pub launch_arguments: Vec<OsString>, + + /// Account to use for running the service. + /// for example: NT Authority\System. + /// use `None` to run as LocalSystem. + pub account_name: Option<OsString>, + + /// Account password. + /// For system accounts this should normally be `None`. + pub account_password: Option<OsString>, +} + +/// Enum describing the service control operations +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ServiceControl { + Continue = winsvc::SERVICE_CONTROL_CONTINUE, + Interrogate = winsvc::SERVICE_CONTROL_INTERROGATE, + NetBindAdd = winsvc::SERVICE_CONTROL_NETBINDADD, + NetBindDisable = winsvc::SERVICE_CONTROL_NETBINDDISABLE, + NetBindEnable = winsvc::SERVICE_CONTROL_NETBINDENABLE, + NetBindRemove = winsvc::SERVICE_CONTROL_NETBINDREMOVE, + ParamChange = winsvc::SERVICE_CONTROL_PARAMCHANGE, + Pause = winsvc::SERVICE_CONTROL_PAUSE, + Preshutdown = winsvc::SERVICE_CONTROL_PRESHUTDOWN, + Shutdown = winsvc::SERVICE_CONTROL_SHUTDOWN, + Stop = winsvc::SERVICE_CONTROL_STOP, +} + +impl ServiceControl { + pub fn from_raw(raw_value: u32) -> Result<Self> { + let service_control = match raw_value { + x if x == ServiceControl::Continue.to_raw() => ServiceControl::Continue, + x if x == ServiceControl::Interrogate.to_raw() => ServiceControl::Interrogate, + x if x == ServiceControl::NetBindAdd.to_raw() => ServiceControl::NetBindAdd, + x if x == ServiceControl::NetBindDisable.to_raw() => ServiceControl::NetBindDisable, + x if x == ServiceControl::NetBindEnable.to_raw() => ServiceControl::NetBindEnable, + x if x == ServiceControl::NetBindRemove.to_raw() => ServiceControl::NetBindRemove, + x if x == ServiceControl::ParamChange.to_raw() => ServiceControl::ParamChange, + x if x == ServiceControl::Pause.to_raw() => ServiceControl::Pause, + x if x == ServiceControl::Preshutdown.to_raw() => ServiceControl::Preshutdown, + x if x == ServiceControl::Shutdown.to_raw() => ServiceControl::Shutdown, + x if x == ServiceControl::Stop.to_raw() => ServiceControl::Stop, + other => Err(ErrorKind::InvalidServiceControl(other))?, + }; + Ok(service_control) + } + + pub fn to_raw(&self) -> u32 { + *self as u32 + } +} + +/// Service state returned as a part of ServiceStatus +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u32)] +pub enum ServiceState { + Stopped = winsvc::SERVICE_STOPPED, + StartPending = winsvc::SERVICE_START_PENDING, + StopPending = winsvc::SERVICE_STOP_PENDING, + Running = winsvc::SERVICE_RUNNING, + ContinuePending = winsvc::SERVICE_CONTINUE_PENDING, + PausePending = winsvc::SERVICE_PAUSE_PENDING, + Paused = winsvc::SERVICE_PAUSED, +} + +impl ServiceState { + fn from_raw(raw_state: u32) -> Result<Self> { + let service_state = match raw_state { + x if x == ServiceState::Stopped.to_raw() => ServiceState::Stopped, + x if x == ServiceState::StartPending.to_raw() => ServiceState::StartPending, + x if x == ServiceState::StopPending.to_raw() => ServiceState::StopPending, + x if x == ServiceState::Running.to_raw() => ServiceState::Running, + x if x == ServiceState::ContinuePending.to_raw() => ServiceState::ContinuePending, + x if x == ServiceState::PausePending.to_raw() => ServiceState::PausePending, + x if x == ServiceState::Paused.to_raw() => ServiceState::Paused, + other => Err(ErrorKind::InvalidServiceState(other))?, + }; + Ok(service_state) + } + + fn to_raw(&self) -> u32 { + *self as u32 + } +} + +/// Service exit code abstraction. +/// +/// This struct provides a logic around the relationship between `win32_exit_code` and +/// `service_specific_exit_code`. +/// +/// The service can either return a win32 error code or a custom error +/// code. In that case `win32_exit_code` has to be set to `ERROR_SERVICE_SPECIFIC_ERROR` and +/// the `service_specific_exit_code` assigned with custom error code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ServiceExitCode { + Win32(u32), + ServiceSpecific(u32), +} + +impl ServiceExitCode { + fn copy_to(&self, raw_service_status: &mut winsvc::SERVICE_STATUS) { + match *self { + ServiceExitCode::Win32(win32_error_code) => { + raw_service_status.dwWin32ExitCode = win32_error_code; + raw_service_status.dwServiceSpecificExitCode = 0; + } + ServiceExitCode::ServiceSpecific(service_error_code) => { + raw_service_status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; + raw_service_status.dwServiceSpecificExitCode = service_error_code; + } + } + } +} + +impl<'a> From<&'a winsvc::SERVICE_STATUS> for ServiceExitCode { + fn from(service_status: &'a winsvc::SERVICE_STATUS) -> Self { + if service_status.dwWin32ExitCode == ERROR_SERVICE_SPECIFIC_ERROR { + ServiceExitCode::ServiceSpecific(service_status.dwServiceSpecificExitCode) + } else { + ServiceExitCode::Win32(service_status.dwWin32ExitCode) + } + } +} + +/// Flags describing accepted types of service control requests +bitflags! { + pub struct ServiceControlAccept: u32 { + /// The service is a network component that can accept changes in its binding without being + /// stopped and restarted. This allows service to receive `ServiceControl::Netbind*` + /// family of events. + const NETBIND_CHANGE = winsvc::SERVICE_ACCEPT_NETBINDCHANGE; + + /// The service can reread its startup parameters without being stopped and restarted. + const PARAM_CHANGE = winsvc::SERVICE_ACCEPT_PARAMCHANGE; + + /// The service can be paused and continued. + const PAUSE_CONTINUE = winsvc::SERVICE_ACCEPT_PAUSE_CONTINUE; + + /// The service can perform preshutdown tasks. + /// Mutually exclusive with shutdown. + const PRESHUTDOWN = winsvc::SERVICE_ACCEPT_PRESHUTDOWN; + + /// The service is notified when system shutdown occurs. + /// Mutually exclusive with preshutdown. + const SHUTDOWN = winsvc::SERVICE_ACCEPT_SHUTDOWN; + + /// The service can be stopped. + const STOP = winsvc::SERVICE_ACCEPT_STOP; + } +} + +/// Service status. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ServiceStatus { + /// Type of service + pub service_type: ServiceType, + + /// Current state of the service + pub current_state: ServiceState, + + /// Control commands that service accepts. + pub controls_accepted: ServiceControlAccept, + + /// Service exit code + pub exit_code: ServiceExitCode, + + /// Service initialization progress value that should be increased during a lengthy start, + /// stop, pause or continue eration. For example the service should increment the value as + /// it completes each step of initialization. + /// This value must be zero if the service does not have any pending start, stop, pause or + /// continue operations. + pub checkpoint: u32, + + /// Estimated time for pending operation. + /// This basically works as a timeout until the service manager assumes that the service hung. + /// This could be either circumvented by updating the `current_state` or incrementing a + /// `checkpoint` value. + pub wait_hint: Duration, +} + +impl ServiceStatus { + pub(super) fn to_raw(&self) -> winsvc::SERVICE_STATUS { + let mut raw_status = unsafe { mem::zeroed::<winsvc::SERVICE_STATUS>() }; + raw_status.dwServiceType = self.service_type.to_raw(); + raw_status.dwCurrentState = self.current_state.to_raw(); + raw_status.dwControlsAccepted = self.controls_accepted.bits(); + + self.exit_code.copy_to(&mut raw_status); + + raw_status.dwCheckPoint = self.checkpoint; + + // we lose precision here but dwWaitHint should never be too big. + raw_status.dwWaitHint = (self.wait_hint.as_secs() * 1000) as u32; + + raw_status + } + + fn from_raw(raw_status: winsvc::SERVICE_STATUS) -> Result<Self> { + Ok(ServiceStatus { + service_type: ServiceType::from_raw(raw_status.dwServiceType)?, + current_state: ServiceState::from_raw(raw_status.dwCurrentState)?, + controls_accepted: ServiceControlAccept::from_bits_truncate( + raw_status.dwControlsAccepted, + ), + exit_code: ServiceExitCode::from(&raw_status), + checkpoint: raw_status.dwCheckPoint, + wait_hint: Duration::from_millis(raw_status.dwWaitHint as u64), + }) + } +} + + +pub struct Service(winsvc::SC_HANDLE); + +impl Service { + /// Internal constructor + pub(super) unsafe fn from_handle(handle: winsvc::SC_HANDLE) -> Self { + Service(handle) + } + + pub fn stop(&self) -> Result<ServiceStatus> { + self.send_control_command(ServiceControl::Stop) + } + + pub fn query_status(&self) -> Result<ServiceStatus> { + let mut raw_status = unsafe { mem::zeroed::<winsvc::SERVICE_STATUS>() }; + let success = unsafe { winsvc::QueryServiceStatus(self.0, &mut raw_status) }; + if success == 1 { + ServiceStatus::from_raw(raw_status) + } else { + Err(io::Error::last_os_error().into()) + } + } + + pub fn delete(self) -> io::Result<()> { + let success = unsafe { winsvc::DeleteService(self.0) }; + if success == 1 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + + fn send_control_command(&self, command: ServiceControl) -> Result<ServiceStatus> { + let mut raw_status = unsafe { mem::zeroed::<winsvc::SERVICE_STATUS>() }; + let success = unsafe { winsvc::ControlService(self.0, command.to_raw(), &mut raw_status) }; + + if success == 1 { + ServiceStatus::from_raw(raw_status).map_err(|err| err.into()) + } else { + Err(io::Error::last_os_error().into()) + } + } +} + +impl Drop for Service { + fn drop(&mut self) { + unsafe { winsvc::CloseServiceHandle(self.0) }; + } +} diff --git a/windows-service/src/service_control_handler.rs b/windows-service/src/service_control_handler.rs new file mode 100644 index 0000000000..b722e94f52 --- /dev/null +++ b/windows-service/src/service_control_handler.rs @@ -0,0 +1,144 @@ +use std::ffi::OsStr; +use std::io; +use widestring::WideCString; +use winapi::shared::winerror::{ERROR_CALL_NOT_IMPLEMENTED, NO_ERROR}; +use winapi::um::winsvc; + +use service::{ServiceControl, ServiceStatus}; + +mod errors { + error_chain! { + errors { + InvalidServiceName { + description("Invalid service name") + } + } + foreign_links { + System(::std::io::Error); + } + } +} +pub use self::errors::*; + +/// Struct that holds unique token for updating the status of the corresponding service. +#[derive(Debug, Clone, Copy)] +pub struct ServiceStatusHandle(winsvc::SERVICE_STATUS_HANDLE); + +impl ServiceStatusHandle { + fn from_handle(handle: winsvc::SERVICE_STATUS_HANDLE) -> Self { + ServiceStatusHandle(handle) + } + + /// Report the new service status to the system + pub fn set_service_status(&self, service_status: ServiceStatus) -> io::Result<()> { + let mut raw_service_status = service_status.to_raw(); + let result = unsafe { winsvc::SetServiceStatus(self.0, &mut raw_service_status) }; + if result == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} + +// Underlying SERVICE_STATUS_HANDLE is thread safe. +// See remarks section for more info: +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686241(v=vs.85).aspx +unsafe impl Send for ServiceStatusHandle {} + +/// Abstraction over the return value of service control handler. +/// The meaning of each of variants in this enum depends on the type of received event. +/// See the "Return value" section of corresponding MSDN article for more info: +/// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683241(v=vs.85).aspx +#[derive(Debug)] +pub enum ServiceControlHandlerResult { + /// Either used to aknowledge the call or grant the permission in advanced events. + NoError, + /// The received event is not implemented. + NotImplemented, + /// This variant is used to deny permission and return the reason error code in advanced + /// events. + Other(u32), +} + +impl ServiceControlHandlerResult { + pub fn to_raw(&self) -> u32 { + match *self { + ServiceControlHandlerResult::NoError => NO_ERROR, + ServiceControlHandlerResult::NotImplemented => ERROR_CALL_NOT_IMPLEMENTED, + ServiceControlHandlerResult::Other(code) => code, + } + } +} + +/// Register a closure for receiving service events. +/// Returns `ServiceStatusHandle` that can be used to report the service status back to the system. +pub fn register_control_handler< + S: AsRef<OsStr>, + F: Fn(ServiceControl) -> ServiceControlHandlerResult + 'static, +>( + service_name: S, + event_handler: F, +) -> Result<ServiceStatusHandle> { + // Move closure data on heap. + // The Box<HandlerFn> is a trait object and is stored on stack at this point. + let heap_event_handler = Box::new(event_handler) as Box<HandlerFn>; + + // Box again to move trait object to heap. + let boxed_event_handler: Box<Box<HandlerFn>> = Box::new(heap_event_handler); + + // Important: leak the Box<Box<HandlerFn>> which will be released in `service_control_handler`. + let context = Box::into_raw(boxed_event_handler) as *mut ::std::os::raw::c_void; + + let service_name = + WideCString::from_str(service_name).chain_err(|| ErrorKind::InvalidServiceName)?; + let status_handle = unsafe { + winsvc::RegisterServiceCtrlHandlerExW( + service_name.as_ptr(), + Some(service_control_handler), + context, + ) + }; + + if status_handle.is_null() { + Err(io::Error::last_os_error().into()) + } else { + Ok(ServiceStatusHandle::from_handle(status_handle)) + } +} + +/// Alias for control event handler closure. +type HandlerFn = Fn(ServiceControl) -> ServiceControlHandlerResult; + +/// Static service control handler +#[allow(dead_code)] +extern "system" fn service_control_handler( + control: u32, + _event_type: u32, + _event_data: *mut ::std::os::raw::c_void, + context: *mut ::std::os::raw::c_void, +) -> u32 { + // Important: cast context to &mut Box<HandlerFn> without taking ownership. + let handler_fn = unsafe { &mut *(context as *mut Box<HandlerFn>) }; + + match ServiceControl::from_raw(control) { + Ok(service_control) => { + let return_code = ((handler_fn)(service_control)).to_raw(); + + // Important: release context upon Stop, Shutdown or Preshutdown at the end of the + // service lifecycle. + match service_control { + ServiceControl::Stop | ServiceControl::Shutdown | ServiceControl::Preshutdown => { + let _owned_boxed_handler: Box<Box<HandlerFn>> = + unsafe { Box::from_raw(context as *mut Box<HandlerFn>) }; + } + _ => (), + }; + + return_code + } + + // Report all unknown control commands as unimplemented + Err(_) => ServiceControlHandlerResult::NotImplemented.to_raw(), + } +} diff --git a/windows-service/src/service_dispatcher.rs b/windows-service/src/service_dispatcher.rs new file mode 100644 index 0000000000..fb9d5df8f3 --- /dev/null +++ b/windows-service/src/service_dispatcher.rs @@ -0,0 +1,90 @@ +use std::ffi::{OsStr, OsString}; +use std::{io, ptr}; +use widestring::{WideCStr, WideCString}; +use winapi::um::winsvc; + +mod errors { + error_chain! { + errors { + InvalidServiceName { + description("Invalid service name") + } + } + foreign_links { + System(::std::io::Error); + } + } +} +pub use self::errors::*; + +/// Macro to generate a "service_main" function for Windows service. +/// +/// The `service_main` function parses service arguments provided by the system +/// and passes them with a call to `$service_main_handler`. +/// +/// `$function_name` - name of the "service_main" callback. +/// `$service_main_handler` - function with a signature `fn(Vec<OsString>)` that's called from +/// generated `$function_name`. Accepts parsed service arguments as `Vec<OsString>`. Its +/// responsibility is to create a `ServiceControlHandler`, start processing control events and +/// report the service status to the system. +/// +#[macro_export] +macro_rules! define_windows_service { + ($function_name:ident, $service_main_handler:ident) => { + /// Static callback used by the system to bootstrap the service. + /// Do not call it directly. + extern "system" fn $function_name(argc: u32, argv: *mut *mut u16) { + let arguments = unsafe { $crate::service_dispatcher::parse_raw_arguments(argc, argv) }; + + $service_main_handler(arguments); + } + }; +} + +/// Start service control dispatcher. +/// +/// Once started the service control dispatcher blocks the current thread execution +/// until the service is stopped. +/// +/// Upon successful initialization, system calls the `service_main` in +/// background thread which parses service arguments received from the system and +/// passes them to higher level `$service_main_handler` handler. +/// +/// On failure: immediately returns an error, no threads are spawned. +/// +pub fn start_dispatcher<T: AsRef<OsStr>>( + service_name: T, + service_main: extern "system" fn(u32, *mut *mut u16), +) -> Result<()> { + let service_name = + WideCString::from_str(service_name).chain_err(|| ErrorKind::InvalidServiceName)?; + let service_table: &[winsvc::SERVICE_TABLE_ENTRYW] = &[ + winsvc::SERVICE_TABLE_ENTRYW { + lpServiceName: service_name.as_ptr(), + lpServiceProc: Some(service_main), + }, + // the last item has to be { null, null } + winsvc::SERVICE_TABLE_ENTRYW { + lpServiceName: ptr::null(), + lpServiceProc: None, + }, + ]; + + let result = unsafe { winsvc::StartServiceCtrlDispatcherW(service_table.as_ptr()) }; + if result == 0 { + Err(io::Error::last_os_error().into()) + } else { + Ok(()) + } +} + +/// Parse raw arguments received from `service_main` into Vec. +pub unsafe fn parse_raw_arguments(argc: u32, argv: *mut *mut u16) -> Vec<OsString> { + (0..argc) + .into_iter() + .map(|i| { + let array_element_ptr: *mut *mut u16 = argv.offset(i as isize); + WideCStr::from_ptr_str(*array_element_ptr).to_os_string() + }) + .collect() +} diff --git a/windows-service/src/service_manager.rs b/windows-service/src/service_manager.rs new file mode 100644 index 0000000000..b4dceb9d3e --- /dev/null +++ b/windows-service/src/service_manager.rs @@ -0,0 +1,196 @@ +use std::borrow::Cow; +use std::ffi::OsStr; +use std::{io, ptr}; + +use widestring::{NulError, WideCString, WideString}; +use winapi::um::winsvc; + +use service::{Service, ServiceAccess, ServiceInfo}; +use shell_escape; + +mod errors { + error_chain! { + errors { + InvalidAccountName { + description("Invalid account name") + } + InvalidAccountPassword { + description("Invalid account password") + } + InvalidDisplayName { + description("Invalid display name") + } + InvalidDatabaseName { + description("Invalid database name") + } + InvalidExecutablePath { + description("Invalid executable path") + } + InvalidLaunchArgument { + description("Invalid launch argument") + } + InvalidMachineName { + description("Invalid machine name") + } + InvalidServiceName { + description("Invalid service name") + } + } + foreign_links { + System(::std::io::Error); + } + } +} +pub use self::errors::*; + +/// Flags describing access permissions for ServiceManager +bitflags! { + pub struct ServiceManagerAccess: u32 { + /// Can connect to service control manager + const CONNECT = winsvc::SC_MANAGER_CONNECT; + + /// Can create services + const CREATE_SERVICE = winsvc::SC_MANAGER_CREATE_SERVICE; + + /// Can enumerate services + const ENUMERATE_SERVICE = winsvc::SC_MANAGER_ENUMERATE_SERVICE; + } +} + +/// Service control manager +pub struct ServiceManager(winsvc::SC_HANDLE); + +impl ServiceManager { + /// Private initializer + /// Passing None for machine connects to local machine + /// Passing None for database connects to active database + fn new<M: AsRef<OsStr>, D: AsRef<OsStr>>( + machine: Option<M>, + database: Option<D>, + request_access: ServiceManagerAccess, + ) -> Result<Self> { + let machine_name = to_wide(machine).chain_err(|| ErrorKind::InvalidMachineName)?; + let database_name = to_wide(database).chain_err(|| ErrorKind::InvalidDatabaseName)?; + let handle = unsafe { + winsvc::OpenSCManagerW( + machine_name.map_or(ptr::null(), |s| s.as_ptr()), + database_name.map_or(ptr::null(), |s| s.as_ptr()), + request_access.bits(), + ) + }; + + if handle.is_null() { + Err(io::Error::last_os_error().into()) + } else { + Ok(ServiceManager(handle)) + } + } + + /// Passing None for database connects to active database + pub fn local_computer<D: AsRef<OsStr>>( + database: Option<D>, + request_access: ServiceManagerAccess, + ) -> Result<Self> { + ServiceManager::new(None::<&OsStr>, database, request_access) + } + + /// Passing None for database connects to active database + pub fn remote_computer<M: AsRef<OsStr>, D: AsRef<OsStr>>( + machine: M, + database: Option<D>, + request_access: ServiceManagerAccess, + ) -> Result<Self> { + ServiceManager::new(Some(machine), database, request_access) + } + + pub fn create_service( + &self, + service_info: ServiceInfo, + service_access: ServiceAccess, + ) -> Result<Service> { + let service_name = + WideCString::from_str(service_info.name).chain_err(|| ErrorKind::InvalidServiceName)?; + let display_name = WideCString::from_str(service_info.display_name) + .chain_err(|| ErrorKind::InvalidDisplayName)?; + let account_name = + to_wide(service_info.account_name).chain_err(|| ErrorKind::InvalidAccountName)?; + let account_password = + to_wide(service_info.account_password).chain_err(|| ErrorKind::InvalidAccountPassword)?; + + // escape executable path and arguments and combine them into single command + let executable_path = escape_wide(service_info.executable_path) + .chain_err(|| ErrorKind::InvalidExecutablePath)?; + + let mut launch_command_buffer = WideString::new(); + launch_command_buffer.push(executable_path); + + for launch_argument in service_info.launch_arguments.iter() { + let wide = escape_wide(launch_argument).chain_err(|| ErrorKind::InvalidLaunchArgument)?; + + launch_command_buffer.push_str(" "); + launch_command_buffer.push(wide); + } + + let launch_command = WideCString::from_wide_str(launch_command_buffer).unwrap(); + + let service_handle = unsafe { + winsvc::CreateServiceW( + self.0, + service_name.as_ptr(), + display_name.as_ptr(), + service_access.bits(), + service_info.service_type.to_raw(), + service_info.start_type.to_raw(), + service_info.error_control.to_raw(), + launch_command.as_ptr(), + ptr::null(), // load ordering group + ptr::null_mut(), // tag id within the load ordering group + ptr::null(), // service dependencies + account_name.map_or(ptr::null(), |s| s.as_ptr()), + account_password.map_or(ptr::null(), |s| s.as_ptr()), + ) + }; + + if service_handle.is_null() { + Err(io::Error::last_os_error().into()) + } else { + Ok(unsafe { Service::from_handle(service_handle) }) + } + } + + pub fn open_service<T: AsRef<OsStr>>( + &self, + name: T, + request_access: ServiceAccess, + ) -> Result<Service> { + let service_name = WideCString::from_str(name).chain_err(|| ErrorKind::InvalidServiceName)?; + let service_handle = + unsafe { winsvc::OpenServiceW(self.0, service_name.as_ptr(), request_access.bits()) }; + + if service_handle.is_null() { + Err(io::Error::last_os_error().into()) + } else { + Ok(unsafe { Service::from_handle(service_handle) }) + } + } +} + +impl Drop for ServiceManager { + fn drop(&mut self) { + unsafe { winsvc::CloseServiceHandle(self.0) }; + } +} + +fn to_wide<T: AsRef<OsStr>>(s: Option<T>) -> ::std::result::Result<Option<WideCString>, NulError> { + if let Some(s) = s { + Ok(Some(WideCString::from_str(s)?)) + } else { + Ok(None) + } +} + +fn escape_wide<T: AsRef<OsStr>>(s: T) -> ::std::result::Result<WideString, NulError> { + let escaped = shell_escape::escape(Cow::Borrowed(s.as_ref())); + let wide = WideCString::from_str(escaped)?; + Ok(wide.to_wide_string()) +} diff --git a/windows-service/src/shell_escape.rs b/windows-service/src/shell_escape.rs new file mode 100644 index 0000000000..12014a3b15 --- /dev/null +++ b/windows-service/src/shell_escape.rs @@ -0,0 +1,120 @@ +use std::borrow::Cow; +use std::ffi::{OsStr, OsString}; +use std::iter::repeat; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; + +/// Common UTF-16 code points. +mod utf16 { + pub const DOUBLEQUOTE: u16 = '"' as u16; + pub const BACKSLASH: u16 = '\\' as u16; + pub const SPACE: u16 = ' ' as u16; + pub const LINEFEED: u16 = '\n' as u16; + pub const HTAB: u16 = '\t' as u16; + pub const VTAB: u16 = 0x000B; // '\v' +} + +/// Loselessly escape shell arguments on Windows. +/// +/// Inspired by https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/. +/// Heavily based on https://github.com/sfackler/shell-escape +pub fn escape(s: Cow<OsStr>) -> Cow<OsStr> { + static ESCAPE_CHARS: &'static [u16] = &[ + utf16::DOUBLEQUOTE, + utf16::SPACE, + utf16::LINEFEED, + utf16::HTAB, + utf16::VTAB, + ]; + let needs_escape = s.is_empty() || s.encode_wide().any(|ref c| ESCAPE_CHARS.contains(c)); + if !needs_escape { + return s; + } + + let mut escaped_wide_string: Vec<u16> = Vec::with_capacity(s.len() + 2); + escaped_wide_string.push(utf16::DOUBLEQUOTE); + + let mut chars = s.encode_wide().peekable(); + loop { + let mut num_slashes = 0; + while let Some(&utf16::BACKSLASH) = chars.peek() { + chars.next(); + num_slashes += 1; + } + + match chars.next() { + Some(utf16::DOUBLEQUOTE) => { + escaped_wide_string.extend(repeat(utf16::BACKSLASH).take(num_slashes * 2 + 1)); + escaped_wide_string.push(utf16::DOUBLEQUOTE); + } + Some(c) => { + escaped_wide_string.extend(repeat(utf16::BACKSLASH).take(num_slashes)); + escaped_wide_string.push(c); + } + None => { + escaped_wide_string.extend(repeat(utf16::BACKSLASH).take(num_slashes * 2)); + break; + } + } + } + + escaped_wide_string.push(utf16::DOUBLEQUOTE); + + Cow::Owned(OsString::from_wide(&escaped_wide_string)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_no_escape() { + assert_eq!( + escape(Cow::Borrowed(OsStr::new("--aaa=bbb-ccc"))), + OsStr::new("--aaa=bbb-ccc") + ); + } + + #[test] + fn test_escape_empty_argument() { + assert_eq!(escape(Cow::Borrowed(OsStr::new(""))), OsStr::new(r#""""#)); + } + + #[test] + fn test_escape_argument_with_spaces() { + assert_eq!( + escape(Cow::Borrowed(OsStr::new("linker=gcc -L/foo -Wl,bar"))), + OsStr::new(r#""linker=gcc -L/foo -Wl,bar""#) + ); + } + + #[test] + fn test_escape_nested_quotes() { + assert_eq!( + escape(Cow::Borrowed(OsStr::new(r#"--features="default""#))), + OsStr::new(r#""--features=\"default\"""#) + ); + } + + + #[test] + fn test_escape_multiple_backslashes_and_nested_quotes() { + assert_eq!( + escape(Cow::Borrowed(OsStr::new(r#"hello \\\"quote\\\""#))), + OsStr::new(r#""hello \\\\\\\"quote\\\\\\\"""#) + ); + } + + // Input: + // child.exe "\some\directory with\spaces\" argument2 + // + // Parsed as: + // 0: [child.exe] + // 1: [\some\directory with\spaces" argument2] + #[test] + fn test_escape_trailing_backslash() { + assert_eq!( + escape(Cow::Borrowed(OsStr::new(r#"\some\directory with\spaces\"#))), + OsStr::new(r#""\some\directory with\spaces\\""#) + ); + } +} |
