aboutsummaryrefslogtreecommitdiffhomepage
path: root/oxish/src/bin/oxish-server.rs
blob: 321d0644c2190b5f47cdf4ad41986777dcd82727 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use core::net::{Ipv4Addr, SocketAddr};
use std::{
    env,
    fs::{self, File},
    io::{self, Write},
    path::PathBuf,
    sync::Arc,
};

#[cfg(debug_assertions)]
use clap::ArgAction;
use clap::Parser;
use listenfd::ListenFd;
use oxish::{Config, DEFAULT_PROVIDER, DefaultStore, Server};
use proto::{
    key_exchange::HostKeys,
    named::{Named, PublicKeyAlgorithm},
};
use tokio::net::TcpListener;
use tracing::info;
use zeroize::Zeroizing;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let provider = DEFAULT_PROVIDER;
    let args = Args::parse();
    let host_keys = if args.generate_host_key {
        match File::create_new(&args.host_key_file) {
            Ok(mut host_key_file) => {
                let Ok((_, pkcs8)) = provider.generate_signing_key(&args.host_key_type) else {
                    anyhow::bail!("failed to generate host key");
                };

                // FIXME ensure the host key is only readable by the ssh server user
                let pkcs8 = Zeroizing::new(pkcs8);
                let result = host_key_file.write_all(&pkcs8);
                result?;

                eprintln!("generated host key at {}", args.host_key_file);
                return Ok(());
            }
            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {
                anyhow::bail!("host key file `{}` already exists", args.host_key_file);
            }
            Err(err) => return Err(err.into()),
        }
    } else {
        let pkcs8 = Zeroizing::new(fs::read(&args.host_key_file)?);
        HostKeys::new([pkcs8].into_iter(), provider)?
    };

    let session_bin = match args.session_bin {
        Some(path) => path,
        None => {
            let exe = env::current_exe()?;
            let Some(dir) = exe.parent() else {
                anyhow::bail!("cannot determine directory of current executable");
            };
            dir.join("oxish-session")
        }
    };

    if !session_bin.is_file() {
        anyhow::bail!("session binary `{}` not found", session_bin.display());
    }

    let listener = match (ListenFd::from_env().take_tcp_listener(0)?, args.port) {
        (Some(listener), None) => {
            listener.set_nonblocking(true)?;
            TcpListener::from_std(listener)?
        }
        (None, Some(port)) => {
            let addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, port));
            TcpListener::bind(addr).await?
        }
        (Some(_), Some(_)) => anyhow::bail!("LISTEN_FDS and --port conflict with each other"),
        (None, None) => anyhow::bail!("unless LISTEN_FDS is set, --port is required"),
    };
    info!(addr = %listener.local_addr()?, "listening for connections");

    #[cfg_attr(not(debug_assertions), expect(unused_mut))]
    let mut config = Config::default();
    #[cfg(debug_assertions)]
    {
        config.spawn = args.spawn;
    }

    Arc::new(
        Server::new(
            DefaultStore::new(provider)?,
            host_keys,
            session_bin,
            provider,
        )?
        .with_config(config),
    )
    .run(listener)
    .await
}

#[derive(Debug, Parser)]
struct Args {
    #[clap(short, long)]
    port: Option<u16>,
    #[clap(long, default_value = "ssh_host_ed25519_key")]
    host_key_file: String,
    #[clap(long)]
    generate_host_key: bool,
    #[clap(long, value_parser = host_key_type, default_value = "ssh-ed25519")]
    host_key_type: PublicKeyAlgorithm<'static>,
    /// Path to the `oxish-session` binary (defaults to a sibling of this executable)
    #[clap(long)]
    session_bin: Option<PathBuf>,
    #[cfg(debug_assertions)]
    #[clap(long, action = ArgAction::Set, default_value_t = true)]
    spawn: bool,
}

fn host_key_type(name: &str) -> Result<PublicKeyAlgorithm<'static>, String> {
    match PublicKeyAlgorithm::typed(name) {
        PublicKeyAlgorithm::Unknown(_) => Err(format!("unsupported host key type `{name}`")),
        algorithm => Ok(algorithm.to_owned()),
    }
}