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 }