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
//! Websocket url utilities.
use std::str;

use rand::{self, seq::SliceRandom, Rng};
use url::Url;

mod error {
    use std::{error, fmt};

    /// Error turning a screeps API url into a websocket url.
    #[derive(Clone, Debug)]
    pub enum Error {
        /// Error parsing the URL.
        Parse {
            /// The parse error.
            err: url::ParseError,
            /// URL that failed to parse.
            url: String,
        },
        /// Found an unexpected scheme, not `http` or `https`.
        WrongScheme {
            /// The scheme that wasn't `http` or `https`.
            scheme: String,
            /// The URL that had this scheme.
            url: url::Url,
        },
    }

    impl Error {
        /// Creates an error given the `ParseError` and url `String`.
        pub fn from_err(error: url::ParseError, url: String) -> Self {
            Error::Parse {
                err: error,
                url: url,
            }
        }

        /// Creates an error given the scheme `String` and the url `Url`.
        pub fn wrong_scheme(scheme: String, url: url::Url) -> Self {
            Error::WrongScheme {
                url: url,
                scheme: scheme,
            }
        }
    }

    impl fmt::Display for Error {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            match *self {
                Error::Parse { ref err, ref url } => {
                    write!(f, "URL parse error: {} | url: {}", err, url)
                }
                Error::WrongScheme {
                    ref scheme,
                    ref url,
                } => write!(
                    f,
                    "expected HTTP or HTTPS url, found {} | url: {}",
                    scheme, url
                ),
            }
        }
    }

    impl error::Error for Error {
        fn description(&self) -> &str {
            match *self {
                Error::Parse { .. } => "expected URL to succeed parsing, but it failed",
                Error::WrongScheme { .. } => "expected HTTP or HTTPS url, found a different scheme",
            }
        }

        fn cause(&self) -> Option<&error::Error> {
            match *self {
                Error::Parse { ref err, .. } => Some(err),
                Error::WrongScheme { .. } => None,
            }
        }
    }
}

pub use self::error::Error as UrlError;

/// Creates a new (random) websocket URL to connect to the official server.
pub fn default_url() -> Url {
    transform_url(crate::DEFAULT_OFFICIAL_API_URL)
        .expect("expected known good default URL to parse successfully.")
}

/// Method for finding a websocket URL given the screeps API URL.
///
/// This method uses the thread-local `rand` crate rng to come up with a unique
/// session id, and the resulting url should not be reused over multiple connections.
///
/// The input URL should be an API url in the format of `https://screeps.com/api/`.
pub fn transform_url<U: AsRef<str> + ?Sized>(url: &U) -> Result<Url, UrlError> {
    use std::fmt;

    let mut url = match url.as_ref().parse::<Url>() {
        Ok(v) => v,
        Err(e) => return Err(UrlError::from_err(e, url.as_ref().to_owned())),
    };

    let new_scheme = match url.scheme() {
        "http" => Ok("ws"),
        "https" => Ok("wss"),
        other => Err(other.to_string()),
    };

    let new_scheme = match new_scheme {
        Ok(v) => v,
        Err(other) => return Err(UrlError::wrong_scheme(other, url)),
    };

    url.set_scheme(new_scheme)
        .expect("expected `ws` and `wss` to be valid url schemes.");

    // we could probably just use gen_ascii_chars for the session ID, but to be safe
    // we just use the subset that `sockjs-client` does.
    const VALID_CHARS: &'static [u8] = b"abcdefghijklmnopqrstuvwxyz012345";

    // avoiding allocations!
    struct GenServerAndSessionId;

    impl fmt::Display for GenServerAndSessionId {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            let mut rng = rand::thread_rng();
            write!(f, "../socket/{:04}/", rng.gen_range(0, 1000))?;

            for _ in 0..8 {
                write!(f, "{}", *VALID_CHARS.choose(&mut rng).unwrap() as char)?;
            }
            write!(f, "/websocket")?;

            Ok(())
        }
    }

    let result = url
        .join(&GenServerAndSessionId.to_string())
        .expect("expected generated string known to be correct to parse successfully");

    Ok(result)
}