igd_next/common/
parsing.rs

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
14// Parse the result.
15pub 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
312/// One port mapping entry as returned by GetGenericPortMappingEntry
313pub struct PortMappingEntry {
314    /// The remote host for which the mapping is valid
315    /// Can be an IP address or a host name
316    pub remote_host: String,
317    /// The external port of the mapping
318    pub external_port: u16,
319    /// The protocol of the mapping
320    pub protocol: PortMappingProtocol,
321    /// The internal (local) port
322    pub internal_port: u16,
323    /// The internal client of the port mapping
324    /// Can be an IP address or a host name
325    pub internal_client: String,
326    /// A flag whether this port mapping is enabled
327    pub enabled: bool,
328    /// A description for this port mapping
329    pub port_mapping_description: String,
330    /// The lease duration of this port mapping in seconds
331    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}