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