diff options
| -rw-r--r-- | Cargo.lock | 201 | ||||
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | page.html | 3 | ||||
| -rw-r--r-- | src/main.rs | 92 | ||||
| -rw-r--r-- | src/tls_stuff.rs | 159 | 
5 files changed, 425 insertions, 34 deletions
| @@ -30,6 +30,12 @@ dependencies = [  ]  [[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]]  name = "byteorder"  version = "1.4.3"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -42,6 +48,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"  [[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]]  name = "cfg-if"  version = "1.0.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -267,6 +279,19 @@ dependencies = [  ]  [[package]] +name = "hyper-tungstenite" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ea2c1b59596d6b1302fe616266257a58b079f68fee329d6d111f79241cb7fd" +dependencies = [ + "hyper", + "pin-project", + "tokio", + "tokio-tungstenite", + "tungstenite", +] + +[[package]]  name = "idna"  version = "0.2.3"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -294,6 +319,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"  [[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +dependencies = [ + "wasm-bindgen", +] + +[[package]]  name = "lazy_static"  version = "1.4.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -414,6 +448,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"  [[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]]  name = "pin-project-lite"  version = "0.2.9"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -489,12 +543,58 @@ dependencies = [  ]  [[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +dependencies = [ + "base64", +] + +[[package]]  name = "scopeguard"  version = "1.1.0"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"  [[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]]  name = "sha-1"  version = "0.10.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -537,6 +637,12 @@ dependencies = [  ]  [[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]]  name = "syn"  version = "1.0.92"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -614,6 +720,17 @@ dependencies = [  ]  [[package]] +name = "tokio-rustls" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]]  name = "tokio-tungstenite"  version = "0.17.1"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -730,6 +847,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"  [[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]]  name = "url"  version = "2.2.2"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -776,6 +899,80 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"  [[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]]  name = "websweeper"  version = "1.0.0"  dependencies = [ @@ -783,9 +980,11 @@ dependencies = [   "futures-channel",   "futures-util",   "hyper", + "hyper-tungstenite",   "rand", + "rustls-pemfile",   "tokio", - "tokio-tungstenite", + "tokio-rustls",  ]  [[package]] @@ -9,7 +9,9 @@ edition = "2018"  [dependencies]  hyper = { version = "0.14", features = ["full"] }  tokio = { version = "1", features = ["full"] } -tokio-tungstenite = "*" +tokio-rustls = "0.23" +rustls-pemfile = "1" +hyper-tungstenite = "0.8"  rand = "0.8"  futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }  futures-channel = "0.3" @@ -45,7 +45,8 @@    </body>    <script>      window.id = NaN; -    let s = new WebSocket(`ws://${window.location.hostname}:31236`); +    let wsproto = (window.location.protocol == "https:")? "wss:": "ws:"; +    let s = new WebSocket(`${wsproto}//${window.location.hostname}:${window.location.port}`);      let info_elem = document.getElementById("miscinfo");      let board_elem = document.getElementById("board");      let name = "deadbeef"; diff --git a/src/main.rs b/src/main.rs index d42ad9f..1367c60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,12 @@ use std::{  };  mod minesweeper; +mod tls_stuff;  use minesweeper::*; -//use std::convert::{ TryFrom, TryInto }; +use tls_stuff::*;  use hyper::{ Method, StatusCode, Body, Request, Response, Server }; +use hyper::server::conn::{ AddrStream, AddrIncoming };  use hyper::service::{make_service_fn, service_fn};  use tokio::sync::{      RwLock, @@ -21,9 +23,9 @@ type HtmlResult = Result<Response<Body>, Response<Body>>;  use futures_channel::mpsc::{unbounded, UnboundedSender};  use futures_util::{future, pin_mut, stream::TryStreamExt, StreamExt, sink::SinkExt}; -use tokio::net::{TcpListener, TcpStream};  use tokio::fs; -use tokio_tungstenite::tungstenite::protocol::Message; +use hyper_tungstenite::{ tungstenite::protocol::Message, HyperWebsocket }; +use tokio_rustls::{ rustls, Accept, server::TlsStream };  type Tx = UnboundedSender<Message>;  type MovReqTx = mpsc::UnboundedSender<MetaMove>; @@ -41,23 +43,51 @@ const PAGE_RELPATH: &str = "./page.html";  const FONT_FILE_FUCKIT: &[u8] = include_bytes!("./VT323-Regular.ttf");  #[tokio::main] -async fn main() { +async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {      let sequential_id = Arc::new(AtomicUsize::new(0));      let peers = PeerMap::new(RwLock::new(HashMap::new()));      let peer_info: PeerInfo = (peers.clone(), sequential_id.clone()); -    let http_addr = SocketAddr::from(([0, 0, 0, 0], 31235)); -    println!("Http on {}", http_addr); +    let addr = SocketAddr::from(([0, 0, 0, 0], 31235)); + +    // Build TLS configuration. +    let tls_cfg = { +        // Load public certificate. +        let certs = load_certs("cert.pem")?; +        // Load private key. +        let key = load_private_key("cert.rsa")?; +        // Do not use client certificate authentication. +        let mut cfg = rustls::ServerConfig::builder() +            .with_safe_defaults() +            .with_no_client_auth() +            .with_single_cert(certs, key) +            .map_err(|e| error(format!("{}", e)))?; +        // Configure ALPN to accept HTTP/2, HTTP/1.1 in that order. +        cfg.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; +        Arc::new(cfg) +    }; + +    // Create a TCP listener via tokio. +    let incoming = AddrIncoming::bind(&addr)?;      let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();      let game_t = tokio::spawn(gameloop(cmd_rx, peers.clone())); -    // need to await this one at some point -    let conn_l = tokio::spawn(conn_listener(peer_info.clone(), cmd_tx.clone())); -    let http_serv = make_service_fn(|_| { +    let serv = make_service_fn(|socket: &tls_stuff::TlsStream| { +        let addr = { +            if let State::Streaming(ref s) = &socket.state { +                s.get_ref().0.remote_addr() +            } else { +                std::net::SocketAddr::new(std::net::Ipv4Addr::new(0,0,0,0).into(),0) +            } +        }; +        let peer_info = peer_info.clone(); +        let cmd_tx = cmd_tx.clone();          async move {              Ok::<_, Infallible>(service_fn(move |req: Request<Body>| { +                let peer_info = peer_info.clone(); +                let cmd_tx = cmd_tx.clone();                  async move { -                    Ok::<_,Infallible>(match handle_http_req(req).await { +                    Ok::<_,Infallible>(match handle_req(req, peer_info.clone(), cmd_tx.clone(), addr).await {                          Ok(r) => r,                          Err(r) => r,                      }) @@ -66,12 +96,15 @@ async fn main() {          }      }); -    let server = Server::bind(&http_addr) -        .serve(http_serv) +    // Run the future, keep going until an error occurs. +    println!("Starting to serve on https://{}.", addr); +    let server = Server::builder(TlsAcceptor::new(tls_cfg, incoming)) +        .serve(serv)          .with_graceful_shutdown(shutdown_signal());      if let Err(e) = server.await {          eprint!("server error: {}", e);      } +    Ok(())  }  // If a move is made, broadcast new board, else just send current board @@ -111,32 +144,17 @@ async fn gameloop(mut move_rx: mpsc::UnboundedReceiver<MetaMove>, peers: PeerMap      }  } -async fn conn_listener(peer_info: PeerInfo, cmd_tx: MovReqTx) { -    let ws_addr = SocketAddr::from(([0, 0, 0, 0], 31236)); -    let ws_socket = TcpListener::bind(&ws_addr).await; -    let ws_listener = ws_socket.expect("Failed to bind"); -    // Let's spawn the handling of each connection in a separate task. -    println!("Websocket on {}", ws_addr); -    while let Ok((stream, addr)) = ws_listener.accept().await { -        tokio::spawn(peer_connection(peer_info.clone(), cmd_tx.clone(), stream, addr)); -    } -} - -async fn peer_connection(peer_info: PeerInfo, cmd_tx: MovReqTx, raw_stream: TcpStream, addr: SocketAddr) { +async fn peer_connection(peer_info: PeerInfo, cmd_tx: MovReqTx, socket: HyperWebsocket, addr: SocketAddr) { +    let socket = socket.await.unwrap(); // FIXME error handling      println!("Incoming TCP connection from: {}", addr); -    let ws_stream = tokio_tungstenite::accept_async(raw_stream) -        .await -        .expect("Error during the websocket handshake occurred"); -    println!("WebSocket connection established: {}", addr); -      let peer_map = peer_info.0;      let peer_seqid = peer_info.1.fetch_add(1, atomic::Ordering::AcqRel);      let mut peer_name = "unknown".to_string();      let (tx, rx) = unbounded(); -    let (outgoing, mut incoming) = ws_stream.split(); +    let (outgoing, mut incoming) = socket.split();      let process_incoming = async {          while let Ok(cmd) = incoming.try_next().await { @@ -249,7 +267,15 @@ async fn peer_connection(peer_info: PeerInfo, cmd_tx: MovReqTx, raw_stream: TcpS      peer_map.write().await.remove(&addr);  } -async fn handle_http_req(request: Request<Body>) -> HtmlResult { +async fn handle_req(mut request: Request<Body>, peer_info: PeerInfo, cmd_tx: MovReqTx, addr: SocketAddr) -> HtmlResult { +    if hyper_tungstenite::is_upgrade_request(&request) { +        let (resp, wsocket) = hyper_tungstenite::upgrade(&mut request, None).expect("couldn't upgrade to websocket"); +        tokio::spawn(async move { +            peer_connection(peer_info.clone(), cmd_tx.clone(), wsocket, addr).await; +        }); +        return Ok(resp); +    } +      let page = fs::read_to_string(PAGE_RELPATH).await.unwrap();      let mut uri_path = request.uri().path().split('/').skip(1);      let actual_path = uri_path.next(); @@ -289,6 +315,10 @@ fn errpage<T: Error>(e: T) -> Response<Body> {          .unwrap()  } +fn error(err: String) -> std::io::Error { +    std::io::Error::new(std::io::ErrorKind::Other, err) +} +  async fn shutdown_signal() {      tokio::signal::ctrl_c()          .await diff --git a/src/tls_stuff.rs b/src/tls_stuff.rs new file mode 100644 index 0000000..83c0489 --- /dev/null +++ b/src/tls_stuff.rs @@ -0,0 +1,159 @@ +//! Simple HTTPS echo service based on hyper-rustls +//! +//! First parameter is the mandatory port to use. +//! Certificate and private key are hardcoded to sample files. +//! hyper will automatically use HTTP/2 if a client starts talking HTTP/2, +//! otherwise HTTP/1.1 will be used. +use core::task::{Context, Poll}; +use futures_util::ready; +use hyper::server::accept::Accept; +use hyper::server::conn::{AddrIncoming, AddrStream}; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::vec::Vec; +use std::{fs, io}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_rustls::rustls::{self, ServerConfig}; + +fn error(err: String) -> io::Error { +    io::Error::new(io::ErrorKind::Other, err) +} + +pub enum State { +    Handshaking(tokio_rustls::Accept<AddrStream>), +    Streaming(tokio_rustls::server::TlsStream<AddrStream>), +} + +// tokio_rustls::server::TlsStream doesn't expose constructor methods, +// so we have to TlsAcceptor::accept and handshake to have access to it +// TlsStream implements AsyncRead/AsyncWrite handshaking tokio_rustls::Accept first +pub struct TlsStream { +    pub state: State, +} + +impl TlsStream { +    pub fn new(stream: AddrStream, config: Arc<ServerConfig>) -> TlsStream { +        let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); +        TlsStream { +            state: State::Handshaking(accept), +        } +    } +} + +impl AsyncRead for TlsStream { +    fn poll_read( +        self: Pin<&mut Self>, +        cx: &mut Context, +        buf: &mut ReadBuf, +    ) -> Poll<io::Result<()>> { +        let pin = self.get_mut(); +        match pin.state { +            State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { +                Ok(mut stream) => { +                    let result = Pin::new(&mut stream).poll_read(cx, buf); +                    pin.state = State::Streaming(stream); +                    result +                } +                Err(err) => Poll::Ready(Err(err)), +            }, +            State::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), +        } +    } +} + +impl AsyncWrite for TlsStream { +    fn poll_write( +        self: Pin<&mut Self>, +        cx: &mut Context<'_>, +        buf: &[u8], +    ) -> Poll<io::Result<usize>> { +        let pin = self.get_mut(); +        match pin.state { +            State::Handshaking(ref mut accept) => match ready!(Pin::new(accept).poll(cx)) { +                Ok(mut stream) => { +                    let result = Pin::new(&mut stream).poll_write(cx, buf); +                    pin.state = State::Streaming(stream); +                    result +                } +                Err(err) => Poll::Ready(Err(err)), +            }, +            State::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), +        } +    } + +    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { +        match self.state { +            State::Handshaking(_) => Poll::Ready(Ok(())), +            State::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), +        } +    } + +    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { +        match self.state { +            State::Handshaking(_) => Poll::Ready(Ok(())), +            State::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), +        } +    } +} + +pub struct TlsAcceptor { +    config: Arc<ServerConfig>, +    incoming: AddrIncoming, +} + +impl TlsAcceptor { +    pub fn new(config: Arc<ServerConfig>, incoming: AddrIncoming) -> TlsAcceptor { +        TlsAcceptor { config, incoming } +    } +} + +impl Accept for TlsAcceptor { +    type Conn = TlsStream; +    type Error = io::Error; + +    fn poll_accept( +        self: Pin<&mut Self>, +        cx: &mut Context<'_>, +    ) -> Poll<Option<Result<Self::Conn, Self::Error>>> { +        let pin = self.get_mut(); +        match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { +            Some(Ok(sock)) => Poll::Ready(Some(Ok(TlsStream::new(sock, pin.config.clone())))), +            Some(Err(e)) => Poll::Ready(Some(Err(e))), +            None => Poll::Ready(None), +        } +    } +} + +// Load public certificate from file. +pub fn load_certs(filename: &str) -> io::Result<Vec<rustls::Certificate>> { +    // Open certificate file. +    let certfile = fs::File::open(filename) +        .map_err(|e| error(format!("failed to open {}: {}", filename, e)))?; +    let mut reader = io::BufReader::new(certfile); + +    // Load and return certificate. +    let certs = rustls_pemfile::certs(&mut reader) +        .map_err(|_| error("failed to load certificate".into()))?; +    Ok(certs +        .into_iter() +        .map(rustls::Certificate) +        .collect()) +} + +// Load private key from file. +pub fn load_private_key(filename: &str) -> io::Result<rustls::PrivateKey> { +    // Open keyfile. +    let keyfile = fs::File::open(filename) +        .map_err(|e| error(format!("failed to open {}: {}", filename, e)))?; +    let mut reader = io::BufReader::new(keyfile); + +    // Load and return a single private key. +    let keys = rustls_pemfile::rsa_private_keys(&mut reader) +        .map_err(|_| error("failed to load private key".into()))?; +    if keys.len() != 1 { +        return Err(error("expected a single private key".into())); +    } + +    Ok(rustls::PrivateKey(keys[0].clone())) +} | 
