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}