expose_rs/
expose.rs

1use core::str;
2use std::{ net::{ Ipv4Addr, UdpSocket }, time::Duration };
3
4use crate::{ find_lines, utils::{ format_url, get_bind, find_line, get_xml_tag_childs, open_http, Url } };
5
6macro_rules! soap {
7    ($($body:expr),+) => {
8        "<?xml version=\"1.0\"?>\r\n\
9        <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n\
10        <s:Body>\r\n".to_owned() + $($body + "\r\n" +)+ "</s:Body>\r\n\
11        </s:Envelope>\r\n"
12    };
13}
14
15macro_rules! component {
16    ($name:expr, $content:expr) => {
17        &format!("<u:{} xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">{}</u:{}>", $name, $content, $name)
18    };
19}
20
21pub struct Session {
22    pub device_addr: String,
23    pub name:        String,
24    pub local_ip:    String,
25    pub endpoint:    String,
26}
27
28const MULTICASTADDR: &str = "239.255.255.250:1900";
29
30pub fn discover () -> Result<Session, std::io::Error> {
31    let discovery_message = format!("M-SEARCH * HTTP/1.1\r\n\
32    HOST: {}\r\n\
33    MAN: \"ssdp:discover\"\r\n\
34    MX: 2\r\n\
35    ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n\
36    \r\n", MULTICASTADDR);
37
38    let bind = get_bind()?;
39    let socket = UdpSocket::bind(bind).expect("Exposer failed to bind socket");
40    socket.set_broadcast(true)?;
41    socket.set_read_timeout(Some(Duration::from_secs(5)))?;
42
43    socket.send_to(discovery_message.as_bytes(), MULTICASTADDR)?;
44
45    let mut buf = [0; 2048];
46    let amt = socket.recv(&mut buf)?;
47
48    let response = str::from_utf8(&buf[..amt]).unwrap();
49
50    let (location, server) = find_lines!(response.lines(), "location", "server");
51
52    match (location, server) {
53        (Some(location), Some(server)) => {
54            let url = format_url(location);
55
56            let igd = open_http(&url, "GET", "")?;
57            if let Some(x) = igd.find("urn:upnp-org:serviceId:WANIPConn1") {
58                let endpoint = get_xml_tag_childs(&igd[x..], "controlURL");
59                if let Some(endpoint) = endpoint {
60                    return Ok(Session {
61                        endpoint: endpoint.replacen("/", "", 1),
62                        device_addr: url.host,
63                        name: server.split_whitespace().last().unwrap().to_string(),
64                        local_ip: bind.ip().to_string()
65                    })
66                }
67            }
68
69            return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Not found endpoint for WANIPConnection1"))
70        },
71        _ => Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Not found location and name of device"))
72    }
73}
74
75pub fn get_external_ip (session: &Session) -> Result<Ipv4Addr, std::io::Error> {
76    let soap_request = soap!(
77        component!("GetExternalIPAddress", "")
78    );
79
80    let response = open_http(&Url {
81        protocol: "http".to_string(),
82        host: session.device_addr.clone(),
83        path: session.endpoint.clone()
84    }, "POST",
85    &format!("CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n\
86    SOAPACTION: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress\"\r\n\
87    CONTENT-LENGTH: {}\r\n\
88    \r\n\
89    {soap_request}", soap_request.len()))?;
90
91    if let Some(ip) = get_xml_tag_childs(&response, "NewExternalIPAddress") {
92        return Ok(ip.parse::<Ipv4Addr>().unwrap())
93    }
94    else {
95        Err(std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, "Cannot get external IP address"))
96    }
97}
98
99pub fn forward_port (session: &Session, protocol: &str, internal_port: u16, external_port: u16, description: &str, duration: usize) -> Result<(), std::io::Error> {
100    let local_ip = &session.local_ip;
101    let soap_request = soap!(
102        component!("AddPortMapping", format!("<NewRemoteHost></NewRemoteHost>\r\n\
103        <NewExternalPort>{external_port}</NewExternalPort>\r\n\
104        <NewProtocol>{protocol}</NewProtocol>\r\n\
105        <NewInternalPort>{internal_port}</NewInternalPort>\r\n\
106        <NewInternalClient>{local_ip}</NewInternalClient>
107        <NewEnabled>1</NewEnabled>\r\n\
108        <NewPortMappingDescription>{description}</NewPortMappingDescription>\r\n\
109        <NewLeaseDuration>{duration}</NewLeaseDuration>
110        "))
111    );
112
113    open_http(&Url {
114        protocol: "http".to_string(),
115        host: session.device_addr.clone(),
116        path: session.endpoint.clone()
117    }, "POST", &format!("CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n\
118    SOAPACTION: \"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"\r\n\
119    CONTENT-LENGTH: {}\r\n\
120    \r\n\
121    {soap_request}", soap_request.len()))?;
122    
123    Ok(())
124}
125
126pub fn remove_port (session: &Session, protocol: &str, external_port: u16) -> Result<(), std::io::Error> {
127    let soap_request = soap!(
128        component!("DeletePortMapping", format!("<NewRemoteHost></NewRemoteHost>\r\n\
129        <NewExternalPort>{external_port}</NewExternalPort>\r\n\
130        <NewProtocol>{protocol}</NewProtocol>\r\n\
131        "))
132    );
133    
134    open_http(&Url {
135        protocol: "http".to_string(),
136        host: session.device_addr.clone(),
137        path: session.endpoint.clone()
138    }, "POST", &format!("CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n\
139    SOAPACTION: \"urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping\"\r\n\
140    CONTENT-LENGTH: {}\r\n\
141    \r\n\
142    {soap_request}", soap_request.len()))?;
143
144    Ok(())
145}