summaryrefslogtreecommitdiffstats
path: root/src/bin/client/connection.rs
blob: 6bc9b098e09869e713a603c61cccb1cddd50865a (plain) (blame)
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
use crate::tls::TlsConnector;
use parking_lot::Mutex;
use rusqlite::Connection;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use zagan_bet::client::database;
use zagan_bet::client::ui::first_contact_lines;
use zagan_bet::version::{is_newer, parse_hello, VERSION};

static UNSAFE_MODE: Mutex<bool> = Mutex::new(false);

pub fn set_unsafe_mode(unsafe_mode: bool) {
    *UNSAFE_MODE.lock() = unsafe_mode;
}

type ConnectResult = Result<(TlsStream, Option<String>), Box<dyn std::error::Error + Send + Sync>>;

pub fn connect(
    addr: &str,
    conn: &Connection,
) -> Result<(TlsStream, Option<String>), Box<dyn std::error::Error>> {
    let stored_fingerprint = database::get_fingerprint(conn, addr);
    let unsafe_mode = *UNSAFE_MODE.lock();

    let (tx, rx) = mpsc::channel::<ConnectResult>();
    let addr_str = addr.to_string();
    let server_name = addr_str
        .split(':')
        .next()
        .unwrap_or("127.0.0.1")
        .to_string();

    thread::spawn(move || {
        let outcome = (|| -> ConnectResult {
            let tcp = TcpStream::connect(&addr_str)?;
            let connector =
                TlsConnector::new(&server_name, stored_fingerprint.clone(), unsafe_mode)?;
            let tls_stream = connector.connect(tcp)?;
            let fp = connector.observed_fingerprint();
            Ok((TlsStream { inner: tls_stream }, fp))
        })();
        let _ = tx.send(outcome);
    });

    // Channel carries (stream, observed fingerprint).
    let (tls, observed) = match rx.recv_timeout(Duration::from_secs(10)) {
        Ok(Ok((tls, fp))) => (tls, fp),
        Ok(Err(e)) => return Err(Box::new(TrustError(format!("{e}")))),
        Err(mpsc::RecvTimeoutError::Timeout) => {
            return Err("Connection timed out after 10 seconds".into());
        }
        Err(_) => return Err("Connection attempt failed".into()),
    };

    // TOFU: persist the fingerprint on first contact.
    if !unsafe_mode {
        if let Some(fp) = observed.as_deref() {
            if database::get_fingerprint(conn, addr).is_none() {
                database::save_fingerprint(conn, addr, fp)?;
                for line in first_contact_lines(addr, fp) {
                    println!("{}", line);
                }
            }
        }
    }

    let mut tls = tls;
    tls.set_read_timeout(Some(Duration::from_secs(10)))?;

    // Version lockstep: the first line after TLS is the handshake, and no
    // credentials are sent until the server accepts it.
    let start = Instant::now();
    tls.write_line(&format!("HELLO {VERSION}"))?;
    let mut response = String::new();
    if tls.read_line(&mut response)? == 0 {
        return Err("server closed the connection during HELLO".into());
    }
    let elapsed = start.elapsed();
    let response = response.trim();

    if let Some(message) = response.strip_prefix("ERROR") {
        return Err(message.trim().to_string().into());
    }
    let Some(hello) = parse_hello(response) else {
        return Err(format!("unexpected HELLO reply: {response}").into());
    };

    println!(
        "\n\rConnected to {} with ping delay: {:.1}ms (TLS)",
        addr,
        elapsed.as_secs_f64() * 1000.0
    );
    if is_newer(&hello.latest, VERSION) {
        println!(
            "A newer version is available: {} ({})",
            hello.latest, hello.url
        );
    }

    // Credentials are stored per server identity, so any address presenting
    // this certificate key can log in with the same stored account.
    let identity = observed
        .clone()
        .or_else(|| database::get_fingerprint(conn, addr));
    if let Some(identity) = identity {
        if let Some((username, password)) = database::get_credentials(conn, &identity) {
            let auth_msg = format!("AUTH {} {}", username, password);
            tls.write_line(&auth_msg)?;
            let mut auth_response = String::new();
            tls.read_line(&mut auth_response)?;
            if auth_response.trim().starts_with("OK") {
                println!("Authenticated as {}", username);
                return Ok((tls, Some(username)));
            }
            println!("Authentication failed: {}", auth_response.trim());
        }
    }
    Ok((tls, None))
}

/// Wrapper so TLS handshake failures surface as errors with their message
/// intact (rustls errors already carry the mismatch details).
struct TrustError(String);

impl std::fmt::Display for TrustError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}
impl std::fmt::Debug for TrustError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{self}")
    }
}
impl std::error::Error for TrustError {}

pub struct TlsStream {
    inner: crate::tls::TlsStream,
}

impl TlsStream {
    pub fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
        self.inner.read_line(buf)
    }

    pub fn write_line(&mut self, msg: &str) -> std::io::Result<()> {
        self.inner.write_line(msg)
    }

    /// One line if the socket already has one, without blocking.
    pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> {
        self.inner.read_available_line()
    }

    pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> {
        self.inner.set_read_timeout(duration)
    }
}

impl Read for TlsStream {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.inner.read(buf)
    }
}

impl Write for TlsStream {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.inner.write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.inner.flush()
    }
}