stash/pkg/dlna/dms.go
SmallCoccinelle e14bb8432c
Enable gocritic (#1848)
* Don't capitalize local variables

ValidCodecs -> validCodecs

* Capitalize deprecation markers

A deprecated marker should be capitalized.

* Use re.MustCompile for static regexes

If the regex fails to compile, it's a programmer error, and should be
treated as such. The regex is entirely static.

* Simplify else-if constructions

Rewrite

   else { if cond {}}

to

   else if cond {}

* Use a switch statement to analyze formats

Break an if-else chain. While here, simplify code flow.

Also introduce a proper static error for unsupported image formats,
paving the way for being able to check against the error.

* Rewrite ifElse chains into switch statements

The "Effective Go" https://golang.org/doc/effective_go#switch document
mentions it is more idiomatic to write if-else chains as switches when
it is possible.

Find all the plain rewrite occurrences in the code base and rewrite.
In some cases, the if-else chains are replaced by a switch scrutinizer.
That is, the code sequence

  if x == 1 {
      ..
  } else if x == 2 {
      ..
  } else if x == 3 {
      ...
  }

can be rewritten into

  switch x {
  case 1:
    ..
  case 2:
    ..
  case 3:
    ..
  }

which is clearer for the compiler: it can decide if the switch is
better served by a jump-table then a branch-chain.

* Rewrite switches, introduce static errors

Introduce two new static errors:

* `ErrNotImplmented`
* `ErrNotSupported`

And use these rather than forming new generative errors whenever the
code is called. Code can now test on the errors (since they are static
and the pointers to them wont change).

Also rewrite ifElse chains into switches in this part of the code base.

* Introduce a StashBoxError in configuration

Since all stashbox errors are the same, treat them as such in the code
base. While here, rewrite an ifElse chain.

In the future, it might be beneifical to refactor configuration errors
into one error which can handle missing fields, which context the error
occurs in and so on. But for now, try to get an overview of the error
categories by hoisting them into static errors.

* Get rid of an else-block in transaction handling

If we succesfully `recover()`, we then always `panic()`. This means the
rest of the code is not reachable, so we can avoid having an else-block
here.

It also solves an ifElse-chain style check in the code base.

* Use strings.ReplaceAll

Rewrite

    strings.Replace(s, o, n, -1)

into

    strings.ReplaceAll(s, o, n)

To make it consistent and clear that we are doing an all-replace in the
string rather than replacing parts of it. It's more of a nitpick since
there are no implementation differences: the stdlib implementation is
just to supply -1.

* Rewrite via gocritic's assignOp

Statements of the form

    x = x + e

is rewritten into

    x += e

where applicable.

* Formatting

* Review comments handled

Stash-box is a proper noun.

Rewrite a switch into an if-chain which returns on the first error
encountered.

* Use context.TODO() over context.Background()

Patch in the same vein as everything else: use the TODO() marker so we
can search for it later and link it into the context tree/tentacle once
it reaches down to this level in the code base.

* Tell the linter to ignore a section in manager_tasks.go

The section is less readable, so mark it with a nolint for now. Because
the rewrite enables a ifElseChain, also mark that as nolint for now.

* Use strings.ReplaceAll over strings.Replace

* Apply an ifElse rewrite

else { if .. { .. } } rewrite into else if { .. }

* Use switch-statements over ifElseChains

Rewrite chains of if-else into switch statements. Where applicable,
add an early nil-guard to simplify case analysis. Also, in
ScanTask's Start(..), invert the logic to outdent the whole block, and
help the reader: if it's not a scene, the function flow is now far more
local to the top of the function, and it's clear that the rest of the
function has to do with scene management.

* Enable gocritic on the code base.

Disable appendAssign for now since we aren't passing that check yet.

* Document the nolint additions

* Document StashBoxBatchPerformerTagInput
2021-10-18 14:12:40 +11:00

706 lines
20 KiB
Go

package dlna
// Derived from: https://github.com/anacrolix/dms
// Copyright (c) 2012, Matt Joiner <anacrolix@gmail.com>.
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the <organization> nor the
// names of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import (
"bytes"
"context"
"crypto/md5"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/http/pprof"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/anacrolix/dms/soap"
"github.com/anacrolix/dms/ssdp"
"github.com/anacrolix/dms/upnp"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
const (
serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0"
rootDeviceType = "urn:schemas-upnp-org:device:MediaServer:1"
rootDeviceModelName = "dms 1.0xb"
resPath = "/res"
iconPath = "/icon"
rootDescPath = "/rootDesc.xml"
contentDirectoryEventSubURL = "/evt/ContentDirectory"
serviceControlURL = "/ctl"
deviceIconPath = "/deviceIcon"
)
func makeDeviceUuid(unique string) string {
h := md5.New()
if _, err := io.WriteString(h, unique); err != nil {
panic("makeDeviceUuid write failed: " + err.Error())
}
buf := h.Sum(nil)
return upnp.FormatUUID(buf)
}
// Groups the service definition with its XML description.
type service struct {
upnp.Service
SCPD string
}
// Exposed UPnP AV services.
var services = []*service{
{
Service: upnp.Service{
ServiceType: "urn:schemas-upnp-org:service:ContentDirectory:1",
ServiceId: "urn:upnp-org:serviceId:ContentDirectory",
EventSubURL: contentDirectoryEventSubURL,
ControlURL: serviceControlURL,
},
SCPD: contentDirectoryServiceDescription,
},
{
Service: upnp.Service{
ServiceType: "urn:schemas-upnp-org:service:ConnectionManager:1",
ServiceId: "urn:upnp-org:serviceId:ConnectionManager",
ControlURL: serviceControlURL,
},
SCPD: connectionManagerServiceDescription,
},
{
Service: upnp.Service{
ServiceType: "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1",
ServiceId: "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar",
ControlURL: serviceControlURL,
},
SCPD: xmsMediaReceiverServiceDescription,
},
}
func init() {
for _, s := range services {
p := path.Join("/scpd", s.ServiceId)
s.SCPDURL = p
}
}
func devices() []string {
return []string{
"urn:schemas-upnp-org:device:MediaServer:1",
}
}
func serviceTypes() (ret []string) {
for _, s := range services {
ret = append(ret, s.ServiceType)
}
return
}
func (me *Server) httpPort() int {
return me.HTTPConn.Addr().(*net.TCPAddr).Port
}
func (me *Server) serveHTTP() error {
srv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if me.LogHeaders {
logger.Debugf("%s %s", r.Method, r.RequestURI)
for k, v := range r.Header {
logger.Debugf("%s: %s", k, v)
}
}
w.Header().Set("Ext", "")
w.Header().Set("Server", serverField)
me.httpServeMux.ServeHTTP(&mitmRespWriter{
ResponseWriter: w,
logHeader: me.LogHeaders,
}, r)
}),
}
err := srv.Serve(me.HTTPConn)
select {
case <-me.closed:
return nil
default:
return err
}
}
// An interface with these flags should be valid for SSDP.
const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast
func (me *Server) doSSDP() {
active := 0
stopped := make(chan struct{})
for _, if_ := range me.Interfaces {
active++
go func(if_ net.Interface) {
defer func() {
stopped <- struct{}{}
}()
me.ssdpInterface(if_)
}(if_)
}
for active > 0 {
<-stopped
active--
}
}
// Run SSDP server on an interface.
func (me *Server) ssdpInterface(if_ net.Interface) {
s := ssdp.Server{
Interface: if_,
Devices: devices(),
Services: serviceTypes(),
Location: func(ip net.IP) string {
return me.location(ip)
},
Server: serverField,
UUID: me.rootDeviceUUID,
NotifyInterval: me.NotifyInterval,
}
if err := s.Init(); err != nil {
if if_.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags {
// Didn't expect it to work anyway.
return
}
if strings.Contains(err.Error(), "listen") {
// OSX has a lot of dud interfaces. Failure to create a socket on
// the interface are what we're expecting if the interface is no
// good.
return
}
logger.Errorf("error creating ssdp server on %s: %s", if_.Name, err)
return
}
defer s.Close()
logger.Debugf("started SSDP on %s", if_.Name)
stopped := make(chan struct{})
go func() {
defer close(stopped)
if err := s.Serve(); err != nil {
logger.Errorf("%q: %q\n", if_.Name, err)
}
}()
select {
case <-me.closed:
// Returning will close the server.
case <-stopped:
}
}
var (
startTime time.Time
)
type Icon struct {
Width, Height, Depth int
Mimetype string
io.ReadSeeker
}
type Server struct {
HTTPConn net.Listener
FriendlyName string
Interfaces []net.Interface
httpServeMux *http.ServeMux
RootObjectPath string
rootDescXML []byte
rootDeviceUUID string
closed chan struct{}
ssdpStopped chan struct{}
// The service SOAP handler keyed by service URN.
services map[string]UPnPService
LogHeaders bool
Icons []Icon
// Stall event subscription requests until they drop. A workaround for
// some bad clients.
StallEventSubscribe bool
// Time interval between SSPD announces
NotifyInterval time.Duration
txnManager models.TransactionManager
sceneServer sceneServer
ipWhitelistManager *ipWhitelistManager
}
// UPnP SOAP service.
type UPnPService interface {
Handle(action string, argsXML []byte, r *http.Request) (respArgs map[string]string, err error)
Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error)
Unsubscribe(sid string) error
}
type Cache interface {
Set(key interface{}, value interface{})
Get(key interface{}) (value interface{}, ok bool)
}
func init() {
startTime = time.Now()
}
func xmlMarshalOrPanic(value interface{}) []byte {
ret, err := xml.MarshalIndent(value, "", " ")
if err != nil {
panic(fmt.Sprintf("xmlMarshalOrPanic failed to marshal %v: %s", value, err))
}
return ret
}
// TODO: Document the use of this for debugging.
type mitmRespWriter struct {
http.ResponseWriter
loggedHeader bool
logHeader bool
}
func (me *mitmRespWriter) WriteHeader(code int) {
me.doLogHeader(code)
me.ResponseWriter.WriteHeader(code)
}
func (me *mitmRespWriter) doLogHeader(code int) {
if !me.logHeader {
return
}
logger.Debugf("Response: %d", code)
for k, v := range me.Header() {
logger.Debugf("%s: %s", k, v)
}
me.loggedHeader = true
}
func (me *mitmRespWriter) Write(b []byte) (int, error) {
if !me.loggedHeader {
me.doLogHeader(200)
}
return me.ResponseWriter.Write(b)
}
// Deprecated: the CloseNotifier interface predates Go's context package.
// New code should use Request.Context instead.
func (me *mitmRespWriter) CloseNotify() <-chan bool {
return me.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Set the SCPD serve paths.
func init() {
for _, s := range services {
p := path.Join("/scpd", s.ServiceId)
s.SCPDURL = p
}
}
// Install handlers to serve SCPD for each UPnP service.
func handleSCPDs(mux *http.ServeMux) {
for _, s := range services {
mux.HandleFunc(s.SCPDURL, func(serviceDesc string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", `text/xml; charset="utf-8"`)
http.ServeContent(w, r, ".xml", startTime, bytes.NewReader([]byte(serviceDesc)))
}
}(s.SCPD))
}
}
// Marshal SOAP response arguments into a response XML snippet.
func marshalSOAPResponse(sa upnp.SoapAction, args map[string]string) []byte {
soapArgs := make([]soap.Arg, 0, len(args))
for argName, value := range args {
soapArgs = append(soapArgs, soap.Arg{
XMLName: xml.Name{Local: argName},
Value: value,
})
}
return []byte(fmt.Sprintf(`<u:%[1]sResponse xmlns:u="%[2]s">%[3]s</u:%[1]sResponse>`, sa.Action, sa.ServiceURN.String(), xmlMarshalOrPanic(soapArgs)))
}
// Handle a SOAP request and return the response arguments or UPnP error.
func (me *Server) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) (map[string]string, error) {
service, ok := me.services[sa.Type]
if !ok {
// TODO: What's the invalid service error?!
return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type)
}
logger.Tracef("%s::Handle %s - %s", sa.Type, sa.Action, actionRequestXML)
ret, err := service.Handle(sa.Action, actionRequestXML, r)
if err == nil {
logger.Tracef("< %v", ret)
}
return ret, err
}
// Handle a service control HTTP request.
func (me *Server) serviceControlHandler(w http.ResponseWriter, r *http.Request) {
clientIp, _, _ := net.SplitHostPort(r.RemoteAddr)
ip := net.ParseIP(clientIp).String()
if !me.ipWhitelistManager.ipAllowed(ip) {
// only log if we haven't seen it
if !me.ipWhitelistManager.addRecent(ip) {
logger.Infof("not allowed client %s", clientIp)
}
http.Error(w, "forbidden", http.StatusForbidden)
return
}
soapActionString := r.Header.Get("SOAPACTION")
soapAction, err := upnp.ParseActionHTTPHeader(soapActionString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var env soap.Envelope
if err := xml.NewDecoder(r.Body).Decode(&env); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// AwoX/1.1 UPnP/1.0 DLNADOC/1.50
w.Header().Set("Content-Type", `text/xml; charset="utf-8"`)
w.Header().Set("Ext", "")
w.Header().Set("Server", serverField)
soapRespXML, code := func() ([]byte, int) {
respArgs, err := me.soapActionResponse(soapAction, env.Body.Action, r)
if err != nil {
upnpErr := upnp.ConvertError(err)
return xmlMarshalOrPanic(soap.NewFault("UPnPError", upnpErr)), 500
}
return marshalSOAPResponse(soapAction, respArgs), 200
}()
bodyStr := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8" standalone="yes"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>%s</s:Body></s:Envelope>`, soapRespXML)
w.WriteHeader(code)
if _, err := w.Write([]byte(bodyStr)); err != nil {
logger.Errorf(err.Error())
}
}
func (me *Server) serveIcon(w http.ResponseWriter, r *http.Request) {
sceneId := r.URL.Query().Get("scene")
if sceneId == "" {
return
}
var scene *models.Scene
err := me.txnManager.WithReadTxn(r.Context(), func(r models.ReaderRepository) error {
idInt, err := strconv.Atoi(sceneId)
if err != nil {
return nil
}
scene, _ = r.Scene().Find(idInt)
return nil
})
if err != nil {
logger.Warnf("failed to execute read transaction while trying to serve an icon: %v", err)
}
if scene == nil {
return
}
me.sceneServer.ServeScreenshot(scene, w, r)
}
func (me *Server) contentDirectoryInitialEvent(ctx context.Context, urls []*url.URL, sid string) {
body := xmlMarshalOrPanic(upnp.PropertySet{
Properties: []upnp.Property{
{
Variable: upnp.Variable{
XMLName: xml.Name{
Local: "SystemUpdateID",
},
Value: "0",
},
},
// upnp.Property{
// Variable: upnp.Variable{
// XMLName: xml.Name{
// Local: "ContainerUpdateIDs",
// },
// },
// },
// upnp.Property{
// Variable: upnp.Variable{
// XMLName: xml.Name{
// Local: "TransferIDs",
// },
// },
// },
},
Space: "urn:schemas-upnp-org:event-1-0",
})
body = append([]byte(`<?xml version="1.0"?>`+"\n"), body...)
for _, _url := range urls {
bodyReader := bytes.NewReader(body)
req, err := http.NewRequestWithContext(ctx, "NOTIFY", _url.String(), bodyReader)
if err != nil {
logger.Errorf("Could not create a request to notify %s: %s", _url.String(), err)
continue
}
req.Header["CONTENT-TYPE"] = []string{`text/xml; charset="utf-8"`}
req.Header["NT"] = []string{"upnp:event"}
req.Header["NTS"] = []string{"upnp:propchange"}
req.Header["SID"] = []string{sid}
req.Header["SEQ"] = []string{"0"}
// req.Header["TRANSFER-ENCODING"] = []string{"chunked"}
// req.ContentLength = int64(bodyReader.Len())
resp, err := http.DefaultClient.Do(req)
if err != nil {
logger.Errorf("Could not notify %s: %s", _url.String(), err)
continue
}
b, _ := io.ReadAll(resp.Body)
if len(b) > 0 {
logger.Debug(string(b))
}
resp.Body.Close()
}
}
func (me *Server) contentDirectoryEventSubHandler(w http.ResponseWriter, r *http.Request) {
if me.StallEventSubscribe {
// I have an LG TV that doesn't like my eventing implementation.
// Returning unimplemented (501?) errors, results in repeat subscribe
// attempts which hits some kind of error count limit on the TV
// causing it to forcefully disconnect. It also won't work if the CDS
// service doesn't include an EventSubURL. The best thing I can do is
// cause every attempt to subscribe to timeout on the TV end, which
// reduces the error rate enough that the TV continues to operate
// without eventing.
//
// I've not found a reliable way to identify this TV, since it and
// others don't seem to include any client-identifying headers on
// SUBSCRIBE requests.
//
// TODO: Get eventing to work with the problematic TV.
t := time.Now()
<-r.Context().Done()
logger.Debugf("stalled subscribe connection went away after %s", time.Since(t))
return
}
// The following code is a work in progress. It partially implements
// the spec on eventing but hasn't been completed as I have nothing to
// test it with.
service := me.services["ContentDirectory"]
switch {
case r.Method == "SUBSCRIBE" && r.Header.Get("SID") == "":
urls := upnp.ParseCallbackURLs(r.Header.Get("CALLBACK"))
var timeout int
fmt.Sscanf(r.Header.Get("TIMEOUT"), "Second-%d", &timeout)
sid, timeout, _ := service.Subscribe(urls, timeout)
w.Header()["SID"] = []string{sid}
w.Header()["TIMEOUT"] = []string{fmt.Sprintf("Second-%d", timeout)}
// TODO: Shouldn't have to do this to get headers logged.
w.WriteHeader(http.StatusOK)
go func() {
time.Sleep(100 * time.Millisecond)
me.contentDirectoryInitialEvent(r.Context(), urls, sid)
}()
case r.Method == "SUBSCRIBE":
http.Error(w, "meh", http.StatusPreconditionFailed)
default:
logger.Debugf("unhandled event method: %s", r.Method)
}
}
func (me *Server) initMux(mux *http.ServeMux) {
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("content-type", "text/html")
err := rootTmpl.Execute(resp, struct {
Readonly bool
Path string
}{
true,
me.RootObjectPath,
})
if err != nil {
logger.Errorf(err.Error())
}
})
mux.HandleFunc(contentDirectoryEventSubURL, me.contentDirectoryEventSubHandler)
mux.HandleFunc(iconPath, me.serveIcon)
mux.HandleFunc(resPath, func(w http.ResponseWriter, r *http.Request) {
sceneId := r.URL.Query().Get("scene")
var scene *models.Scene
err := me.txnManager.WithReadTxn(r.Context(), func(r models.ReaderRepository) error {
sceneIdInt, err := strconv.Atoi(sceneId)
if err != nil {
return nil
}
scene, _ = r.Scene().Find(sceneIdInt)
return nil
})
if err != nil {
logger.Warnf("failed to execute read transaction for scene id (%v): %v", sceneId, err)
}
if scene == nil {
return
}
me.sceneServer.StreamSceneDirect(scene, w, r)
})
mux.HandleFunc(rootDescPath, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", `text/xml; charset="utf-8"`)
w.Header().Set("content-length", fmt.Sprint(len(me.rootDescXML)))
w.Header().Set("server", serverField)
if k, err := w.Write(me.rootDescXML); err != nil {
logger.Warnf("could not write rootDescXML (wrote %v bytes of %v): %v", k, len(me.rootDescXML), err)
}
})
handleSCPDs(mux)
mux.HandleFunc(serviceControlURL, me.serviceControlHandler)
mux.HandleFunc("/debug/pprof/", pprof.Index)
for i, di := range me.Icons {
mux.HandleFunc(fmt.Sprintf("%s/%d", deviceIconPath, i), func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", di.Mimetype)
http.ServeContent(w, r, "", time.Time{}, di.ReadSeeker)
})
}
}
func (me *Server) initServices() {
me.services = map[string]UPnPService{
"ContentDirectory": &contentDirectoryService{
Server: me,
txnManager: me.txnManager,
},
"ConnectionManager": &connectionManagerService{
Server: me,
},
"X_MS_MediaReceiverRegistrar": &mediaReceiverRegistrarService{
Server: me,
},
}
}
func (me *Server) Serve() (err error) {
me.initServices()
me.closed = make(chan struct{})
if me.HTTPConn == nil {
me.HTTPConn, err = net.Listen("tcp", "")
if err != nil {
return
}
}
if me.Interfaces == nil {
ifs, err := net.Interfaces()
if err != nil {
logger.Errorf(err.Error())
}
var tmp []net.Interface
for _, if_ := range ifs {
if if_.Flags&net.FlagUp == 0 || if_.MTU <= 0 {
continue
}
tmp = append(tmp, if_)
}
me.Interfaces = tmp
}
me.httpServeMux = http.NewServeMux()
me.rootDeviceUUID = makeDeviceUuid(me.FriendlyName)
me.rootDescXML, err = xml.MarshalIndent(
upnp.DeviceDesc{
SpecVersion: upnp.SpecVersion{Major: 1, Minor: 0},
Device: upnp.Device{
DeviceType: rootDeviceType,
FriendlyName: me.FriendlyName,
Manufacturer: me.FriendlyName,
ModelName: rootDeviceModelName,
UDN: me.rootDeviceUUID,
ServiceList: func() (ss []upnp.Service) {
for _, s := range services {
ss = append(ss, s.Service)
}
return
}(),
IconList: func() (ret []upnp.Icon) {
for i, di := range me.Icons {
ret = append(ret, upnp.Icon{
Height: di.Height,
Width: di.Width,
Depth: di.Depth,
Mimetype: di.Mimetype,
URL: fmt.Sprintf("%s/%d", deviceIconPath, i),
})
}
return
}(),
},
},
" ", " ")
if err != nil {
return
}
me.rootDescXML = append([]byte(`<?xml version="1.0"?>`), me.rootDescXML...)
logger.Debug("HTTP srv on", me.HTTPConn.Addr())
me.initMux(me.httpServeMux)
me.ssdpStopped = make(chan struct{})
go func() {
me.doSSDP()
close(me.ssdpStopped)
}()
return me.serveHTTP()
}
func (me *Server) Close() (err error) {
close(me.closed)
err = me.HTTPConn.Close()
<-me.ssdpStopped
return
}
func didl_lite(chardata string) string {
return `<DIDL-Lite` +
` xmlns:dc="http://purl.org/dc/elements/1.1/"` +
` xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"` +
` xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"` +
` xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">` +
chardata +
`</DIDL-Lite>`
}
func (me *Server) location(ip net.IP) string {
url := url.URL{
Scheme: "http",
Host: (&net.TCPAddr{
IP: ip,
Port: me.httpPort(),
}).String(),
Path: rootDescPath,
}
return url.String()
}