github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/serve/dlna/dlna.go (about) 1 package dlna 2 3 import ( 4 "bytes" 5 "encoding/xml" 6 "fmt" 7 "net" 8 "net/http" 9 "net/url" 10 "os" 11 "strconv" 12 "strings" 13 "text/template" 14 "time" 15 16 dms_dlna "github.com/anacrolix/dms/dlna" 17 "github.com/anacrolix/dms/soap" 18 "github.com/anacrolix/dms/ssdp" 19 "github.com/anacrolix/dms/upnp" 20 "github.com/ncw/rclone/cmd" 21 "github.com/ncw/rclone/cmd/serve/dlna/data" 22 "github.com/ncw/rclone/cmd/serve/dlna/dlnaflags" 23 "github.com/ncw/rclone/fs" 24 "github.com/ncw/rclone/vfs" 25 "github.com/ncw/rclone/vfs/vfsflags" 26 "github.com/spf13/cobra" 27 ) 28 29 func init() { 30 dlnaflags.AddFlags(Command.Flags()) 31 vfsflags.AddFlags(Command.Flags()) 32 } 33 34 // Command definition for cobra. 35 var Command = &cobra.Command{ 36 Use: "dlna remote:path", 37 Short: `Serve remote:path over DLNA`, 38 Long: `rclone serve dlna is a DLNA media server for media stored in a rclone remote. Many 39 devices, such as the Xbox and PlayStation, can automatically discover this server in the LAN 40 and play audio/video from it. VLC is also supported. Service discovery uses UDP multicast 41 packets (SSDP) and will thus only work on LANs. 42 43 Rclone will list all files present in the remote, without filtering based on media formats or 44 file extensions. Additionally, there is no media transcoding support. This means that some 45 players might show files that they are not able to play back correctly. 46 47 ` + dlnaflags.Help + vfs.Help, 48 Run: func(command *cobra.Command, args []string) { 49 cmd.CheckArgs(1, 1, command, args) 50 f := cmd.NewFsSrc(args) 51 52 cmd.Run(false, false, command, func() error { 53 s := newServer(f, &dlnaflags.Opt) 54 if err := s.Serve(); err != nil { 55 return err 56 } 57 s.Wait() 58 return nil 59 }) 60 }, 61 } 62 63 const ( 64 serverField = "Linux/3.4 DLNADOC/1.50 UPnP/1.0 DMS/1.0" 65 rootDescPath = "/rootDesc.xml" 66 resPath = "/res" 67 serviceControlURL = "/ctl" 68 ) 69 70 type server struct { 71 // The service SOAP handler keyed by service URN. 72 services map[string]UPnPService 73 74 Interfaces []net.Interface 75 76 HTTPConn net.Listener 77 httpListenAddr string 78 handler http.Handler 79 80 RootDeviceUUID string 81 82 FriendlyName string 83 84 // For waiting on the listener to close 85 waitChan chan struct{} 86 87 // Time interval between SSPD announces 88 AnnounceInterval time.Duration 89 90 f fs.Fs 91 vfs *vfs.VFS 92 } 93 94 func newServer(f fs.Fs, opt *dlnaflags.Options) *server { 95 friendlyName := opt.FriendlyName 96 if friendlyName == "" { 97 friendlyName = makeDefaultFriendlyName() 98 } 99 100 s := &server{ 101 AnnounceInterval: 10 * time.Second, 102 FriendlyName: friendlyName, 103 RootDeviceUUID: makeDeviceUUID(friendlyName), 104 Interfaces: listInterfaces(), 105 106 httpListenAddr: opt.ListenAddr, 107 108 f: f, 109 vfs: vfs.New(f, &vfsflags.Opt), 110 } 111 112 s.services = map[string]UPnPService{ 113 "ContentDirectory": &contentDirectoryService{ 114 server: s, 115 }, 116 "ConnectionManager": &connectionManagerService{ 117 server: s, 118 }, 119 } 120 121 // Setup the various http routes. 122 r := http.NewServeMux() 123 r.HandleFunc(resPath, s.resourceHandler) 124 if opt.LogTrace { 125 r.Handle(rootDescPath, traceLogging(http.HandlerFunc(s.rootDescHandler))) 126 r.Handle(serviceControlURL, traceLogging(http.HandlerFunc(s.serviceControlHandler))) 127 } else { 128 r.HandleFunc(rootDescPath, s.rootDescHandler) 129 r.HandleFunc(serviceControlURL, s.serviceControlHandler) 130 } 131 r.Handle("/static/", http.StripPrefix("/static/", 132 withHeader("Cache-Control", "public, max-age=86400", 133 http.FileServer(data.Assets)))) 134 s.handler = logging(withHeader("Server", serverField, r)) 135 136 return s 137 } 138 139 // UPnPService is the interface for the SOAP service. 140 type UPnPService interface { 141 Handle(action string, argsXML []byte, r *http.Request) (respArgs map[string]string, err error) 142 Subscribe(callback []*url.URL, timeoutSeconds int) (sid string, actualTimeout int, err error) 143 Unsubscribe(sid string) error 144 } 145 146 // Formats the server as a string (used for logging.) 147 func (s *server) String() string { 148 return fmt.Sprintf("DLNA server on %v", s.httpListenAddr) 149 } 150 151 // Returns rclone version number as the model number. 152 func (s *server) ModelNumber() string { 153 return fs.Version 154 } 155 156 // Template used to generate the root device XML descriptor. 157 // 158 // Due to the use of namespaces and various subtleties with device compatibility, 159 // it turns out to be easier to use a template than to marshal XML. 160 // 161 // For rendering, it is passed the server object for context. 162 var rootDescTmpl = template.Must(template.New("rootDesc").Parse(`<?xml version="1.0"?> 163 <root xmlns="urn:schemas-upnp-org:device-1-0" 164 xmlns:dlna="urn:schemas-dlna-org:device-1-0" 165 xmlns:sec="http://www.sec.co.kr/dlna"> 166 <specVersion> 167 <major>1</major> 168 <minor>0</minor> 169 </specVersion> 170 <device> 171 <deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType> 172 <friendlyName>{{.FriendlyName}}</friendlyName> 173 <manufacturer>rclone (rclone.org)</manufacturer> 174 <manufacturerURL>https://rclone.org/</manufacturerURL> 175 <modelDescription>rclone</modelDescription> 176 <modelName>rclone</modelName> 177 <modelNumber>{{.ModelNumber}}</modelNumber> 178 <modelURL>https://rclone.org/</modelURL> 179 <serialNumber>00000000</serialNumber> 180 <UDN>{{.RootDeviceUUID}}</UDN> 181 <dlna:X_DLNACAP/> 182 <dlna:X_DLNADOC>DMS-1.50</dlna:X_DLNADOC> 183 <dlna:X_DLNADOC>M-DMS-1.50</dlna:X_DLNADOC> 184 <sec:ProductCap>smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec</sec:ProductCap> 185 <sec:X_ProductCap>smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec</sec:X_ProductCap> 186 <iconList> 187 <icon> 188 <mimetype>image/png</mimetype> 189 <width>48</width> 190 <height>48</height> 191 <depth>8</depth> 192 <url>/static/rclone-48x48.png</url> 193 </icon> 194 <icon> 195 <mimetype>image/png</mimetype> 196 <width>120</width> 197 <height>120</height> 198 <depth>8</depth> 199 <url>/static/rclone-120x120.png</url> 200 </icon> 201 </iconList> 202 <serviceList> 203 <service> 204 <serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType> 205 <serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId> 206 <SCPDURL>/static/ContentDirectory.xml</SCPDURL> 207 <controlURL>/ctl</controlURL> 208 <eventSubURL></eventSubURL> 209 </service> 210 <service> 211 <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType> 212 <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId> 213 <SCPDURL>/static/ConnectionManager.xml</SCPDURL> 214 <controlURL>/ctl</controlURL> 215 <eventSubURL></eventSubURL> 216 </service> 217 <service> 218 <serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType> 219 <serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId> 220 <SCPDURL>/static/X_MS_MediaReceiverRegistrar.xml</SCPDURL> 221 <controlURL>/ctl</controlURL> 222 <eventSubURL></eventSubURL> 223 </service> 224 </serviceList> 225 <presentationURL>/</presentationURL> 226 </device> 227 </root>`)) 228 229 // Renders the root device descriptor. 230 func (s *server) rootDescHandler(w http.ResponseWriter, r *http.Request) { 231 buffer := new(bytes.Buffer) 232 err := rootDescTmpl.Execute(buffer, s) 233 if err != nil { 234 serveError(s, w, "Failed to create root descriptor XML", err) 235 return 236 } 237 238 w.Header().Set("content-type", `text/xml; charset="utf-8"`) 239 w.Header().Set("cache-control", "private, max-age=60") 240 w.Header().Set("content-length", strconv.FormatInt(int64(buffer.Len()), 10)) 241 _, err = buffer.WriteTo(w) 242 if err != nil { 243 // Network error 244 fs.Debugf(s, "Error writing rootDesc: %v", err) 245 } 246 } 247 248 // Handle a service control HTTP request. 249 func (s *server) serviceControlHandler(w http.ResponseWriter, r *http.Request) { 250 soapActionString := r.Header.Get("SOAPACTION") 251 soapAction, err := upnp.ParseActionHTTPHeader(soapActionString) 252 if err != nil { 253 serveError(s, w, "Could not parse SOAPACTION header", err) 254 return 255 } 256 var env soap.Envelope 257 if err := xml.NewDecoder(r.Body).Decode(&env); err != nil { 258 serveError(s, w, "Could not parse SOAP request body", err) 259 return 260 } 261 262 w.Header().Set("Content-Type", `text/xml; charset="utf-8"`) 263 w.Header().Set("Ext", "") 264 soapRespXML, code := func() ([]byte, int) { 265 respArgs, err := s.soapActionResponse(soapAction, env.Body.Action, r) 266 if err != nil { 267 fs.Errorf(s, "Error invoking %v: %v", soapAction, err) 268 upnpErr := upnp.ConvertError(err) 269 return mustMarshalXML(soap.NewFault("UPnPError", upnpErr)), http.StatusInternalServerError 270 } 271 return marshalSOAPResponse(soapAction, respArgs), http.StatusOK 272 }() 273 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) 274 w.WriteHeader(code) 275 if _, err := w.Write([]byte(bodyStr)); err != nil { 276 fs.Infof(s, "Error writing response: %v", err) 277 } 278 } 279 280 // Handle a SOAP request and return the response arguments or UPnP error. 281 func (s *server) soapActionResponse(sa upnp.SoapAction, actionRequestXML []byte, r *http.Request) (map[string]string, error) { 282 service, ok := s.services[sa.Type] 283 if !ok { 284 // TODO: What's the invalid service error? 285 return nil, upnp.Errorf(upnp.InvalidActionErrorCode, "Invalid service: %s", sa.Type) 286 } 287 return service.Handle(sa.Action, actionRequestXML, r) 288 } 289 290 // Serves actual resources (media files). 291 func (s *server) resourceHandler(w http.ResponseWriter, r *http.Request) { 292 remotePath := r.URL.Query().Get("path") 293 node, err := s.vfs.Stat(remotePath) 294 if err != nil { 295 http.NotFound(w, r) 296 return 297 } 298 299 w.Header().Set("Content-Length", strconv.FormatInt(node.Size(), 10)) 300 301 // add some DLNA specific headers 302 if r.Header.Get("getContentFeatures.dlna.org") != "" { 303 w.Header().Set("contentFeatures.dlna.org", dms_dlna.ContentFeatures{ 304 SupportRange: true, 305 }.String()) 306 } 307 w.Header().Set("transferMode.dlna.org", "Streaming") 308 309 file := node.(*vfs.File) 310 in, err := file.Open(os.O_RDONLY) 311 if err != nil { 312 serveError(node, w, "Could not open resource", err) 313 return 314 } 315 defer fs.CheckClose(in, &err) 316 317 http.ServeContent(w, r, remotePath, node.ModTime(), in) 318 } 319 320 // Serve runs the server - returns the error only if 321 // the listener was not started; does not block, so 322 // use s.Wait() to block on the listener indefinitely. 323 func (s *server) Serve() (err error) { 324 if s.HTTPConn == nil { 325 s.HTTPConn, err = net.Listen("tcp", s.httpListenAddr) 326 if err != nil { 327 return 328 } 329 } 330 331 go func() { 332 s.startSSDP() 333 }() 334 335 go func() { 336 fs.Logf(s.f, "Serving HTTP on %s", s.HTTPConn.Addr().String()) 337 338 err = s.serveHTTP() 339 if err != nil { 340 fs.Logf(s.f, "Error on serving HTTP server: %v", err) 341 } 342 }() 343 344 return nil 345 } 346 347 // Wait blocks while the listener is open. 348 func (s *server) Wait() { 349 <-s.waitChan 350 } 351 352 func (s *server) Close() { 353 err := s.HTTPConn.Close() 354 if err != nil { 355 fs.Errorf(s.f, "Error closing HTTP server: %v", err) 356 return 357 } 358 close(s.waitChan) 359 } 360 361 // Run SSDP (multicast for server discovery) on all interfaces. 362 func (s *server) startSSDP() { 363 active := 0 364 stopped := make(chan struct{}) 365 for _, intf := range s.Interfaces { 366 active++ 367 go func(intf2 net.Interface) { 368 defer func() { 369 stopped <- struct{}{} 370 }() 371 s.ssdpInterface(intf2) 372 }(intf) 373 } 374 for active > 0 { 375 <-stopped 376 active-- 377 } 378 } 379 380 // Run SSDP server on an interface. 381 func (s *server) ssdpInterface(intf net.Interface) { 382 // Figure out which HTTP location to advertise based on the interface IP. 383 advertiseLocationFn := func(ip net.IP) string { 384 url := url.URL{ 385 Scheme: "http", 386 Host: (&net.TCPAddr{ 387 IP: ip, 388 Port: s.HTTPConn.Addr().(*net.TCPAddr).Port, 389 }).String(), 390 Path: rootDescPath, 391 } 392 return url.String() 393 } 394 395 // Note that the devices and services advertised here via SSDP should be 396 // in agreement with the rootDesc XML descriptor that is defined above. 397 ssdpServer := ssdp.Server{ 398 Interface: intf, 399 Devices: []string{ 400 "urn:schemas-upnp-org:device:MediaServer:1"}, 401 Services: []string{ 402 "urn:schemas-upnp-org:service:ContentDirectory:1", 403 "urn:schemas-upnp-org:service:ConnectionManager:1", 404 "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"}, 405 Location: advertiseLocationFn, 406 Server: serverField, 407 UUID: s.RootDeviceUUID, 408 NotifyInterval: s.AnnounceInterval, 409 } 410 411 // An interface with these flags should be valid for SSDP. 412 const ssdpInterfaceFlags = net.FlagUp | net.FlagMulticast 413 414 if err := ssdpServer.Init(); err != nil { 415 if intf.Flags&ssdpInterfaceFlags != ssdpInterfaceFlags { 416 // Didn't expect it to work anyway. 417 return 418 } 419 if strings.Contains(err.Error(), "listen") { 420 // OSX has a lot of dud interfaces. Failure to create a socket on 421 // the interface are what we're expecting if the interface is no 422 // good. 423 return 424 } 425 fs.Errorf(s, "Error creating ssdp server on %s: %s", intf.Name, err) 426 return 427 } 428 defer ssdpServer.Close() 429 fs.Infof(s, "Started SSDP on %v", intf.Name) 430 stopped := make(chan struct{}) 431 go func() { 432 defer close(stopped) 433 if err := ssdpServer.Serve(); err != nil { 434 fs.Errorf(s, "%q: %q\n", intf.Name, err) 435 } 436 }() 437 select { 438 case <-s.waitChan: 439 // Returning will close the server. 440 case <-stopped: 441 } 442 } 443 444 func (s *server) serveHTTP() error { 445 srv := &http.Server{ 446 Handler: s.handler, 447 } 448 err := srv.Serve(s.HTTPConn) 449 select { 450 case <-s.waitChan: 451 return nil 452 default: 453 return err 454 } 455 }