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.
|
||||
persist := openDB(*configService)
|
||||
flamenco := buildFlamencoAPI(configService, persist)
|
||||
e := buildWebService(flamenco, persist)
|
||||
e := buildWebService(flamenco, persist, ssdp)
|
||||
|
||||
installSignalHandler(mainCtxCancel)
|
||||
|
||||
@ -121,7 +121,11 @@ func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) ap
|
||||
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.HideBanner = true
|
||||
|
||||
@ -154,6 +158,13 @@ func buildWebService(flamenco api.ServerInterface, persist api_impl.PersistenceS
|
||||
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
|
||||
routeLogger := log.Level(zerolog.DebugLevel)
|
||||
routeLogger.Debug().Msg("available routes:")
|
||||
|
@ -22,8 +22,11 @@ package upnp_ssdp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"git.blender.org/flamenco/internal/appinfo"
|
||||
"github.com/fromkeith/gossdp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@ -60,8 +63,9 @@ func (s *Server) AddAdvertisement(serviceLocation string) {
|
||||
|
||||
// AddAdvertisementURLs constructs a service location from the given URLs, and
|
||||
// adds the advertisement for it.
|
||||
func (s *Server) AddAdvertisementURLs(urls []url.URL) {
|
||||
for _, url := range urls {
|
||||
func (s *Server) AddAdvertisementURLs(baseURLs []url.URL) {
|
||||
for _, url := range baseURLs {
|
||||
url.Path = path.Join(url.Path, serviceDescriptionPath)
|
||||
s.AddAdvertisement(url.String())
|
||||
}
|
||||
}
|
||||
@ -99,3 +103,26 @@ func (s *Server) Run(ctx context.Context) {
|
||||
|
||||
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 (
|
||||
FlamencoUUID = "aa80bc5f-d0af-46b8-8630-23bd7e80ec4d"
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -76,14 +77,28 @@ func autodiscoverManager(ctx context.Context) (string, error) {
|
||||
return usableURLs[0], nil
|
||||
}
|
||||
|
||||
// pingManager connects to a Manager and returns true if it responds.
|
||||
func pingManager(ctx context.Context, url string) bool {
|
||||
logger := log.With().Str("manager", url).Logger()
|
||||
// pingManager connects to a Manager and returns the service URL if it responds.
|
||||
func pingManager(ctx context.Context, descriptionURL string) string {
|
||||
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 {
|
||||
logger.Warn().Err(err).Msg("unable to create API client with this URL")
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
// 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")
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
if resp.JSON200 == nil {
|
||||
logger.Warn().
|
||||
Int("httpStatus", resp.StatusCode()).
|
||||
Msg("unable to get Flamenco version, unexpected reply")
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("version", resp.JSON200.Version).
|
||||
Str("name", resp.JSON200.Name).
|
||||
Msg("found Flamenco Manager")
|
||||
return true
|
||||
return serviceURL
|
||||
}
|
||||
|
||||
// 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))
|
||||
mutex := new(sync.Mutex)
|
||||
|
||||
pingURL := func(idx int, url string) {
|
||||
pingURL := func(idx int, descriptionURL string) {
|
||||
defer wg.Done()
|
||||
ok := pingManager(ctx, url)
|
||||
serviceURL := pingManager(ctx, descriptionURL)
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if !ok {
|
||||
// Erase the URL from the usable list.
|
||||
// Modifying the original slice instead of appending to a new one ensures
|
||||
// the original order is maintained.
|
||||
urls[idx] = ""
|
||||
}
|
||||
// This is either the actual service URL, or an empty string if the
|
||||
// description URL couldn't be reached/parsed.
|
||||
urls[idx] = serviceURL
|
||||
}
|
||||
|
||||
for idx, url := range urls {
|
||||
|
Loading…
x
Reference in New Issue
Block a user