ssdp.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package ssdp
  2. import (
  3. "errors"
  4. "io/ioutil"
  5. "log"
  6. "net"
  7. "net/http"
  8. "os/exec"
  9. "runtime"
  10. "strconv"
  11. "strings"
  12. "time"
  13. ssdp "github.com/koron/go-ssdp"
  14. "github.com/valyala/fasttemplate"
  15. )
  16. type SSDPOption struct {
  17. URLBase string
  18. Hostname string
  19. Vendor string
  20. VendorURL string
  21. ModelName string
  22. ModelDesc string
  23. Serial string
  24. UUID string
  25. }
  26. type SSDPHost struct {
  27. ADV *ssdp.Advertiser
  28. advStarted bool
  29. SSDPTemplateFile string
  30. Option *SSDPOption
  31. quit chan bool
  32. }
  33. func NewSSDPHost(outboundIP string, port int, templateFile string, option SSDPOption) (*SSDPHost, error) {
  34. if runtime.GOOS == "linux" {
  35. //In case there are more than 1 network interface connect to the same LAN, choose the first one by default
  36. interfaceName, err := getFirstNetworkInterfaceName()
  37. if err != nil {
  38. //Ignore the interface binding
  39. log.Println("[WARN] No connected network interface. Starting SSDP anyway.")
  40. } else {
  41. mainNIC, err := net.InterfaceByName(interfaceName)
  42. if err != nil {
  43. log.Println("[WARN] Unable to get interface by name: " + interfaceName + ". Starting SSDP on all IPv4 interfaces.")
  44. } else {
  45. ssdp.Interfaces = []net.Interface{*mainNIC}
  46. }
  47. }
  48. }
  49. ad, err := ssdp.Advertise(
  50. "upnp:rootdevice", // send as "ST"
  51. "uuid:"+option.UUID, // send as "USN"
  52. "http://"+outboundIP+":"+strconv.Itoa(port)+"/ssdp.xml", // send as "LOCATION"
  53. "arozos/"+outboundIP, // send as "SERVER"
  54. 30) // send as "maxAge" in "CACHE-CONTROL"
  55. if err != nil {
  56. return &SSDPHost{}, err
  57. }
  58. return &SSDPHost{
  59. ADV: ad,
  60. advStarted: false,
  61. SSDPTemplateFile: templateFile,
  62. Option: &option,
  63. }, nil
  64. }
  65. func (a *SSDPHost) Start() {
  66. //Advertise ssdp
  67. http.HandleFunc("/ssdp.xml", a.handleSSDP)
  68. log.Println("Starting SSDP Discovery Service: " + a.Option.URLBase)
  69. var aliveTick <-chan time.Time
  70. aliveTick = time.Tick(time.Duration(5) * time.Second)
  71. quit := make(chan bool)
  72. a.quit = quit
  73. a.advStarted = true
  74. go func(ad *ssdp.Advertiser) {
  75. for {
  76. select {
  77. case <-aliveTick:
  78. ad.Alive()
  79. case <-quit:
  80. ad.Bye()
  81. ad.Close()
  82. break
  83. }
  84. }
  85. }(a.ADV)
  86. }
  87. func (a *SSDPHost) Close() {
  88. if a != nil {
  89. if a.advStarted {
  90. a.quit <- true
  91. }
  92. }
  93. }
  94. //Serve the xml file with the given properties
  95. func (a *SSDPHost) handleSSDP(w http.ResponseWriter, r *http.Request) {
  96. //Load the ssdp xml from file
  97. template, err := ioutil.ReadFile(a.SSDPTemplateFile)
  98. if err != nil {
  99. w.Write([]byte("SSDP.XML NOT FOUND"))
  100. return
  101. }
  102. t := fasttemplate.New(string(template), "{{", "}}")
  103. s := t.ExecuteString(map[string]interface{}{
  104. "urlbase": a.Option.URLBase,
  105. "hostname": a.Option.Hostname,
  106. "vendor": a.Option.Vendor,
  107. "vendorurl": a.Option.VendorURL,
  108. "modeldesc": a.Option.ModelDesc,
  109. "modelname": a.Option.ModelName,
  110. "uuid": a.Option.UUID,
  111. "serial": a.Option.Serial,
  112. })
  113. w.Write([]byte(s))
  114. }
  115. //Helper functions
  116. func getFirstNetworkInterfaceName() (string, error) {
  117. if runtime.GOOS == "linux" {
  118. if pkg_exists("ip") {
  119. //Use the fast method
  120. cmd := exec.Command("bash", "-c", `ip route | grep default | sed -e "s/^.*dev.//" -e "s/.proto.*//"`)
  121. out, _ := cmd.CombinedOutput()
  122. if strings.TrimSpace(string(out)) == "" {
  123. //No interface found.
  124. return "", errors.New("No interface found")
  125. } else {
  126. return strings.Split(strings.TrimSpace(string(out)), "\n")[0], nil
  127. }
  128. } else if pkg_exists("ifconfig") {
  129. //Guess it from ifconfig list
  130. cmd := exec.Command("bash", "-c", `ifconfig -a | sed -E 's/[[:space:]:].*//;/^$/d'`)
  131. out, _ := cmd.CombinedOutput()
  132. if strings.TrimSpace(string(out)) == "" {
  133. //No interface found.
  134. return "", errors.New("No interface found")
  135. } else {
  136. return strings.Split(strings.TrimSpace(string(out)), "\n")[0], nil
  137. }
  138. }
  139. }
  140. return "", errors.New("Not supported platform or missing package")
  141. }
  142. func pkg_exists(pkgname string) bool {
  143. cmd := exec.Command("which", pkgname)
  144. out, _ := cmd.CombinedOutput()
  145. if len(string(out)) > 1 {
  146. return true
  147. } else {
  148. return false
  149. }
  150. }