1use std::collections::HashMap;
2use std::io;
3use std::net::{IpAddr, SocketAddr};
4
5use url::Url;
6use xmltree::{self, Element};
7
8use crate::errors::{
9 AddAnyPortError, AddPortError, GetExternalIpError, GetGenericPortMappingEntryError, RemovePortError, RequestError,
10 SearchError,
11};
12use crate::PortMappingProtocol;
13
14pub fn parse_search_result(text: &str) -> Result<(SocketAddr, String), SearchError> {
16 use SearchError::InvalidResponse;
17
18 for line in text.lines() {
19 let line = line.trim();
20 if line.to_ascii_lowercase().starts_with("location:") {
21 if let Some(colon) = line.find(':') {
22 let url_text = &line[colon + 1..].trim();
23 let url = Url::parse(url_text).map_err(|_| InvalidResponse)?;
24 let addr: IpAddr = url
25 .host_str()
26 .ok_or(InvalidResponse)
27 .and_then(|s| s.parse().map_err(|_| InvalidResponse))?;
28 let port: u16 = url.port_or_known_default().ok_or(InvalidResponse)?;
29
30 return Ok((SocketAddr::new(addr, port), url.path().to_string()));
31 }
32 }
33 }
34 Err(InvalidResponse)
35}
36
37pub fn parse_control_urls<R>(resp: R) -> Result<(String, String), SearchError>
38where
39 R: io::Read,
40{
41 let root = Element::parse(resp)?;
42
43 let mut urls = root.children.iter().filter_map(|child| {
44 let child = child.as_element()?;
45 if child.name == "device" {
46 Some(parse_device(child)?)
47 } else {
48 None
49 }
50 });
51
52 urls.next().ok_or(SearchError::InvalidResponse)
53}
54
55fn parse_device(device: &Element) -> Option<(String, String)> {
56 let services = device.get_child("serviceList").and_then(|service_list| {
57 service_list
58 .children
59 .iter()
60 .filter_map(|child| {
61 let child = child.as_element()?;
62 if child.name == "service" {
63 parse_service(child)
64 } else {
65 None
66 }
67 })
68 .next()
69 });
70 let devices = device.get_child("deviceList").and_then(parse_device_list);
71 services.or(devices)
72}
73
74fn parse_device_list(device_list: &Element) -> Option<(String, String)> {
75 device_list
76 .children
77 .iter()
78 .filter_map(|child| {
79 let child = child.as_element()?;
80 if child.name == "device" {
81 parse_device(child)
82 } else {
83 None
84 }
85 })
86 .next()
87}
88
89fn parse_service(service: &Element) -> Option<(String, String)> {
90 let service_type = service.get_child("serviceType")?;
91 let service_type = service_type
92 .get_text()
93 .map(|s| s.into_owned())
94 .unwrap_or_else(|| "".into());
95 if [
96 "urn:schemas-upnp-org:service:WANPPPConnection:1",
97 "urn:schemas-upnp-org:service:WANIPConnection:1",
98 "urn:schemas-upnp-org:service:WANIPConnection:2",
99 ]
100 .contains(&service_type.as_str())
101 {
102 let scpd_url = service.get_child("SCPDURL");
103 let control_url = service.get_child("controlURL");
104 if let (Some(scpd_url), Some(control_url)) = (scpd_url, control_url) {
105 Some((
106 scpd_url.get_text().map(|s| s.into_owned()).unwrap_or_else(|| "".into()),
107 control_url
108 .get_text()
109 .map(|s| s.into_owned())
110 .unwrap_or_else(|| "".into()),
111 ))
112 } else {
113 None
114 }
115 } else {
116 None
117 }
118}
119
120pub fn parse_schemas<R>(resp: R) -> Result<HashMap<String, Vec<String>>, SearchError>
121where
122 R: io::Read,
123{
124 let root = Element::parse(resp)?;
125
126 let mut schema = root.children.iter().filter_map(|child| {
127 let child = child.as_element()?;
128 if child.name == "actionList" {
129 parse_action_list(child)
130 } else {
131 None
132 }
133 });
134
135 schema.next().ok_or(SearchError::InvalidResponse)
136}
137
138fn parse_action_list(action_list: &Element) -> Option<HashMap<String, Vec<String>>> {
139 Some(
140 action_list
141 .children
142 .iter()
143 .filter_map(|child| {
144 let child = child.as_element()?;
145 if child.name == "action" {
146 parse_action(child)
147 } else {
148 None
149 }
150 })
151 .collect(),
152 )
153}
154
155fn parse_action(action: &Element) -> Option<(String, Vec<String>)> {
156 Some((
157 action.get_child("name")?.get_text()?.into_owned(),
158 parse_argument_list(action.get_child("argumentList")?)?,
159 ))
160}
161
162fn parse_argument_list(argument_list: &Element) -> Option<Vec<String>> {
163 Some(
164 argument_list
165 .children
166 .iter()
167 .filter_map(|child| {
168 let child = child.as_element()?;
169 if child.name == "argument" {
170 parse_argument(child)
171 } else {
172 None
173 }
174 })
175 .collect(),
176 )
177}
178
179fn parse_argument(action: &Element) -> Option<String> {
180 if action.get_child("direction")?.get_text()?.into_owned().as_str() == "in" {
181 Some(action.get_child("name")?.get_text()?.into_owned())
182 } else {
183 None
184 }
185}
186
187pub struct RequestReponse {
188 text: String,
189 xml: xmltree::Element,
190}
191
192pub type RequestResult = Result<RequestReponse, RequestError>;
193
194pub fn parse_response(text: String, ok: &str) -> RequestResult {
195 let mut xml = match xmltree::Element::parse(text.as_bytes()) {
196 Ok(xml) => xml,
197 Err(..) => return Err(RequestError::InvalidResponse(text)),
198 };
199 let body = match xml.get_mut_child("Body") {
200 Some(body) => body,
201 None => return Err(RequestError::InvalidResponse(text)),
202 };
203 if let Some(ok) = body.take_child(ok) {
204 return Ok(RequestReponse { text, xml: ok });
205 }
206 let upnp_error = match body
207 .get_child("Fault")
208 .and_then(|e| e.get_child("detail"))
209 .and_then(|e| e.get_child("UPnPError"))
210 {
211 Some(upnp_error) => upnp_error,
212 None => return Err(RequestError::InvalidResponse(text)),
213 };
214
215 match (
216 upnp_error.get_child("errorCode"),
217 upnp_error.get_child("errorDescription"),
218 ) {
219 (Some(e), Some(d)) => match (e.get_text().as_ref(), d.get_text().as_ref()) {
220 (Some(et), Some(dt)) => match et.parse::<u16>() {
221 Ok(en) => Err(RequestError::ErrorCode(en, From::from(&dt[..]))),
222 Err(..) => Err(RequestError::InvalidResponse(text)),
223 },
224 _ => Err(RequestError::InvalidResponse(text)),
225 },
226 _ => Err(RequestError::InvalidResponse(text)),
227 }
228}
229
230pub fn parse_get_external_ip_response(result: RequestResult) -> Result<IpAddr, GetExternalIpError> {
231 match result {
232 Ok(resp) => match resp
233 .xml
234 .get_child("NewExternalIPAddress")
235 .and_then(|e| e.get_text())
236 .and_then(|t| t.parse::<IpAddr>().ok())
237 {
238 Some(ipv4_addr) => Ok(ipv4_addr),
239 None => Err(GetExternalIpError::RequestError(RequestError::InvalidResponse(
240 resp.text,
241 ))),
242 },
243 Err(RequestError::ErrorCode(606, _)) => Err(GetExternalIpError::ActionNotAuthorized),
244 Err(e) => Err(GetExternalIpError::RequestError(e)),
245 }
246}
247
248pub fn parse_add_any_port_mapping_response(result: RequestResult) -> Result<u16, AddAnyPortError> {
249 match result {
250 Ok(resp) => {
251 match resp
252 .xml
253 .get_child("NewReservedPort")
254 .and_then(|e| e.get_text())
255 .and_then(|t| t.parse::<u16>().ok())
256 {
257 Some(port) => Ok(port),
258 None => Err(AddAnyPortError::RequestError(RequestError::InvalidResponse(resp.text))),
259 }
260 }
261 Err(err) => Err(match err {
262 RequestError::ErrorCode(605, _) => AddAnyPortError::DescriptionTooLong,
263 RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized,
264 RequestError::ErrorCode(728, _) => AddAnyPortError::NoPortsAvailable,
265 e => AddAnyPortError::RequestError(e),
266 }),
267 }
268}
269
270pub fn convert_add_random_port_mapping_error(error: RequestError) -> Option<AddAnyPortError> {
271 match error {
272 RequestError::ErrorCode(724, _) => None,
273 RequestError::ErrorCode(605, _) => Some(AddAnyPortError::DescriptionTooLong),
274 RequestError::ErrorCode(606, _) => Some(AddAnyPortError::ActionNotAuthorized),
275 RequestError::ErrorCode(718, _) => Some(AddAnyPortError::NoPortsAvailable),
276 RequestError::ErrorCode(725, _) => Some(AddAnyPortError::OnlyPermanentLeasesSupported),
277 e => Some(AddAnyPortError::RequestError(e)),
278 }
279}
280
281pub fn convert_add_same_port_mapping_error(error: RequestError) -> AddAnyPortError {
282 match error {
283 RequestError::ErrorCode(606, _) => AddAnyPortError::ActionNotAuthorized,
284 RequestError::ErrorCode(718, _) => AddAnyPortError::ExternalPortInUse,
285 RequestError::ErrorCode(725, _) => AddAnyPortError::OnlyPermanentLeasesSupported,
286 e => AddAnyPortError::RequestError(e),
287 }
288}
289
290pub fn convert_add_port_error(err: RequestError) -> AddPortError {
291 match err {
292 RequestError::ErrorCode(605, _) => AddPortError::DescriptionTooLong,
293 RequestError::ErrorCode(606, _) => AddPortError::ActionNotAuthorized,
294 RequestError::ErrorCode(718, _) => AddPortError::PortInUse,
295 RequestError::ErrorCode(724, _) => AddPortError::SamePortValuesRequired,
296 RequestError::ErrorCode(725, _) => AddPortError::OnlyPermanentLeasesSupported,
297 e => AddPortError::RequestError(e),
298 }
299}
300
301pub fn parse_delete_port_mapping_response(result: RequestResult) -> Result<(), RemovePortError> {
302 match result {
303 Ok(_) => Ok(()),
304 Err(err) => Err(match err {
305 RequestError::ErrorCode(606, _) => RemovePortError::ActionNotAuthorized,
306 RequestError::ErrorCode(714, _) => RemovePortError::NoSuchPortMapping,
307 e => RemovePortError::RequestError(e),
308 }),
309 }
310}
311
312pub struct PortMappingEntry {
314 pub remote_host: String,
317 pub external_port: u16,
319 pub protocol: PortMappingProtocol,
321 pub internal_port: u16,
323 pub internal_client: String,
326 pub enabled: bool,
328 pub port_mapping_description: String,
330 pub lease_duration: u32,
332}
333
334pub fn parse_get_generic_port_mapping_entry(
335 result: RequestResult,
336) -> Result<PortMappingEntry, GetGenericPortMappingEntryError> {
337 let response = result?;
338 let xml = response.xml;
339 let make_err = |msg: String| || GetGenericPortMappingEntryError::RequestError(RequestError::InvalidResponse(msg));
340 let extract_field = |field: &str| xml.get_child(field).ok_or_else(make_err(format!("{field} is missing")));
341 let remote_host = extract_field("NewRemoteHost")?
342 .get_text()
343 .map(|c| c.into_owned())
344 .unwrap_or_else(|| "".into());
345 let external_port = extract_field("NewExternalPort")?
346 .get_text()
347 .and_then(|t| t.parse::<u16>().ok())
348 .ok_or_else(make_err("Field NewExternalPort is invalid".into()))?;
349 let protocol = match extract_field("NewProtocol")?.get_text() {
350 Some(std::borrow::Cow::Borrowed("UDP")) => PortMappingProtocol::UDP,
351 Some(std::borrow::Cow::Borrowed("TCP")) => PortMappingProtocol::TCP,
352 _ => {
353 return Err(GetGenericPortMappingEntryError::RequestError(
354 RequestError::InvalidResponse("Field NewProtocol is invalid".into()),
355 ))
356 }
357 };
358 let internal_port = extract_field("NewInternalPort")?
359 .get_text()
360 .and_then(|t| t.parse::<u16>().ok())
361 .ok_or_else(make_err("Field NewInternalPort is invalid".into()))?;
362 let internal_client = extract_field("NewInternalClient")?
363 .get_text()
364 .map(|c| c.into_owned())
365 .ok_or_else(make_err("Field NewInternalClient is empty".into()))?;
366 let enabled = match extract_field("NewEnabled")?
367 .get_text()
368 .and_then(|t| t.parse::<u16>().ok())
369 .ok_or_else(make_err("Field Enabled is invalid".into()))?
370 {
371 0 => false,
372 1 => true,
373 _ => {
374 return Err(GetGenericPortMappingEntryError::RequestError(
375 RequestError::InvalidResponse("Field NewEnabled is invalid".into()),
376 ))
377 }
378 };
379 let port_mapping_description = extract_field("NewPortMappingDescription")?
380 .get_text()
381 .map(|c| c.into_owned())
382 .unwrap_or_else(|| "".into());
383 let lease_duration = extract_field("NewLeaseDuration")?
384 .get_text()
385 .and_then(|t| t.parse::<u32>().ok())
386 .ok_or_else(make_err("Field NewLeaseDuration is invalid".into()))?;
387 Ok(PortMappingEntry {
388 remote_host,
389 external_port,
390 protocol,
391 internal_port,
392 internal_client,
393 enabled,
394 port_mapping_description,
395 lease_duration,
396 })
397}
398
399#[test]
400fn test_parse_search_result_case_insensitivity() {
401 assert!(parse_search_result("location:http://0.0.0.0:0/control_url").is_ok());
402 assert!(parse_search_result("LOCATION:http://0.0.0.0:0/control_url").is_ok());
403}
404
405#[test]
406fn test_parse_search_result_ok() {
407 let result = parse_search_result("location:http://0.0.0.0:0/control_url").unwrap();
408 assert_eq!(result.0.ip(), IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)));
409 assert_eq!(result.0.port(), 0);
410 assert_eq!(&result.1[..], "/control_url");
411}
412
413#[test]
414fn test_parse_search_result_fail() {
415 assert!(parse_search_result("content-type:http://0.0.0.0:0/control_url").is_err());
416}
417
418#[test]
419fn test_parse_device1() {
420 let text = r#"<?xml version="1.0" encoding="UTF-8"?>
421<root xmlns="urn:schemas-upnp-org:device-1-0">
422 <specVersion>
423 <major>1</major>
424 <minor>0</minor>
425 </specVersion>
426 <device>
427 <deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
428 <friendlyName></friendlyName>
429 <manufacturer></manufacturer>
430 <manufacturerURL></manufacturerURL>
431 <modelDescription></modelDescription>
432 <modelName></modelName>
433 <modelNumber>1</modelNumber>
434 <serialNumber>00000000</serialNumber>
435 <UDN></UDN>
436 <serviceList>
437 <service>
438 <serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
439 <serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
440 <controlURL>/ctl/L3F</controlURL>
441 <eventSubURL>/evt/L3F</eventSubURL>
442 <SCPDURL>/L3F.xml</SCPDURL>
443 </service>
444 </serviceList>
445 <deviceList>
446 <device>
447 <deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
448 <friendlyName>WANDevice</friendlyName>
449 <manufacturer>MiniUPnP</manufacturer>
450 <manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
451 <modelDescription>WAN Device</modelDescription>
452 <modelName>WAN Device</modelName>
453 <modelNumber>20180615</modelNumber>
454 <modelURL>http://miniupnp.free.fr/</modelURL>
455 <serialNumber>00000000</serialNumber>
456 <UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
457 <UPC>MINIUPNPD</UPC>
458 <serviceList>
459 <service>
460 <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
461 <serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
462 <controlURL>/ctl/CmnIfCfg</controlURL>
463 <eventSubURL>/evt/CmnIfCfg</eventSubURL>
464 <SCPDURL>/WANCfg.xml</SCPDURL>
465 </service>
466 </serviceList>
467 <deviceList>
468 <device>
469 <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
470 <friendlyName>WANConnectionDevice</friendlyName>
471 <manufacturer>MiniUPnP</manufacturer>
472 <manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
473 <modelDescription>MiniUPnP daemon</modelDescription>
474 <modelName>MiniUPnPd</modelName>
475 <modelNumber>20180615</modelNumber>
476 <modelURL>http://miniupnp.free.fr/</modelURL>
477 <serialNumber>00000000</serialNumber>
478 <UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
479 <UPC>MINIUPNPD</UPC>
480 <serviceList>
481 <service>
482 <serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
483 <serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
484 <controlURL>/ctl/IPConn</controlURL>
485 <eventSubURL>/evt/IPConn</eventSubURL>
486 <SCPDURL>/WANIPCn.xml</SCPDURL>
487 </service>
488 </serviceList>
489 </device>
490 </deviceList>
491 </device>
492 </deviceList>
493 <presentationURL>http://192.168.0.1/</presentationURL>
494 </device>
495</root>"#;
496
497 let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
498 assert_eq!(control_url, "/ctl/IPConn");
499 assert_eq!(control_schema_url, "/WANIPCn.xml");
500}
501
502#[test]
503fn test_parse_device2() {
504 let text = r#"<?xml version="1.0" encoding="UTF-8"?>
505 <root xmlns="urn:schemas-upnp-org:device-1-0">
506 <specVersion>
507 <major>1</major>
508 <minor>0</minor>
509 </specVersion>
510 <device>
511 <deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
512 <friendlyName>FRITZ!Box 7430</friendlyName>
513 <manufacturer>AVM Berlin</manufacturer>
514 <manufacturerURL>http://www.avm.de</manufacturerURL>
515 <modelDescription>FRITZ!Box 7430</modelDescription>
516 <modelName>FRITZ!Box 7430</modelName>
517 <modelNumber>avm</modelNumber>
518 <modelURL>http://www.avm.de</modelURL>
519 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
520 <iconList>
521 <icon>
522 <mimetype>image/gif</mimetype>
523 <width>118</width>
524 <height>119</height>
525 <depth>8</depth>
526 <url>/ligd.gif</url>
527 </icon>
528 </iconList>
529 <serviceList>
530 <service>
531 <serviceType>urn:schemas-any-com:service:Any:1</serviceType>
532 <serviceId>urn:any-com:serviceId:any1</serviceId>
533 <controlURL>/igdupnp/control/any</controlURL>
534 <eventSubURL>/igdupnp/control/any</eventSubURL>
535 <SCPDURL>/any.xml</SCPDURL>
536 </service>
537 </serviceList>
538 <deviceList>
539 <device>
540 <deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
541 <friendlyName>WANDevice - FRITZ!Box 7430</friendlyName>
542 <manufacturer>AVM Berlin</manufacturer>
543 <manufacturerURL>www.avm.de</manufacturerURL>
544 <modelDescription>WANDevice - FRITZ!Box 7430</modelDescription>
545 <modelName>WANDevice - FRITZ!Box 7430</modelName>
546 <modelNumber>avm</modelNumber>
547 <modelURL>www.avm.de</modelURL>
548 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
549 <UPC>AVM IGD</UPC>
550 <serviceList>
551 <service>
552 <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
553 <serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
554 <controlURL>/igdupnp/control/WANCommonIFC1</controlURL>
555 <eventSubURL>/igdupnp/control/WANCommonIFC1</eventSubURL>
556 <SCPDURL>/igdicfgSCPD.xml</SCPDURL>
557 </service>
558 </serviceList>
559 <deviceList>
560 <device>
561 <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
562 <friendlyName>WANConnectionDevice - FRITZ!Box 7430</friendlyName>
563 <manufacturer>AVM Berlin</manufacturer>
564 <manufacturerURL>www.avm.de</manufacturerURL>
565 <modelDescription>WANConnectionDevice - FRITZ!Box 7430</modelDescription>
566 <modelName>WANConnectionDevice - FRITZ!Box 7430</modelName>
567 <modelNumber>avm</modelNumber>
568 <modelURL>www.avm.de</modelURL>
569 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
570 <UPC>AVM IGD</UPC>
571 <serviceList>
572 <service>
573 <serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
574 <serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
575 <controlURL>/igdupnp/control/WANDSLLinkC1</controlURL>
576 <eventSubURL>/igdupnp/control/WANDSLLinkC1</eventSubURL>
577 <SCPDURL>/igddslSCPD.xml</SCPDURL>
578 </service>
579 <service>
580 <serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
581 <serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
582 <controlURL>/igdupnp/control/WANIPConn1</controlURL>
583 <eventSubURL>/igdupnp/control/WANIPConn1</eventSubURL>
584 <SCPDURL>/igdconnSCPD.xml</SCPDURL>
585 </service>
586 <service>
587 <serviceType>urn:schemas-upnp-org:service:WANIPv6FirewallControl:1</serviceType>
588 <serviceId>urn:upnp-org:serviceId:WANIPv6Firewall1</serviceId>
589 <controlURL>/igd2upnp/control/WANIPv6Firewall1</controlURL>
590 <eventSubURL>/igd2upnp/control/WANIPv6Firewall1</eventSubURL>
591 <SCPDURL>/igd2ipv6fwcSCPD.xml</SCPDURL>
592 </service>
593 </serviceList>
594 </device>
595 </deviceList>
596 </device>
597 </deviceList>
598 <presentationURL>http://fritz.box</presentationURL>
599 </device>
600 </root>
601 "#;
602 let result = parse_control_urls(text.as_bytes());
603 assert!(result.is_ok());
604 let (control_schema_url, control_url) = result.unwrap();
605 assert_eq!(control_url, "/igdupnp/control/WANIPConn1");
606 assert_eq!(control_schema_url, "/igdconnSCPD.xml");
607}
608
609#[test]
610fn test_parse_device3() {
611 let text = r#"<?xml version="1.0" encoding="UTF-8"?>
612<root xmlns="urn:schemas-upnp-org:device-1-0">
613<specVersion>
614 <major>1</major>
615 <minor>0</minor>
616</specVersion>
617<device xmlns="urn:schemas-upnp-org:device-1-0">
618 <deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
619 <friendlyName></friendlyName>
620 <manufacturer></manufacturer>
621 <manufacturerURL></manufacturerURL>
622 <modelDescription></modelDescription>
623 <modelName></modelName>
624 <modelNumber></modelNumber>
625 <serialNumber></serialNumber>
626 <presentationURL>http://192.168.1.1</presentationURL>
627 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
628 <UPC>999999999001</UPC>
629 <iconList>
630 <icon>
631 <mimetype>image/png</mimetype>
632 <width>16</width>
633 <height>16</height>
634 <depth>8</depth>
635 <url>/ligd.png</url>
636 </icon>
637 </iconList>
638 <deviceList>
639 <device>
640 <deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
641 <friendlyName></friendlyName>
642 <manufacturer></manufacturer>
643 <manufacturerURL></manufacturerURL>
644 <modelDescription></modelDescription>
645 <modelName></modelName>
646 <modelNumber></modelNumber>
647 <modelURL></modelURL>
648 <serialNumber></serialNumber>
649 <presentationURL>http://192.168.1.254</presentationURL>
650 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
651 <UPC>999999999001</UPC>
652 <serviceList>
653 <service>
654 <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
655 <serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
656 <controlURL>/upnp/control/WANCommonIFC1</controlURL>
657 <eventSubURL>/upnp/control/WANCommonIFC1</eventSubURL>
658 <SCPDURL>/332b484d/wancomicfgSCPD.xml</SCPDURL>
659 </service>
660 </serviceList>
661 <deviceList>
662 <device>
663 <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
664 <friendlyName></friendlyName>
665 <manufacturer></manufacturer>
666 <manufacturerURL></manufacturerURL>
667 <modelDescription></modelDescription>
668 <modelName></modelName>
669 <modelNumber></modelNumber>
670 <modelURL></modelURL>
671 <serialNumber></serialNumber>
672 <presentationURL>http://192.168.1.254</presentationURL>
673 <UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
674 <UPC>999999999001</UPC>
675 <serviceList>
676 <service>
677 <serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
678 <serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
679 <controlURL>/upnp/control/WANIPConn1</controlURL>
680 <eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
681 <SCPDURL>/332b484d/wanipconnSCPD.xml</SCPDURL>
682 </service>
683 </serviceList>
684 </device>
685 </deviceList>
686 </device>
687 </deviceList>
688</device>
689</root>"#;
690
691 let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
692 assert_eq!(control_url, "/upnp/control/WANIPConn1");
693 assert_eq!(control_schema_url, "/332b484d/wanipconnSCPD.xml");
694}