decred.org/dcrdex@v1.0.5/server/noderelay/cmd/sourcenode/main.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  // sourcenode is the client for a NodeRelay. A NodeRelay is a remote server that
     5  // can request data from the API of a local service, presumably running on a
     6  // private machine which is not accessible from any static IP address or domain.
     7  // In this inverted consumer-provider model, the provider connects to the
     8  // consumer, and then accepts data requests over WebSockets, routing them to the
     9  // local service and responding with the results.
    10  
    11  package main
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"crypto/tls"
    17  	"crypto/x509"
    18  	"encoding/json"
    19  	"errors"
    20  	"flag"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"os/signal"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"decred.org/dcrdex/client/comms"
    31  	"decred.org/dcrdex/dex"
    32  	"decred.org/dcrdex/server/noderelay"
    33  )
    34  
    35  var log = dex.StdOutLogger("NODESRC", dex.LevelDebug)
    36  
    37  func main() {
    38  	if err := mainErr(); err != nil {
    39  		fmt.Fprint(os.Stderr, err, "\n")
    40  		os.Exit(1)
    41  	}
    42  	os.Exit(0)
    43  }
    44  
    45  func mainErr() (err error) {
    46  	var (
    47  		// port is a required argument.
    48  		port string
    49  		// optional, but required for e.g. dcrd, which uses an encrypted
    50  		// connection.
    51  		localNodeCert string
    52  
    53  		// User can provide NodeRelay server configuration in one of two ways.
    54  		// 1) nexusAddr + relayID + certPath
    55  		nexusAddr string
    56  		relayID   string
    57  		certPath  string // NodeRelay's TLS certificate
    58  		// 2) relayfile, provided by NodeRelay operator
    59  		relayFilepath string
    60  	)
    61  
    62  	// required
    63  	flag.StringVar(&port, "port", "", "The port that the local service is listening on")
    64  	// optional. (dcrd needs by default)
    65  	flag.StringVar(&localNodeCert, "localcert", "", "The path to a TLS certificate for the local service") // optional
    66  	// connect either this way...
    67  	flag.StringVar(&relayFilepath, "relayfile", "", "The path to a relay file (provided by the server)")
    68  	// or connect with these parameters
    69  	flag.StringVar(&relayID, "relayid", "", "The relay ID")
    70  	flag.StringVar(&certPath, "certpath", "", "The path to a TLS certificate for the server")
    71  	flag.StringVar(&nexusAddr, "addr", "", "The address to the server")
    72  
    73  	flag.Parse()
    74  
    75  	if port == "" {
    76  		return errors.New("no local port provided")
    77  	}
    78  
    79  	var certB []byte
    80  	if relayFilepath != "" {
    81  		b, err := os.ReadFile(relayFilepath)
    82  		if err != nil {
    83  			return fmt.Errorf("error reading relay file @ %q: %w", relayFilepath, err)
    84  		}
    85  		var relayFile noderelay.RelayFile
    86  		if err := json.Unmarshal(b, &relayFile); err != nil {
    87  			return fmt.Errorf("error parsing relay file: %w", err)
    88  		}
    89  		relayID = relayFile.RelayID
    90  		certB = relayFile.Cert
    91  		nexusAddr = relayFile.Addr
    92  	} else {
    93  		if certPath == "" {
    94  			return errors.New("specify a --certpath")
    95  		}
    96  		var err error
    97  		certB, err = os.ReadFile(certPath)
    98  		if err != nil {
    99  			return fmt.Errorf("error reading server certificate at %q: %v", certPath, err)
   100  		}
   101  	}
   102  
   103  	if port == "" {
   104  		return errors.New("specify the --port that the local service is listening on")
   105  	}
   106  	if relayID == "" {
   107  		return errors.New("specify a --relayid")
   108  	}
   109  	if nexusAddr == "" {
   110  		return errors.New("specify a --addr for the server")
   111  	}
   112  
   113  	if len(certB) == 0 {
   114  		return errors.New("no server TLS certificate provided")
   115  	}
   116  
   117  	registration, err := json.Marshal(&noderelay.RelayedMessage{
   118  		MessageID: 0, // must be zero for registration.
   119  		Body:      []byte(relayID),
   120  	})
   121  	if err != nil {
   122  		return fmt.Errorf("error json-encoding registration message: %v", err)
   123  	}
   124  
   125  	ctx, cancel := context.WithCancel(context.Background())
   126  	defer cancel()
   127  
   128  	killChan := make(chan os.Signal, 1)
   129  	signal.Notify(killChan, os.Interrupt)
   130  	go func() {
   131  		<-killChan
   132  		fmt.Println("Shutting down...")
   133  		cancel()
   134  	}()
   135  
   136  	// Keep track of some basic stats.
   137  	var stats struct {
   138  		requests uint32
   139  		errors   uint32
   140  		received uint64
   141  		sent     uint64
   142  	}
   143  
   144  	// Periodically print the node usage statistics.
   145  	go func() {
   146  		start := time.Now()
   147  		for {
   148  			select {
   149  			case <-time.After(time.Minute * 10):
   150  			case <-ctx.Done():
   151  				return
   152  			}
   153  			log.Infof("%d requests, %.4g MB received, %.4g MB sent, %d errors in %s",
   154  				atomic.LoadUint32(&stats.requests), float64(atomic.LoadUint64(&stats.received))/1e6,
   155  				float64(atomic.LoadUint64(&stats.sent))/1e6, atomic.LoadUint32(&stats.errors),
   156  				time.Since(start))
   157  		}
   158  	}()
   159  
   160  	localNodeURL := "http://127.0.0.1" + ":" + port
   161  	httpClient := http.DefaultClient
   162  	if localNodeCert != "" {
   163  		localNodeURL = "https://127.0.0.1" + ":" + port
   164  		pem, err := os.ReadFile(localNodeCert)
   165  		if err != nil {
   166  			return err
   167  		}
   168  
   169  		uri, err := url.Parse(localNodeURL)
   170  		if err != nil {
   171  			return fmt.Errorf("error parsing URL: %v", err)
   172  		}
   173  
   174  		pool := x509.NewCertPool()
   175  		if ok := pool.AppendCertsFromPEM(pem); !ok {
   176  			return fmt.Errorf("invalid certificate file: %v", localNodeCert)
   177  		}
   178  		tlsConfig := &tls.Config{
   179  			RootCAs:    pool,
   180  			ServerName: uri.Hostname(),
   181  		}
   182  
   183  		httpClient = &http.Client{
   184  			Transport: &http.Transport{
   185  				TLSClientConfig: tlsConfig,
   186  			},
   187  		}
   188  	}
   189  
   190  	// Define this now, so we can create a ReconnectSync function.
   191  	var cl comms.WsConn
   192  
   193  	registerOrReregister := func() {
   194  		if err := cl.SendRaw(registration); err != nil {
   195  			log.Errorf("Error sending registration message: %v", err)
   196  		}
   197  	}
   198  
   199  	cl, err = comms.NewWsConn(&comms.WsCfg{
   200  		URL:      "wss://" + nexusAddr,
   201  		PingWait: noderelay.PingPeriod * 2,
   202  		Cert:     certB,
   203  		// On a disconnect, wsConn will attempt to reconnect immediately. If
   204  		// the first attempt is unsuccessful, it will wait 5 seconds for next
   205  		// success, then 10, 15 ... up to a minute.
   206  		// We'll just send the registration on reconnect.
   207  		ReconnectSync:    registerOrReregister,
   208  		ConnectEventFunc: func(s comms.ConnectionStatus) {},
   209  		Logger:           dex.StdOutLogger("CL", dex.LevelDebug),
   210  		RawHandler: func(b []byte) {
   211  			atomic.AddUint64(&stats.received, uint64(len(b)))
   212  			atomic.AddUint32(&stats.requests, 1)
   213  			// Request received from server.
   214  			var msg noderelay.RelayedMessage
   215  			if err := json.Unmarshal(b, &msg); err != nil {
   216  				atomic.AddUint32(&stats.errors, 1)
   217  				log.Errorf("json unmarshal error: %v", err)
   218  				return
   219  			}
   220  			// Prepare mirrored request for local service.
   221  			ctx, cancel := context.WithTimeout(ctx, time.Second*30)
   222  			defer cancel()
   223  			req, err := http.NewRequestWithContext(ctx, msg.Method, localNodeURL, bytes.NewReader(msg.Body))
   224  			if err != nil {
   225  				atomic.AddUint32(&stats.errors, 1)
   226  				log.Errorf("Error constructing request: %v", err)
   227  				return
   228  			}
   229  			req.Header = msg.Headers
   230  			// Send request to local service.
   231  			resp, err := httpClient.Do(req)
   232  			if err != nil {
   233  				atomic.AddUint32(&stats.errors, 1)
   234  				log.Errorf("error processing request: %v", err)
   235  				return
   236  			}
   237  			// Read response from local service and encode for the node relay.
   238  			b, err = io.ReadAll(resp.Body)
   239  			resp.Body.Close()
   240  			if err != nil {
   241  				atomic.AddUint32(&stats.errors, 1)
   242  				log.Errorf("Error reading response: %v", err)
   243  				return
   244  			}
   245  			atomic.AddUint64(&stats.sent, uint64(len(b)))
   246  			encResp, err := json.Marshal(&noderelay.RelayedMessage{
   247  				MessageID: msg.MessageID,
   248  				Body:      b,
   249  				Headers:   resp.Header,
   250  			})
   251  			if err != nil {
   252  				log.Error("Error during json encoding: %v", err)
   253  			}
   254  			if err := cl.SendRaw(encResp); err != nil {
   255  				log.Errorf("SendRaw error: %v", err)
   256  			}
   257  
   258  		},
   259  	})
   260  
   261  	cm := dex.NewConnectionMaster(cl)
   262  	if err := cm.ConnectOnce(ctx); err != nil {
   263  		return fmt.Errorf("websocketHandler client connect: %v", err)
   264  	}
   265  
   266  	// The default read limit is 1024, I think.
   267  	if ws, is := cl.(interface {
   268  		SetReadLimit(limit int64)
   269  	}); is {
   270  		const readLimit = 2_097_152 // 2 MiB
   271  		ws.SetReadLimit(readLimit)
   272  	}
   273  
   274  	registerOrReregister()
   275  
   276  	cm.Wait()
   277  	return nil
   278  }