Skip to main content

opentok_server/
lib.rs

1extern crate rustc_serialize;
2
3use rand::Rng;
4use rustc_serialize::hex::ToHex;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::time::{SystemTime, UNIX_EPOCH};
8use thiserror::Error;
9
10mod http_client;
11
12static SERVER_URL: &str = "https://api.opentok.com";
13static API_ENDPOINT_PATH_START: &str = "/v2/project/";
14
15/// Unique session identifier.
16pub type SessionId = String;
17
18/// OpenTokError enumerates all possible errors returned by this library.
19#[derive(Debug, Error, PartialEq)]
20pub enum OpenTokError {
21    #[error("Bad request {0}")]
22    BadRequest(String),
23    #[error("Cannot encode request")]
24    EncodingError,
25    #[error("OpenTok server error {0}")]
26    ServerError(String),
27    #[error("Unexpected response {0}")]
28    UnexpectedResponse(String),
29    #[error("Unknown error")]
30    __Unknown,
31}
32
33impl From<surf::Error> for OpenTokError {
34    fn from(error: surf::Error) -> OpenTokError {
35        match error.status().into() {
36            400..=499 => OpenTokError::BadRequest(error.to_string()),
37            500..=599 => OpenTokError::ServerError(error.to_string()),
38            _ => OpenTokError::__Unknown,
39        }
40    }
41}
42
43/// Determines whether a session will transmit streams using the OpenTok Media Router
44/// or not.
45#[derive(Debug, PartialEq)]
46pub enum MediaMode {
47    /// The session will try to transmit streams directly between clients.
48    Relayed,
49    /// The session will transmit streams using the OpenTok Media Router.
50    Routed,
51}
52
53impl fmt::Display for MediaMode {
54    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
55        write!(formatter, "{}", format!("{:?}", self).to_lowercase())
56    }
57}
58
59/// Determines whether a session is automatically archived or not.
60/// Archiving is currently unsupported.
61#[derive(Debug)]
62pub enum ArchiveMode {
63    /// The session will always be archived automatically.
64    Always,
65    /// A POST request to /archive is required to archive the session.
66    /// Currently unsupported.
67    Manual,
68}
69
70impl fmt::Display for ArchiveMode {
71    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
72        write!(formatter, "{}", format!("{:?}", self).to_lowercase())
73    }
74}
75
76/// OpenTok Session options to be provided at Session creation time.
77#[derive(Default)]
78pub struct SessionOptions<'a> {
79    /// An IP address that the OpenTok servers will use to situate the session in the global
80    /// OpenTok network. If you do not set a location hint, the OpenTok servers will be based
81    /// on the first client connecting to the session.
82    pub location: Option<&'a str>,
83    /// Determines whether the session will transmit streams using the OpenTok Media Router
84    /// ("routed") or not ("relayed"). By default, the setting is "relayed".
85    /// With the media_mode parameter set to "relayed", the session will attempt to transmit
86    /// streams directly between clients. If clients cannot connect due to firewall restrictions,
87    /// the session uses the OpenTok TURN server to relay audio-video streams.
88    pub media_mode: Option<MediaMode>,
89    /// Whether the session is automatically archived ("always") or not ("manual").
90    /// By default, the setting is "manual". To archive the session (either automatically or not),
91    /// you must set the media_mode parameter to "routed".
92    /// Archiving is currently unsupported.
93    pub archive_mode: Option<ArchiveMode>,
94}
95
96#[derive(Serialize)]
97#[serde(rename_all = "camelCase")]
98struct CreateSessionBody<'a> {
99    archive_mode: String,
100    location: Option<&'a str>,
101    #[serde(rename = "p2p.preference")]
102    p2p_preference: &'a str,
103}
104
105impl<'a> From<SessionOptions<'a>> for CreateSessionBody<'a> {
106    fn from(options: SessionOptions) -> CreateSessionBody {
107        CreateSessionBody {
108            archive_mode: options
109                .archive_mode
110                .map(|mode| mode.to_string())
111                .unwrap_or_else(|| "manual".into()),
112            location: options.location,
113            p2p_preference: options
114                .media_mode
115                .map(|mode| {
116                    if mode == MediaMode::Relayed {
117                        "enabled"
118                    } else {
119                        "disabled"
120                    }
121                })
122                .unwrap_or("disabled"),
123        }
124    }
125}
126
127#[derive(Deserialize)]
128struct CreateSessionResponse {
129    session_id: String,
130}
131
132#[derive(Debug)]
133pub enum TokenRole {
134    Publisher,
135    Subscriber,
136    Moderator,
137}
138
139impl fmt::Display for TokenRole {
140    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
141        write!(formatter, "{}", format!("{:?}", self).to_lowercase())
142    }
143}
144
145#[derive(Debug)]
146struct TokenData<'a> {
147    session_id: &'a str,
148    create_time: u64,
149    expire_time: u64,
150    nonce: u64,
151    role: TokenRole,
152}
153
154impl<'a> TokenData<'a> {
155    pub fn new(session_id: &'a str, role: TokenRole) -> Self {
156        let now = SystemTime::now()
157            .duration_since(UNIX_EPOCH)
158            .expect("Time went backwards, Doc!")
159            .as_secs();
160        let mut rng = rand::thread_rng();
161        Self {
162            session_id,
163            create_time: now,
164            expire_time: now + (60 * 60 * 24),
165            nonce: rng.gen::<u64>(),
166            role,
167        }
168    }
169}
170
171impl<'a> fmt::Display for TokenData<'a> {
172    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
173        write!(
174            formatter,
175            "session_id={}&create_time={}&expire_time={}&nonce={}&role={}",
176            self.session_id, self.create_time, self.expire_time, self.nonce, self.role,
177        )
178    }
179}
180
181#[derive(Debug, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum VideoType {
184    Camera,
185    Screen,
186    Custom,
187}
188
189impl fmt::Display for VideoType {
190    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
191        write!(formatter, "{}", format!("{:?}", self).to_lowercase())
192    }
193}
194
195#[derive(Debug, Deserialize)]
196#[serde(rename_all = "camelCase")]
197#[allow(dead_code)]
198pub struct StreamInfo {
199    id: String,
200    video_type: VideoType,
201    name: String,
202    layout_class_list: Vec<String>,
203}
204
205/// Top level entry point exposing the OpenTok server SDK functionality.
206/// Contains methods for creating OpenTok sessions, generating tokens and
207/// getting information about streams.
208pub struct OpenTok {
209    api_key: String,
210    api_secret: String,
211}
212
213impl OpenTok {
214    /// Create a new instance of OpenTok. Requires an OpenTok API key and
215    /// the API secret for your TokBox account. Do not publicly share your
216    /// API secret.
217    pub fn new(api_key: String, api_secret: String) -> Self {
218        Self {
219            api_key,
220            api_secret,
221        }
222    }
223
224    /// Creates a new OpenTok session.
225    /// On success, a session ID is provided.
226    pub async fn create_session<'a>(
227        &self,
228        options: SessionOptions<'a>,
229    ) -> Result<String, OpenTokError> {
230        let body: CreateSessionBody = options.into();
231        let endpoint = format!("{}{}", SERVER_URL, "/session/create");
232        let mut response =
233            http_client::post(&endpoint, &self.api_key, &self.api_secret, &body).await?;
234        let response_str = response.body_string().await?;
235        let mut response: Vec<CreateSessionResponse> =
236            serde_json::from_str::<Vec<CreateSessionResponse>>(&response_str)
237                .map_err(|_| OpenTokError::UnexpectedResponse(response_str.clone()))?;
238        assert_eq!(response.len(), 1);
239        match response.pop() {
240            Some(session) => Ok(session.session_id),
241            None => Err(OpenTokError::UnexpectedResponse(response_str)),
242        }
243    }
244
245    pub fn generate_token(&self, session_id: &str, role: TokenRole) -> String {
246        let token_data = TokenData::new(session_id, role);
247        let signed = hmacsha1::hmac_sha1(
248            self.api_secret.as_bytes(),
249            token_data.to_string().as_bytes(),
250        )
251        .to_hex();
252        let decoded = format!("partner_id={}&sig={}:{}", self.api_key, signed, token_data);
253        let encoded = base64::encode(decoded);
254        format!("T1=={}", encoded)
255    }
256
257    pub async fn get_stream_info(
258        &self,
259        session_id: &str,
260        stream_id: &str,
261    ) -> Result<StreamInfo, OpenTokError> {
262        let endpoint = format!(
263            "{}{}{}/session/{}/stream/{}",
264            SERVER_URL, API_ENDPOINT_PATH_START, self.api_key, session_id, stream_id
265        );
266        let mut response = http_client::get(&endpoint, &self.api_key, &self.api_secret).await?;
267        let response_str = response.body_string().await?;
268        serde_json::from_str::<StreamInfo>(&response_str)
269            .map_err(|_| OpenTokError::UnexpectedResponse(response_str.clone()))
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    use futures::executor::LocalPool;
278    use opentok::utils::common::Credentials;
279    use opentok::utils::publisher::Publisher;
280    use std::env;
281
282    #[test]
283    fn test_create_session_invalid_credentials() {
284        let opentok = OpenTok::new("sancho".into(), "quijote".into());
285        let mut pool = LocalPool::new();
286        assert!(pool
287            .run_until(opentok.create_session(SessionOptions::default()))
288            .is_err());
289    }
290
291    #[test]
292    fn test_create_session() {
293        let api_key = env::var("OPENTOK_KEY").unwrap();
294        let api_secret = env::var("OPENTOK_SECRET").unwrap();
295        let opentok = OpenTok::new(api_key, api_secret);
296        let mut pool = LocalPool::new();
297        let session_id = pool
298            .run_until(opentok.create_session(SessionOptions::default()))
299            .unwrap();
300        assert!(!session_id.is_empty());
301    }
302
303    #[test]
304    fn test_generate_token() {
305        let api_key = env::var("OPENTOK_KEY").unwrap();
306        let api_secret = env::var("OPENTOK_SECRET").unwrap();
307        let opentok = OpenTok::new(api_key, api_secret);
308        let mut pool = LocalPool::new();
309        let session_id = pool
310            .run_until(opentok.create_session(SessionOptions::default()))
311            .unwrap();
312        assert!(!session_id.is_empty());
313        let token = opentok.generate_token(&session_id, TokenRole::Publisher);
314        assert!(!token.is_empty());
315    }
316
317    #[test]
318    fn test_get_stream_info() {
319        let api_key = env::var("OPENTOK_KEY").unwrap();
320        let api_secret = env::var("OPENTOK_SECRET").unwrap();
321        let opentok = OpenTok::new(api_key.clone(), api_secret);
322        let mut pool = LocalPool::new();
323        let session_id = pool
324            .run_until(opentok.create_session(SessionOptions::default()))
325            .unwrap();
326        assert!(!session_id.is_empty());
327        let token = opentok.generate_token(&session_id, TokenRole::Publisher);
328        assert!(!token.is_empty());
329
330        opentok::init().unwrap();
331
332        let publisher = Publisher::new(
333            Credentials {
334                api_key,
335                session_id: session_id.clone(),
336                token,
337            },
338            Some(Box::new(move |publisher, stream_id| {
339                let mut pool = LocalPool::new();
340                let stream_info = pool
341                    .run_until(opentok.get_stream_info(&session_id, &stream_id))
342                    .unwrap();
343                assert_eq!(stream_info.id, stream_id);
344                publisher.stop();
345            })),
346            None,
347        );
348
349        publisher.run().unwrap();
350
351        opentok::deinit().unwrap();
352    }
353}