package ssdp import ( "errors" "io/ioutil" "log" "net" "net/http" "os/exec" "runtime" "strconv" "strings" "time" ssdp "github.com/koron/go-ssdp" "github.com/valyala/fasttemplate" ) type SSDPOption struct { URLBase string Hostname string Vendor string VendorURL string ModelName string ModelDesc string Serial string UUID string } type SSDPHost struct { ADV *ssdp.Advertiser advStarted bool SSDPTemplateFile string Option *SSDPOption quit chan bool } func NewSSDPHost(outboundIP string, port int, templateFile string, option SSDPOption) (*SSDPHost, error) { if runtime.GOOS == "linux" { //In case there are more than 1 network interface connect to the same LAN, choose the first one by default interfaceName, err := getFirstNetworkInterfaceName() if err != nil { //Ignore the interface binding log.Println("[WARN] No connected network interface. Starting SSDP anyway.") } else { mainNIC, err := net.InterfaceByName(interfaceName) if err != nil { log.Println("[WARN] Unable to get interface by name: " + interfaceName + ". Starting SSDP on all IPv4 interfaces.") } else { ssdp.Interfaces = []net.Interface{*mainNIC} } } } ad, err := ssdp.Advertise( "upnp:rootdevice", // send as "ST" "uuid:"+option.UUID, // send as "USN" "http://"+outboundIP+":"+strconv.Itoa(port)+"/ssdp.xml", // send as "LOCATION" "arozos/"+outboundIP, // send as "SERVER" 30) // send as "maxAge" in "CACHE-CONTROL" if err != nil { return &SSDPHost{}, err } return &SSDPHost{ ADV: ad, advStarted: false, SSDPTemplateFile: templateFile, Option: &option, }, nil } func (a *SSDPHost) Start() { //Advertise ssdp http.HandleFunc("/ssdp.xml", a.handleSSDP) log.Println("Starting SSDP Discovery Service: " + a.Option.URLBase) var aliveTick <-chan time.Time aliveTick = time.Tick(time.Duration(5) * time.Second) quit := make(chan bool) a.quit = quit a.advStarted = true go func(ad *ssdp.Advertiser) { for { select { case <-aliveTick: ad.Alive() case <-quit: ad.Bye() ad.Close() break } } }(a.ADV) } func (a *SSDPHost) Close() { if a != nil { if a.advStarted { a.quit <- true } } } //Serve the xml file with the given properties func (a *SSDPHost) handleSSDP(w http.ResponseWriter, r *http.Request) { //Load the ssdp xml from file template, err := ioutil.ReadFile(a.SSDPTemplateFile) if err != nil { w.Write([]byte("SSDP.XML NOT FOUND")) return } t := fasttemplate.New(string(template), "{{", "}}") s := t.ExecuteString(map[string]interface{}{ "urlbase": a.Option.URLBase, "hostname": a.Option.Hostname, "vendor": a.Option.Vendor, "vendorurl": a.Option.VendorURL, "modeldesc": a.Option.ModelDesc, "modelname": a.Option.ModelName, "uuid": a.Option.UUID, "serial": a.Option.Serial, }) w.Write([]byte(s)) } //Helper functions func getFirstNetworkInterfaceName() (string, error) { if runtime.GOOS == "linux" { if pkg_exists("ip") { //Use the fast method cmd := exec.Command("bash", "-c", `ip route | grep default | sed -e "s/^.*dev.//" -e "s/.proto.*//"`) out, _ := cmd.CombinedOutput() if strings.TrimSpace(string(out)) == "" { //No interface found. return "", errors.New("No interface found") } else { return strings.Split(strings.TrimSpace(string(out)), "\n")[0], nil } } else if pkg_exists("ifconfig") { //Guess it from ifconfig list cmd := exec.Command("bash", "-c", `ifconfig -a | sed -E 's/[[:space:]:].*//;/^$/d'`) out, _ := cmd.CombinedOutput() if strings.TrimSpace(string(out)) == "" { //No interface found. return "", errors.New("No interface found") } else { return strings.Split(strings.TrimSpace(string(out)), "\n")[0], nil } } } return "", errors.New("Not supported platform or missing package") } func pkg_exists(pkgname string) bool { cmd := exec.Command("which", pkgname) out, _ := cmd.CombinedOutput() if len(string(out)) > 1 { return true } else { return false } }