├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── main.rs ├── network.rs ├── node.rs ├── push.rs └── server.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustysignal" 3 | version = "2.0.2" 4 | authors = ["rasviitanen "] 5 | readme = "README.md" 6 | license = "MIT/Apache-2.0" 7 | description = "A signaling server for WebRTC" 8 | repository = "https://github.com/rasviitanen/rustysignal" 9 | 10 | [dependencies] 11 | serde = "1.0" 12 | serde_derive = "1.0" 13 | serde_json = "1.0" 14 | lazy_static = "1.0" 15 | clap = "2.32.0" 16 | log = "0.4.0" 17 | env_logger = "0.6.0" 18 | ws = { version = "0.8.0", features = ["ssl"] } 19 | openssl = "0.10.16" 20 | web-push = { version = "0.4.1", optional = true } 21 | tokio = "0.1.15" 22 | base64 = "0.10.1" 23 | futures = "0.1.25" 24 | 25 | [features] 26 | ssl = ["ws/ssl"] 27 | push = ["web-push"] 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Rasmus Viitanen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rustysignal 2 | [![Cargo](https://img.shields.io/crates/v/rustysignal.svg)](https://crates.io/crates/rustysignal) 3 | 4 | > :warning: The version on the master-branch is currently untested. If you want to use a more stable version, install it from cargo according to the instructions below. 5 | 6 | A signaling server written in Rust for WebRTC that supports SSL and Push notifications. 7 | The signaling server is used to enable nodes on the network to exchange metadata in order to establish a peer-to-peer connection. 8 | This signaling server supplies the ability to set usernames on the network, and users have the ability to send messages to a specific peer, or broadcast messages to everyone on the network. 9 | 10 | ## Installation 11 | *You need [**Cargo**](https://doc.rust-lang.org/cargo/getting-started/installation.html) to be able to install this binary.* 12 | 13 | Install the signaling server without SSL 14 | (Suitable for local testing) 15 | 16 | `cargo install rustysignal` 17 | 18 | If you want to enable SSL, make sure to include it as a feature 19 | (Needed when using WebRTC in production) 20 | 21 | `cargo install rustysignal --features ssl` 22 | 23 | Once installed, you can start it by executing `rustysignal 127.0.0.1:3012` in your terminal, which will start the server and listen to messages on the address `127.0.0.1:3012`. 24 | 25 | If you are using SSL, you will need to provide your certificate. 26 | `rustysignal 127.0.0.1:3015 ` 27 | 28 | ### Push 29 | If you want to use push, you will need to build rustysignal from source. Clone the master branch, and run the server with `--features push`. For both push and ssl functionality, run it with `--features 'ssl push'` 30 | The push is sent by including a connection request in the request payload. I.e. `action: "connection_request`. See `src/push.rs` for more information. 31 | 32 | ## Connecting to the network as a peer 33 | When connecting to the network, i.e. Websocket, one should provide a username as a simple argument. 34 | > wss://signalserverhost?user=yourname 35 | 36 | Peers can be found by: 37 | 1. A one-to-one request via a provided username, which sends your information to only the node tied to that username. 38 | 2. A one-to-all request, which sends your information to everyone on the network. 39 | 3. A one-to-self request, which sends your information back to yourself. 40 | 41 | To specify which type of method, specify it in your websocket send command in a field called `protocol`. 42 | 43 | ``` 44 | var json_message = { protocol: "one-to-one", to: "receiver_username", "action": actiontype, "data": data }; 45 | ws.send(JSON.stringify(json_message)); 46 | ``` 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate ws; 2 | #[macro_use] 3 | extern crate serde_json; 4 | 5 | extern crate clap; 6 | extern crate env_logger; 7 | 8 | extern crate serde; 9 | extern crate tokio; 10 | extern crate base64; 11 | extern crate futures; 12 | 13 | #[cfg(feature = "ssl")] 14 | extern crate openssl; 15 | #[cfg(feature = "push")] 16 | extern crate web_push; 17 | 18 | mod server; 19 | 20 | mod node; 21 | mod network; 22 | 23 | fn main() { 24 | server::run() 25 | } -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use std::rc::Rc; 3 | use std::rc::Weak; 4 | use std::cell::RefCell; 5 | use std::collections::HashMap; 6 | #[cfg(feature = "push")] 7 | use std::{ 8 | fs::File, 9 | time::Duration, 10 | }; 11 | 12 | #[cfg(feature = "push")] 13 | use futures::{ 14 | future::{ 15 | lazy, 16 | }, 17 | Future, 18 | }; 19 | #[cfg(feature = "push")] 20 | use web_push::*; 21 | 22 | use node::Node; 23 | 24 | #[cfg(feature = "push")] 25 | #[derive(Default)] 26 | pub struct Network { 27 | pub nodemap: Rc>>>>, 28 | pub pushmap: Rc>>, 29 | pub vapid_path: String, 30 | } 31 | 32 | #[cfg(not(feature = "push"))] 33 | #[derive(Default)] 34 | pub struct Network { 35 | pub nodemap: Rc>>>>, 36 | } 37 | 38 | impl Network { 39 | pub fn add_user(&mut self, owner: &str, node: &std::rc::Rc>) { 40 | if !self.nodemap.borrow().contains_key(owner) { 41 | node.borrow_mut().owner = Some(owner.into()); 42 | self.nodemap.borrow_mut().insert(owner.to_string(), Rc::downgrade(node)); 43 | println!("Node {:?} connected to the network.", owner); 44 | } else { 45 | println!("{:?} tried to connect, but the username was taken", owner); 46 | node.borrow().sender.send("The username is taken").ok(); 47 | } 48 | } 49 | 50 | pub fn remove(&mut self, owner: &str) { 51 | self.nodemap.borrow_mut().remove(owner); 52 | } 53 | 54 | pub fn size(&self) -> usize { 55 | self.nodemap.borrow().len() 56 | } 57 | 58 | #[cfg(feature = "push")] 59 | pub fn add_subscription(&mut self, subscription: &str, node: &std::rc::Rc>) { 60 | println!("Node {:?} updated its subscription data", node.borrow().owner); 61 | node.borrow_mut().subscription = Some(subscription.into()); 62 | let owner = node.borrow().owner.clone(); 63 | 64 | self.pushmap.borrow_mut().insert(owner.unwrap(), subscription.to_string()); 65 | } 66 | 67 | #[cfg(feature = "push")] 68 | pub fn set_vapid_path(&mut self, vapid_path: &str) { 69 | self.vapid_path = vapid_path.to_string(); 70 | } 71 | 72 | #[cfg(feature = "push")] 73 | pub fn send_push(&self, sender: &str, endpoint: &str) { 74 | println!("!!!!!! Sending PUSH !!!!!!!"); 75 | 76 | let payload = 77 | json!({"body": format!("{}\nwants to connect with you", sender), 78 | "sender": sender, 79 | "actions": [ 80 | {"action": "allowConnection", "title": "✔️ Allow"}, 81 | {"action": "denyConnection", "title": "✖️ Deny"}]}).to_string(); 82 | 83 | if let Some(subscription) = self.pushmap.borrow().get(endpoint) { 84 | let subscription_info: SubscriptionInfo = serde_json::from_str(subscription).unwrap(); 85 | 86 | let mut builder = WebPushMessageBuilder::new(&subscription_info).unwrap(); 87 | builder.set_payload(ContentEncoding::AesGcm, payload.as_bytes()); 88 | 89 | let vapid_file = File::open(&self.vapid_path).unwrap(); 90 | 91 | let sig_builder = VapidSignatureBuilder::from_pem(vapid_file, &subscription_info).unwrap(); 92 | let signature = sig_builder.build().unwrap(); 93 | 94 | builder.set_ttl(3600); 95 | builder.set_vapid_signature(signature); 96 | 97 | match builder.build() { 98 | Ok(message) => { 99 | let client = WebPushClient::new().unwrap(); 100 | tokio::run(lazy(move || { 101 | client 102 | .send_with_timeout(message, Duration::from_secs(4)) 103 | .map(|response| { 104 | println!("Sent: {:?}", response); 105 | }).map_err(|error| { 106 | println!("Error: {:?}", error) 107 | }) 108 | })); 109 | }, 110 | Err(error) => { 111 | println!("ERROR in building message: {:?}", error) 112 | } 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/node.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "push")] 2 | pub struct Node { 3 | pub owner: Option, 4 | pub subscription: Option, 5 | pub sender: ws::Sender 6 | } 7 | 8 | #[cfg(feature = "push")] 9 | impl Node { 10 | pub fn new(sender: ws::Sender) -> Node { 11 | Node { 12 | owner: None, 13 | subscription: None, 14 | sender: sender 15 | } 16 | } 17 | } 18 | 19 | #[cfg(not(feature = "push"))] 20 | pub struct Node { 21 | pub owner: Option, 22 | pub sender: ws::Sender 23 | } 24 | 25 | #[cfg(not(feature = "push"))] 26 | impl Node { 27 | pub fn new(sender: ws::Sender) -> Node { 28 | Node { 29 | owner: None, 30 | sender: sender 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/push.rs: -------------------------------------------------------------------------------- 1 | use web_push::*; 2 | 3 | use std::{ 4 | fs::File, 5 | io::Read, 6 | time::Duration, 7 | }; 8 | 9 | use futures::{ 10 | future::{ 11 | lazy, 12 | }, 13 | Future, 14 | }; 15 | 16 | pub fn push(push_payload: &str, subscription: &str) { 17 | println!("!!!!!! Sending PUSH !!!!!!!"); 18 | println!("{:?}", subscription); 19 | 20 | let subscription_info: SubscriptionInfo = serde_json::from_str(subscription).unwrap(); 21 | 22 | let mut builder = WebPushMessageBuilder::new(&subscription_info).unwrap(); 23 | 24 | builder.set_payload(ContentEncoding::AesGcm, push_payload.as_bytes()); 25 | 26 | let file = File::open("cert/vapid/private.pem").unwrap(); 27 | 28 | let sig_builder = VapidSignatureBuilder::from_pem(file, &subscription_info).unwrap(); 29 | let signature = sig_builder.build().unwrap(); 30 | 31 | builder.set_ttl(3600); 32 | builder.set_vapid_signature(signature); 33 | 34 | match builder.build() { 35 | Ok(message) => { 36 | let client = WebPushClient::new().unwrap(); 37 | tokio::run(lazy(move || { 38 | client 39 | .send_with_timeout(message, Duration::from_secs(4)) 40 | .map(|response| { 41 | println!("Sent: {:?}", response); 42 | }).map_err(|error| { 43 | println!("Error: {:?}", error) 44 | }) 45 | })); 46 | }, 47 | Err(error) => { 48 | println!("ERROR in building message: {:?}", error) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use std::rc::Rc; 3 | use std::cell::RefCell; 4 | #[cfg(feature = "ssl")] 5 | use std::thread::sleep; 6 | #[cfg(feature = "ssl")] 7 | use std::time::Duration; 8 | 9 | use serde_json::Value; 10 | 11 | use ws::{Handler, Result, Message, Handshake, CloseCode}; 12 | #[cfg(feature = "ssl")] 13 | use ws::util::TcpStream; 14 | 15 | #[cfg(feature = "ssl")] 16 | use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream}; 17 | 18 | use node::Node; 19 | use network::Network; 20 | 21 | struct Server { 22 | node: Rc>, 23 | #[cfg(feature = "ssl")] 24 | ssl: Rc, 25 | network: Rc>, 26 | } 27 | 28 | impl Server { 29 | #[cfg(feature = "push")] 30 | fn handle_push_requests(&mut self, json_message: &Value) { 31 | match json_message["action"].as_str() { 32 | Some("subscribe-push") => { 33 | match json_message["subscriptionData"].as_str() { 34 | Some(data) => { 35 | self.network.borrow_mut().add_subscription(data, &self.node); 36 | }, 37 | _ => { println!("No subscription data") } 38 | } 39 | }, 40 | Some("connection-request") => { 41 | match json_message["endpoint"].as_str() { 42 | Some(endpoint) => { 43 | let user_sending_request = self.node.borrow().owner.clone().unwrap(); 44 | self.network.borrow().send_push(&user_sending_request, &endpoint); 45 | } 46 | _ => { println!("No endpoint for connection request") } 47 | } 48 | }, 49 | _ => { /* Do nothing if the user is not interested in the push */ } 50 | }; 51 | } 52 | 53 | } 54 | 55 | impl Handler for Server { 56 | fn on_open(&mut self, handshake: Handshake) -> Result<()> { 57 | // Get the aruments from a URL 58 | // i.e localhost:8000/?user=testuser 59 | 60 | // skip()ing everything before the first '?' allows us to run the 61 | // server behind a reverse proxy like nginx with minimal fuss 62 | let url_arguments = handshake.request.resource() 63 | .split(|c| c=='?'||c=='='||c=='&').skip(1); 64 | // Beeing greedy by not collecting pairs 65 | // Instead every even number (including 0) will be an identifier 66 | // and every odd number will be the assigned value 67 | let argument_vector: Vec<&str> = url_arguments.collect(); 68 | 69 | if argument_vector.len() >= 2 && argument_vector[0] == "user" { 70 | let username: &str = argument_vector[1]; 71 | self.network.borrow_mut().add_user(username, &self.node); 72 | } else { 73 | println!("New node didn't provide a username"); 74 | } 75 | 76 | println!("Network expanded to {:?} connected nodes", self.network.borrow().size()); 77 | Ok(()) 78 | } 79 | 80 | #[cfg(feature = "ssl")] 81 | fn upgrade_ssl_server(&mut self, sock: TcpStream) -> ws::Result> { 82 | println!("Server node upgraded"); 83 | // TODO This is weird, but the sleep is needed... 84 | sleep(Duration::from_millis(200)); 85 | self.ssl.accept(sock).map_err(From::from) 86 | } 87 | 88 | fn on_message(&mut self, msg: Message) -> Result<()> { 89 | let text_message: &str = msg.as_text()?; 90 | let json_message: Value = 91 | serde_json::from_str(text_message).unwrap_or(Value::default()); 92 | 93 | // !!! WARNING !!! 94 | // The word "protocol" match is protcol specific. 95 | // Thus a client should make sure to send a viable protocol 96 | let protocol = match json_message["protocol"].as_str() { 97 | Some(desired_protocol) => { Some(desired_protocol) }, 98 | _ => { None } 99 | }; 100 | 101 | 102 | // The words below are protcol specific. 103 | // Thus a client should make sure to use a viable protocol 104 | let ret = match protocol { 105 | Some("one-to-all") => { 106 | self.node.borrow().sender.broadcast(text_message) 107 | }, 108 | Some("one-to-self") => { 109 | self.node.borrow().sender.send(text_message) 110 | }, 111 | Some("one-to-one") => { 112 | match json_message["endpoint"].as_str() { 113 | Some(endpoint) => { 114 | let network = self.network.borrow(); 115 | let endpoint_node = network.nodemap.borrow().get(endpoint) 116 | .and_then(|node| node.upgrade()); 117 | 118 | match endpoint_node { 119 | Some(node) => { node.borrow().sender.send(text_message) } 120 | _ => {self.node.borrow().sender 121 | .send(format!("Could not find a node with the name {}", endpoint))} 122 | } 123 | } 124 | _ => { 125 | self.node.borrow().sender.send( 126 | "No field 'endpoint' provided" 127 | ) 128 | } 129 | } 130 | 131 | } 132 | _ => { 133 | self.node.borrow().sender.send( 134 | "Invalid protocol, valid protocols include: 135 | 'one-to-one' 136 | 'one-to-self' 137 | 'one-to-all'" 138 | ) 139 | } 140 | }; 141 | 142 | #[cfg(feature = "push")] 143 | self.handle_push_requests(&json_message); 144 | 145 | return ret 146 | } 147 | 148 | fn on_close(&mut self, code: CloseCode, reason: &str) { 149 | // Remove the node from the network 150 | if let Some(owner) = &self.node.borrow().owner { 151 | match code { 152 | CloseCode::Normal => 153 | println!("{:?} is done with the connection.", owner), 154 | CloseCode::Away => 155 | println!("{:?} left the site.", owner), 156 | CloseCode::Abnormal => 157 | println!("Closing handshake for {:?} failed!", owner), 158 | _ => 159 | println!("{:?} encountered an error: {:?}", owner, reason), 160 | }; 161 | 162 | self.network.borrow_mut().remove(owner) 163 | } 164 | 165 | println!("Network shrinked to {:?} connected nodes\n", self.network.borrow().size()); 166 | } 167 | 168 | fn on_error(&mut self, err: ws::Error) { 169 | println!("The server encountered an error: {:?}", err); 170 | } 171 | } 172 | 173 | 174 | pub fn run() { 175 | // Setup logging 176 | env_logger::init(); 177 | 178 | // setup command line arguments 179 | let mut app = clap::App::new("Rustysignal") 180 | .version("2.0.0") 181 | .author("Rasmus Viitanen ") 182 | .about("A signaling server implemented in Rust that can be used for e.g. WebRTC, see https://github.com/rasviitanen/rustysignal") 183 | .arg( 184 | clap::Arg::with_name("ADDR") 185 | .help("Address on which to bind the server e.g. 127.0.0.1:3012") 186 | .required(true) 187 | ); 188 | 189 | if cfg!(feature = "ssl") { 190 | app = app 191 | .arg( 192 | clap::Arg::with_name("CERT") 193 | .help("Path to the SSL certificate.") 194 | .required(true) 195 | ) 196 | .arg( 197 | clap::Arg::with_name("KEY") 198 | .help("Path to the SSL certificate key.") 199 | .required(true) 200 | ); 201 | } 202 | 203 | if cfg!(feature = "push") { 204 | app = app.arg( 205 | clap::Arg::with_name("VAPIDKEY") 206 | .help("A NIST P256 EC private key to create a VAPID signature, used for push") 207 | .required(true) 208 | ); 209 | } 210 | 211 | let matches = app.get_matches(); 212 | 213 | #[cfg(feature = "ssl")] 214 | let acceptor = Rc::new({ 215 | println!("Building acceptor"); 216 | let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); 217 | builder.set_private_key_file(matches.value_of("KEY").unwrap(), SslFiletype::PEM).unwrap(); 218 | builder.set_certificate_chain_file(matches.value_of("CERT").unwrap()).unwrap(); 219 | 220 | builder.build() 221 | }); 222 | 223 | println!("------------------------------------"); 224 | #[cfg(not(feature = "ssl"))] 225 | { 226 | println!("rustysignal is listening on address"); 227 | println!("ws://{}", matches.value_of("ADDR").unwrap()); 228 | println!("To use SSL you need to reinstall rustysignal using 'cargo install rustysignal --features ssl --force"); 229 | #[cfg(not(feature = "push"))] 230 | { 231 | println!("To enable push notifications, you need to reinstall rustysignal using 'cargo install rustysignal --features push --force"); 232 | println!("For both, please reinstall using 'cargo install rustysignal --features 'ssl push' --force"); 233 | } 234 | } 235 | 236 | #[cfg(feature = "ssl")] 237 | { 238 | println!("rustysignal is listening on securily on address"); 239 | println!("wss://{}", matches.value_of("ADDR").unwrap()); 240 | println!("To disable SSL you need to reinstall rustysignal using 'cargo install rustysignal --force"); 241 | #[cfg(not(feature = "push"))] 242 | println!("To enable push notifications, you need to reinstall rustysignal using 'cargo install rustysignal --features 'ssl push' --force"); 243 | } 244 | println!("-------------------------------------"); 245 | 246 | let network = Rc::new(RefCell::new(Network::default())); 247 | 248 | #[cfg(feature = "push")] 249 | network.borrow_mut().set_vapid_path(matches.value_of("VAPIDKEY").unwrap()); 250 | 251 | 252 | #[cfg(feature = "ssl")] 253 | let encrypt_server = true; 254 | #[cfg(not(feature = "ssl"))] 255 | let encrypt_server = false; 256 | 257 | ws::Builder::new() 258 | .with_settings(ws::Settings { 259 | encrypt_server: encrypt_server, 260 | ..ws::Settings::default() 261 | }) 262 | .build(|sender: ws::Sender| { 263 | println!("Building server"); 264 | let node = Node::new(sender); 265 | Server { 266 | node: Rc::new(RefCell::new(node)), 267 | #[cfg(feature = "ssl")] 268 | ssl: acceptor.clone(), 269 | network: network.clone() 270 | } 271 | }) 272 | .unwrap().listen(matches.value_of("ADDR").unwrap()) 273 | .unwrap(); 274 | } 275 | --------------------------------------------------------------------------------