UPnP/SSDP: actually serve a description.xml
The `description.xml` file is part of the UPnP Service Discovery protocol, see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rxad/9225d145-6b2c-40d5-8ee8-7d837379fc25
This commit is contained in:
parent
353da58ee9
commit
842255a065
@ -73,7 +73,7 @@ func main() {
|
|||||||
// Construct the services.
|
// Construct the services.
|
||||||
persist := openDB(*configService)
|
persist := openDB(*configService)
|
||||||
flamenco := buildFlamencoAPI(configService, persist)
|
flamenco := buildFlamencoAPI(configService, persist)
|
||||||
e := buildWebService(flamenco, persist)
|
e := buildWebService(flamenco, persist, ssdp)
|
||||||
|
|
||||||
installSignalHandler(mainCtxCancel)
|
installSignalHandler(mainCtxCancel)
|
||||||
|
|
||||||
@ -121,7 +121,11 @@ func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) ap
|
|||||||
return flamenco
|
return flamenco
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceService) *echo.Echo {
|
func buildWebService(
|
||||||
|
flamenco api.ServerInterface,
|
||||||
|
persist api_impl.PersistenceService,
|
||||||
|
ssdp *upnp_ssdp.Server,
|
||||||
|
) *echo.Echo {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
|
|
||||||
@ -154,6 +158,13 @@ func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceS
|
|||||||
return c.Redirect(http.StatusTemporaryRedirect, "/api/swagger-ui/")
|
return c.Redirect(http.StatusTemporaryRedirect, "/api/swagger-ui/")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Serve UPnP service descriptions.
|
||||||
|
if ssdp != nil {
|
||||||
|
e.GET(ssdp.DescriptionPath(), func(c echo.Context) error {
|
||||||
|
return c.XMLPretty(http.StatusOK, ssdp.Description(), " ")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Log available routes
|
// Log available routes
|
||||||
routeLogger := log.Level(zerolog.DebugLevel)
|
routeLogger := log.Level(zerolog.DebugLevel)
|
||||||
routeLogger.Debug().Msg("available routes:")
|
routeLogger.Debug().Msg("available routes:")
|
||||||
|
@ -22,8 +22,11 @@ package upnp_ssdp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"git.blender.org/flamenco/internal/appinfo"
|
||||||
"github.com/fromkeith/gossdp"
|
"github.com/fromkeith/gossdp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
@ -60,8 +63,9 @@ func (s *Server) AddAdvertisement(serviceLocation string) {
|
|||||||
|
|
||||||
// AddAdvertisementURLs constructs a service location from the given URLs, and
|
// AddAdvertisementURLs constructs a service location from the given URLs, and
|
||||||
// adds the advertisement for it.
|
// adds the advertisement for it.
|
||||||
func (s *Server) AddAdvertisementURLs(urls []url.URL) {
|
func (s *Server) AddAdvertisementURLs(baseURLs []url.URL) {
|
||||||
for _, url := range urls {
|
for _, url := range baseURLs {
|
||||||
|
url.Path = path.Join(url.Path, serviceDescriptionPath)
|
||||||
s.AddAdvertisement(url.String())
|
s.AddAdvertisement(url.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,3 +103,26 @@ func (s *Server) Run(ctx context.Context) {
|
|||||||
|
|
||||||
s.log.Info().Msg("UPnP/SSDP advertisement stopped")
|
s.log.Info().Msg("UPnP/SSDP advertisement stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Description() Description {
|
||||||
|
return Description{
|
||||||
|
SpecVersion: SpecVersion{1, 0},
|
||||||
|
URLBase: "/",
|
||||||
|
Device: Device{
|
||||||
|
DeviceType: FlamencoServiceType,
|
||||||
|
FriendlyName: appinfo.FormattedApplicationInfo(),
|
||||||
|
ModelName: appinfo.ApplicationName,
|
||||||
|
ModelDescription: "Flamenco render farm, Manager component",
|
||||||
|
ModelURL: "https://flamenco.io/",
|
||||||
|
UDN: fmt.Sprintf("uuid:%s", FlamencoUUID),
|
||||||
|
Manufacturer: "Blender",
|
||||||
|
ManufacturerURL: "https://www.blender.org/",
|
||||||
|
ServiceList: []string{},
|
||||||
|
PresentationURL: "/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) DescriptionPath() string {
|
||||||
|
return serviceDescriptionPath
|
||||||
|
}
|
||||||
|
@ -24,4 +24,30 @@ package upnp_ssdp
|
|||||||
const (
|
const (
|
||||||
FlamencoUUID = "aa80bc5f-d0af-46b8-8630-23bd7e80ec4d"
|
FlamencoUUID = "aa80bc5f-d0af-46b8-8630-23bd7e80ec4d"
|
||||||
FlamencoServiceType = "urn:flamenco:manager:0"
|
FlamencoServiceType = "urn:flamenco:manager:0"
|
||||||
|
|
||||||
|
serviceDescriptionPath = "/upnp/description.xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SSDP Service description, usually served on some URL ending in `/description.xml`.
|
||||||
|
type Description struct {
|
||||||
|
XMLName string `xml:"urn:schemas-upnp-org:device-1-0 root"`
|
||||||
|
SpecVersion SpecVersion `xml:"specVersion"`
|
||||||
|
URLBase string `xml:"URLBase"`
|
||||||
|
Device Device `xml:"device"`
|
||||||
|
}
|
||||||
|
type SpecVersion struct {
|
||||||
|
Major int `xml:"major"`
|
||||||
|
Minor int `xml:"minor"`
|
||||||
|
}
|
||||||
|
type Device struct {
|
||||||
|
DeviceType string `xml:"deviceType"`
|
||||||
|
FriendlyName string `xml:"friendlyName"`
|
||||||
|
Manufacturer string `xml:"manufacturer"`
|
||||||
|
ManufacturerURL string `xml:"manufacturerURL"`
|
||||||
|
ModelDescription string `xml:"modelDescription"`
|
||||||
|
ModelName string `xml:"modelName"`
|
||||||
|
ModelURL string `xml:"modelURL"`
|
||||||
|
UDN string `xml:"UDN"`
|
||||||
|
ServiceList []string `xml:"serviceList"` // not []string, but since the list is empty, it doesn't matter.
|
||||||
|
PresentationURL string `xml:"presentationURL"`
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -76,14 +77,28 @@ func autodiscoverManager(ctx context.Context) (string, error) {
|
|||||||
return usableURLs[0], nil
|
return usableURLs[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pingManager connects to a Manager and returns true if it responds.
|
// pingManager connects to a Manager and returns the service URL if it responds.
|
||||||
func pingManager(ctx context.Context, url string) bool {
|
func pingManager(ctx context.Context, descriptionURL string) string {
|
||||||
logger := log.With().Str("manager", url).Logger()
|
var err error
|
||||||
|
parsedURL, err := url.Parse(descriptionURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Str("description", descriptionURL).
|
||||||
|
Err(err).
|
||||||
|
Msg("received invalid URL from autodiscovery, ignoring")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
client, err := api.NewClientWithResponses(url)
|
// TODO: actually get the description XML from Flamenco Manager and use the path in there.
|
||||||
|
// For now, just assume it's on the root.
|
||||||
|
parsedURL.Path = "/"
|
||||||
|
serviceURL := parsedURL.String()
|
||||||
|
|
||||||
|
logger := log.With().Str("url", serviceURL).Logger()
|
||||||
|
client, err := api.NewClientWithResponses(serviceURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("unable to create API client with this URL")
|
logger.Warn().Err(err).Msg("unable to create API client with this URL")
|
||||||
return false
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.GetVersionWithResponse(ctx)
|
resp, err := client.GetVersionWithResponse(ctx)
|
||||||
@ -91,21 +106,21 @@ func pingManager(ctx context.Context, url string) bool {
|
|||||||
// It is expected that some URLs will not work. Showing the error message at
|
// It is expected that some URLs will not work. Showing the error message at
|
||||||
// info/warn level will likely confuse people, so leave it at debug level.
|
// info/warn level will likely confuse people, so leave it at debug level.
|
||||||
logger.Debug().Err(err).Msg("unable to get Flamenco version from Manager")
|
logger.Debug().Err(err).Msg("unable to get Flamenco version from Manager")
|
||||||
return false
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.JSON200 == nil {
|
if resp.JSON200 == nil {
|
||||||
logger.Warn().
|
logger.Warn().
|
||||||
Int("httpStatus", resp.StatusCode()).
|
Int("httpStatus", resp.StatusCode()).
|
||||||
Msg("unable to get Flamenco version, unexpected reply")
|
Msg("unable to get Flamenco version, unexpected reply")
|
||||||
return false
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().
|
logger.Info().
|
||||||
Str("version", resp.JSON200.Version).
|
Str("version", resp.JSON200.Version).
|
||||||
Str("name", resp.JSON200.Name).
|
Str("name", resp.JSON200.Name).
|
||||||
Msg("found Flamenco Manager")
|
Msg("found Flamenco Manager")
|
||||||
return true
|
return serviceURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// pingManagers pings all URLs in parallel, returning only those that responded.
|
// pingManagers pings all URLs in parallel, returning only those that responded.
|
||||||
@ -116,19 +131,16 @@ func pingManagers(ctx context.Context, urls []string) []string {
|
|||||||
wg.Add(len(urls))
|
wg.Add(len(urls))
|
||||||
mutex := new(sync.Mutex)
|
mutex := new(sync.Mutex)
|
||||||
|
|
||||||
pingURL := func(idx int, url string) {
|
pingURL := func(idx int, descriptionURL string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ok := pingManager(ctx, url)
|
serviceURL := pingManager(ctx, descriptionURL)
|
||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
defer mutex.Unlock()
|
defer mutex.Unlock()
|
||||||
|
|
||||||
if !ok {
|
// This is either the actual service URL, or an empty string if the
|
||||||
// Erase the URL from the usable list.
|
// description URL couldn't be reached/parsed.
|
||||||
// Modifying the original slice instead of appending to a new one ensures
|
urls[idx] = serviceURL
|
||||||
// the original order is maintained.
|
|
||||||
urls[idx] = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, url := range urls {
|
for idx, url := range urls {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user