github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/ui/discovery/ssdp.go (about)

     1  /*
     2   * Copyright (C) 2019 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package discovery
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"net"
    24  	"net/http"
    25  	"net/url"
    26  	"runtime"
    27  	"sync"
    28  	"text/template"
    29  	"time"
    30  
    31  	"github.com/gofrs/uuid"
    32  	"github.com/koron/go-ssdp"
    33  	"github.com/mysteriumnetwork/node/core/ip"
    34  	"github.com/mysteriumnetwork/node/metadata"
    35  	"github.com/mysteriumnetwork/node/requests"
    36  	"github.com/pkg/errors"
    37  	"github.com/rs/zerolog/log"
    38  )
    39  
    40  const deviceDescriptionTemplate = `<?xml version="1.0"?>
    41  <root xmlns="urn:schemas-upnp-org:device-1-0" configId="1">
    42  	<specVersion>
    43  		<major>1</major>
    44  		<minor>1</minor>
    45  	</specVersion>
    46  	<device>
    47  		<deviceType>urn:schemas-upnp-org:device:node:1</deviceType>
    48  		<friendlyName>Mysterium Node</friendlyName>
    49  
    50  		<manufacturer>Mysterium Network</manufacturer>
    51  		<manufacturerURL>https://mysterium.network/</manufacturerURL>
    52  
    53  		<modelName>Raspberry Node</modelName>
    54  		<modelNumber>{{.Version}}</modelNumber>
    55  		<modelURL>https://mysterium.network/node/</modelURL>
    56  
    57  		<UDN>uuid:{{.UUID}}</UDN>
    58  		<presentationURL>{{.URL}}</presentationURL>
    59  	</device>
    60  </root>`
    61  
    62  type ssdpServer struct {
    63  	uiPort     int
    64  	uuid       string
    65  	ssdp       *ssdp.Advertiser
    66  	quit       chan struct{}
    67  	once       sync.Once
    68  	httpClient *requests.HTTPClient
    69  }
    70  
    71  func newSSDPServer(uiPort int, httpClient *requests.HTTPClient) *ssdpServer {
    72  	return &ssdpServer{
    73  		uiPort:     uiPort,
    74  		quit:       make(chan struct{}),
    75  		httpClient: httpClient,
    76  	}
    77  }
    78  
    79  func (ss *ssdpServer) Start() (err error) {
    80  	ss.uuid, err = generateUUID()
    81  	if err != nil {
    82  		return errors.Wrap(err, "failed to generate new UUID")
    83  	}
    84  
    85  	url, err := ss.serveDeviceDescriptionDocument()
    86  	if err != nil {
    87  		return errors.Wrap(err, "failed to serve device description document")
    88  	}
    89  
    90  	ss.ssdp, err = ssdp.Advertise(
    91  		"upnp:rootdevice",                   // ST: Type
    92  		"uuid:"+ss.uuid+"::upnp:rootdevice", // USN: ID
    93  		url.String(),                        // LOCATION: location header
    94  		runtime.GOOS+" UPnP/1.1 node/"+metadata.VersionAsString(), // SERVER: server header
    95  		1800) // cache control, max-age. A duration for which the advertisement is valid
    96  	if err != nil {
    97  		return errors.Wrap(err, "failed to start SSDP advertiser")
    98  	}
    99  
   100  	if ss.ssdpPubliclyAccessible() {
   101  		log.Warn().Msg("SSDP publicly accessible. Stopping it to prevent abusing service")
   102  		err := ss.Stop()
   103  		if err != nil {
   104  			log.Error().Err(err).Msg("Failed to stop SSDP")
   105  		}
   106  	}
   107  
   108  	for {
   109  		select {
   110  		case <-time.After(30 * time.Second):
   111  			if err := ss.ssdp.Alive(); err != nil {
   112  				log.Warn().Err(err).Msg("Failed to sent SSDP Alive message")
   113  			}
   114  		case <-ss.quit:
   115  			return nil
   116  		}
   117  	}
   118  }
   119  
   120  func (ss *ssdpServer) Stop() error {
   121  	var closeError error
   122  	ss.once.Do(func() {
   123  		close(ss.quit)
   124  		if ss.ssdp != nil {
   125  			if err := ss.ssdp.Bye(); err != nil {
   126  				log.Error().Err(err).Msg("Failed to send SSDP bye message")
   127  			}
   128  			closeError = errors.Wrap(ss.ssdp.Close(), "failed to send SSDP bye message")
   129  		}
   130  	})
   131  
   132  	return closeError
   133  }
   134  
   135  func (ss *ssdpServer) serveDeviceDescriptionDocument() (url.URL, error) {
   136  	resolver := ip.NewResolver(ss.httpClient, "0.0.0.0", "", ip.IPFallbackAddresses)
   137  
   138  	outIP, err := resolver.GetOutboundIP()
   139  	if err != nil {
   140  		return url.URL{}, err
   141  	}
   142  
   143  	listener, err := net.Listen("tcp", ":0")
   144  	if err != nil {
   145  		return url.URL{}, err
   146  	}
   147  
   148  	deviceDoc := ss.deviceDescription(outIP)
   149  
   150  	mux := http.NewServeMux()
   151  	mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
   152  		fmt.Fprint(w, deviceDoc)
   153  	})
   154  
   155  	go func() {
   156  		go http.Serve(listener, mux)
   157  		<-ss.quit
   158  		listener.Close()
   159  	}()
   160  
   161  	port := listener.Addr().(*net.TCPAddr).Port
   162  	return url.URL{
   163  		Scheme: "http",
   164  		Host:   fmt.Sprintf("%s:%d", outIP, port),
   165  	}, nil
   166  }
   167  
   168  func (ss *ssdpServer) deviceDescription(ip string) string {
   169  	var buf bytes.Buffer
   170  	deviceDescription := template.Must(template.New("SSDPDeviceDescription").Parse(deviceDescriptionTemplate))
   171  	_ = deviceDescription.Execute(&buf,
   172  		struct{ URL, Version, UUID string }{
   173  			fmt.Sprintf("http://%s:%d/", ip, ss.uiPort),
   174  			metadata.VersionAsString(),
   175  			ss.uuid,
   176  		})
   177  
   178  	return buf.String()
   179  }
   180  
   181  func (ss *ssdpServer) ssdpPubliclyAccessible() bool {
   182  	// https://github.com/cloudflare/badupnp
   183  	// https://blog.cloudflare.com/ssdp-100gbps/
   184  	// > Furthermore, we prepared on online checking website.
   185  	// > Click if you want to know if your public IP address has a vulnerable SSDP service:
   186  	// > https://badupnp.benjojo.co.uk/
   187  
   188  	req, err := requests.NewGetRequest("https://badupnp.benjojo.co.uk", "test", nil)
   189  	if err != nil {
   190  		log.Error().Err(err).Msg("Failed to create HTTP request to test a vulnerable SSDP service")
   191  		return false
   192  	}
   193  
   194  	var result struct {
   195  		Result bool `json:"result"`
   196  	}
   197  
   198  	err = ss.httpClient.DoRequestAndParseResponse(req, &result)
   199  	if err != nil {
   200  		log.Error().Err(err).Msg("Failed to detect IP has a vulnerable SSDP service")
   201  		return false
   202  	}
   203  
   204  	return result.Result
   205  }
   206  
   207  func generateUUID() (string, error) {
   208  	uid, err := uuid.NewV4()
   209  	if err != nil {
   210  		return "", err
   211  	}
   212  
   213  	return uid.String(), nil
   214  }