romp 0.5.2

STOMP server and WebSockets platform
Documentation
//! Code for validating a WebSockets Upgrade HTTP request.
//!
//! <pre>
//! GET /foo?ba=true HTTP/1.1
//! Host: xtomp.tp23.org
//! ignore: me
//! Upgrade: websocket
//! Connection: Upgrade
//! Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
//! Sec-WebSocket-Protocol: stomp
//! Sec-WebSocket-Version: 13
//! Origin: http://tp23.org
//! </pre>
//!
use log::*;
use log::Level::Debug;

use sha1::Sha1;

use crate::init::CONFIG;
use crate::message::stomp_message::StompMessage;



#[derive(Debug, PartialEq)]
pub enum WsUpgradeError {
    OriginDenied,
    Syntax,
    HostMissing,
    ProtocolError,
}

/// Validate all the HTTP headers required to upgrade to WebSockets, spec writers had a field day here!
/// All that is really necessary is  `GET / HTTP/1.1\nUpgrade:websocket\n\n`
pub fn ws_validate_hdrs(message: &StompMessage) -> Result<(), WsUpgradeError> {
//    if let Stomp = &message.message_type {
//        return Err(WsUpgradeError::Syntax);
//    }
    if ! ws_validate_hdr_websocket_origin(message) {
        return Err(WsUpgradeError::OriginDenied);
    }
    if ! ws_validate_hdr_host(message) {
        return Err(WsUpgradeError::HostMissing);
    }
    if ! ws_validate_hdr_sec_protocol(message) {
        return Err(WsUpgradeError::ProtocolError);
    }
    if ! ws_validate_hdr_upgrade(message) {
        return Err(WsUpgradeError::ProtocolError);
    }

    if  ws_validate_hdr_connection(message) &&
            ws_validate_hdr_upgrade(message) &&
            ws_validate_hdr_websocket_version(message) &&
            ws_validate_hdr_websocket_key(message) {
        return Ok(());
    }

    Err(WsUpgradeError::Syntax)
}


/// Get the return accept key, key should be 16 bytes base64 encoded but
/// servers do not need to verify this or decode the string
pub fn ws_get_websocket_accept_key(message: &StompMessage) -> String {
    if let Some(web_socket_key) = message.get_header_case_insensitive("Sec-WebSocket-Key") {
        let mut sha1 = Sha1::new();
        sha1.update(web_socket_key.trim().as_bytes());
        sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" as &[u8]);
        let sha_res = sha1.digest().bytes();
        return base64::encode(sha_res.as_ref());
    }
    // probably should die
    return String::from("");
}

/// parse the HTTP message's request line
// Request line of HTTP messages is saved as a header, see parser.rs request_line_done()
pub fn ws_parse_request_line(message: &StompMessage) -> Result<String,()> {
    match message.get_header("request-line") {
        Some(line) => {
            for (i, part) in line.split_whitespace().enumerate() {
                if i == 0 && ! part.eq("GET") {
                    // BUG
                    return Err(());
                }
                if i == 2  && ! part.eq("HTTP/1.1") {
                    // TODO should we support 2.0 and 3.0?
                    return Err(());
                }
                if i == 1  {
                    // part is the URI
                    return Ok(String::from(part));
                }
            }
            Err(())
        },
        _ => Err(())
    }
}

/// Required by rfc6455 ensures we are HTTP/1.1 at least
/// // TODO virtual hosting
pub fn ws_validate_hdr_host(message: &StompMessage) -> bool {
    if let Some(host) = message.get_header_case_insensitive("Host") {
        if ! CONFIG.name.eq(host.trim()) {
            if log_enabled!(Debug) {
                debug!("unexpected host header: {}", host.trim());
            }
        }
        // return true anyway could be using IP address or localhost
        return true;
    }
    false
}

/// Required by rfc6455 to indicate converting an HTTP stream to something else.
fn ws_validate_hdr_upgrade(message: &StompMessage) -> bool {
    if let Some(value) = message.get_header_case_insensitive("Upgrade") {
        return "websocket".eq(value.trim());
    }
    false
}

/// Required by rfc6455, but pointless, FireFox now sends "keep-alive, Upgrade"
fn ws_validate_hdr_connection(message: &StompMessage) -> bool {
    if let Some(value) = message.get_header_case_insensitive("Connection") {
        for con_flag in value.split(",") {
            if "Upgrade".eq_ignore_ascii_case(con_flag.trim()) {
                return true;
            }
        }
    }
    false
}

/// Validate secondary protocol is STOMP, fail fast if something weird connects to us.
fn ws_validate_hdr_sec_protocol(message: &StompMessage) -> bool {
    if let Some(protocol) = message.get_header_case_insensitive("Sec-WebSocket-Protocol") {
        return "stomp".eq_ignore_ascii_case(protocol.trim());
    }
    false
}

/// Pretty pointless, but rfc6455 requires Sec-WebSocket-Version: 13 be sent
// TODO why should we care about silly reqs like this
fn ws_validate_hdr_websocket_version(message: &StompMessage) -> bool {
    if let Some(version) = message.get_header_case_insensitive("Sec-WebSocket-Version") {
        return "13".eq(version.trim());
    }
    false
}

/// Validate the Origin header, browsers MUST sewnd this so we should validate it to stop
/// injected JavaScript hitting us from insecure websites.
/// TGhe header is optional for Apps and non-browser connections.
fn ws_validate_hdr_websocket_origin(message: &StompMessage) -> bool {
    if let Some(origin) = message.get_header_case_insensitive("Origin") {
        if let Some(allowed_origins) = &CONFIG.websockets_origin {
            if "*".eq(allowed_origins) {
                return true;
            }
            for allowed_origin in allowed_origins.split_whitespace() {
                if allowed_origin.eq_ignore_ascii_case(origin) {
                    return true;
                }
            }
        }
    }
    true
}

/// validate Sec-WebSocket-Key header
fn ws_validate_hdr_websocket_key(message: &StompMessage) -> bool {
    message.get_header_case_insensitive("Sec-WebSocket-Key").is_some()
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::stomp_message::MessageType::Http;
    use crate::message::stomp_message::Ownership;


    #[test]
    fn test_happy_path() {
        let mut message = StompMessage::new(Ownership::Session);
        message.message_type = Http;
        message.add_header("request-line", "GET / HTTP/1.1");
        message.add_header("Host", "romp");
        message.add_header("Connection", "Upgrade");
        message.add_header("Upgrade", "websocket");
        message.add_header("Sec-WebSocket-Key", "x3JJHMbDL1EzLkh9GBhXDw==");
        message.add_header("Sec-WebSocket-Protocol", "stomp");
        message.add_header("Sec-WebSocket-Version", "13");
        message.add_header("Origin", "http://tp23.org");
        match ws_validate_hdrs(&message) {
            Ok(_) => {
                assert_eq!("HSmrc0sMlYUkAGmm5OPpG2HaGWk=", ws_get_websocket_accept_key(&message));
            },
            _ => panic!("Headers not ok")
        }
    }


    #[test]
    fn test_happy_path2() {
        let mut message = StompMessage::new(Ownership::Session);
        message.message_type = Http;
        message.add_header("request-line", "GET / HTTP/1.1");
        message.add_header("Host", "romp");
        message.add_header("Connection", "Upgrade");
        message.add_header("Upgrade", "websocket");
        message.add_header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==");
        message.add_header("Sec-WebSocket-Protocol", "stomp");
        message.add_header("Sec-WebSocket-Version", "13");
        message.add_header("Origin", "http://tp23.org");
        match ws_validate_hdrs(&message) {
            Ok(_) => {
                assert_eq!("s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", ws_get_websocket_accept_key(&message));
            },
            _ => panic!("Headers not ok")
        }
    }

    #[test]
    fn test_neg_no_host() {
        let mut message = StompMessage::new(Ownership::Session);
        message.message_type = Http;
        message.add_header("request-line", "GET / HTTP/1.1");
        //message.add_header("Host", "romp");
        message.add_header("Connection", "Upgrade");
        message.add_header("Upgrade", "websocket");
        message.add_header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==");
        message.add_header("Sec-WebSocket-Protocol", "stomp");
        message.add_header("Sec-WebSocket-Version", "13");
        message.add_header("Origin", "http://tp23.org");
        match ws_validate_hdrs(&message) {
            Err(WsUpgradeError::HostMissing) => {
                // expected
            },
            _ => panic!("Host checking failed")
        }
    }

    #[test]
    fn test_neg_no_upgrade() {
        let mut message = StompMessage::new(Ownership::Session);
        message.message_type = Http;
        message.add_header("request-line", "GET / HTTP/1.1");
        message.add_header("Host", "romp");
        message.add_header("Connection", "Upgrade");
        //message.add_header("Upgrade", "websocket");
        message.add_header("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==");
        message.add_header("Sec-WebSocket-Protocol", "stomp");
        message.add_header("Sec-WebSocket-Version", "13");
        message.add_header("Origin", "http://tp23.org");
        match ws_validate_hdrs(&message) {
            Err(WsUpgradeError::ProtocolError) => {
                // expected
            },
            _ => panic!("Upgrade header check failed")
        }
    }

    // TODO we could ignore all other errors??
}