Skip to content

Commit 918bdaa

Browse files
committed
Add URLBase handling to XML parsers and corresponding tests
1 parent 57fd0e3 commit 918bdaa

File tree

2 files changed

+196
-2
lines changed

2 files changed

+196
-2
lines changed

soapcalls/xmlparsers.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type deviceNode struct {
3737

3838
type rootNode struct {
3939
XMLName xml.Name `xml:"root"`
40+
URLBase string `xml:"URLBase"`
4041
Device deviceNode `xml:"device"`
4142
}
4243

@@ -137,7 +138,7 @@ func ParseDMRFromXML(xmlbody []byte, baseURL *url.URL) (*DMRextracted, error) {
137138
return nil, fmt.Errorf("ParseDMRFromXML unmarshal error: %w", err)
138139
}
139140

140-
ex := extractServicesFromDevice(&root.Device, baseURL)
141+
ex := extractServicesFromDevice(&root.Device, resolveDescriptionBaseURL(baseURL, root.URLBase))
141142
if ex != nil && ex.AvtransportControlURL != "" {
142143
return ex, nil
143144
}
@@ -156,7 +157,7 @@ func ParseAllDMRFromXML(xmlbody []byte, baseURL *url.URL) ([]*DMRextracted, erro
156157
}
157158

158159
var results []*DMRextracted
159-
extractAllServicesFromDevice(&root.Device, baseURL, &results)
160+
extractAllServicesFromDevice(&root.Device, resolveDescriptionBaseURL(baseURL, root.URLBase), &results)
160161

161162
if len(results) == 0 {
162163
return nil, ErrWrongDMR
@@ -279,6 +280,36 @@ func toAbsoluteServiceURL(baseURL *url.URL, rawServiceURL string) string {
279280
return baseURL.ResolveReference(parsedServiceURL).String()
280281
}
281282

283+
func resolveDescriptionBaseURL(locationBase *url.URL, rawURLBase string) *url.URL {
284+
urlBase := strings.TrimSpace(rawURLBase)
285+
if urlBase == "" {
286+
return locationBase
287+
}
288+
289+
parsedURLBase, err := url.Parse(urlBase)
290+
if err != nil {
291+
return locationBase
292+
}
293+
294+
if parsedURLBase.IsAbs() {
295+
if (parsedURLBase.Scheme == "http" || parsedURLBase.Scheme == "https") && parsedURLBase.Host != "" {
296+
return parsedURLBase
297+
}
298+
return locationBase
299+
}
300+
301+
if locationBase == nil {
302+
return nil
303+
}
304+
305+
resolvedURLBase := locationBase.ResolveReference(parsedURLBase)
306+
if resolvedURLBase == nil || resolvedURLBase.Scheme == "" || resolvedURLBase.Host == "" {
307+
return locationBase
308+
}
309+
310+
return resolvedURLBase
311+
}
312+
282313
// ParseEventNotify parses the Notify messages from the DMR device.
283314
// Transport state drives playback transitions; actions are optional.
284315
func ParseEventNotify(xmlbody string) (EventNotify, error) {

soapcalls/xmlparsers_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,111 @@ func TestParseDMRFromXMLResolvesRelativeServiceURLs(t *testing.T) {
345345
}
346346
}
347347

348+
func TestParseDMRFromXMLURLBaseHandling(t *testing.T) {
349+
tests := []struct {
350+
name string
351+
raw string
352+
wantControl string
353+
wantEventSub string
354+
}{
355+
{
356+
name: "Absolute URLBase Overrides Location Base",
357+
raw: `<?xml version="1.0"?>
358+
<root>
359+
<URLBase>http://example.com:8080/upnp/</URLBase>
360+
<device>
361+
<serviceList>
362+
<service>
363+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
364+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
365+
<controlURL>ctrl</controlURL>
366+
<eventSubURL>event</eventSubURL>
367+
</service>
368+
</serviceList>
369+
</device>
370+
</root>`,
371+
wantControl: "http://example.com:8080/upnp/ctrl",
372+
wantEventSub: "http://example.com:8080/upnp/event",
373+
},
374+
{
375+
name: "Relative URLBase Resolved Against Location Base",
376+
raw: `<?xml version="1.0"?>
377+
<root>
378+
<URLBase>/upnp/</URLBase>
379+
<device>
380+
<serviceList>
381+
<service>
382+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
383+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
384+
<controlURL>ctrl</controlURL>
385+
<eventSubURL>event</eventSubURL>
386+
</service>
387+
</serviceList>
388+
</device>
389+
</root>`,
390+
wantControl: "http://example.com:8080/upnp/ctrl",
391+
wantEventSub: "http://example.com:8080/upnp/event",
392+
},
393+
{
394+
name: "Malformed URLBase Falls Back To Location Base",
395+
raw: `<?xml version="1.0"?>
396+
<root>
397+
<URLBase>http:/upnp</URLBase>
398+
<device>
399+
<serviceList>
400+
<service>
401+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
402+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
403+
<controlURL>/ctrl</controlURL>
404+
<eventSubURL>/event</eventSubURL>
405+
</service>
406+
</serviceList>
407+
</device>
408+
</root>`,
409+
wantControl: "http://example.com:8080/ctrl",
410+
wantEventSub: "http://example.com:8080/event",
411+
},
412+
{
413+
name: "Malformed Service URL Uses URLBase After Sanitization",
414+
raw: `<?xml version="1.0"?>
415+
<root>
416+
<URLBase>http://example.com:8080/upnp/</URLBase>
417+
<device>
418+
<serviceList>
419+
<service>
420+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
421+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
422+
<controlURL>_urn:schemas-upnp-org:service:AVTransport_control</controlURL>
423+
<eventSubURL>_urn:schemas-upnp-org:service:AVTransport_event</eventSubURL>
424+
</service>
425+
</serviceList>
426+
</device>
427+
</root>`,
428+
wantControl: "http://example.com:8080/upnp/_urn:schemas-upnp-org:service:AVTransport_control",
429+
wantEventSub: "http://example.com:8080/upnp/_urn:schemas-upnp-org:service:AVTransport_event",
430+
},
431+
}
432+
433+
locationBase, _ := url.Parse("http://example.com:8080/xml/device_description.xml")
434+
435+
for _, tt := range tests {
436+
t.Run(tt.name, func(t *testing.T) {
437+
result, err := ParseDMRFromXML([]byte(tt.raw), locationBase)
438+
if err != nil {
439+
t.Fatalf("ParseDMRFromXML() unexpected error: %v", err)
440+
}
441+
442+
if result.AvtransportControlURL != tt.wantControl {
443+
t.Fatalf("AvtransportControlURL = %q, want %q", result.AvtransportControlURL, tt.wantControl)
444+
}
445+
446+
if result.AvtransportEventSubURL != tt.wantEventSub {
447+
t.Fatalf("AvtransportEventSubURL = %q, want %q", result.AvtransportEventSubURL, tt.wantEventSub)
448+
}
449+
})
450+
}
451+
}
452+
348453
func TestDMRextractorEmbeddedDevice(t *testing.T) {
349454
// Test full HTTP flow with embedded device XML
350455
raw := `<?xml version="1.0" encoding="utf-8"?>
@@ -520,6 +625,64 @@ func TestParseAllDMRFromXML(t *testing.T) {
520625
}
521626
}
522627

628+
func TestParseAllDMRFromXMLUsesURLBase(t *testing.T) {
629+
raw := `<?xml version="1.0"?>
630+
<root>
631+
<URLBase>http://example.com:8080/upnp/</URLBase>
632+
<device>
633+
<deviceList>
634+
<device>
635+
<friendlyName>Zone 1</friendlyName>
636+
<serviceList>
637+
<service>
638+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
639+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
640+
<controlURL>zone1/AVTransport</controlURL>
641+
<eventSubURL>zone1/event</eventSubURL>
642+
</service>
643+
</serviceList>
644+
</device>
645+
<device>
646+
<friendlyName>Zone 2</friendlyName>
647+
<serviceList>
648+
<service>
649+
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
650+
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
651+
<controlURL>zone2/AVTransport</controlURL>
652+
<eventSubURL>zone2/event</eventSubURL>
653+
</service>
654+
</serviceList>
655+
</device>
656+
</deviceList>
657+
</device>
658+
</root>`
659+
660+
baseURL, _ := url.Parse("http://example.com:8080/xml/device_description.xml")
661+
results, err := ParseAllDMRFromXML([]byte(raw), baseURL)
662+
if err != nil {
663+
t.Fatalf("ParseAllDMRFromXML() unexpected error: %v", err)
664+
}
665+
666+
if len(results) != 2 {
667+
t.Fatalf("ParseAllDMRFromXML() returned %d devices, want 2", len(results))
668+
}
669+
670+
expectedControls := map[string]string{
671+
"Zone 1": "http://example.com:8080/upnp/zone1/AVTransport",
672+
"Zone 2": "http://example.com:8080/upnp/zone2/AVTransport",
673+
}
674+
675+
for _, dev := range results {
676+
want, ok := expectedControls[dev.FriendlyName]
677+
if !ok {
678+
t.Fatalf("unexpected device in results: %q", dev.FriendlyName)
679+
}
680+
if dev.AvtransportControlURL != want {
681+
t.Fatalf("AvtransportControlURL for %q = %q, want %q", dev.FriendlyName, dev.AvtransportControlURL, want)
682+
}
683+
}
684+
}
685+
523686
func TestLoadDevicesFromLocationMultiple(t *testing.T) {
524687
// Test full HTTP flow with multiple MediaRenderers
525688
raw := `<?xml version="1.0"?>

0 commit comments

Comments
 (0)