#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
#![warn(missing_docs)]
use std::fs;
use std::io::{self, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpStream, TcpListener};
use std::path::PathBuf;
use anyhow::anyhow;
use fs2::FileExt;
use capnp_rpc::{RpcSystem, twoparty};
use capnp_rpc::rpc_twoparty_capnp::Side;
pub use capnp_rpc as capnp_rpc;
#[cfg(unix)]
use std::os::unix::{io::{IntoRawFd, FromRawFd}, fs::OpenOptionsExt};
#[cfg(windows)]
use std::os::windows::io::{AsRawSocket, IntoRawSocket, FromRawSocket};
#[cfg(windows)]
use winapi::um::winsock2;
use std::process::{Command, Stdio};
use std::thread;
use sequoia_openpgp as openpgp;
#[macro_use] mod macros;
pub mod assuan;
pub mod gnupg;
pub mod keybox;
mod keygrip;
pub use self::keygrip::Keygrip;
pub mod sexp;
mod core;
pub use crate::core::{Config, Context, IPCPolicy};
#[cfg(test)]
mod tests;
pub trait Handler {
fn handle(&self,
network: capnp_rpc::twoparty::VatNetwork<tokio_util::compat::Compat<tokio::net::tcp::OwnedReadHalf>>)
-> RpcSystem<Side>;
}
pub type HandlerFactory = fn(
descriptor: Descriptor,
local: &tokio::task::LocalSet
) -> Result<Box<dyn Handler>>;
#[derive(Clone)]
pub struct Descriptor {
ctx: core::Context,
rendezvous: PathBuf,
executable: PathBuf,
factory: HandlerFactory,
}
impl Descriptor {
pub fn new(ctx: &core::Context, rendezvous: PathBuf,
executable: PathBuf, factory: HandlerFactory)
-> Self {
Descriptor {
ctx: ctx.clone(),
rendezvous,
executable,
factory,
}
}
pub fn context(&self) -> &core::Context {
&self.ctx
}
pub fn connect(&self) -> Result<RpcSystem<Side>> {
self.connect_with_policy(*self.ctx.ipc_policy())
}
pub fn connect_with_policy(&self, policy: core::IPCPolicy)
-> Result<RpcSystem<Side>> {
let do_connect = |cookie: Cookie, mut s: TcpStream| {
cookie.send(&mut s)?;
s.set_nonblocking(true)?;
let stream = tokio::net::TcpStream::from_std(s)?;
stream.set_nodelay(true)?;
let (reader, writer) = stream.into_split();
use tokio_util::compat::TokioAsyncReadCompatExt;
use tokio_util::compat::TokioAsyncWriteCompatExt;
let (reader, writer) = (reader.compat(), writer.compat_write());
let network =
Box::new(twoparty::VatNetwork::new(reader, writer,
Side::Client,
Default::default()));
Ok(RpcSystem::new(network, None))
};
fs::create_dir_all(self.ctx.home())?;
let mut file = fs::OpenOptions::new();
file
.read(true)
.write(true)
.create(true);
#[cfg(unix)]
file.mode(0o600);
let mut file = file.open(&self.rendezvous)?;
file.lock_exclusive()?;
let mut c = vec![];
file.read_to_end(&mut c)?;
if let Some((cookie, rest)) = Cookie::extract(c) {
let stream = String::from_utf8(rest).map_err(drop)
.and_then(|rest| rest.parse::<SocketAddr>().map_err(drop))
.and_then(|addr| TcpStream::connect(addr).map_err(drop));
if let Ok(s) = stream {
do_connect(cookie, s)
} else {
file.set_len(0)?;
drop(file);
self.connect()
}
} else {
let cookie = Cookie::new();
let (addr, external) = match policy {
core::IPCPolicy::Internal => self.start(false)?,
core::IPCPolicy::External => self.start(true)?,
core::IPCPolicy::Robust => self.start(true)
.or_else(|_| self.start(false))?
};
cookie.send(&mut TcpStream::connect(addr)?)?;
if external {
file.set_len(0)?;
file.write_all(&cookie.0)?;
write!(file, "{}", addr)?;
}
drop(file);
do_connect(cookie, TcpStream::connect(addr)?)
}
}
fn start(&self, external: bool) -> Result<(SocketAddr, bool)> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
let addr = listener.local_addr()?;
if external {
self.fork(listener)?;
} else {
self.spawn(listener)?;
}
Ok((addr, external))
}
fn fork(&self, listener: TcpListener) -> Result<()> {
let mut cmd = new_background_command(&self.executable);
cmd
.arg("--home")
.arg(self.ctx.home())
.arg("--lib")
.arg(self.ctx.lib())
.arg("--ephemeral")
.arg(self.ctx.ephemeral().to_string())
.stdout(Stdio::null())
.stderr(Stdio::null());
platform! {
unix => {
cmd.stdin(unsafe { Stdio::from_raw_fd(listener.into_raw_fd()) });
},
windows => {
unsafe {
match winapi::um::handleapi::SetHandleInformation(
listener.as_raw_socket() as _,
winapi::um::winbase::HANDLE_FLAG_INHERIT,
winapi::um::winbase::HANDLE_FLAG_INHERIT,
) {
0 => Err(std::io::Error::last_os_error()),
_ => Ok(())
}?
};
cmd.env("SOCKET", format!("{}", listener.into_raw_socket()));
}
}
cmd.spawn()?;
Ok(())
}
fn spawn(&self, l: TcpListener) -> Result<()> {
let descriptor = self.clone();
thread::spawn(move || -> Result<()> {
Server::new(descriptor)
.expect("Failed to spawn server") .serve_listener(l)
.expect("Failed to spawn server"); Ok(())
});
Ok(())
}
}
pub struct Server {
runtime: tokio::runtime::Runtime,
descriptor: Descriptor,
}
impl Server {
pub fn new(descriptor: Descriptor) -> Result<Self> {
Ok(Server {
runtime: tokio::runtime::Runtime::new()?,
descriptor,
})
}
pub fn context() -> Result<core::Context> {
use std::env::args;
let args: Vec<String> = args().collect();
if args.len() != 7 || args[1] != "--home"
|| args[3] != "--lib" || args[5] != "--ephemeral" {
return Err(anyhow!(
"Usage: {} --home <HOMEDIR> --lib <LIBDIR> \
--ephemeral true|false", args[0]));
}
let mut cfg = core::Context::configure()
.home(&args[2]).lib(&args[4]);
if let Ok(ephemeral) = args[6].parse() {
if ephemeral {
cfg.set_ephemeral();
}
} else {
return Err(anyhow!(
"Expected 'true' or 'false' for --ephemeral, got: {}",
args[6]));
}
cfg.build()
}
pub fn serve(&mut self) -> Result<()> {
let listener = platform! {
unix => unsafe { TcpListener::from_raw_fd(0) },
windows => {
let socket = std::env::var("SOCKET")?.parse()?;
unsafe { TcpListener::from_raw_socket(socket) }
}
};
self.serve_listener(listener)
}
fn serve_listener(&mut self, l: TcpListener) -> Result<()> {
let cookie = {
let mut i = l.accept()?;
Cookie::receive(&mut i.0)?
};
let local = tokio::task::LocalSet::new();
let handler = (self.descriptor.factory)(self.descriptor.clone(), &local)?;
let server = async move {
let socket = tokio::net::TcpListener::from_std(l).unwrap();
loop {
let (mut socket, _) = socket.accept().await?;
let _ = socket.set_nodelay(true);
let received_cookie = Cookie::receive_async(&mut socket).await?;
if received_cookie != cookie {
return Err(anyhow::anyhow!("Bad cookie"));
}
let (reader, writer) = socket.into_split();
use tokio_util::compat::TokioAsyncReadCompatExt;
use tokio_util::compat::TokioAsyncWriteCompatExt;
let (reader, writer) = (reader.compat(), writer.compat_write());
let network =
twoparty::VatNetwork::new(reader, writer,
Side::Server, Default::default());
let rpc_system = handler.handle(network);
let _ = tokio::task::spawn_local(rpc_system).await;
}
};
local.block_on(&self.runtime, server)
}
}
struct Cookie(Vec<u8>);
use rand::RngCore;
use rand::rngs::OsRng;
impl Cookie {
const SIZE: usize = 32;
fn new() -> Self {
let mut c = vec![0; Cookie::SIZE];
OsRng.fill_bytes(&mut c);
Cookie(c)
}
fn from(buf: &[u8]) -> Option<Self> {
if buf.len() == Cookie::SIZE {
let mut c = Vec::with_capacity(Cookie::SIZE);
c.extend_from_slice(buf);
Some(Cookie(c))
} else {
None
}
}
fn extract(mut buf: Vec<u8>) -> Option<(Self, Vec<u8>)> {
if buf.len() >= Cookie::SIZE {
let r = buf.split_off(Cookie::SIZE);
Some((Cookie(buf), r))
} else {
None
}
}
fn receive<R: Read>(from: &mut R) -> Result<Self> {
let mut buf = vec![0; Cookie::SIZE];
from.read_exact(&mut buf)?;
Ok(Cookie(buf))
}
async fn receive_async(socket: &mut tokio::net::TcpStream) -> io::Result<Cookie> {
use tokio::io::AsyncReadExt;
let mut buf = vec![0; Cookie::SIZE];
socket.read_exact(&mut buf).await?;
Ok(Cookie::from(&buf).expect("enough bytes read"))
}
fn send<W: Write>(&self, to: &mut W) -> io::Result<()> {
to.write_all(&self.0)
}
}
impl PartialEq for Cookie {
fn eq(&self, other: &Cookie) -> bool {
self.0.len() == other.0.len()
&& unsafe {
::memsec::memeq(self.0.as_ptr(),
other.0.as_ptr(),
self.0.len())
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Handshake failed: {0}")]
HandshakeFailed(String),
#[error("Connection closed unexpectedly.")]
ConnectionClosed(Vec<u8>),
}
pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
#[cfg(windows)]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(windows)]
static WSA_INITED: AtomicBool = AtomicBool::new(false);
#[cfg(windows)]
#[ctor::ctor]
fn wsa_startup() {
unsafe {
let ret = winsock2::WSAStartup(
0x202, &mut std::mem::zeroed(),
);
WSA_INITED.store(ret != 0, Ordering::SeqCst);
}
}
#[cfg(windows)]
#[ctor::dtor]
fn wsa_cleanup() {
if WSA_INITED.load(Ordering::SeqCst) {
let _ = unsafe { winsock2::WSACleanup() };
}
}
#[allow(clippy::let_and_return)]
pub(crate) fn new_background_command<S>(program: S) -> Command
where
S: AsRef<std::ffi::OsStr>,
{
let command = Command::new(program);
#[cfg(windows)]
let command = {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut command = command;
command.creation_flags(CREATE_NO_WINDOW);
command
};
command
}