igd/common/
parsing.rs

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