├── rustfmt.toml ├── .gitignore ├── examples ├── mcast_search.rs ├── debug_ssdp.rs └── async_notify.rs ├── appveyor.yml ├── Cargo.toml ├── .travis.yml ├── src ├── lib.rs ├── message │ ├── multicast.rs │ ├── notify.rs │ ├── listen.rs │ ├── mod.rs │ ├── search.rs │ └── ssdp.rs ├── net │ ├── packet.rs │ ├── connector.rs │ ├── sender.rs │ └── mod.rs ├── header │ ├── man.rs │ ├── securelocation.rs │ ├── searchport.rs │ ├── st.rs │ ├── bootid.rs │ ├── configid.rs │ ├── nts.rs │ ├── mx.rs │ ├── mod.rs │ ├── nt.rs │ └── usn.rs ├── error.rs ├── receiver.rs └── field.rs ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 110 2 | ideal_width = 100 3 | fn_call_width = 100 4 | format_strings = false 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | .idea 7 | 8 | # Executables 9 | *.exe 10 | 11 | # Generated by Cargo 12 | /target/ 13 | Cargo.lock 14 | 15 | # Test Files 16 | /spike/ 17 | 18 | # Temporary 19 | /design/ 20 | 21 | # IDE - vscode 22 | .vscode 23 | -------------------------------------------------------------------------------- /examples/mcast_search.rs: -------------------------------------------------------------------------------- 1 | extern crate ssdp; 2 | 3 | use ssdp::header::{HeaderMut, Man, MX, ST}; 4 | use ssdp::message::{SearchRequest, Multicast}; 5 | 6 | fn main() { 7 | // Create Our Search Request 8 | let mut request = SearchRequest::new(); 9 | 10 | // Set Our Desired Headers (Not Verified By The Library) 11 | request.set(Man); 12 | request.set(MX(5)); 13 | request.set(ST::All); 14 | 15 | // Iterate Over Streaming Responses 16 | for (msg, src) in request.multicast().unwrap() { 17 | println!("Received The Following Message From {}:\n{:?}\n\n", src, msg); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - TARGET: i686-pc-windows-gnu 4 | OPENSSL: C:\OpenSSL-Win32 5 | 6 | install: 7 | - SET OPENSSL_INCLUDE_DIR=%OPENSSL%\include 8 | - SET OPENSSL_LIB_DIR=%OPENSSL%\bin 9 | - CP %OPENSSL%\bin\ssleay32.dll %OPENSSL%\bin\libssl32.dll 10 | - ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" 11 | - rust-nightly-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust" 12 | - SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin 13 | - SET PATH=%PATH%;C:\MinGW\bin 14 | - rustc -V 15 | - cargo -V 16 | 17 | build: false 18 | 19 | test_script: 20 | - cargo build 21 | - cargo test --verbose -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssdp" 3 | version = "0.7.0" 4 | authors = ["GGist ", "Ignacio Corderi "] 5 | description = "An asynchronous abstraction for discovering devices and services on a network." 6 | documentation = "http://ggist.github.io/ssdp-rs/index.html" 7 | homepage = "https://github.com/GGist/ssdp-rs" 8 | keywords = ["upnp", "ssdp", "simple", "service", "discovery"] 9 | license = "MIT/Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/GGist/ssdp-rs" 12 | 13 | [dependencies] 14 | log = "0.3" 15 | net2 = "0.2.23" 16 | time = "0.1" 17 | error-chain = "0.10" 18 | get_if_addrs = "0.5.3" 19 | 20 | [dependencies.hyper] 21 | default-features = false 22 | version = "0.10.4" 23 | 24 | [features] 25 | unstable = [] 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: nightly 3 | 4 | sudo: false 5 | 6 | addons: 7 | apt: 8 | packages: 9 | - libcurl4-openssl-dev 10 | - libelf-dev 11 | - libdw-dev 12 | 13 | env: 14 | global: 15 | - secure: lVC2BUp6+ayYz5bysex1SQi1kMK95b6pVRK4HH0ku6IRUPLeecRrZwiQnkNBAEVkd/GAWtq0wCR4khH+VuUcqY9rQzUEs0JD6OY2AdBs4oVDU1CJmuzb5NTDIlvYuQ+kFj12mwi+7Xg/AA8YhdAazzlq/aCu0KK0i06nPqfqe2c= 16 | 17 | branches: 18 | only: 19 | - master 20 | 21 | before_script: 22 | - | 23 | pip install 'travis-cargo<0.2' --user && 24 | export PATH=$HOME/.local/bin:$PATH 25 | 26 | script: 27 | - travis-cargo build 28 | - travis-cargo test 29 | - travis-cargo doc 30 | 31 | after_success: 32 | - travis-cargo doc-upload 33 | - travis-cargo coveralls --no-sudo --verify 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_features)] 2 | #![feature(ip)] 3 | #![recursion_limit = "1024"] 4 | 5 | //! An asynchronous abstraction for discovering devices and services on a network. 6 | //! 7 | //! SSDP stands for Simple Service Discovery Protocol and it is a protocol that uses 8 | //! HTTPMU to distribute messages across a local network for devices and services to 9 | //! discover each other. SSDP can most commonly be found in devices that implement 10 | //! `UPnP` as it is used as the discovery mechanism for that standard. 11 | 12 | extern crate hyper; 13 | #[macro_use] 14 | extern crate log; 15 | extern crate time; 16 | extern crate get_if_addrs; 17 | extern crate net2; 18 | #[macro_use] 19 | extern crate error_chain; 20 | 21 | mod error; 22 | mod field; 23 | mod net; 24 | mod receiver; 25 | 26 | pub mod header; 27 | pub mod message; 28 | 29 | pub use error::{SSDPError, SSDPErrorKind, SSDPResultExt, SSDPResult}; 30 | pub use field::FieldMap; 31 | pub use receiver::{SSDPReceiver, SSDPIter}; 32 | pub use net::IpVersionMode; 33 | -------------------------------------------------------------------------------- /examples/debug_ssdp.rs: -------------------------------------------------------------------------------- 1 | extern crate log; 2 | extern crate ssdp; 3 | 4 | use log::{Log, LogRecord, LogLevelFilter, LogMetadata}; 5 | 6 | use ssdp::header::{HeaderMut, Man, MX, ST}; 7 | use ssdp::message::{SearchRequest, Multicast}; 8 | 9 | struct SimpleLogger; 10 | 11 | impl Log for SimpleLogger { 12 | fn enabled(&self, _: &LogMetadata) -> bool { 13 | true 14 | } 15 | 16 | fn log(&self, record: &LogRecord) { 17 | if self.enabled(record.metadata()) { 18 | println!("{} - {}", record.level(), record.args()); 19 | } 20 | } 21 | } 22 | 23 | fn main() { 24 | log::set_logger(|max_level| { 25 | max_level.set(LogLevelFilter::Debug); 26 | Box::new(SimpleLogger) 27 | }) 28 | .unwrap(); 29 | 30 | // Create Our Search Request 31 | let mut request = SearchRequest::new(); 32 | 33 | // Set Our Desired Headers (Not Verified By The Library) 34 | request.set(Man); 35 | request.set(MX(5)); 36 | request.set(ST::All); 37 | 38 | // Collect Our Responses 39 | request.multicast().unwrap().into_iter().collect::>(); 40 | } 41 | -------------------------------------------------------------------------------- /examples/async_notify.rs: -------------------------------------------------------------------------------- 1 | extern crate ssdp; 2 | 3 | use std::io::{self, Read}; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | use ssdp::FieldMap; 8 | use ssdp::header::{HeaderMut, NT, NTS, USN}; 9 | use ssdp::message::{NotifyListener, NotifyMessage, Listen, Multicast}; 10 | 11 | fn main() { 12 | thread::spawn(|| { 13 | for (msg, src) in NotifyListener::listen().unwrap() { 14 | println!("Received The Following Message From {}:\n{:?}\n", src, msg); 15 | } 16 | }); 17 | 18 | // Make Sure Thread Has Started 19 | thread::sleep(Duration::new(1, 0)); 20 | 21 | // Create A Test Message 22 | let mut message = NotifyMessage::new(); 23 | 24 | // Set Some Headers 25 | message.set(NTS::ByeBye); 26 | message.set(NT(FieldMap::upnp("rootdevice"))); 27 | message.set(USN(FieldMap::uuid("Hello, This Is Not A UUID!!!"), None)); 28 | 29 | message.multicast().unwrap(); 30 | 31 | // Wait Until User Is Done Listening For Notify Messages 32 | println!("Press Enter When You Wish To Exit...\n"); 33 | let input = io::stdin(); 34 | 35 | input.bytes().next(); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 ssdp-rs Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ssdp-rs 2 | ======= 3 | [![Build Status](https://travis-ci.org/GGist/ssdp-rs.svg?branch=master)](https://travis-ci.org/GGist/ssdp-rs) [![Build status](https://ci.appveyor.com/api/projects/status/aoupr0fsxl28a35q?svg=true)](https://ci.appveyor.com/project/GGist/ssdp-rs) [![Documentation](https://docs.rs/ssdp/badge.svg)](https://docs.rs/ssdp) [![Crate](http://meritbadge.herokuapp.com/ssdp)](https://crates.io/crates/ssdp) [![Coverage Status](https://coveralls.io/repos/GGist/ssdp-rs/badge.svg?branch=master)](https://coveralls.io/r/GGist/ssdp-rs?branch=master) 4 | 5 | An SSDP library that provides messaging and streaming primitives. 6 | 7 | **Informative Links:** 8 | * ~~https://tools.ietf.org/html/draft-cai-ssdp-v1-03~~ EXPIRED 9 | * http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v2.0.pdf 10 | 11 | License 12 | ------- 13 | 14 | Licensed under either of 15 | 16 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 17 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 18 | 19 | at your option. 20 | 21 | Contribution 22 | ------------ 23 | 24 | Unless you explicitly state otherwise, any contribution intentionally submitted 25 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 26 | additional terms or conditions. 27 | -------------------------------------------------------------------------------- /src/message/multicast.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, SocketAddrV6}; 2 | use std::str::FromStr; 3 | 4 | use error::SSDPResult; 5 | use net::connector::UdpConnector; 6 | use message::{self, Config}; 7 | use message::ssdp::SSDPMessage; 8 | 9 | 10 | pub trait Multicast { 11 | type Item; 12 | 13 | fn multicast(&mut self) -> SSDPResult { 14 | self.multicast_with_config(&Default::default()) 15 | } 16 | 17 | fn multicast_with_config(&self, config: &Config) -> SSDPResult; 18 | } 19 | 20 | pub fn send(message: &SSDPMessage, config: &Config) -> SSDPResult> { 21 | let mut connectors = try!(message::all_local_connectors(Some(config.ttl), &config.mode)); 22 | 23 | for conn in &mut connectors { 24 | match try!(conn.local_addr()) { 25 | SocketAddr::V4(n) => { 26 | let mcast_addr = (config.ipv4_addr.as_str(), config.port); 27 | debug!("Sending ipv4 multicast through {} to {:?}", n, mcast_addr); 28 | try!(message.send(conn, &mcast_addr)); 29 | } 30 | SocketAddr::V6(n) => { 31 | debug!("Sending Ipv6 multicast through {} to {}:{}", n, config.ipv6_addr, config.port); 32 | //try!(message.send(conn, &mcast_addr)); 33 | try!(message.send(conn, 34 | &SocketAddrV6::new(try!(FromStr::from_str(config.ipv6_addr.as_str())), 35 | config.port, 36 | n.flowinfo(), 37 | n.scope_id()))) 38 | } 39 | } 40 | } 41 | 42 | Ok(connectors) 43 | } 44 | -------------------------------------------------------------------------------- /src/net/packet.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Error, ErrorKind}; 2 | use std::net::{UdpSocket, SocketAddr}; 3 | use std::fmt; 4 | 5 | /// Maximum length for packets received on a `PacketReceiver`. 6 | pub const MAX_PCKT_LEN: usize = 1500; 7 | 8 | /// A `PacketReceiver` that abstracts over a network socket and reads full packets 9 | /// from the connection. Packets received from this connection are assumed to 10 | /// be no larger than what the typical MTU would be on a standard router. 11 | /// 12 | /// See `net::packet::MAX_PCKT_LEN`. 13 | pub struct PacketReceiver(UdpSocket); 14 | 15 | impl PacketReceiver { 16 | /// Create a new PacketReceiver from the given UdpSocket. 17 | pub fn new(udp: UdpSocket) -> PacketReceiver { 18 | PacketReceiver(udp) 19 | } 20 | 21 | /// Receive a packet from the underlying connection. 22 | pub fn recv_pckt(&self) -> io::Result<(Vec, SocketAddr)> { 23 | let mut pckt_buf = vec![0u8; MAX_PCKT_LEN]; 24 | 25 | let (size, addr) = try!(self.0.recv_from(&mut pckt_buf)); 26 | 27 | // Check For Something That SHOULD NEVER Occur. 28 | if size > pckt_buf.len() { 29 | Err(Error::new(ErrorKind::Other, "UdpSocket Reported Receive Length Greater Than Buffer")) 30 | } else { 31 | // `truncate` does not reallocate the vec's backing storage 32 | pckt_buf.truncate(size); 33 | 34 | Ok((pckt_buf, addr)) 35 | } 36 | } 37 | } 38 | 39 | impl fmt::Display for PacketReceiver { 40 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 41 | match self.0.local_addr() { 42 | Ok(addr) => write!(f, "{}", addr), 43 | Err(err) => write!(f, "{}", err), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/header/man.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const MAN_HEADER_NAME: &'static str = "MAN"; 7 | const MAN_HEADER_VALUE: &'static str = "\"ssdp:discover\""; 8 | 9 | /// Represents a header used to specify HTTP extension. 10 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 11 | pub struct Man; 12 | 13 | impl Header for Man { 14 | fn header_name() -> &'static str { 15 | MAN_HEADER_NAME 16 | } 17 | 18 | fn parse_header(raw: &[Vec]) -> error::Result { 19 | if raw.len() != 1 { 20 | return Err(Error::Header); 21 | } 22 | 23 | let man_bytes = MAN_HEADER_VALUE.as_bytes(); 24 | match &raw[0][..] { 25 | n if n == man_bytes => Ok(Man), 26 | _ => Err(Error::Header), 27 | } 28 | } 29 | } 30 | 31 | impl HeaderFormat for Man { 32 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 33 | try!(fmt.write_str(MAN_HEADER_VALUE)); 34 | 35 | Ok(()) 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use hyper::header::Header; 42 | 43 | use super::Man; 44 | 45 | #[test] 46 | fn positive_man() { 47 | let man_header = &[b"\"ssdp:discover\""[..].to_vec()]; 48 | 49 | Man::parse_header(man_header).unwrap(); 50 | } 51 | 52 | #[test] 53 | #[should_panic] 54 | fn negative_wrong_case() { 55 | let wrong_case_man_header = &[b"\"SSDP:discover\""[..].to_vec()]; 56 | 57 | Man::parse_header(wrong_case_man_header).unwrap(); 58 | } 59 | 60 | #[test] 61 | #[should_panic] 62 | fn negative_missing_quotes() { 63 | let missing_quotes_man_header = &[b"ssdp:discover"[..].to_vec()]; 64 | 65 | Man::parse_header(missing_quotes_man_header).unwrap(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net; 3 | use hyper; 4 | 5 | /// Enumerates all errors that can occur when dealing with an SSDP message. 6 | error_chain! { 7 | 8 | types { 9 | SSDPError, SSDPErrorKind, SSDPResultExt, SSDPResult; 10 | } 11 | 12 | errors { 13 | /// Message is not valid HTTP. 14 | /// 15 | /// Message is supplied as a list of bytes. 16 | InvalidHttp(message:Vec) { 17 | description("invalid HTTP") 18 | display("invalid HTTP message: '{:?}'", message) 19 | } 20 | /// Message did not specify HTTP/1.1 as version. 21 | InvalidHttpVersion { } 22 | /// Message consists of an error code. 23 | /// 24 | /// Error code is supplied. 25 | ResponseCode(code:u16) { 26 | description("HTTP Error response") 27 | display("HTTP Error response: {}", code) 28 | } 29 | /// Method supplied is not a valid SSDP method. 30 | /// 31 | /// Method received is supplied. 32 | InvalidMethod(method:String) { 33 | description("invalid SSDP method") 34 | display("invalid SSDP method: '{}'", method) 35 | } 36 | /// Uri supplied is not a valid SSDP uri. 37 | /// 38 | /// URI received is supplied. 39 | InvalidUri(uri:String) { 40 | description("invalid URI") 41 | display("invalid URI: '{}'", uri) 42 | } 43 | /// Header is missing from the message. 44 | /// 45 | /// Expected header is supplied. 46 | MissingHeader(header:&'static str) { 47 | description("missing header") 48 | display("missing header: '{}'", header) 49 | } 50 | /// Header has an invalid value. 51 | /// 52 | /// Header name with error message are supplied. 53 | InvalidHeader(header:&'static str, msg:&'static str) { 54 | description("invalid header") 55 | display("invalid header: '{}': {}", header, msg) 56 | } 57 | } 58 | 59 | foreign_links { 60 | Io(io::Error); 61 | AddrParseError(net::AddrParseError); 62 | Hyper(hyper::Error); 63 | HyperParseError(hyper::error::ParseError); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/header/securelocation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const SECURELOCATION_HEADER_NAME: &'static str = "SECURELOCATION.UPNP.ORG"; 7 | 8 | /// Represents a header used to specify a secure url for a device's DDD. 9 | /// 10 | /// Can be used instead of the `Location` header field. 11 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 12 | pub struct SecureLocation(pub String); 13 | 14 | impl Header for SecureLocation { 15 | fn header_name() -> &'static str { 16 | SECURELOCATION_HEADER_NAME 17 | } 18 | 19 | fn parse_header(raw: &[Vec]) -> error::Result { 20 | if raw.len() != 1 || raw[0].is_empty() { 21 | return Err(Error::Header); 22 | } 23 | 24 | let owned_bytes = raw[0].clone(); 25 | 26 | match String::from_utf8(owned_bytes) { 27 | Ok(n) => Ok(SecureLocation(n)), 28 | Err(_) => Err(Error::Header), 29 | } 30 | } 31 | } 32 | 33 | impl HeaderFormat for SecureLocation { 34 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 35 | try!(fmt.write_str(&self.0)); 36 | 37 | Ok(()) 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use hyper::header::Header; 44 | 45 | use super::SecureLocation; 46 | 47 | #[test] 48 | fn positive_securelocation() { 49 | let securelocation_header_value = &[b"https://192.168.1.1/"[..].to_vec()]; 50 | 51 | SecureLocation::parse_header(securelocation_header_value).unwrap(); 52 | } 53 | 54 | #[test] 55 | fn positive_invalid_url() { 56 | let securelocation_header_value = &[b"just some text"[..].to_vec()]; 57 | 58 | SecureLocation::parse_header(securelocation_header_value).unwrap(); 59 | } 60 | 61 | #[test] 62 | #[should_panic] 63 | fn negative_empty() { 64 | let securelocation_header_value = &[b""[..].to_vec()]; 65 | 66 | SecureLocation::parse_header(securelocation_header_value).unwrap(); 67 | } 68 | 69 | #[test] 70 | #[should_panic] 71 | fn negative_invalid_utf8() { 72 | let securelocation_header_value = &[b"https://192.168.1.1/\x80"[..].to_vec()]; 73 | 74 | SecureLocation::parse_header(securelocation_header_value).unwrap(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/header/searchport.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const SEARCHPORT_HEADER_NAME: &'static str = "SEARCHPORT.UPNP.ORG"; 7 | 8 | pub const SEARCHPORT_MIN_VALUE: u16 = 49152; 9 | 10 | /// Represents a header used to specify a unicast port to send search requests to. 11 | /// 12 | /// If a `SearchPort` header is not included in a message then the device must 13 | /// respond to unicast search requests on the standard port of 1900. 14 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 15 | pub struct SearchPort(pub u16); 16 | 17 | impl Header for SearchPort { 18 | fn header_name() -> &'static str { 19 | SEARCHPORT_HEADER_NAME 20 | } 21 | 22 | fn parse_header(raw: &[Vec]) -> error::Result { 23 | if raw.len() != 1 { 24 | return Err(Error::Header); 25 | } 26 | 27 | let cow_str = String::from_utf8_lossy(&raw[0][..]); 28 | 29 | let value = match u16::from_str_radix(&*cow_str, 10) { 30 | Ok(n) => n, 31 | Err(_) => return Err(Error::Header), 32 | }; 33 | 34 | if value >= SEARCHPORT_MIN_VALUE { 35 | Ok(SearchPort(value)) 36 | } else { 37 | Err(Error::Header) 38 | } 39 | } 40 | } 41 | 42 | impl HeaderFormat for SearchPort { 43 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 44 | try!(fmt.write_fmt(format_args!("{}", self.0))); 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use hyper::header::Header; 53 | 54 | use super::SearchPort; 55 | 56 | #[test] 57 | fn positive_searchport() { 58 | let searchport_header_value = &[b"50000"[..].to_vec()]; 59 | 60 | SearchPort::parse_header(searchport_header_value).unwrap(); 61 | } 62 | 63 | #[test] 64 | fn positive_lower_bound() { 65 | let searchport_header_value = &[b"49152"[..].to_vec()]; 66 | 67 | SearchPort::parse_header(searchport_header_value).unwrap(); 68 | } 69 | 70 | #[test] 71 | fn positive_upper_bound() { 72 | let searchport_header_value = &[b"65535"[..].to_vec()]; 73 | 74 | SearchPort::parse_header(searchport_header_value).unwrap(); 75 | } 76 | 77 | #[test] 78 | #[should_panic] 79 | fn negative_reserved() { 80 | let searchport_header_value = &[b"49151"[..].to_vec()]; 81 | 82 | SearchPort::parse_header(searchport_header_value).unwrap(); 83 | } 84 | 85 | #[test] 86 | #[should_panic] 87 | fn negative_nan() { 88 | let searchport_header_value = &[b"49151a"[..].to_vec()]; 89 | 90 | SearchPort::parse_header(searchport_header_value).unwrap(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/net/connector.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::{UdpSocket, ToSocketAddrs, SocketAddr, SocketAddrV4, SocketAddrV6}; 3 | use std::str::FromStr; 4 | 5 | use hyper::error; 6 | use hyper::net::NetworkConnector; 7 | 8 | use net::sender::UdpSender; 9 | use net; 10 | 11 | /// A `UdpConnector` allows Hyper to obtain `NetworkStream` objects over `UdpSockets` 12 | /// so that Http messages created by Hyper can be sent over UDP instead of TCP. 13 | pub struct UdpConnector(UdpSocket); 14 | 15 | impl UdpConnector { 16 | /// Create a new UdpConnector that will be bound to the given local address. 17 | pub fn new(local_addr: A, _: Option) -> io::Result { 18 | let addr = try!(net::addr_from_trait(local_addr)); 19 | debug!("Attempting to connect to {}", addr); 20 | 21 | let udp = try!(UdpSocket::bind(addr)); 22 | 23 | // TODO: This throws an invalid argument error 24 | // if let Some(n) = multicast_ttl { 25 | // trace!("Setting ttl to {}", n); 26 | // try!(udp.set_multicast_ttl_v4(n)); 27 | // } 28 | 29 | Ok(UdpConnector(udp)) 30 | } 31 | 32 | pub fn local_addr(&self) -> io::Result { 33 | self.0.local_addr() 34 | } 35 | 36 | /// Destroy the UdpConnector and return the underlying UdpSocket. 37 | pub fn deconstruct(self) -> UdpSocket { 38 | self.0 39 | } 40 | } 41 | 42 | impl NetworkConnector for UdpConnector { 43 | type Stream = UdpSender; 44 | 45 | fn connect(&self, host: &str, port: u16, _: &str) -> error::Result<::Stream> { 46 | let udp_sock = try!(self.0.try_clone()); 47 | let sock_addr = match try!(self.local_addr()) { 48 | SocketAddr::V4(_) => { 49 | SocketAddr::V4(SocketAddrV4::new(try!(FromStr::from_str(host).map_err(|err| { 50 | io::Error::new(io::ErrorKind::InvalidInput, err) 51 | })), 52 | port)) 53 | } 54 | SocketAddr::V6(n) => { 55 | let mut addr: SocketAddrV6 = if host.find('[') == Some(0) && 56 | host.rfind(']') == Some(host.len() - 1) { 57 | try!(FromStr::from_str(format!("{}:{}", host, port).as_str()) 58 | .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))) 59 | } else { 60 | try!(FromStr::from_str(format!("[{}]:{}", host, port).as_str()) 61 | .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))) 62 | }; 63 | addr.set_flowinfo(n.flowinfo()); 64 | addr.set_scope_id(n.scope_id()); 65 | SocketAddr::V6(addr) 66 | } 67 | }; 68 | 69 | Ok(UdpSender::new(udp_sock, sock_addr)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/net/sender.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, ErrorKind, Read, Write}; 2 | use std::net::{UdpSocket, SocketAddr}; 3 | use std::time::Duration; 4 | use hyper::net::NetworkStream; 5 | 6 | /// A type that wraps a `UdpSocket` and a `SocketAddr` and implements the `NetworkStream` 7 | /// trait. 8 | /// 9 | /// Note that reading from this stream will generate an error, this object is 10 | /// used for intercepting Http messages from Hyper and sending them out via Udp. 11 | /// The response(s) from client(s) are to be handled by some other object that 12 | /// has a cloned handle to our internal `UdpSocket` handle. 13 | pub struct UdpSender { 14 | udp: UdpSocket, 15 | dst: SocketAddr, 16 | buf: Vec, 17 | } 18 | 19 | impl UdpSender { 20 | /// Creates a new UdpSender object. 21 | pub fn new(udp: UdpSocket, dst: SocketAddr) -> UdpSender { 22 | UdpSender { 23 | udp: udp, 24 | dst: dst, 25 | buf: Vec::new(), 26 | } 27 | } 28 | } 29 | 30 | impl NetworkStream for UdpSender { 31 | fn peer_addr(&mut self) -> io::Result { 32 | Ok(self.dst) 33 | } 34 | fn set_read_timeout(&self, _dur: Option) -> io::Result<()> { 35 | Ok(()) 36 | } 37 | fn set_write_timeout(&self, _dur: Option) -> io::Result<()> { 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl Read for UdpSender { 43 | fn read(&mut self, _: &mut [u8]) -> io::Result { 44 | // Simulate Some Network Error So Our Process Doesnt Hang 45 | Err(io::Error::new(ErrorKind::ConnectionAborted, "UdpSender Can Not Be Read From")) 46 | } 47 | } 48 | 49 | impl Write for UdpSender { 50 | fn write(&mut self, buf: &[u8]) -> io::Result { 51 | // Hyper will generate a request with a /, we need to intercept that. 52 | let mut buffer = vec![0u8; buf.len()]; 53 | 54 | let mut found = false; 55 | for (src, dst) in buf.iter().zip(buffer.iter_mut()) { 56 | if *src == b'/' && !found && buf[0] != b'H' { 57 | *dst = b'*'; 58 | found = true; 59 | } else { 60 | *dst = *src; 61 | } 62 | } 63 | 64 | self.buf.append(&mut buffer); 65 | 66 | Ok(buf.len()) 67 | } 68 | 69 | fn flush(&mut self) -> io::Result<()> { 70 | debug!("Sent HTTP Request:\n{}", String::from_utf8_lossy(&self.buf[..])); 71 | 72 | let result = self.udp.send_to(&self.buf[..], self.dst); 73 | self.buf.clear(); 74 | 75 | result.map(|_| ()) 76 | } 77 | } 78 | 79 | impl Clone for UdpSender { 80 | fn clone(&self) -> UdpSender { 81 | let udp_clone = self.udp.try_clone().unwrap(); 82 | 83 | UdpSender { 84 | udp: udp_clone, 85 | dst: self.dst, 86 | buf: self.buf.clone(), 87 | } 88 | } 89 | 90 | fn clone_from(&mut self, source: &UdpSender) { 91 | let udp_clone = source.udp.try_clone().unwrap(); 92 | 93 | self.udp = udp_clone; 94 | self.dst = source.dst; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/header/st.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Display, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | use FieldMap; 7 | 8 | const ST_HEADER_NAME: &'static str = "ST"; 9 | 10 | const ST_ALL_VALUE: &'static str = "ssdp:all"; 11 | 12 | /// Represents a header which specifies the search target. 13 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 14 | pub enum ST { 15 | All, 16 | Target(FieldMap), 17 | } 18 | 19 | impl Header for ST { 20 | fn header_name() -> &'static str { 21 | ST_HEADER_NAME 22 | } 23 | 24 | fn parse_header(raw: &[Vec]) -> error::Result { 25 | if raw.len() != 1 { 26 | return Err(Error::Header); 27 | } 28 | 29 | if &raw[0][..] == ST_ALL_VALUE.as_bytes() { 30 | Ok(ST::All) 31 | } else { 32 | FieldMap::parse_bytes(&raw[0][..]).map(ST::Target).ok_or(Error::Header) 33 | } 34 | } 35 | } 36 | 37 | impl HeaderFormat for ST { 38 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 39 | match *self { 40 | ST::All => try!(fmt.write_str(ST_ALL_VALUE)), 41 | ST::Target(ref n) => try!(Display::fmt(n, fmt)), 42 | }; 43 | 44 | Ok(()) 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use hyper::header::Header; 51 | 52 | use FieldMap; 53 | use super::ST; 54 | 55 | #[test] 56 | fn positive_all() { 57 | let st_all_header = &[b"ssdp:all"[..].to_vec()]; 58 | 59 | match ST::parse_header(st_all_header) { 60 | Ok(ST::All) => (), 61 | _ => panic!("Failed To Match ST::All Header"), 62 | } 63 | } 64 | 65 | #[test] 66 | fn positive_field_upnp() { 67 | let st_upnp_root_header = &[b"upnp:some_identifier"[..].to_vec()]; 68 | 69 | match ST::parse_header(st_upnp_root_header) { 70 | Ok(ST::Target(FieldMap::UPnP(_))) => (), 71 | _ => panic!("Failed To Match ST::Target Header To FieldMap::UPnP"), 72 | } 73 | } 74 | 75 | #[test] 76 | fn positive_field_urn() { 77 | let st_urn_root_header = &[b"urn:some_identifier"[..].to_vec()]; 78 | 79 | match ST::parse_header(st_urn_root_header) { 80 | Ok(ST::Target(FieldMap::URN(_))) => (), 81 | _ => panic!("Failed To Match ST::Target Header To FieldMap::URN"), 82 | } 83 | } 84 | 85 | #[test] 86 | fn positive_field_uuid() { 87 | let st_uuid_root_header = &[b"uuid:some_identifier"[..].to_vec()]; 88 | 89 | match ST::parse_header(st_uuid_root_header) { 90 | Ok(ST::Target(FieldMap::UUID(_))) => (), 91 | _ => panic!("Failed To Match ST::Target Header To FieldMap::UUID"), 92 | } 93 | } 94 | 95 | #[test] 96 | #[should_panic] 97 | fn negative_multiple_headers() { 98 | let st_multiple_headers = &[b"uuid:some_identifier"[..].to_vec(), b"ssdp:all"[..].to_vec()]; 99 | 100 | ST::parse_header(st_multiple_headers).unwrap(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/header/bootid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const BOOTID_HEADER_NAME: &'static str = "BOOTID.UPNP.ORG"; 7 | 8 | /// Represents a header used to denote the boot instance of a root device. 9 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 10 | pub struct BootID(pub u32); 11 | 12 | impl Header for BootID { 13 | fn header_name() -> &'static str { 14 | BOOTID_HEADER_NAME 15 | } 16 | 17 | fn parse_header(raw: &[Vec]) -> error::Result { 18 | if raw.len() != 1 { 19 | return Err(Error::Header); 20 | } 21 | 22 | let cow_str = String::from_utf8_lossy(&raw[0][..]); 23 | 24 | // Value needs to be a 31 bit non-negative integer, so convert to i32 25 | let value = match i32::from_str_radix(&*cow_str, 10) { 26 | Ok(n) => n, 27 | Err(_) => return Err(Error::Header), 28 | }; 29 | 30 | // Check if value is negative, then convert to u32 31 | if value.is_negative() { 32 | Err(Error::Header) 33 | } else { 34 | Ok(BootID(value as u32)) 35 | } 36 | } 37 | } 38 | 39 | impl HeaderFormat for BootID { 40 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 41 | try!(fmt.write_fmt(format_args!("{}", self.0))); 42 | 43 | Ok(()) 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use hyper::header::Header; 50 | 51 | use super::BootID; 52 | 53 | #[test] 54 | fn positive_bootid() { 55 | let bootid_header_value = &[b"1216907400"[..].to_vec()]; 56 | 57 | BootID::parse_header(bootid_header_value).unwrap(); 58 | } 59 | 60 | #[test] 61 | fn positive_leading_zeros() { 62 | let bootid_header_value = &[b"0000001216907400"[..].to_vec()]; 63 | 64 | BootID::parse_header(bootid_header_value).unwrap(); 65 | } 66 | 67 | #[test] 68 | fn positive_lower_bound() { 69 | let bootid_header_value = &[b"0"[..].to_vec()]; 70 | 71 | BootID::parse_header(bootid_header_value).unwrap(); 72 | } 73 | 74 | #[test] 75 | fn positive_upper_bound() { 76 | let bootid_header_value = &[b"2147483647"[..].to_vec()]; 77 | 78 | BootID::parse_header(bootid_header_value).unwrap(); 79 | } 80 | 81 | #[test] 82 | fn positive_negative_zero() { 83 | let bootid_header_value = &[b"-0"[..].to_vec()]; 84 | 85 | BootID::parse_header(bootid_header_value).unwrap(); 86 | } 87 | 88 | #[test] 89 | #[should_panic] 90 | fn negative_overflow() { 91 | let bootid_header_value = &[b"2290649224"[..].to_vec()]; 92 | 93 | BootID::parse_header(bootid_header_value).unwrap(); 94 | } 95 | 96 | #[test] 97 | #[should_panic] 98 | fn negative_negative_overflow() { 99 | let bootid_header_value = &[b"-2290649224"[..].to_vec()]; 100 | 101 | BootID::parse_header(bootid_header_value).unwrap(); 102 | } 103 | 104 | #[test] 105 | #[should_panic] 106 | fn negative_nan() { 107 | let bootid_header_value = &[b"2290wow649224"[..].to_vec()]; 108 | 109 | BootID::parse_header(bootid_header_value).unwrap(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/message/notify.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::Debug; 3 | 4 | use hyper::header::{Header, HeaderFormat}; 5 | 6 | use error::SSDPResult; 7 | use header::{HeaderRef, HeaderMut}; 8 | use message::{MessageType, Listen, Config}; 9 | use message::ssdp::SSDPMessage; 10 | use message::multicast::{self, Multicast}; 11 | use receiver::FromRawSSDP; 12 | 13 | 14 | /// Notify message that can be sent via multicast to devices on the network. 15 | #[derive(Debug, Clone)] 16 | pub struct NotifyMessage { 17 | message: SSDPMessage, 18 | } 19 | 20 | impl NotifyMessage { 21 | /// Construct a new NotifyMessage. 22 | pub fn new() -> Self { 23 | NotifyMessage { message: SSDPMessage::new(MessageType::Notify) } 24 | } 25 | } 26 | 27 | impl Multicast for NotifyMessage { 28 | type Item = (); 29 | 30 | fn multicast_with_config(&self, config: &Config) -> SSDPResult { 31 | multicast::send(&self.message, config)?; 32 | Ok(()) 33 | } 34 | } 35 | 36 | impl Default for NotifyMessage { 37 | fn default() -> Self { 38 | NotifyMessage::new() 39 | } 40 | } 41 | 42 | impl FromRawSSDP for NotifyMessage { 43 | fn raw_ssdp(bytes: &[u8]) -> SSDPResult { 44 | let message = try!(SSDPMessage::raw_ssdp(bytes)); 45 | 46 | if message.message_type() != MessageType::Notify { 47 | try!(Err("SSDP Message Received Is Not A NotifyMessage")) 48 | } else { 49 | Ok(NotifyMessage { message: message }) 50 | } 51 | } 52 | } 53 | 54 | impl HeaderRef for NotifyMessage { 55 | fn get(&self) -> Option<&H> 56 | where H: Header + HeaderFormat 57 | { 58 | self.message.get::() 59 | } 60 | 61 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 62 | self.message.get_raw(name) 63 | } 64 | } 65 | 66 | impl HeaderMut for NotifyMessage { 67 | fn set(&mut self, value: H) 68 | where H: Header + HeaderFormat 69 | { 70 | self.message.set(value) 71 | } 72 | 73 | fn set_raw(&mut self, name: K, value: Vec>) 74 | where K: Into> + Debug 75 | { 76 | self.message.set_raw(name, value) 77 | } 78 | } 79 | 80 | /// Notify listener that can listen to notify messages sent within the network. 81 | pub struct NotifyListener; 82 | 83 | impl Listen for NotifyListener { 84 | type Message = NotifyMessage; 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::NotifyMessage; 90 | use receiver::FromRawSSDP; 91 | 92 | #[test] 93 | fn positive_notify_message_type() { 94 | let raw_message = "NOTIFY * HTTP/1.1\r\nHOST: 192.168.1.1\r\n\r\n"; 95 | 96 | NotifyMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 97 | } 98 | 99 | #[test] 100 | #[should_panic] 101 | fn negative_search_message_type() { 102 | let raw_message = "M-SEARCH * HTTP/1.1\r\nHOST: 192.168.1.1\r\n\r\n"; 103 | 104 | NotifyMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 105 | } 106 | 107 | #[test] 108 | #[should_panic] 109 | fn negative_response_message_type() { 110 | let raw_message = "HTTP/1.1 200 OK\r\n\r\n"; 111 | 112 | NotifyMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/message/listen.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, IpAddr}; 2 | 3 | use error::SSDPResult; 4 | use message::{self, Config}; 5 | use receiver::{SSDPReceiver, FromRawSSDP}; 6 | use net; 7 | 8 | 9 | pub trait Listen { 10 | type Message: FromRawSSDP + Send + 'static; 11 | 12 | /// Listen for messages on all local network interfaces. 13 | /// 14 | /// This will call `listen_with_config()` with _default_ values. 15 | fn listen() -> SSDPResult> { 16 | Self::listen_with_config(&Default::default()) 17 | } 18 | 19 | /// Listen for messages on all local network interfaces. 20 | /// 21 | /// # Notes 22 | /// This will _bind_ to each interface, **NOT** to `INADDR_ANY`. 23 | /// 24 | /// If you are on an environment where the network interface will be changing, 25 | /// you will have to stop listening and start listening again, 26 | /// or we recommend using `listen_anyaddr_with_config()` instead. 27 | fn listen_with_config(config: &Config) -> SSDPResult> { 28 | let mut ipv4_sock = None; 29 | let mut ipv6_sock = None; 30 | 31 | // Generate a list of reused sockets on the standard multicast address. 32 | let addrs: Vec = try!(message::map_local(|&addr| Ok(Some(addr)))); 33 | 34 | for addr in addrs { 35 | match addr { 36 | SocketAddr::V4(_) => { 37 | let mcast_ip = config.ipv4_addr.parse().unwrap(); 38 | 39 | if ipv4_sock.is_none() { 40 | ipv4_sock = Some(try!(net::bind_reuse(("0.0.0.0", config.port)))); 41 | } 42 | 43 | let ref sock = ipv4_sock.as_ref().unwrap(); 44 | 45 | debug!("Joining ipv4 multicast {} at iface: {}", mcast_ip, addr); 46 | try!(net::join_multicast(&sock, &addr, &mcast_ip)); 47 | } 48 | SocketAddr::V6(_) => { 49 | let mcast_ip = config.ipv6_addr.parse().unwrap(); 50 | 51 | if ipv6_sock.is_none() { 52 | ipv6_sock = Some(try!(net::bind_reuse(("::", config.port)))); 53 | } 54 | 55 | let ref sock = ipv6_sock.as_ref().unwrap(); 56 | 57 | debug!("Joining ipv6 multicast {} at iface: {}", mcast_ip, addr); 58 | try!(net::join_multicast(&sock, &addr, &IpAddr::V6(mcast_ip))); 59 | } 60 | } 61 | } 62 | 63 | let sockets = vec![ipv4_sock, ipv6_sock] 64 | .into_iter() 65 | .flat_map(|opt_interface| opt_interface) 66 | .collect(); 67 | 68 | Ok(try!(SSDPReceiver::new(sockets, None))) 69 | } 70 | 71 | /// Listen on any interface 72 | /// 73 | /// # Important 74 | /// 75 | /// This version of the `listen`()` will _bind_ to `INADDR_ANY` instead of binding to each interface 76 | #[cfg(linux)] 77 | fn listen_anyaddr_with_config(config: &Config) -> SSDPResult> { 78 | // Ipv4 79 | let mcast_ip = config.ipv4_address.parse().unwrap(); 80 | let ipv4_sock = try!(net::bind_reuse(("0.0.0.0", config.port))); 81 | try!(ipv4_sock.join_multicast_v4(&mcast_ip, &"0.0.0.0".parse().unwrap())); 82 | 83 | // Ipv6 84 | let mcast_ip = config.ipv6_address.parse().unwrap(); 85 | let ipv6_sock = try!(net::bind_reuse(("::", config.port))); 86 | try!(ipv6_sock.join_multicast_v6(&mcast_ip, 0)); 87 | 88 | let sockets = vec![ipv4_sock, ipv6_sock]; 89 | Ok(try!(SSDPReceiver::new(sockets, None))) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/header/configid.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const CONFIGID_HEADER_NAME: &'static str = "CONFIGID.UPNP.ORG"; 7 | 8 | /// Represents a header used to denote the configuration of a device's DDD. 9 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 10 | pub struct ConfigID(pub u32); 11 | 12 | impl Header for ConfigID { 13 | fn header_name() -> &'static str { 14 | CONFIGID_HEADER_NAME 15 | } 16 | 17 | fn parse_header(raw: &[Vec]) -> error::Result { 18 | if raw.len() != 1 { 19 | return Err(Error::Header); 20 | } 21 | 22 | let cow_str = String::from_utf8_lossy(&raw[0][..]); 23 | 24 | // Value needs to be a 31 bit non-negative integer, so convert to i32 25 | let value = match i32::from_str_radix(&*cow_str, 10) { 26 | Ok(n) => n, 27 | Err(_) => return Err(Error::Header), 28 | }; 29 | 30 | // UPnP 1.1 spec says higher numbers are reserved for future use by the 31 | // technical committee. Devices should use numbers in the range 0 to 32 | // 16777215 (2^24-1) but I am not sure where the reserved numbers will 33 | // appear so we will ignore checking that the range is satisfied here. 34 | 35 | // Check if value is negative, then convert to u32 36 | if value.is_negative() { 37 | Err(Error::Header) 38 | } else { 39 | Ok(ConfigID(value as u32)) 40 | } 41 | } 42 | } 43 | 44 | impl HeaderFormat for ConfigID { 45 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 46 | try!(fmt.write_fmt(format_args!("{}", self.0))); 47 | 48 | Ok(()) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use hyper::header::Header; 55 | 56 | use super::ConfigID; 57 | 58 | #[test] 59 | fn positive_configid() { 60 | let configid_header_value = &[b"1777215"[..].to_vec()]; 61 | 62 | ConfigID::parse_header(configid_header_value).unwrap(); 63 | } 64 | 65 | #[test] 66 | fn positive_reserved() { 67 | let configid_header_value = &[b"20720000"[..].to_vec()]; 68 | 69 | ConfigID::parse_header(configid_header_value).unwrap(); 70 | } 71 | 72 | #[test] 73 | fn positive_lower_bound() { 74 | let configid_header_value = &[b"0"[..].to_vec()]; 75 | 76 | ConfigID::parse_header(configid_header_value).unwrap(); 77 | } 78 | 79 | #[test] 80 | fn positive_upper_bound() { 81 | let configid_header_value = &[b"2147483647"[..].to_vec()]; 82 | 83 | ConfigID::parse_header(configid_header_value).unwrap(); 84 | } 85 | 86 | #[test] 87 | fn positive_negative_zero() { 88 | let configid_header_value = &[b"-0"[..].to_vec()]; 89 | 90 | ConfigID::parse_header(configid_header_value).unwrap(); 91 | } 92 | 93 | #[test] 94 | #[should_panic] 95 | fn negative_overflow() { 96 | let configid_header_value = &[b"2290649224"[..].to_vec()]; 97 | 98 | ConfigID::parse_header(configid_header_value).unwrap(); 99 | } 100 | 101 | #[test] 102 | #[should_panic] 103 | fn negative_negative_overflow() { 104 | let configid_header_value = &[b"-2290649224"[..].to_vec()]; 105 | 106 | ConfigID::parse_header(configid_header_value).unwrap(); 107 | } 108 | 109 | #[test] 110 | #[should_panic] 111 | fn negative_nan() { 112 | let configid_header_value = &[b"2290wow649224"[..].to_vec()]; 113 | 114 | ConfigID::parse_header(configid_header_value).unwrap(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/header/nts.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | const NTS_HEADER_NAME: &'static str = "NTS"; 7 | 8 | const ALIVE_HEADER: &'static str = "ssdp:alive"; 9 | const UPDATE_HEADER: &'static str = "ssdp:update"; 10 | const BYEBYE_HEADER: &'static str = "ssdp:byebye"; 11 | 12 | /// Represents a header which specifies a notification sub type. 13 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 14 | pub enum NTS { 15 | /// An entity is announcing itself to the network. 16 | Alive, 17 | /// An entity is updating its presence on the network. Introduced in UPnP 1.0. 18 | /// 19 | /// Contrary to it's name, an update message will only appear when some UPnP 20 | /// enabled interface is added to an already existing UPnP device on a network. 21 | Update, 22 | /// An entity is removing itself from the network. 23 | ByeBye, 24 | } 25 | 26 | impl Header for NTS { 27 | fn header_name() -> &'static str { 28 | NTS_HEADER_NAME 29 | } 30 | 31 | fn parse_header(raw: &[Vec]) -> error::Result { 32 | if raw.len() != 1 { 33 | return Err(Error::Header); 34 | } 35 | 36 | if &raw[0][..] == ALIVE_HEADER.as_bytes() { 37 | Ok(NTS::Alive) 38 | } else if &raw[0][..] == UPDATE_HEADER.as_bytes() { 39 | Ok(NTS::Update) 40 | } else if &raw[0][..] == BYEBYE_HEADER.as_bytes() { 41 | Ok(NTS::ByeBye) 42 | } else { 43 | Err(Error::Header) 44 | } 45 | } 46 | } 47 | 48 | impl HeaderFormat for NTS { 49 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 50 | match *self { 51 | NTS::Alive => try!(fmt.write_str(ALIVE_HEADER)), 52 | NTS::Update => try!(fmt.write_str(UPDATE_HEADER)), 53 | NTS::ByeBye => try!(fmt.write_str(BYEBYE_HEADER)), 54 | }; 55 | 56 | Ok(()) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use hyper::header::Header; 63 | 64 | use super::NTS; 65 | 66 | #[test] 67 | fn positive_alive() { 68 | let alive_header = &[b"ssdp:alive"[..].to_vec()]; 69 | 70 | match NTS::parse_header(alive_header) { 71 | Ok(NTS::Alive) => (), 72 | _ => panic!("Didn't Match With NTS::Alive"), 73 | }; 74 | } 75 | 76 | #[test] 77 | fn positive_update() { 78 | let update_header = &[b"ssdp:update"[..].to_vec()]; 79 | 80 | match NTS::parse_header(update_header) { 81 | Ok(NTS::Update) => (), 82 | _ => panic!("Didn't Match With NTS::Update"), 83 | }; 84 | } 85 | 86 | #[test] 87 | fn positive_byebye() { 88 | let byebye_header = &[b"ssdp:byebye"[..].to_vec()]; 89 | 90 | match NTS::parse_header(byebye_header) { 91 | Ok(NTS::ByeBye) => (), 92 | _ => panic!("Didn't Match With NTS::ByeBye"), 93 | }; 94 | } 95 | 96 | #[test] 97 | #[should_panic] 98 | fn negative_alive_extra() { 99 | let alive_extra_header = &[b"ssdp:alive_someotherbytes"[..].to_vec()]; 100 | 101 | NTS::parse_header(alive_extra_header).unwrap(); 102 | } 103 | 104 | #[test] 105 | #[should_panic] 106 | fn negative_unknown() { 107 | let unknown_header = &[b"ssdp:somestring"[..].to_vec()]; 108 | 109 | NTS::parse_header(unknown_header).unwrap(); 110 | } 111 | 112 | #[test] 113 | #[should_panic] 114 | fn negative_empty() { 115 | let empty_header = &[b""[..].to_vec()]; 116 | 117 | NTS::parse_header(empty_header).unwrap(); 118 | } 119 | 120 | #[test] 121 | #[should_panic] 122 | fn negative_no_value() { 123 | let no_value_header = &[b"ssdp:"[..].to_vec()]; 124 | 125 | NTS::parse_header(no_value_header).unwrap(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/header/mx.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | use {SSDPResult, SSDPErrorKind}; 7 | 8 | const MX_HEADER_NAME: &'static str = "MX"; 9 | 10 | /// Minimum wait time specified in the `UPnP` 1.0 standard. 11 | pub const MX_HEADER_MIN: u8 = 1; 12 | 13 | /// Maximum wait time specified in the `UPnP` 1.0 standard. 14 | pub const MX_HEADER_MAX: u8 = 120; 15 | 16 | /// Represents a header used to specify the maximum time that devices should wait 17 | /// before sending a response. 18 | /// 19 | /// Should only be increased as the number of devices expected to respond 20 | /// increases, not because of latency or propagation delay. In practice, some 21 | /// devices will not respond to requests with an MX value above some threshold 22 | /// (but lower than the maximum threshold) because of resources it may not want 23 | /// to tie up. 24 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] 25 | pub struct MX(pub u8); 26 | 27 | impl MX { 28 | pub fn new(wait_bound: u8) -> SSDPResult { 29 | if wait_bound < MX_HEADER_MIN || wait_bound > MX_HEADER_MAX { 30 | Err(SSDPErrorKind::InvalidHeader(MX_HEADER_NAME, "Supplied Wait Bound Is Out Of Bounds").into()) 31 | } else { 32 | Ok(MX(wait_bound)) 33 | } 34 | } 35 | } 36 | 37 | impl Header for MX { 38 | fn header_name() -> &'static str { 39 | MX_HEADER_NAME 40 | } 41 | 42 | fn parse_header(raw: &[Vec]) -> error::Result { 43 | if raw.len() != 1 { 44 | return Err(Error::Header); 45 | } 46 | 47 | let cow_string = String::from_utf8_lossy(&raw[0][..]); 48 | 49 | match u8::from_str_radix(&cow_string, 10) { 50 | Ok(n) if n >= MX_HEADER_MIN && n <= MX_HEADER_MAX => Ok(MX(n)), 51 | _ => Err(Error::Header), 52 | } 53 | } 54 | } 55 | 56 | impl HeaderFormat for MX { 57 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 58 | try!(fmt.write_fmt(format_args!("{}", self.0))); 59 | 60 | Ok(()) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use hyper::header::Header; 67 | 68 | use super::MX; 69 | 70 | #[test] 71 | fn positive_lower_bound() { 72 | let mx_lower_header = &[b"1"[..].to_vec()]; 73 | 74 | match MX::parse_header(mx_lower_header) { 75 | Ok(n) if n == MX(1) => (), 76 | _ => panic!("Failed To Accept 1 As MX Value"), 77 | }; 78 | } 79 | 80 | #[test] 81 | fn positive_inner_bound() { 82 | let mx_inner_header = &[b"5"[..].to_vec()]; 83 | 84 | match MX::parse_header(mx_inner_header) { 85 | Ok(n) if n == MX(5) => (), 86 | _ => panic!("Failed To Accept 5 As MX Value"), 87 | }; 88 | } 89 | 90 | #[test] 91 | fn positive_upper_bound() { 92 | let mx_upper_header = &[b"120"[..].to_vec()]; 93 | 94 | match MX::parse_header(mx_upper_header) { 95 | Ok(n) if n == MX(120) => (), 96 | _ => panic!("Failed To Accept 120 As MX Value"), 97 | }; 98 | } 99 | 100 | #[test] 101 | #[should_panic] 102 | fn negative_decimal_bound() { 103 | let mx_decimal_header = &[b"0.5"[..].to_vec()]; 104 | 105 | MX::parse_header(mx_decimal_header).unwrap(); 106 | } 107 | 108 | #[test] 109 | #[should_panic] 110 | fn negative_negative_bound() { 111 | let mx_negative_header = &[b"-5"[..].to_vec()]; 112 | 113 | MX::parse_header(mx_negative_header).unwrap(); 114 | } 115 | 116 | #[test] 117 | #[should_panic] 118 | fn negative_too_high_bound() { 119 | let mx_too_high_header = &[b"121"[..].to_vec()]; 120 | 121 | MX::parse_header(mx_too_high_header).unwrap(); 122 | } 123 | 124 | #[test] 125 | #[should_panic] 126 | fn negative_zero_bound() { 127 | let mx_zero_header = &[b"0"[..].to_vec()]; 128 | 129 | MX::parse_header(mx_zero_header).unwrap(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/net/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implements the HTTPMU and lower layers of the `UPnP` standard. 2 | //! 3 | //! This module deals with primitives for working with external libraries to write 4 | //! data to UDP sockets as a stream, and read data from UDP sockets as packets. 5 | 6 | use std::io::{self, ErrorKind}; 7 | use std::net::{ToSocketAddrs, UdpSocket}; 8 | use std::net::{SocketAddr, IpAddr}; 9 | 10 | #[cfg(not(windows))] 11 | use net2::unix::UnixUdpBuilderExt; 12 | use net2::UdpBuilder; 13 | 14 | pub mod connector; 15 | pub mod packet; 16 | pub mod sender; 17 | 18 | #[derive(Copy, Clone)] 19 | pub enum IpVersionMode { 20 | V4Only, 21 | V6Only, 22 | Any, 23 | } 24 | 25 | impl IpVersionMode { 26 | pub fn from_addr(addr: A) -> io::Result { 27 | match try!(addr_from_trait(addr)) { 28 | SocketAddr::V4(_) => Ok(IpVersionMode::V4Only), 29 | SocketAddr::V6(_) => Ok(IpVersionMode::V6Only), 30 | } 31 | } 32 | } 33 | 34 | /// Accept a type implementing `ToSocketAddrs` and tries to extract the first address. 35 | pub fn addr_from_trait(addr: A) -> io::Result { 36 | let mut sock_iter = try!(addr.to_socket_addrs()); 37 | 38 | match sock_iter.next() { 39 | Some(n) => Ok(n), 40 | None => Err(io::Error::new(ErrorKind::InvalidInput, "Failed To Parse SocketAddr")), 41 | } 42 | } 43 | 44 | /// Bind to a `UdpSocket`, setting `SO_REUSEADDR` on the underlying socket before binding. 45 | pub fn bind_reuse(local_addr: A) -> io::Result { 46 | let local_addr = try!(addr_from_trait(local_addr)); 47 | 48 | let builder = match local_addr { 49 | SocketAddr::V4(_) => try!(UdpBuilder::new_v4()), 50 | SocketAddr::V6(_) => try!(UdpBuilder::new_v6()), 51 | }; 52 | 53 | try!(reuse_port(&builder)); 54 | builder.bind(local_addr) 55 | } 56 | 57 | #[cfg(windows)] 58 | fn reuse_port(builder: &UdpBuilder) -> io::Result<()> { 59 | // Allow wildcards + specific to not overlap 60 | try!(builder.reuse_address(true)); 61 | Ok(()) 62 | } 63 | 64 | #[cfg(not(windows))] 65 | fn reuse_port(builder: &UdpBuilder) -> io::Result<()> { 66 | // Allow wildcards + specific to not overlap 67 | try!(builder.reuse_address(true)); 68 | // Allow multiple listeners on the same port 69 | try!(builder.reuse_port(true)); 70 | Ok(()) 71 | } 72 | 73 | /// Join a multicast address on the current `UdpSocket`. 74 | pub fn join_multicast(sock: &UdpSocket, iface: &SocketAddr, mcast_addr: &IpAddr) -> io::Result<()> { 75 | match (iface, mcast_addr) { 76 | (&SocketAddr::V4(ref i), &IpAddr::V4(ref m)) => sock.join_multicast_v4(m, i.ip()), 77 | (&SocketAddr::V6(ref i), &IpAddr::V6(ref m)) => sock.join_multicast_v6(m, i.scope_id()), 78 | _ => { 79 | Err(io::Error::new(ErrorKind::InvalidInput, 80 | "Multicast And Interface Addresses Are Not The Same Version")) 81 | } 82 | } 83 | } 84 | 85 | /// Leave a multicast address on the current `UdpSocket`. 86 | #[allow(dead_code)] // TODO: call this from somewhere? 87 | pub fn leave_multicast(sock: &UdpSocket, iface_addr: &SocketAddr, mcast_addr: &SocketAddr) -> io::Result<()> { 88 | match (iface_addr, mcast_addr) { 89 | (&SocketAddr::V4(ref i), &SocketAddr::V4(ref m)) => sock.leave_multicast_v4(m.ip(), i.ip()), 90 | (&SocketAddr::V6(ref i), &SocketAddr::V6(ref m)) => sock.leave_multicast_v6(m.ip(), i.scope_id()), 91 | _ => { 92 | Err(io::Error::new(ErrorKind::InvalidInput, 93 | "Multicast And Interface Addresses Are Not The Same Version")) 94 | } 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | 101 | #[test] 102 | fn positive_addr_from_trait() { 103 | super::addr_from_trait("192.168.0.1:0").unwrap(); 104 | } 105 | 106 | #[test] 107 | #[should_panic] 108 | fn negative_addr_from_trait() { 109 | super::addr_from_trait("192.168.0.1").unwrap(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/message/mod.rs: -------------------------------------------------------------------------------- 1 | //! Messaging primitives for discovering devices and services. 2 | 3 | use std::io; 4 | use std::net::SocketAddr; 5 | 6 | use net::connector::UdpConnector; 7 | use net::IpVersionMode; 8 | 9 | mod notify; 10 | mod search; 11 | mod ssdp; 12 | pub mod listen; 13 | pub mod multicast; 14 | 15 | use get_if_addrs; 16 | 17 | pub use message::multicast::Multicast; 18 | pub use message::search::{SearchRequest, SearchResponse, SearchListener}; 19 | pub use message::notify::{NotifyMessage, NotifyListener}; 20 | pub use message::listen::Listen; 21 | 22 | /// Multicast Socket Information 23 | pub const UPNP_MULTICAST_IPV4_ADDR: &'static str = "239.255.255.250"; 24 | pub const UPNP_MULTICAST_IPV6_LINK_LOCAL_ADDR: &'static str = "FF02::C"; 25 | pub const UPNP_MULTICAST_PORT: u16 = 1900; 26 | 27 | /// Default TTL For Multicast 28 | pub const UPNP_MULTICAST_TTL: u32 = 2; 29 | 30 | /// Enumerates different types of SSDP messages. 31 | #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] 32 | pub enum MessageType { 33 | /// A notify message. 34 | Notify, 35 | /// A search message. 36 | Search, 37 | /// A response to a search message. 38 | Response, 39 | } 40 | 41 | #[derive(Clone)] 42 | pub struct Config { 43 | pub ipv4_addr: String, 44 | pub ipv6_addr: String, 45 | pub port: u16, 46 | pub ttl: u32, 47 | pub mode: IpVersionMode, 48 | } 49 | 50 | impl Config { 51 | pub fn new() -> Self { 52 | Default::default() 53 | } 54 | 55 | pub fn set_ipv4_addr>(mut self, value: S) -> Self { 56 | self.ipv4_addr = value.into(); 57 | self 58 | } 59 | 60 | pub fn set_ipv6_addr>(mut self, value: S) -> Self { 61 | self.ipv6_addr = value.into(); 62 | self 63 | } 64 | 65 | pub fn set_port(mut self, value: u16) -> Self { 66 | self.port = value; 67 | self 68 | } 69 | 70 | pub fn set_ttl(mut self, value: u32) -> Self { 71 | self.ttl = value; 72 | self 73 | } 74 | 75 | pub fn set_mode(mut self, value: IpVersionMode) -> Self { 76 | self.mode = value; 77 | self 78 | } 79 | } 80 | 81 | impl Default for Config { 82 | fn default() -> Self { 83 | Config { 84 | ipv4_addr: UPNP_MULTICAST_IPV4_ADDR.to_string(), 85 | ipv6_addr: UPNP_MULTICAST_IPV6_LINK_LOCAL_ADDR.to_string(), 86 | port: UPNP_MULTICAST_PORT, 87 | ttl: UPNP_MULTICAST_TTL, 88 | mode: IpVersionMode::Any, 89 | } 90 | } 91 | } 92 | 93 | /// Generate `UdpConnector` objects for all local `IPv4` interfaces. 94 | fn all_local_connectors(multicast_ttl: Option, filter: &IpVersionMode) -> io::Result> { 95 | trace!("Fetching all local connectors"); 96 | map_local(|&addr| match (filter, addr) { 97 | (&IpVersionMode::V4Only, SocketAddr::V4(n)) | 98 | (&IpVersionMode::Any, SocketAddr::V4(n)) => { 99 | Ok(Some(try!(UdpConnector::new((*n.ip(), 0), multicast_ttl)))) 100 | } 101 | (&IpVersionMode::V6Only, SocketAddr::V6(n)) | 102 | (&IpVersionMode::Any, SocketAddr::V6(n)) => Ok(Some(try!(UdpConnector::new(n, multicast_ttl)))), 103 | _ => Ok(None), 104 | }) 105 | } 106 | 107 | /// Invoke the closure for every local address found on the system 108 | /// 109 | /// This method filters out _loopback_ and _global_ addresses. 110 | fn map_local(mut f: F) -> io::Result> 111 | where F: FnMut(&SocketAddr) -> io::Result> 112 | { 113 | let addrs_iter = try!(get_local_addrs()); 114 | 115 | let mut obj_list = Vec::with_capacity(addrs_iter.len()); 116 | 117 | for addr in addrs_iter { 118 | trace!("Found {}", addr); 119 | match addr { 120 | SocketAddr::V4(n) if !n.ip().is_loopback() => { 121 | if let Some(x) = try!(f(&addr)) { 122 | obj_list.push(x); 123 | } 124 | } 125 | // Filter all loopback and global IPv6 addresses 126 | SocketAddr::V6(n) if !n.ip().is_loopback() && !n.ip().is_global() => { 127 | if let Some(x) = try!(f(&addr)) { 128 | obj_list.push(x); 129 | } 130 | } 131 | _ => (), 132 | } 133 | } 134 | 135 | Ok(obj_list) 136 | } 137 | 138 | /// Generate a list of some object R constructed from all local `Ipv4Addr` objects. 139 | /// 140 | /// If any of the `SocketAddr`'s fail to resolve, this function will not return an error. 141 | fn get_local_addrs() -> io::Result> { 142 | let iface_iter = try!(get_if_addrs::get_if_addrs()).into_iter(); 143 | Ok(iface_iter.filter_map(|iface| Some(SocketAddr::new(iface.addr.ip(), 0))) 144 | .collect()) 145 | } -------------------------------------------------------------------------------- /src/header/mod.rs: -------------------------------------------------------------------------------- 1 | //! Headers and primitives for parsing headers within SSDP requests. 2 | //! 3 | //! This module combines abstractions at both the HTTPU/HTTPMU layer and SSDP 4 | //! layer in order to provide a cleaner interface for extending the underlying 5 | //! HTTP parsing library. 6 | 7 | use std::borrow::Cow; 8 | use std::fmt::Debug; 9 | 10 | use hyper::header::{Headers, Header, HeaderFormat}; 11 | 12 | mod bootid; 13 | mod configid; 14 | mod man; 15 | mod mx; 16 | mod nt; 17 | mod nts; 18 | mod searchport; 19 | mod securelocation; 20 | mod st; 21 | mod usn; 22 | 23 | pub use self::bootid::BootID; 24 | pub use self::configid::ConfigID; 25 | pub use self::man::Man; 26 | pub use self::mx::MX; 27 | pub use self::nt::NT; 28 | pub use self::nts::NTS; 29 | pub use self::searchport::SearchPort; 30 | pub use self::securelocation::SecureLocation; 31 | pub use self::st::ST; 32 | pub use self::usn::USN; 33 | 34 | // Re-exports 35 | pub use hyper::header::{Location, Server, CacheControl, CacheDirective}; 36 | 37 | /// Trait for viewing the contents of a header structure. 38 | pub trait HeaderRef: Debug { 39 | /// View a reference to a header field if it exists. 40 | fn get(&self) -> Option<&H> where H: Header + HeaderFormat; 41 | 42 | /// View a reference to the raw bytes of a header field if it exists. 43 | fn get_raw(&self, name: &str) -> Option<&[Vec]>; 44 | } 45 | 46 | impl<'a, T: ?Sized> HeaderRef for &'a T 47 | where T: HeaderRef 48 | { 49 | fn get(&self) -> Option<&H> 50 | where H: Header + HeaderFormat 51 | { 52 | HeaderRef::get::(*self) 53 | } 54 | 55 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 56 | HeaderRef::get_raw(*self, name) 57 | } 58 | } 59 | 60 | impl<'a, T: ?Sized> HeaderRef for &'a mut T 61 | where T: HeaderRef 62 | { 63 | fn get(&self) -> Option<&H> 64 | where H: Header + HeaderFormat 65 | { 66 | HeaderRef::get::(*self) 67 | } 68 | 69 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 70 | HeaderRef::get_raw(*self, name) 71 | } 72 | } 73 | 74 | impl HeaderRef for Headers { 75 | fn get(&self) -> Option<&H> 76 | where H: Header + HeaderFormat 77 | { 78 | Headers::get::(self) 79 | } 80 | 81 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 82 | Headers::get_raw(self, name) 83 | } 84 | } 85 | 86 | /// Trait for manipulating the contents of a header structure. 87 | pub trait HeaderMut: Debug { 88 | /// Set a header to the given value. 89 | fn set(&mut self, value: H) where H: Header + HeaderFormat; 90 | 91 | /// Set a header to the given raw bytes. 92 | fn set_raw(&mut self, name: K, value: Vec>) where K: Into> + Debug; 93 | } 94 | 95 | impl<'a, T: ?Sized> HeaderMut for &'a mut T 96 | where T: HeaderMut 97 | { 98 | fn set(&mut self, value: H) 99 | where H: Header + HeaderFormat 100 | { 101 | HeaderMut::set(*self, value) 102 | } 103 | 104 | fn set_raw(&mut self, name: K, value: Vec>) 105 | where K: Into> + Debug 106 | { 107 | HeaderMut::set_raw(*self, name, value) 108 | } 109 | } 110 | 111 | impl HeaderMut for Headers { 112 | fn set(&mut self, value: H) 113 | where H: Header + HeaderFormat 114 | { 115 | Headers::set(self, value) 116 | } 117 | 118 | fn set_raw(&mut self, name: K, value: Vec>) 119 | where K: Into> + Debug 120 | { 121 | Headers::set_raw(self, name, value) 122 | } 123 | } 124 | 125 | // #[cfg(test)] 126 | // pub mod mock { 127 | // use std::any::{Any}; 128 | // use std::borrow::{ToOwned}; 129 | // use std::clone::{Clone}; 130 | // use std::collections::{HashMap}; 131 | // 132 | // use hyper::header::{Header, HeaderFormat}; 133 | // 134 | // use ssdp::header::{HeaderView}; 135 | // 136 | // #[derive(Debug)] 137 | // pub struct MockHeaderView { 138 | // map: HashMap<&'static str, (Box, [Vec; 1])> 139 | // } 140 | // 141 | // impl MockHeaderView { 142 | // pub fn new() -> MockHeaderView { 143 | // MockHeaderView{ map: HashMap::new() } 144 | // } 145 | // 146 | // pub fn insert(&mut self, value: &str) where H: Header + HeaderFormat { 147 | // let header_bytes = [value.to_owned().into_bytes()]; 148 | // 149 | // let header = match H::parse_header(&header_bytes[..]) { 150 | // Some(n) => n, 151 | // None => panic!("Failed To Parse value As Header!!!") 152 | // }; 153 | // 154 | // self.map.insert(H::header_name(), (Box::new(header), header_bytes)); 155 | // } 156 | // } 157 | // 158 | // impl Clone for MockHeaderView { 159 | // fn clone(&self) -> MockHeaderView { 160 | // panic!("Can Not Clone A MockHeaderView") 161 | // } 162 | // } 163 | // 164 | // impl HeaderView for MockHeaderView { 165 | // fn view(&self) -> Option<&H> where H: Header + HeaderFormat { 166 | // match self.map.get(H::header_name()) { 167 | // Some(&(ref header, _)) => header.downcast_ref::(), 168 | // None => None 169 | // } 170 | // } 171 | // 172 | // fn view_raw(&self, name: &str) -> Option<&[Vec]> { 173 | // match self.map.get(name) { 174 | // Some(&(_, ref header_bytes)) => Some(header_bytes), 175 | // None => None 176 | // } 177 | // } 178 | // } 179 | // } 180 | -------------------------------------------------------------------------------- /src/receiver.rs: -------------------------------------------------------------------------------- 1 | //! Primitives for non-blocking SSDP message receiving. 2 | 3 | use std::io; 4 | use std::result::Result; 5 | use std::thread; 6 | use std::sync::mpsc::{self, Receiver, Sender, TryRecvError, RecvError, Iter}; 7 | use std::net::{UdpSocket, SocketAddr}; 8 | use std::time::Duration; 9 | 10 | use SSDPResult; 11 | use net::packet::PacketReceiver; 12 | 13 | /// Trait for constructing an object from some serialized SSDP message. 14 | pub trait FromRawSSDP: Sized { 15 | fn raw_ssdp(bytes: &[u8]) -> SSDPResult; 16 | } 17 | 18 | /// Iterator for an `SSDPReceiver`. 19 | pub struct SSDPIter { 20 | recv: SSDPReceiver, 21 | } 22 | 23 | impl SSDPIter { 24 | fn new(recv: SSDPReceiver) -> SSDPIter { 25 | SSDPIter { recv: recv } 26 | } 27 | } 28 | 29 | impl Iterator for SSDPIter { 30 | type Item = (T, SocketAddr); 31 | 32 | fn next(&mut self) -> Option { 33 | self.recv.recv().ok() 34 | } 35 | } 36 | 37 | /// A non-blocking SSDP message receiver. 38 | pub struct SSDPReceiver { 39 | recvr: Receiver<(T, SocketAddr)>, 40 | } 41 | 42 | impl SSDPReceiver 43 | where T: FromRawSSDP + Send + 'static 44 | { 45 | /// Construct a receiver that receives bytes from a number of UdpSockets and 46 | /// tries to construct an object T from them. If a duration is provided, the 47 | /// channel will be shutdown after the specified duration. 48 | /// 49 | /// Due to implementation details, none of the UdpSockets should be bound to 50 | /// the default route, 0.0.0.0, address. 51 | pub fn new(socks: Vec, time: Option) -> io::Result> { 52 | let (send, recv) = mpsc::channel(); 53 | 54 | // Ensure `receive_packets` times out in the event the timeout packet is not received 55 | for sock in socks.iter() { 56 | try!(sock.set_read_timeout(time)); 57 | } 58 | 59 | // Spawn Receiver Threads 60 | spawn_receivers(socks, send); 61 | 62 | Ok(SSDPReceiver { recvr: recv }) 63 | } 64 | } 65 | 66 | /// Spawn a number of receiver threads that will receive packets, forward the 67 | /// bytes on to T, and send successfully constructed objects through the sender. 68 | fn spawn_receivers(socks: Vec, sender: Sender<(T, SocketAddr)>) 69 | where T: FromRawSSDP + Send + 'static 70 | { 71 | for sock in socks { 72 | let pckt_recv = PacketReceiver::new(sock); 73 | let sender = sender.clone(); 74 | 75 | thread::spawn(move || { 76 | receive_packets(pckt_recv, sender); 77 | }); 78 | } 79 | } 80 | 81 | impl SSDPReceiver { 82 | /// Non-blocking method that attempts to read a value from the receiver. 83 | pub fn try_recv(&self) -> Result<(T, SocketAddr), TryRecvError> { 84 | self.recvr.try_recv() 85 | } 86 | 87 | /// Blocking method that reads a value from the receiver until one is available. 88 | pub fn recv(&self) -> Result<(T, SocketAddr), RecvError> { 89 | self.recvr.recv() 90 | } 91 | } 92 | 93 | impl<'a, T> IntoIterator for &'a SSDPReceiver { 94 | type Item = (T, SocketAddr); 95 | type IntoIter = Iter<'a, (T, SocketAddr)>; 96 | 97 | fn into_iter(self) -> Self::IntoIter { 98 | self.recvr.iter() 99 | } 100 | } 101 | 102 | impl<'a, T> IntoIterator for &'a mut SSDPReceiver { 103 | type Item = (T, SocketAddr); 104 | type IntoIter = Iter<'a, (T, SocketAddr)>; 105 | 106 | fn into_iter(self) -> Self::IntoIter { 107 | self.recvr.iter() 108 | } 109 | } 110 | 111 | impl IntoIterator for SSDPReceiver { 112 | type Item = (T, SocketAddr); 113 | type IntoIter = SSDPIter; 114 | 115 | fn into_iter(self) -> Self::IntoIter { 116 | SSDPIter::new(self) 117 | } 118 | } 119 | 120 | /// Receives bytes and attempts to construct a T which will be sent through the supplied channel. 121 | /// 122 | /// This should almost always be run in it's own thread. 123 | fn receive_packets(recv: PacketReceiver, send: Sender<(T, SocketAddr)>) 124 | where T: FromRawSSDP + Send 125 | { 126 | // TODO: Add logging to this function. Maybe forward sender IP Address along 127 | // so that we can do some checks when we parse the http. 128 | loop { 129 | trace!("Waiting on packet at {}...", recv); 130 | let (msg_bytes, addr) = match recv.recv_pckt() { 131 | Ok((bytes, addr)) => (bytes, addr), 132 | // Unix returns WouldBlock on timeout while Windows returns TimedOut 133 | Err(ref err) if err.kind() == io::ErrorKind::WouldBlock || 134 | err.kind() == io::ErrorKind::TimedOut => { 135 | // We have waited for at least the desired timeout (or possibly longer) 136 | trace!("Receiver at {} timed out", recv); 137 | return; 138 | } 139 | Err(_) => { 140 | continue; 141 | } 142 | }; 143 | 144 | trace!("Received packet with {} bytes", msg_bytes.len()); 145 | 146 | // Unwrap Will Cause A Panic If Receiver Hung Up Which Is Desired 147 | match T::raw_ssdp(&msg_bytes[..]) { 148 | Ok(n) => send.send((n, addr)).unwrap(), 149 | Err(_) => { 150 | continue; 151 | } 152 | }; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/header/nt.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Display, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | use FieldMap; 7 | 8 | const NT_HEADER_NAME: &'static str = "NT"; 9 | 10 | /// Represents a header used to specify a notification type. 11 | /// 12 | /// Any double colons will not be processed as separate `FieldMap`'s. 13 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 14 | pub struct NT(pub FieldMap); 15 | 16 | impl NT { 17 | pub fn new(field: FieldMap) -> NT { 18 | NT(field) 19 | } 20 | } 21 | 22 | impl Header for NT { 23 | fn header_name() -> &'static str { 24 | NT_HEADER_NAME 25 | } 26 | 27 | fn parse_header(raw: &[Vec]) -> error::Result { 28 | if raw.len() != 1 { 29 | return Err(Error::Header); 30 | } 31 | 32 | match FieldMap::parse_bytes(&raw[0][..]) { 33 | Some(n) => Ok(NT(n)), 34 | None => Err(Error::Header), 35 | } 36 | } 37 | } 38 | 39 | impl HeaderFormat for NT { 40 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 41 | try!(Display::fmt(&self.0, fmt)); 42 | 43 | Ok(()) 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use hyper::header::Header; 50 | 51 | use super::NT; 52 | use FieldMap::{UPnP, UUID, URN, Unknown}; 53 | 54 | #[test] 55 | fn positive_uuid() { 56 | let header = "uuid:a984bc8c-aaf0-5dff-b980-00d098bda247"; 57 | 58 | let data = match NT::parse_header(&[header.to_string().into_bytes()]) { 59 | Ok(NT(UUID(n))) => n, 60 | _ => panic!("uuid Token Not Parsed"), 61 | }; 62 | 63 | assert!(header.chars().skip(5).zip(data.chars()).all(|(a, b)| a == b)); 64 | } 65 | 66 | #[test] 67 | fn positive_upnp() { 68 | let header = "upnp:rootdevice"; 69 | 70 | let data = match NT::parse_header(&[header.to_string().into_bytes()]) { 71 | Ok(NT(UPnP(n))) => n, 72 | _ => panic!("upnp Token Not Parsed"), 73 | }; 74 | 75 | assert!(header.chars().skip(5).zip(data.chars()).all(|(a, b)| a == b)); 76 | } 77 | 78 | #[test] 79 | fn positive_urn() { 80 | let header = "urn:schemas-upnp-org:device:printer:1"; 81 | 82 | let data = match NT::parse_header(&[header.to_string().into_bytes()]) { 83 | Ok(NT(URN(n))) => n, 84 | _ => panic!("urn Token Not Parsed"), 85 | }; 86 | 87 | assert!(header.chars().skip(4).zip(data.chars()).all(|(a, b)| a == b)); 88 | } 89 | 90 | #[test] 91 | fn positive_unknown() { 92 | let header = "max-age:1500::upnp:rootdevice"; 93 | 94 | let (k, v) = match NT::parse_header(&[header.to_string().into_bytes()]) { 95 | Ok(NT(Unknown(k, v))) => (k, v), 96 | _ => panic!("Unknown Token Not Parsed"), 97 | }; 98 | 99 | let sep_iter = ":".chars(); 100 | let mut original_iter = header.chars(); 101 | let mut result_iter = k.chars().chain(sep_iter).chain(v.chars()); 102 | 103 | assert!(original_iter.by_ref().zip(result_iter.by_ref()).all(|(a, b)| a == b)); 104 | assert!(result_iter.next().is_none() && original_iter.next().is_none()); 105 | } 106 | 107 | #[test] 108 | fn positive_short_field() { 109 | let header = "a:a"; 110 | 111 | let (k, v) = match NT::parse_header(&[header.to_string().into_bytes()]) { 112 | Ok(NT(Unknown(k, v))) => (k, v), 113 | _ => panic!("Unknown Short Token Not Parsed"), 114 | }; 115 | 116 | let sep_iter = ":".chars(); 117 | let mut original_iter = header.chars(); 118 | let mut result_iter = k.chars().chain(sep_iter).chain(v.chars()); 119 | 120 | assert!(original_iter.by_ref().zip(result_iter.by_ref()).all(|(a, b)| a == b)); 121 | assert!(result_iter.next().is_none() && original_iter.next().is_none()); 122 | } 123 | 124 | #[test] 125 | fn positive_leading_double_colon() { 126 | let leading_double_colon_header = &["uuid::a984bc8c-aaf0-5dff-b980-00d098bda247" 127 | .to_string() 128 | .into_bytes()]; 129 | 130 | let result = match NT::parse_header(leading_double_colon_header).unwrap() { 131 | NT(UUID(n)) => n, 132 | _ => panic!("NT Double Colon Failed To Parse"), 133 | }; 134 | 135 | assert_eq!(result, ":a984bc8c-aaf0-5dff-b980-00d098bda247"); 136 | } 137 | 138 | #[test] 139 | #[should_panic] 140 | fn negative_double_colon() { 141 | let double_colon_header = &["::".to_string().into_bytes()]; 142 | 143 | NT::parse_header(double_colon_header).unwrap(); 144 | } 145 | 146 | #[test] 147 | #[should_panic] 148 | fn negative_single_colon() { 149 | let single_colon_header = &[":".to_string().into_bytes()]; 150 | 151 | NT::parse_header(single_colon_header).unwrap(); 152 | } 153 | 154 | #[test] 155 | #[should_panic] 156 | fn negative_empty_field() { 157 | let empty_header = &["".to_string().into_bytes()]; 158 | 159 | NT::parse_header(empty_header).unwrap(); 160 | } 161 | 162 | #[test] 163 | #[should_panic] 164 | fn negative_no_colon() { 165 | let no_colon_header = &["some_key-some_value".to_string().into_bytes()]; 166 | 167 | NT::parse_header(no_colon_header).unwrap(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/header/usn.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Formatter, Display, Result}; 2 | 3 | use hyper::error::{self, Error}; 4 | use hyper::header::{HeaderFormat, Header}; 5 | 6 | use FieldMap; 7 | use field; 8 | 9 | const USN_HEADER_NAME: &'static str = "USN"; 10 | 11 | /// Separator for multiple key/values in header fields. 12 | const FIELD_PAIR_SEPARATOR: &'static str = "::"; 13 | 14 | /// Represents a header which specifies a unique service name. 15 | /// 16 | /// Field value can hold up to two `FieldMap`'s. 17 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 18 | pub struct USN(pub FieldMap, pub Option); 19 | 20 | impl USN { 21 | pub fn new(field: FieldMap, opt_field: Option) -> USN { 22 | USN(field, opt_field) 23 | } 24 | } 25 | 26 | impl Header for USN { 27 | fn header_name() -> &'static str { 28 | USN_HEADER_NAME 29 | } 30 | 31 | fn parse_header(raw: &[Vec]) -> error::Result { 32 | if raw.len() != 1 { 33 | return Err(Error::Header); 34 | } 35 | 36 | let (first, second) = match partition_pairs(raw[0][..].iter()) { 37 | Some((n, Some(u))) => (FieldMap::parse_bytes(&n[..]), FieldMap::parse_bytes(&u[..])), 38 | Some((n, None)) => (FieldMap::parse_bytes(&n[..]), None), 39 | None => return Err(Error::Header), 40 | }; 41 | 42 | match first { 43 | Some(n) => Ok(USN(n, second)), 44 | None => Err(Error::Header), 45 | } 46 | } 47 | } 48 | 49 | impl HeaderFormat for USN { 50 | fn fmt_header(&self, fmt: &mut Formatter) -> Result { 51 | try!(Display::fmt(&self.0, fmt)); 52 | 53 | if let Some(ref n) = self.1 { 54 | try!(fmt.write_fmt(format_args!("{}", FIELD_PAIR_SEPARATOR))); 55 | try!(Display::fmt(n, fmt)); 56 | } 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | fn partition_pairs<'a, I>(header_iter: I) -> Option<(Vec, Option>)> 63 | where I: Iterator 64 | { 65 | let mut second_partition = false; 66 | let mut header_iter = header_iter.peekable(); 67 | 68 | let mut last_byte = match header_iter.peek() { 69 | Some(&&n) => n, 70 | None => return None, 71 | }; 72 | 73 | // Seprate field into two vecs, store separators on end of first 74 | let (mut first, second): (Vec, Vec) = header_iter.cloned().partition(|&n| { 75 | if second_partition { 76 | false 77 | } else { 78 | second_partition = [last_byte, n] == FIELD_PAIR_SEPARATOR.as_bytes(); 79 | last_byte = n; 80 | 81 | true 82 | } 83 | }); 84 | 85 | // Remove up to two separators from end of first 86 | for _ in 0..2 { 87 | if let Some(&n) = first[..].last() { 88 | if n == field::PAIR_SEPARATOR as u8 { 89 | first.pop(); 90 | } 91 | } 92 | } 93 | 94 | match (first.is_empty(), second.is_empty()) { 95 | (false, false) => Some((first, Some(second))), 96 | (false, true) => Some((first, None)), 97 | _ => None, 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod tests { 103 | use hyper::header::Header; 104 | 105 | use super::USN; 106 | use FieldMap::{UPnP, UUID, URN, Unknown}; 107 | 108 | #[test] 109 | fn positive_double_pair() { 110 | let double_pair_header = &["uuid:device-UUID::upnp:rootdevice".to_string().into_bytes()]; 111 | let USN(first, second) = USN::parse_header(double_pair_header).unwrap(); 112 | 113 | match first { 114 | UUID(n) => assert_eq!(n, "device-UUID"), 115 | _ => panic!("Didnt Match uuid"), 116 | }; 117 | 118 | match second.unwrap() { 119 | UPnP(n) => assert_eq!(n, "rootdevice"), 120 | _ => panic!("Didnt Match upnp"), 121 | }; 122 | } 123 | 124 | #[test] 125 | fn positive_single_pair() { 126 | let single_pair_header = &["urn:device-URN".to_string().into_bytes()]; 127 | let USN(first, second) = USN::parse_header(single_pair_header).unwrap(); 128 | 129 | match first { 130 | URN(n) => assert_eq!(n, "device-URN"), 131 | _ => panic!("Didnt Match urn"), 132 | }; 133 | 134 | assert!(second.is_none()); 135 | } 136 | 137 | #[test] 138 | fn positive_trailing_double_colon() { 139 | let trailing_double_colon_header = &["upnp:device-UPnP::".to_string().into_bytes()]; 140 | let USN(first, second) = USN::parse_header(trailing_double_colon_header).unwrap(); 141 | 142 | match first { 143 | UPnP(n) => assert_eq!(n, "device-UPnP"), 144 | _ => panic!("Didnt Match upnp"), 145 | }; 146 | 147 | assert!(second.is_none()); 148 | } 149 | 150 | #[test] 151 | fn positive_trailing_single_colon() { 152 | let trailing_single_colon_header = &["some-key:device-UPnP:".to_string().into_bytes()]; 153 | let USN(first, second) = USN::parse_header(trailing_single_colon_header).unwrap(); 154 | 155 | match first { 156 | Unknown(k, v) => { 157 | assert_eq!(k, "some-key"); 158 | assert_eq!(v, "device-UPnP"); 159 | } 160 | _ => panic!("Didnt Match upnp"), 161 | }; 162 | 163 | assert!(second.is_none()); 164 | } 165 | 166 | #[test] 167 | #[should_panic] 168 | fn negative_empty() { 169 | let empty_header = &["".to_string().into_bytes()]; 170 | 171 | USN::parse_header(empty_header).unwrap(); 172 | } 173 | 174 | #[test] 175 | #[should_panic] 176 | fn negative_colon() { 177 | let colon_header = &[":".to_string().into_bytes()]; 178 | 179 | USN::parse_header(colon_header).unwrap(); 180 | } 181 | 182 | #[test] 183 | #[should_panic] 184 | fn negative_double_colon() { 185 | let double_colon_header = &["::".to_string().into_bytes()]; 186 | 187 | USN::parse_header(double_colon_header).unwrap(); 188 | } 189 | 190 | #[test] 191 | #[should_panic] 192 | fn negative_double_colon_value() { 193 | let double_colon_value_header = &["uuid:::".to_string().into_bytes()]; 194 | 195 | USN::parse_header(double_colon_value_header).unwrap(); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/field.rs: -------------------------------------------------------------------------------- 1 | //! Implements the SSDP layer of the `UPnP` standard. 2 | //! 3 | //! This module deals with interface discovery as well as HTTP extensions for 4 | //! accomodating SSDP. 5 | 6 | use std::fmt::{Display, Error, Formatter}; 7 | use std::result::Result; 8 | use std::borrow::Cow; 9 | 10 | /// Separator character for a `FieldMap` and it's value. 11 | pub const PAIR_SEPARATOR: char = ':'; 12 | 13 | /// Prefix for the "upnp" field key. 14 | const UPNP_PREFIX: &'static str = "upnp"; 15 | /// Prefix for the "uuid" field key. 16 | const UUID_PREFIX: &'static str = "uuid"; 17 | /// Prefix for the "usn" field key. 18 | const URN_PREFIX: &'static str = "urn"; 19 | 20 | /// Enumerates key value pairs embedded within SSDP header fields. 21 | #[derive(Clone, PartialEq, Eq, Hash, Debug)] 22 | pub enum FieldMap { 23 | /// The "upnp" key with its associated value. 24 | UPnP(String), 25 | /// The "uuid" key with its associated value. 26 | UUID(String), 27 | /// The "urn" key with its associated value. 28 | URN(String), 29 | /// An undefined key, the key and it's value are returned. 30 | Unknown(String, String), 31 | } 32 | 33 | impl FieldMap { 34 | /// Breaks a field up into a single key and single value which are 35 | /// separated by a colon and neither of which are empty. 36 | /// 37 | /// Separation will occur at the first colon encountered. 38 | pub fn new<'a, S: Into>>(value: S) -> Option { 39 | FieldMap::parse_bytes(value.into().as_bytes()) 40 | } 41 | 42 | /// Breaks a field up into a single key and single value which are 43 | /// separated by a colon and neither of which are empty. 44 | /// 45 | /// Separation will occur at the first colon encountered. 46 | pub fn parse_bytes(field: &[u8]) -> Option { 47 | let split_index = match field.iter().position(|&b| b == PAIR_SEPARATOR as u8) { 48 | Some(n) => n, 49 | None => return None, 50 | }; 51 | let (key, mut value) = field.split_at(split_index); 52 | 53 | // Ignore Separator Byte 54 | value = &value[1..]; 55 | 56 | // Check Empty Byte Slices 57 | if key.len() == 0 || value.len() == 0 { 58 | return None; 59 | } 60 | 61 | let key = String::from_utf8_lossy(key); 62 | let value = String::from_utf8_lossy(value).into_owned(); 63 | 64 | if matches_uuid_key(key.as_ref()) { 65 | Some(FieldMap::UUID(value)) 66 | } else if matches_urn_key(key.as_ref()) { 67 | Some(FieldMap::URN(value)) 68 | } else if matches_upnp_key(key.as_ref()) { 69 | Some(FieldMap::UPnP(value)) 70 | } else { 71 | Some(FieldMap::Unknown(key.into_owned(), value)) 72 | } 73 | } 74 | 75 | pub fn upnp<'a, S: Into>>(value: S) -> Self { 76 | FieldMap::UPnP(value.into().into_owned()) 77 | } 78 | 79 | pub fn uuid<'a, S: Into>>(value: S) -> Self { 80 | FieldMap::UUID(value.into().into_owned()) 81 | } 82 | 83 | pub fn urn<'a, S: Into>>(value: S) -> Self { 84 | FieldMap::URN(value.into().into_owned()) 85 | } 86 | 87 | pub fn unknown<'a, S: Into>, S2: Into>>(key: S, value: S2) -> Self { 88 | FieldMap::Unknown(key.into().into_owned(), value.into().into_owned()) 89 | } 90 | } 91 | 92 | impl Display for FieldMap { 93 | fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { 94 | let value = match *self { 95 | FieldMap::UPnP(ref v) => { 96 | try!(f.write_str(UPNP_PREFIX)); 97 | v 98 | } 99 | FieldMap::UUID(ref v) => { 100 | try!(f.write_str(UUID_PREFIX)); 101 | v 102 | } 103 | FieldMap::URN(ref v) => { 104 | try!(f.write_str(URN_PREFIX)); 105 | v 106 | } 107 | FieldMap::Unknown(ref k, ref v) => { 108 | try!(Display::fmt(k, f)); 109 | v 110 | } 111 | }; 112 | try!(f.write_fmt(format_args!("{}", PAIR_SEPARATOR))); 113 | try!(Display::fmt(value, f)); 114 | Ok(()) 115 | } 116 | } 117 | 118 | /// Returns the header field value if the key matches the uuid key, else returns None. 119 | fn matches_uuid_key(key: &str) -> bool { 120 | UUID_PREFIX == key 121 | } 122 | 123 | /// Returns the header field value if the key matches the urn key, else returns None. 124 | fn matches_urn_key(key: &str) -> bool { 125 | URN_PREFIX == key 126 | } 127 | 128 | /// Returns the header field value if the key matches the upnp key, else returns None. 129 | fn matches_upnp_key(key: &str) -> bool { 130 | UPNP_PREFIX == key 131 | } 132 | 133 | #[cfg(test)] 134 | mod tests { 135 | use super::FieldMap; 136 | 137 | #[test] 138 | fn positive_non_utf8() { 139 | let uuid_pair = FieldMap::parse_bytes(&b"uuid:some_value_\x80"[..]).unwrap(); 140 | assert_eq!(uuid_pair, FieldMap::uuid(String::from_utf8_lossy(&b"some_value_\x80".to_vec()))); 141 | } 142 | 143 | #[test] 144 | fn positive_unknown_non_utf8() { 145 | let unknown_pair = FieldMap::parse_bytes(&b"some_key\x80:some_value_\x80"[..]).unwrap(); 146 | assert_eq!(unknown_pair, 147 | FieldMap::unknown(String::from_utf8_lossy(&b"some_key\x80".to_vec()), 148 | String::from_utf8_lossy(&b"some_value_\x80".to_vec()))); 149 | } 150 | 151 | #[test] 152 | fn positive_upnp() { 153 | let upnp_pair = FieldMap::new("upnp:some_value").unwrap(); 154 | assert_eq!(upnp_pair, FieldMap::upnp("some_value")); 155 | } 156 | 157 | #[test] 158 | fn positive_uuid() { 159 | let uuid_pair = FieldMap::new("uuid:some_value").unwrap(); 160 | assert_eq!(uuid_pair, FieldMap::uuid("some_value")); 161 | } 162 | 163 | #[test] 164 | fn positive_urn() { 165 | let urn_pair = FieldMap::new("urn:some_value").unwrap(); 166 | assert_eq!(urn_pair, FieldMap::urn("some_value")); 167 | } 168 | 169 | #[test] 170 | fn positive_unknown() { 171 | let unknown_pair = FieldMap::new("some_key:some_value").unwrap(); 172 | assert_eq!(unknown_pair, FieldMap::unknown("some_key", "some_value")); 173 | } 174 | 175 | #[test] 176 | #[should_panic] 177 | fn negative_no_colon() { 178 | FieldMap::new("upnpsome_value").unwrap(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/message/search.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fmt::Debug; 3 | use std::net::ToSocketAddrs; 4 | use std::time::Duration; 5 | use std::io; 6 | 7 | use hyper::header::{Header, HeaderFormat}; 8 | 9 | use error::SSDPResult; 10 | use header::{HeaderRef, HeaderMut, MX}; 11 | use message::{self, MessageType, Listen, Config}; 12 | use message::ssdp::SSDPMessage; 13 | use message::multicast::{self, Multicast}; 14 | use receiver::{SSDPReceiver, FromRawSSDP}; 15 | use net; 16 | 17 | 18 | /// Overhead to add to device response times to account for transport time. 19 | const NETWORK_TIMEOUT_OVERHEAD: u8 = 1; 20 | 21 | /// Devices are required to respond within 1 second of receiving unicast message. 22 | const DEFAULT_UNICAST_TIMEOUT: u8 = 1 + NETWORK_TIMEOUT_OVERHEAD; 23 | 24 | /// Search request that can be sent via unicast or multicast to devices on the network. 25 | #[derive(Debug, Clone)] 26 | pub struct SearchRequest { 27 | message: SSDPMessage, 28 | } 29 | 30 | impl SearchRequest { 31 | /// Construct a new SearchRequest. 32 | pub fn new() -> SearchRequest { 33 | SearchRequest { message: SSDPMessage::new(MessageType::Search) } 34 | } 35 | 36 | /// Send this search request to a single host. 37 | /// 38 | /// Currently this sends the unicast message on all available network 39 | /// interfaces. This assumes that the network interfaces are operating 40 | /// on either different subnets or different ip address ranges. 41 | pub fn unicast(&mut self, dst_addr: A) -> SSDPResult> { 42 | let mode = try!(net::IpVersionMode::from_addr(&dst_addr)); 43 | let mut connectors = try!(message::all_local_connectors(None, &mode)); 44 | 45 | // Send On All Connectors 46 | for connector in &mut connectors { 47 | try!(self.message.send(connector, &dst_addr)); 48 | } 49 | 50 | let mut raw_connectors = Vec::with_capacity(connectors.len()); 51 | raw_connectors.extend(connectors.into_iter().map(|conn| conn.deconstruct())); 52 | 53 | let opt_timeout = opt_unicast_timeout(self.get::()); 54 | 55 | Ok(try!(SSDPReceiver::new(raw_connectors, opt_timeout))) 56 | } 57 | } 58 | 59 | impl Multicast for SearchRequest { 60 | type Item = SSDPReceiver; 61 | 62 | fn multicast_with_config(&self, config: &Config) -> SSDPResult { 63 | let connectors = multicast::send(&self.message, config)?; 64 | 65 | let mcast_timeout = try!(multicast_timeout(self.get::())); 66 | let mut raw_connectors = Vec::with_capacity(connectors.len()); 67 | raw_connectors.extend(connectors.into_iter().map(|conn| conn.deconstruct())); 68 | 69 | Ok(try!(SSDPReceiver::new(raw_connectors, Some(mcast_timeout)))) 70 | } 71 | } 72 | 73 | impl Default for SearchRequest { 74 | fn default() -> Self { 75 | SearchRequest::new() 76 | } 77 | } 78 | 79 | /// Get the require timeout to use for a multicast search request. 80 | fn multicast_timeout(mx: Option<&MX>) -> SSDPResult { 81 | match mx { 82 | Some(&MX(n)) => Ok(Duration::new((n + NETWORK_TIMEOUT_OVERHEAD) as u64, 0)), 83 | None => try!(Err("Multicast Searches Require An MX Header")), 84 | } 85 | } 86 | 87 | /// Get the default timeout to use for a unicast search request. 88 | fn opt_unicast_timeout(mx: Option<&MX>) -> Option { 89 | match mx { 90 | Some(&MX(n)) => Some(Duration::new((n + NETWORK_TIMEOUT_OVERHEAD) as u64, 0)), 91 | None => Some(Duration::new(DEFAULT_UNICAST_TIMEOUT as u64, 0)), 92 | } 93 | } 94 | 95 | impl FromRawSSDP for SearchRequest { 96 | fn raw_ssdp(bytes: &[u8]) -> SSDPResult { 97 | let message = try!(SSDPMessage::raw_ssdp(bytes)); 98 | 99 | if message.message_type() != MessageType::Search { 100 | try!(Err("SSDP Message Received Is Not A SearchRequest")) 101 | } else { 102 | Ok(SearchRequest { message: message }) 103 | } 104 | } 105 | } 106 | 107 | impl HeaderRef for SearchRequest { 108 | fn get(&self) -> Option<&H> 109 | where H: Header + HeaderFormat 110 | { 111 | self.message.get::() 112 | } 113 | 114 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 115 | self.message.get_raw(name) 116 | } 117 | } 118 | 119 | impl HeaderMut for SearchRequest { 120 | fn set(&mut self, value: H) 121 | where H: Header + HeaderFormat 122 | { 123 | self.message.set(value) 124 | } 125 | 126 | fn set_raw(&mut self, name: K, value: Vec>) 127 | where K: Into> + Debug 128 | { 129 | self.message.set_raw(name, value) 130 | } 131 | } 132 | 133 | /// Search response that can be received or sent via unicast to devices on the network. 134 | #[derive(Debug, Clone)] 135 | pub struct SearchResponse { 136 | message: SSDPMessage, 137 | } 138 | 139 | impl SearchResponse { 140 | /// Construct a new SearchResponse. 141 | pub fn new() -> SearchResponse { 142 | SearchResponse { message: SSDPMessage::new(MessageType::Response) } 143 | } 144 | 145 | /// Send this search response to a single host. 146 | /// 147 | /// Currently this sends the unicast message on all available network 148 | /// interfaces. This assumes that the network interfaces are operating 149 | /// on either different subnets or different ip address ranges. 150 | pub fn unicast(&mut self, dst_addr: A) -> SSDPResult<()> { 151 | let mode = try!(net::IpVersionMode::from_addr(&dst_addr)); 152 | let mut connectors = try!(message::all_local_connectors(None, &mode)); 153 | 154 | let mut success_count = 0; 155 | let mut error_count = 0; 156 | // Send On All Connectors 157 | for conn in &mut connectors { 158 | // Some routing errors are expected, not all interfaces can find the target addresses 159 | match self.message.send(conn, &dst_addr) { 160 | Ok(_) => success_count += 1, 161 | Err(_) => error_count += 1, 162 | } 163 | } 164 | 165 | if success_count == 0 && error_count > 0 { 166 | try!(Err(io::Error::last_os_error())); 167 | } 168 | 169 | Ok(()) 170 | } 171 | } 172 | 173 | impl Default for SearchResponse { 174 | fn default() -> Self { 175 | SearchResponse::new() 176 | } 177 | } 178 | 179 | /// Search listener that can listen for search messages sent within the network. 180 | pub struct SearchListener; 181 | 182 | impl Listen for SearchListener { 183 | type Message = SearchResponse; 184 | } 185 | 186 | impl FromRawSSDP for SearchResponse { 187 | fn raw_ssdp(bytes: &[u8]) -> SSDPResult { 188 | let message = try!(SSDPMessage::raw_ssdp(bytes)); 189 | 190 | if message.message_type() != MessageType::Response { 191 | try!(Err("SSDP Message Received Is Not A SearchResponse")) 192 | } else { 193 | Ok(SearchResponse { message: message }) 194 | } 195 | } 196 | } 197 | 198 | impl HeaderRef for SearchResponse { 199 | fn get(&self) -> Option<&H> 200 | where H: Header + HeaderFormat 201 | { 202 | self.message.get::() 203 | } 204 | 205 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 206 | self.message.get_raw(name) 207 | } 208 | } 209 | 210 | impl HeaderMut for SearchResponse { 211 | fn set(&mut self, value: H) 212 | where H: Header + HeaderFormat 213 | { 214 | self.message.set(value) 215 | } 216 | 217 | fn set_raw(&mut self, name: K, value: Vec>) 218 | where K: Into> + Debug 219 | { 220 | self.message.set_raw(name, value) 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod tests { 226 | use header::MX; 227 | 228 | #[test] 229 | fn positive_multicast_timeout() { 230 | super::multicast_timeout(Some(&MX(5))).unwrap(); 231 | } 232 | 233 | #[test] 234 | fn positive_some_opt_multicast_timeout() { 235 | super::opt_unicast_timeout(Some(&MX(5))).unwrap(); 236 | } 237 | 238 | #[test] 239 | fn positive_none_opt_multicast_timeout() { 240 | super::opt_unicast_timeout(None).unwrap(); 241 | } 242 | 243 | #[test] 244 | #[should_panic] 245 | fn negative_multicast_timeout() { 246 | super::multicast_timeout(None).unwrap(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2016] [ssdp-rs Developers] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/message/ssdp.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::{Cow, ToOwned}; 2 | use std::fmt::Debug; 3 | use std::io::Write; 4 | use std::net::{ToSocketAddrs, SocketAddr}; 5 | 6 | use hyper::Url; 7 | use hyper::buffer::BufReader; 8 | use hyper::client::request::Request; 9 | use hyper::header::{Headers, Header, HeaderFormat, ContentLength, Host}; 10 | use hyper::http::RawStatus; 11 | use hyper::http::h1::{self, Incoming}; 12 | use hyper::method::Method; 13 | use hyper::net::{NetworkConnector, NetworkStream}; 14 | use hyper::server::response::Response; 15 | use hyper::status::StatusCode; 16 | use hyper::uri::RequestUri; 17 | use hyper::version::HttpVersion; 18 | 19 | use {SSDPResult, SSDPErrorKind}; 20 | use header::{HeaderRef, HeaderMut}; 21 | use message::MessageType; 22 | use net; 23 | use receiver::FromRawSSDP; 24 | 25 | 26 | /// Only Valid `SearchResponse` Code 27 | const VALID_RESPONSE_CODE: u16 = 200; 28 | 29 | /// Appended To Destination Socket Addresses For URLs 30 | const BASE_HOST_URL: &'static str = "http://"; 31 | 32 | /// Case-Sensitive Method Names 33 | const NOTIFY_METHOD: &'static str = "NOTIFY"; 34 | const SEARCH_METHOD: &'static str = "M-SEARCH"; 35 | 36 | /// Represents an SSDP method combined with both SSDP and HTTP headers. 37 | #[derive(Debug, Clone)] 38 | pub struct SSDPMessage { 39 | method: MessageType, 40 | headers: Headers, 41 | } 42 | 43 | impl SSDPMessage { 44 | /// Construct a new SSDPMessage. 45 | pub fn new(message_type: MessageType) -> SSDPMessage { 46 | SSDPMessage { 47 | method: message_type, 48 | headers: Headers::new(), 49 | } 50 | } 51 | 52 | /// Get the type of this message. 53 | pub fn message_type(&self) -> MessageType { 54 | self.method 55 | } 56 | 57 | /// Send this request to the given destination address using the given connector. 58 | /// 59 | /// The host header field will be taken care of by the underlying library. 60 | pub fn send(&self, connector: &mut C, dst_addr: A) -> SSDPResult<()> 61 | where C: NetworkConnector, 62 | S: Into> 63 | { 64 | let dst_sock_addr = try!(net::addr_from_trait(dst_addr)); 65 | match self.method { 66 | MessageType::Notify => { 67 | trace!("Notify to: {:?}", dst_sock_addr); 68 | send_request(NOTIFY_METHOD, &self.headers, connector, dst_sock_addr) 69 | } 70 | MessageType::Search => { 71 | trace!("Sending search request..."); 72 | send_request(SEARCH_METHOD, &self.headers, connector, dst_sock_addr) 73 | } 74 | MessageType::Response => { 75 | trace!("Sending response to: {:?}", dst_sock_addr); 76 | // This might need fixing for IPV6, passing down the IP loses the scope information 77 | let dst_ip_string = dst_sock_addr.ip().to_string(); 78 | let dst_port = dst_sock_addr.port(); 79 | 80 | let net_stream = try!(connector.connect(&dst_ip_string[..], dst_port, "")).into(); 81 | 82 | send_response(&self.headers, net_stream) 83 | } 84 | } 85 | } 86 | } 87 | 88 | #[allow(unused)] 89 | /// Send a request using the connector with the supplied method and headers. 90 | fn send_request(method: &str, 91 | headers: &Headers, 92 | connector: &mut C, 93 | dst_addr: SocketAddr) 94 | -> SSDPResult<()> 95 | where C: NetworkConnector, 96 | S: Into> 97 | { 98 | trace!("Trying to parse url..."); 99 | let url = try!(url_from_addr(dst_addr)); 100 | trace!("Url: {}", url); 101 | 102 | let mut request = try!(Request::with_connector(Method::Extension(method.to_owned()), url, connector)); 103 | 104 | trace!("Copying headers..."); 105 | copy_headers(headers, request.headers_mut()); 106 | trace!("Setting length"); 107 | request.headers_mut().set(ContentLength(0)); 108 | 109 | // Send Will Always Fail Within The UdpConnector Which Is Intended So That 110 | // Hyper Does Not Block For A Response Since We Are Handling That Ourselves. 111 | 112 | trace!("actual .send ..."); 113 | try!(request.start()).send(); 114 | 115 | Ok(()) 116 | } 117 | 118 | /// Send an Ok response on the Writer with the supplied headers. 119 | fn send_response(headers: &Headers, mut dst_writer: W) -> SSDPResult<()> 120 | where W: Write 121 | { 122 | let mut temp_headers = Headers::new(); 123 | 124 | copy_headers(headers, &mut temp_headers); 125 | temp_headers.set(ContentLength(0)); 126 | 127 | let mut response = Response::new(&mut dst_writer as &mut Write, &mut temp_headers); 128 | *response.status_mut() = StatusCode::Ok; 129 | 130 | // Have to make sure response is destroyed here for lifetime issues with temp_headers 131 | try!(try!(response.start()).end()); 132 | 133 | Ok(()) 134 | } 135 | 136 | /// Convert the given address to a Url with a base of "udp://". 137 | fn url_from_addr(addr: SocketAddr) -> SSDPResult { 138 | let str_url = BASE_HOST_URL.chars() 139 | .chain(addr.to_string()[..].chars()) 140 | .collect::(); 141 | 142 | Ok(try!(Url::parse(&str_url[..]))) 143 | } 144 | 145 | /// Copy the headers from the source header to the destination header. 146 | fn copy_headers(src_headers: &Headers, dst_headers: &mut Headers) { 147 | // Not the best solution since we are doing a lot of string 148 | // allocations for no benefit other than to transfer the headers. 149 | 150 | // TODO: See if there is a way around calling to_owned() since set_raw 151 | // requires a Cow<'static, _> and we only have access to Cow<'a, _>. 152 | let iter = src_headers.iter(); 153 | for view in iter { 154 | dst_headers.set_raw(Cow::Owned(view.name().to_owned()), vec![view.value_string().into_bytes()]); 155 | } 156 | } 157 | 158 | impl HeaderRef for SSDPMessage { 159 | fn get(&self) -> Option<&H> 160 | where H: Header + HeaderFormat 161 | { 162 | HeaderRef::get::(&self.headers) 163 | } 164 | 165 | fn get_raw(&self, name: &str) -> Option<&[Vec]> { 166 | HeaderRef::get_raw(&self.headers, name) 167 | } 168 | } 169 | 170 | impl HeaderMut for SSDPMessage { 171 | fn set(&mut self, value: H) 172 | where H: Header + HeaderFormat 173 | { 174 | HeaderMut::set(&mut self.headers, value) 175 | } 176 | 177 | fn set_raw(&mut self, name: K, value: Vec>) 178 | where K: Into> + Debug 179 | { 180 | HeaderMut::set_raw(&mut self.headers, name, value) 181 | } 182 | } 183 | 184 | impl FromRawSSDP for SSDPMessage { 185 | fn raw_ssdp(bytes: &[u8]) -> SSDPResult { 186 | let mut buf_reader = BufReader::new(bytes); 187 | 188 | if let Ok(parts) = h1::parse_request(&mut buf_reader) { 189 | let message_result = message_from_request(parts); 190 | 191 | log_message_result(&message_result, bytes); 192 | message_result 193 | } else { 194 | match h1::parse_response(&mut buf_reader) { 195 | Ok(parts) => { 196 | let message_result = message_from_response(parts); 197 | 198 | log_message_result(&message_result, bytes); 199 | message_result 200 | }, 201 | Err(err) => { 202 | debug!("Failed parsing http response: {}, data: {}", err, String::from_utf8_lossy(bytes)); 203 | 204 | Err(SSDPErrorKind::InvalidHttp(bytes.to_owned()).into()) 205 | } 206 | } 207 | } 208 | } 209 | } 210 | 211 | /// Logs a debug! message based on the value of the `SSDPResult`. 212 | fn log_message_result(result: &SSDPResult, message: &[u8]) { 213 | match *result { 214 | Ok(_) => debug!("Received Valid SSDPMessage:\n{}", String::from_utf8_lossy(message)), 215 | Err(ref e) => debug!("Received Invalid SSDPMessage Error: {}", e), 216 | } 217 | } 218 | 219 | /// Attempts to construct an `SSDPMessage` from the given request pieces. 220 | fn message_from_request(parts: Incoming<(Method, RequestUri)>) -> SSDPResult { 221 | let headers = parts.headers; 222 | 223 | try!(validate_http_version(parts.version)); 224 | try!(validate_http_host(&headers)); 225 | 226 | match parts.subject { 227 | (Method::Extension(n), RequestUri::Star) => { 228 | match &n[..] { 229 | NOTIFY_METHOD => { 230 | Ok(SSDPMessage { 231 | method: MessageType::Notify, 232 | headers: headers, 233 | }) 234 | } 235 | SEARCH_METHOD => { 236 | Ok(SSDPMessage { 237 | method: MessageType::Search, 238 | headers: headers, 239 | }) 240 | } 241 | _ => Err(SSDPErrorKind::InvalidMethod(n).into()), 242 | } 243 | } 244 | (n, RequestUri::Star) => Err(SSDPErrorKind::InvalidMethod(n.to_string()).into()), 245 | (_, RequestUri::AbsolutePath(n)) | 246 | (_, RequestUri::Authority(n)) => Err(SSDPErrorKind::InvalidUri(n).into()), 247 | (_, RequestUri::AbsoluteUri(n)) => Err(SSDPErrorKind::InvalidUri(n.into_string()).into()), 248 | } 249 | } 250 | 251 | /// Attempts to construct an `SSDPMessage` from the given response pieces. 252 | fn message_from_response(parts: Incoming) -> SSDPResult { 253 | let RawStatus(status_code, _) = parts.subject; 254 | let headers = parts.headers; 255 | 256 | try!(validate_http_version(parts.version)); 257 | try!(validate_response_code(status_code)); 258 | 259 | Ok(SSDPMessage { 260 | method: MessageType::Response, 261 | headers: headers, 262 | }) 263 | } 264 | 265 | /// Validate the HTTP version for an SSDP message. 266 | fn validate_http_version(version: HttpVersion) -> SSDPResult<()> { 267 | if version != HttpVersion::Http11 { 268 | Err(SSDPErrorKind::InvalidHttpVersion.into()) 269 | } else { 270 | Ok(()) 271 | } 272 | } 273 | 274 | /// Validate that the Host header is present. 275 | fn validate_http_host(headers: T) -> SSDPResult<()> 276 | where T: HeaderRef 277 | { 278 | // Shouldn't have to do this but hyper doesn't make sure that HTTP/1.1 279 | // messages contain Host headers so we will assure conformance ourselves. 280 | if headers.get::().is_none() { 281 | Err(SSDPErrorKind::MissingHeader(Host::header_name()).into()) 282 | } else { 283 | Ok(()) 284 | } 285 | } 286 | 287 | /// Validate the response code for an SSDP message. 288 | fn validate_response_code(code: u16) -> SSDPResult<()> { 289 | if code != VALID_RESPONSE_CODE { 290 | Err(SSDPErrorKind::ResponseCode(code).into()) 291 | } else { 292 | Ok(()) 293 | } 294 | } 295 | 296 | #[cfg(test)] 297 | mod mocks { 298 | use std::cell::RefCell; 299 | use std::io::{self, Read, Write, ErrorKind}; 300 | use std::net::SocketAddr; 301 | use std::time::Duration; 302 | use std::sync::mpsc::{self, Sender, Receiver}; 303 | 304 | use hyper::error; 305 | use hyper::net::{NetworkConnector, NetworkStream}; 306 | 307 | pub struct MockConnector { 308 | pub receivers: RefCell>>>, 309 | } 310 | 311 | impl MockConnector { 312 | pub fn new() -> MockConnector { 313 | MockConnector { receivers: RefCell::new(Vec::new()) } 314 | } 315 | } 316 | 317 | impl NetworkConnector for MockConnector { 318 | type Stream = MockStream; 319 | 320 | fn connect(&self, _: &str, _: u16, _: &str) -> error::Result { 321 | let (send, recv) = mpsc::channel(); 322 | 323 | self.receivers.borrow_mut().push(recv); 324 | 325 | Ok(MockStream { sender: send }) 326 | } 327 | } 328 | 329 | pub struct MockStream { 330 | sender: Sender>, 331 | } 332 | 333 | impl NetworkStream for MockStream { 334 | fn peer_addr(&mut self) -> io::Result { 335 | Err(io::Error::new(ErrorKind::AddrNotAvailable, "")) 336 | } 337 | fn set_read_timeout(&self, _dur: Option) -> io::Result<()> { 338 | Ok(()) 339 | } 340 | fn set_write_timeout(&self, _dur: Option) -> io::Result<()> { 341 | Ok(()) 342 | } 343 | } 344 | 345 | impl Read for MockStream { 346 | fn read(&mut self, _: &mut [u8]) -> io::Result { 347 | Err(io::Error::new(ErrorKind::ConnectionAborted, "")) 348 | } 349 | } 350 | 351 | impl Write for MockStream { 352 | fn write(&mut self, buf: &[u8]) -> io::Result { 353 | // Hyper will generate a request with a /, we need to intercept that. 354 | let mut buffer = vec![0u8; buf.len()]; 355 | 356 | let mut found = false; 357 | for (src, dst) in buf.iter().zip(buffer.iter_mut()) { 358 | if *src == b'/' && !found && buf[0] != b'H' { 359 | *dst = b'*'; 360 | found = true; 361 | } else { 362 | *dst = *src; 363 | } 364 | } 365 | 366 | self.sender.send(buffer).unwrap(); 367 | 368 | Ok(buf.len()) 369 | } 370 | 371 | fn flush(&mut self) -> io::Result<()> { 372 | Ok(()) 373 | } 374 | } 375 | } 376 | 377 | #[cfg(test)] 378 | mod tests { 379 | mod send { 380 | use std::sync::mpsc::Receiver; 381 | 382 | use super::super::mocks::MockConnector; 383 | use super::super::SSDPMessage; 384 | use message::MessageType; 385 | 386 | fn join_buffers(recv_list: &[Receiver>]) -> Vec { 387 | let mut buffer = Vec::new(); 388 | 389 | for recv in recv_list { 390 | for recv_buf in recv { 391 | buffer.extend(&recv_buf[..]) 392 | } 393 | } 394 | 395 | buffer 396 | } 397 | 398 | #[test] 399 | fn positive_search_method_line() { 400 | let message = SSDPMessage::new(MessageType::Search); 401 | let mut connector = MockConnector::new(); 402 | 403 | message.send(&mut connector, ("127.0.0.1", 0)).unwrap(); 404 | 405 | let sent_message = String::from_utf8(join_buffers(&*connector.receivers.borrow())).unwrap(); 406 | 407 | assert_eq!(&sent_message[..19], "M-SEARCH * HTTP/1.1"); 408 | } 409 | 410 | #[test] 411 | fn positive_notify_method_line() { 412 | let message = SSDPMessage::new(MessageType::Notify); 413 | let mut connector = MockConnector::new(); 414 | 415 | message.send(&mut connector, ("127.0.0.1", 0)).unwrap(); 416 | 417 | let sent_message = String::from_utf8(join_buffers(&*connector.receivers.borrow())).unwrap(); 418 | 419 | assert_eq!(&sent_message[..17], "NOTIFY * HTTP/1.1"); 420 | } 421 | 422 | #[test] 423 | fn positive_response_method_line() { 424 | let message = SSDPMessage::new(MessageType::Response); 425 | let mut connector = MockConnector::new(); 426 | 427 | message.send(&mut connector, ("127.0.0.1", 0)).unwrap(); 428 | 429 | let sent_message = String::from_utf8(join_buffers(&*connector.receivers.borrow())).unwrap(); 430 | 431 | assert_eq!(&sent_message[..15], "HTTP/1.1 200 OK"); 432 | } 433 | 434 | #[test] 435 | fn positive_host_header() { 436 | let message = SSDPMessage::new(MessageType::Search); 437 | let mut connector = MockConnector::new(); 438 | 439 | message.send(&mut connector, ("127.0.0.1", 0)).unwrap(); 440 | 441 | let sent_message = String::from_utf8(join_buffers(&*connector.receivers.borrow())).unwrap(); 442 | 443 | assert!(sent_message.contains("Host: 127.0.0.1:0")); 444 | } 445 | } 446 | 447 | mod parse { 448 | use super::super::SSDPMessage; 449 | use header::HeaderRef; 450 | use receiver::FromRawSSDP; 451 | 452 | #[test] 453 | fn positive_valid_http() { 454 | let raw_message = "NOTIFY * HTTP/1.1\r\nHOST: 192.168.1.1\r\n\r\n"; 455 | 456 | SSDPMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 457 | } 458 | 459 | #[test] 460 | fn positive_intact_header() { 461 | let raw_message = "NOTIFY * HTTP/1.1\r\nHOST: 192.168.1.1\r\n\r\n"; 462 | let message = SSDPMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 463 | 464 | assert_eq!(&message.get_raw("Host").unwrap()[0][..], &b"192.168.1.1"[..]); 465 | } 466 | 467 | #[test] 468 | #[should_panic] 469 | fn negative_http_version() { 470 | let raw_message = "NOTIFY * HTTP/2.0\r\nHOST: 192.168.1.1\r\n\r\n"; 471 | 472 | SSDPMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 473 | } 474 | 475 | #[test] 476 | #[should_panic] 477 | fn negative_no_host() { 478 | let raw_message = "NOTIFY * HTTP/1.1\r\n\r\n"; 479 | 480 | SSDPMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 481 | } 482 | 483 | #[test] 484 | #[should_panic] 485 | fn negative_path_included() { 486 | let raw_message = "NOTIFY / HTTP/1.1\r\n\r\n"; 487 | 488 | SSDPMessage::raw_ssdp(raw_message.as_bytes()).unwrap(); 489 | } 490 | } 491 | } 492 | --------------------------------------------------------------------------------