github.com/decred/dcrlnd@v0.7.6/lntest/itest/lnd_rest_api_test.go (about) 1 package itest 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/tls" 7 "encoding/base64" 8 "encoding/hex" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "regexp" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/decred/dcrlnd/lnrpc" 19 "github.com/decred/dcrlnd/lnrpc/autopilotrpc" 20 "github.com/decred/dcrlnd/lnrpc/chainrpc" 21 "github.com/decred/dcrlnd/lnrpc/routerrpc" 22 "github.com/decred/dcrlnd/lnrpc/verrpc" 23 "github.com/decred/dcrlnd/lnrpc/walletrpc" 24 "github.com/decred/dcrlnd/lntest" 25 "github.com/golang/protobuf/jsonpb" 26 "github.com/golang/protobuf/proto" 27 "github.com/gorilla/websocket" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 "matheusd.com/testctx" 31 ) 32 33 var ( 34 insecureTransport = &http.Transport{ 35 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 36 } 37 restClient = &http.Client{ 38 Transport: insecureTransport, 39 } 40 jsonMarshaler = &jsonpb.Marshaler{ 41 EmitDefaults: true, 42 OrigName: true, 43 Indent: " ", 44 } 45 urlEnc = base64.URLEncoding 46 webSocketDialer = &websocket.Dialer{ 47 HandshakeTimeout: time.Second, 48 TLSClientConfig: insecureTransport.TLSClientConfig, 49 } 50 resultPattern = regexp.MustCompile("{\"result\":(.*)}") 51 closeMsg = websocket.FormatCloseMessage( 52 websocket.CloseNormalClosure, "done", 53 ) 54 55 pingInterval = time.Millisecond * 200 56 pongWait = time.Millisecond * 50 57 ) 58 59 // testRestAPI tests that the most important features of the REST API work 60 // correctly. 61 func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { 62 testCases := []struct { 63 name string 64 run func(*testing.T, *lntest.HarnessNode, *lntest.HarnessNode) 65 }{{ 66 name: "simple GET", 67 run: func(t *testing.T, a, b *lntest.HarnessNode) { 68 // Check that the parsing into the response proto 69 // message works. 70 resp := &lnrpc.GetInfoResponse{} 71 err := invokeGET(a, "/v1/getinfo", resp) 72 require.Nil(t, err, "getinfo") 73 assert.Equal(t, "#3399ff", resp.Color, "node color") 74 75 // Make sure we get the correct field names (snake 76 // case). 77 _, resp2, err := makeRequest( 78 a, "/v1/getinfo", "GET", nil, nil, 79 ) 80 require.Nil(t, err, "getinfo") 81 assert.Contains( 82 t, string(resp2), "best_header_timestamp", 83 "getinfo", 84 ) 85 }, 86 }, { 87 name: "simple POST and GET with query param", 88 run: func(t *testing.T, a, b *lntest.HarnessNode) { 89 // Add an invoice, testing POST in the process. 90 req := &lnrpc.Invoice{Value: 1234, IgnoreMaxInboundAmt: true} 91 resp := &lnrpc.AddInvoiceResponse{} 92 err := invokePOST(a, "/v1/invoices", req, resp) 93 require.Nil(t, err, "add invoice") 94 assert.Equal(t, 32, len(resp.RHash), "invoice rhash") 95 96 // Make sure we can call a GET endpoint with a hex 97 // encoded URL part. 98 url := fmt.Sprintf("/v1/invoice/%x", resp.RHash) 99 resp2 := &lnrpc.Invoice{} 100 err = invokeGET(a, url, resp2) 101 require.Nil(t, err, "query invoice") 102 assert.Equal(t, int64(1234), resp2.Value, "invoice amt") 103 }, 104 }, { 105 name: "GET with base64 encoded byte slice in path", 106 run: func(t *testing.T, a, b *lntest.HarnessNode) { 107 url := "/v2/router/mc/probability/%s/%s/%d" 108 url = fmt.Sprintf( 109 url, urlEnc.EncodeToString(a.PubKey[:]), 110 urlEnc.EncodeToString(b.PubKey[:]), 1234, 111 ) 112 resp := &routerrpc.QueryProbabilityResponse{} 113 err := invokeGET(a, url, resp) 114 require.Nil(t, err, "query probability") 115 assert.Greater(t, resp.Probability, 0.5, "probability") 116 }, 117 }, { 118 name: "GET with map type query param", 119 run: func(t *testing.T, a, b *lntest.HarnessNode) { 120 // Get a new wallet address from Alice. 121 ctxb := context.Background() 122 newAddrReq := &lnrpc.NewAddressRequest{ 123 Type: lnrpc.AddressType_PUBKEY_HASH, 124 } 125 addrRes, err := a.NewAddress(ctxb, newAddrReq) 126 require.Nil(t, err, "get address") 127 128 // Create the full URL with the map query param. 129 // 130 // NOTE(decred): estimatefee is unimplemented so this 131 // test is disabled for the moment. 132 _ = addrRes 133 /* 134 url := "/v1/transactions/fee?target_conf=%d&" + 135 "AddrToAmount[%s]=%d" 136 url = fmt.Sprintf(url, 2, addrRes.Address, 50000) 137 resp := &lnrpc.EstimateFeeResponse{} 138 err = invokeGET(a, url, resp) 139 require.Nil(t, err, "estimate fee") 140 assert.Greater(t, resp.FeeAtoms, int64(253), "fee") 141 */ 142 }, 143 }, { 144 name: "sub RPC servers REST support", 145 run: func(t *testing.T, a, b *lntest.HarnessNode) { 146 // Query autopilot status. 147 res1 := &autopilotrpc.StatusResponse{} 148 err := invokeGET(a, "/v2/autopilot/status", res1) 149 require.Nil(t, err, "autopilot status") 150 assert.Equal(t, false, res1.Active, "autopilot status") 151 152 // Query the version RPC. 153 res2 := &verrpc.Version{} 154 err = invokeGET(a, "/v2/versioner/version", res2) 155 require.Nil(t, err, "version") 156 assert.Greater( 157 t, res2.AppMinor, uint32(0), "lnd minor version", 158 ) 159 160 // Request a new external address from the wallet kit. 161 req1 := &walletrpc.AddrRequest{} 162 res3 := &walletrpc.AddrResponse{} 163 err = invokePOST( 164 a, "/v2/wallet/address/next", req1, res3, 165 ) 166 require.Nil(t, err, "address") 167 assert.NotEmpty(t, res3.Addr, "address") 168 }, 169 }, { 170 name: "CORS headers", 171 run: func(t *testing.T, a, b *lntest.HarnessNode) { 172 // Alice allows all origins. Make sure we get the same 173 // value back in the CORS header that we send in the 174 // Origin header. 175 reqHeaders := make(http.Header) 176 reqHeaders.Add("Origin", "https://foo.bar:9999") 177 resHeaders, body, err := makeRequest( 178 a, "/v1/getinfo", "OPTIONS", nil, reqHeaders, 179 ) 180 require.Nil(t, err, "getinfo") 181 assert.Equal( 182 t, "https://foo.bar:9999", 183 resHeaders.Get("Access-Control-Allow-Origin"), 184 "CORS header", 185 ) 186 assert.Equal(t, 0, len(body)) 187 188 // Make sure that we don't get a value set for Bob which 189 // doesn't allow any CORS origin. 190 resHeaders, body, err = makeRequest( 191 b, "/v1/getinfo", "OPTIONS", nil, reqHeaders, 192 ) 193 require.Nil(t, err, "getinfo") 194 assert.Equal( 195 t, "", 196 resHeaders.Get("Access-Control-Allow-Origin"), 197 "CORS header", 198 ) 199 assert.Equal(t, 0, len(body)) 200 }, 201 }} 202 wsTestCases := []struct { 203 name string 204 run func(ht *harnessTest, net *lntest.NetworkHarness) 205 }{{ 206 name: "websocket subscription", 207 run: wsTestCaseSubscription, 208 }, { 209 name: "websocket subscription with macaroon in protocol", 210 run: wsTestCaseSubscriptionMacaroon, 211 }, { 212 name: "websocket bi-directional subscription", 213 run: wsTestCaseBiDirectionalSubscription, 214 }, { 215 name: "websocket ping and pong timeout", 216 run: wsTestPingPongTimeout, 217 }} 218 219 // Make sure Alice allows all CORS origins. Bob will keep the default. 220 // We also make sure the ping/pong messages are sent very often, so we 221 // can test them without waiting half a minute. 222 net.Alice.Cfg.ExtraArgs = append( 223 net.Alice.Cfg.ExtraArgs, "--restcors=\"*\"", 224 fmt.Sprintf("--ws-ping-interval=%s", pingInterval), 225 fmt.Sprintf("--ws-pong-wait=%s", pongWait), 226 ) 227 err := net.RestartNode(net.Alice, nil) 228 if err != nil { 229 ht.t.Fatalf("Could not restart Alice to set CORS config: %v", 230 err) 231 } 232 233 for _, tc := range testCases { 234 tc := tc 235 ht.t.Run(tc.name, func(t *testing.T) { 236 tc.run(t, net.Alice, net.Bob) 237 238 assertCleanState(ht, net) 239 }) 240 } 241 242 for _, tc := range wsTestCases { 243 tc := tc 244 ht.t.Run(tc.name, func(t *testing.T) { 245 ht := &harnessTest{ 246 t: t, testCase: ht.testCase, lndHarness: net, 247 } 248 tc.run(ht, net) 249 250 assertCleanState(ht, net) 251 }) 252 } 253 } 254 255 func wsTestCaseSubscription(ht *harnessTest, net *lntest.NetworkHarness) { 256 // Find out the current best block so we can subscribe to the next one. 257 hash, height, err := net.Miner.Node.GetBestBlock(testctx.New(ht.t)) 258 require.Nil(ht.t, err, "get best block") 259 260 // Create a new subscription to get block epoch events. 261 req := &chainrpc.BlockEpoch{ 262 Hash: hash.CloneBytes(), 263 Height: uint32(height), 264 } 265 url := "/v2/chainnotifier/register/blocks" 266 c, err := openWebSocket(net.Alice, url, "POST", req, nil) 267 require.Nil(ht.t, err, "websocket") 268 defer func() { 269 err := c.WriteMessage(websocket.CloseMessage, closeMsg) 270 require.NoError(ht.t, err) 271 _ = c.Close() 272 }() 273 274 msgChan := make(chan *chainrpc.BlockEpoch) 275 errChan := make(chan error) 276 timeout := time.After(defaultTimeout) 277 278 // We want to read exactly one message. 279 go func() { 280 defer close(msgChan) 281 282 _, msg, err := c.ReadMessage() 283 if err != nil { 284 errChan <- err 285 return 286 } 287 288 // The chunked/streamed responses come wrapped in either a 289 // {"result":{}} or {"error":{}} wrapper which we'll get rid of 290 // here. 291 msgStr := string(msg) 292 if !strings.Contains(msgStr, "\"result\":") { 293 errChan <- fmt.Errorf("invalid msg: %s", msgStr) 294 return 295 } 296 msgStr = resultPattern.ReplaceAllString(msgStr, "${1}") 297 298 // Make sure we can parse the unwrapped message into the 299 // expected proto message. 300 protoMsg := &chainrpc.BlockEpoch{} 301 err = jsonpb.UnmarshalString(msgStr, protoMsg) 302 if err != nil { 303 errChan <- err 304 return 305 } 306 307 select { 308 case msgChan <- protoMsg: 309 case <-timeout: 310 } 311 }() 312 313 // Mine a block and make sure we get a message for it. 314 blockHashes, err := net.Miner.Node.Generate(testctx.New(ht.t), 1) 315 require.Nil(ht.t, err, "generate blocks") 316 assert.Equal(ht.t, 1, len(blockHashes), "num blocks") 317 select { 318 case msg := <-msgChan: 319 assert.Equal( 320 ht.t, blockHashes[0].CloneBytes(), msg.Hash, 321 "block hash", 322 ) 323 324 case err := <-errChan: 325 ht.t.Fatalf("Received error from WS: %v", err) 326 327 case <-timeout: 328 ht.t.Fatalf("Timeout before message was received") 329 } 330 } 331 332 func wsTestCaseSubscriptionMacaroon(ht *harnessTest, 333 net *lntest.NetworkHarness) { 334 335 // Find out the current best block so we can subscribe to the next one. 336 hash, height, err := net.Miner.Node.GetBestBlock(testctx.New(ht.t)) 337 require.Nil(ht.t, err, "get best block") 338 339 // Create a new subscription to get block epoch events. 340 req := &chainrpc.BlockEpoch{ 341 Hash: hash.CloneBytes(), 342 Height: uint32(height), 343 } 344 url := "/v2/chainnotifier/register/blocks" 345 346 // This time we send the macaroon in the special header 347 // Sec-Websocket-Protocol which is the only header field available to 348 // browsers when opening a WebSocket. 349 mac, err := net.Alice.ReadMacaroon( 350 net.Alice.AdminMacPath(), defaultTimeout, 351 ) 352 require.NoError(ht.t, err, "read admin mac") 353 macBytes, err := mac.MarshalBinary() 354 require.NoError(ht.t, err, "marshal admin mac") 355 356 customHeader := make(http.Header) 357 customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf( 358 "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes), 359 )) 360 c, err := openWebSocket(net.Alice, url, "POST", req, customHeader) 361 require.Nil(ht.t, err, "websocket") 362 defer func() { 363 err := c.WriteMessage(websocket.CloseMessage, closeMsg) 364 require.NoError(ht.t, err) 365 _ = c.Close() 366 }() 367 368 msgChan := make(chan *chainrpc.BlockEpoch) 369 errChan := make(chan error) 370 timeout := time.After(defaultTimeout) 371 372 // We want to read exactly one message. 373 go func() { 374 defer close(msgChan) 375 376 _, msg, err := c.ReadMessage() 377 if err != nil { 378 errChan <- err 379 return 380 } 381 382 // The chunked/streamed responses come wrapped in either a 383 // {"result":{}} or {"error":{}} wrapper which we'll get rid of 384 // here. 385 msgStr := string(msg) 386 if !strings.Contains(msgStr, "\"result\":") { 387 errChan <- fmt.Errorf("invalid msg: %s", msgStr) 388 return 389 } 390 msgStr = resultPattern.ReplaceAllString(msgStr, "${1}") 391 392 // Make sure we can parse the unwrapped message into the 393 // expected proto message. 394 protoMsg := &chainrpc.BlockEpoch{} 395 err = jsonpb.UnmarshalString(msgStr, protoMsg) 396 if err != nil { 397 errChan <- err 398 return 399 } 400 401 select { 402 case msgChan <- protoMsg: 403 case <-timeout: 404 } 405 }() 406 407 // Mine a block and make sure we get a message for it. 408 blockHashes, err := net.Miner.Node.Generate(testctx.New(ht.t), 1) 409 require.Nil(ht.t, err, "generate blocks") 410 assert.Equal(ht.t, 1, len(blockHashes), "num blocks") 411 select { 412 case msg := <-msgChan: 413 assert.Equal( 414 ht.t, blockHashes[0].CloneBytes(), msg.Hash, 415 "block hash", 416 ) 417 418 case err := <-errChan: 419 ht.t.Fatalf("Received error from WS: %v", err) 420 421 case <-timeout: 422 ht.t.Fatalf("Timeout before message was received") 423 } 424 } 425 426 func wsTestCaseBiDirectionalSubscription(ht *harnessTest, 427 net *lntest.NetworkHarness) { 428 429 initialRequest := &lnrpc.ChannelAcceptResponse{} 430 url := "/v1/channels/acceptor" 431 432 // This time we send the macaroon in the special header 433 // Sec-Websocket-Protocol which is the only header field available to 434 // browsers when opening a WebSocket. 435 mac, err := net.Alice.ReadMacaroon( 436 net.Alice.AdminMacPath(), defaultTimeout, 437 ) 438 require.NoError(ht.t, err, "read admin mac") 439 macBytes, err := mac.MarshalBinary() 440 require.NoError(ht.t, err, "marshal admin mac") 441 442 customHeader := make(http.Header) 443 customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf( 444 "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes), 445 )) 446 conn, err := openWebSocket( 447 net.Alice, url, "POST", initialRequest, customHeader, 448 ) 449 require.Nil(ht.t, err, "websocket") 450 defer func() { 451 err := conn.WriteMessage(websocket.CloseMessage, closeMsg) 452 _ = conn.Close() 453 require.NoError(ht.t, err) 454 }() 455 456 // Buffer the message channel to make sure we're always blocking on 457 // conn.ReadMessage() to allow the ping/pong mechanism to work. 458 msgChan := make(chan *lnrpc.ChannelAcceptResponse, 1) 459 errChan := make(chan error) 460 done := make(chan struct{}) 461 timeout := time.After(defaultTimeout) 462 463 // We want to read messages over and over again. We just accept any 464 // channels that are opened. 465 defer close(done) 466 go func() { 467 for { 468 _, msg, err := conn.ReadMessage() 469 if err != nil { 470 select { 471 case errChan <- err: 472 case <-done: 473 } 474 return 475 } 476 477 // The chunked/streamed responses come wrapped in either 478 // a {"result":{}} or {"error":{}} wrapper which we'll 479 // get rid of here. 480 msgStr := string(msg) 481 if !strings.Contains(msgStr, "\"result\":") { 482 select { 483 case errChan <- fmt.Errorf("invalid msg: %s", 484 msgStr): 485 case <-done: 486 } 487 return 488 } 489 msgStr = resultPattern.ReplaceAllString(msgStr, "${1}") 490 491 // Make sure we can parse the unwrapped message into the 492 // expected proto message. 493 protoMsg := &lnrpc.ChannelAcceptRequest{} 494 err = jsonpb.UnmarshalString(msgStr, protoMsg) 495 if err != nil { 496 select { 497 case errChan <- err: 498 case <-done: 499 } 500 return 501 } 502 503 // Send the response that we accept the channel. 504 res := &lnrpc.ChannelAcceptResponse{ 505 Accept: true, 506 PendingChanId: protoMsg.PendingChanId, 507 } 508 resMsg, err := jsonMarshaler.MarshalToString(res) 509 if err != nil { 510 select { 511 case errChan <- err: 512 case <-done: 513 } 514 return 515 } 516 err = conn.WriteMessage( 517 websocket.TextMessage, []byte(resMsg), 518 ) 519 if err != nil { 520 select { 521 case errChan <- err: 522 case <-done: 523 } 524 return 525 } 526 527 // Also send the message on our message channel to make 528 // sure we count it as successful. 529 msgChan <- res 530 531 // Are we done or should there be more messages? 532 select { 533 case <-done: 534 return 535 default: 536 } 537 } 538 }() 539 540 // Before we start opening channels, make sure the two nodes are 541 // connected. 542 net.EnsureConnected(ht.t, net.Alice, net.Bob) 543 544 // Open 3 channels to make sure multiple requests and responses can be 545 // sent over the web socket. 546 const numChannels = 3 547 for i := 0; i < numChannels; i++ { 548 cp := openChannelAndAssert( 549 ht, net, net.Bob, net.Alice, 550 lntest.OpenChannelParams{Amt: 500000}, 551 ) 552 defer closeChannelAndAssert(ht, net, net.Alice, 553 cp, false) 554 555 select { 556 case <-msgChan: 557 case err := <-errChan: 558 ht.t.Fatalf("Received error from WS: %v", err) 559 560 case <-timeout: 561 ht.t.Fatalf("Timeout before message was received") 562 } 563 } 564 } 565 566 func wsTestPingPongTimeout(ht *harnessTest, net *lntest.NetworkHarness) { 567 initialRequest := &lnrpc.InvoiceSubscription{ 568 AddIndex: 1, SettleIndex: 1, 569 } 570 url := "/v1/invoices/subscribe" 571 572 // This time we send the macaroon in the special header 573 // Sec-Websocket-Protocol which is the only header field available to 574 // browsers when opening a WebSocket. 575 mac, err := net.Alice.ReadMacaroon( 576 net.Alice.AdminMacPath(), defaultTimeout, 577 ) 578 require.NoError(ht.t, err, "read admin mac") 579 macBytes, err := mac.MarshalBinary() 580 require.NoError(ht.t, err, "marshal admin mac") 581 582 customHeader := make(http.Header) 583 customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf( 584 "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes), 585 )) 586 conn, err := openWebSocket( 587 net.Alice, url, "GET", initialRequest, customHeader, 588 ) 589 require.Nil(ht.t, err, "websocket") 590 defer func() { 591 err := conn.WriteMessage(websocket.CloseMessage, closeMsg) 592 _ = conn.Close() 593 require.NoError(ht.t, err) 594 }() 595 596 // We want to be able to read invoices for a long time, making sure we 597 // can continue to read even after we've gone through several ping/pong 598 // cycles. 599 invoices := make(chan *lnrpc.Invoice, 1) 600 errChan := make(chan error) 601 done := make(chan struct{}) 602 timeout := time.After(defaultTimeout) 603 604 defer close(done) 605 go func() { 606 for { 607 _, msg, err := conn.ReadMessage() 608 if err != nil { 609 select { 610 case errChan <- err: 611 case <-done: 612 } 613 return 614 } 615 616 // The chunked/streamed responses come wrapped in either 617 // a {"result":{}} or {"error":{}} wrapper which we'll 618 // get rid of here. 619 msgStr := string(msg) 620 if !strings.Contains(msgStr, "\"result\":") { 621 select { 622 case errChan <- fmt.Errorf("invalid msg: %s", 623 msgStr): 624 case <-done: 625 } 626 return 627 } 628 msgStr = resultPattern.ReplaceAllString(msgStr, "${1}") 629 630 // Make sure we can parse the unwrapped message into the 631 // expected proto message. 632 protoMsg := &lnrpc.Invoice{} 633 err = jsonpb.UnmarshalString(msgStr, protoMsg) 634 if err != nil { 635 select { 636 case errChan <- err: 637 case <-done: 638 } 639 return 640 } 641 642 invoices <- protoMsg 643 644 // Make sure we exit the loop once we've sent through 645 // all expected test messages. 646 select { 647 case <-done: 648 return 649 default: 650 } 651 } 652 }() 653 654 // The SubscribeInvoices call returns immediately after the gRPC/REST 655 // connection is established. But it can happen that the goroutine in 656 // lnd that actually registers the subscriber in the invoice backend 657 // didn't get any CPU time just yet. So we can run into the situation 658 // where we add our first invoice _before_ the subscription client is 659 // registered. If that happens, we'll never get notified about the 660 // invoice in question. So all we really can do is wait a bit here to 661 // make sure the subscription is registered correctly. 662 time.Sleep(500 * time.Millisecond) 663 664 // Let's create five invoices and wait for them to arrive. We'll wait 665 // for at least one ping/pong cycle between each invoice. 666 ctxb := context.Background() 667 const numInvoices = 5 668 const value = 123 669 const memo = "websocket" 670 for i := 0; i < numInvoices; i++ { 671 _, err := net.Alice.AddInvoice(ctxb, &lnrpc.Invoice{ 672 Value: value, 673 Memo: memo, 674 675 IgnoreMaxInboundAmt: true, 676 }) 677 require.NoError(ht.t, err) 678 679 select { 680 case streamMsg := <-invoices: 681 require.Equal(ht.t, int64(value), streamMsg.Value) 682 require.Equal(ht.t, memo, streamMsg.Memo) 683 684 case err := <-errChan: 685 require.Fail(ht.t, "Error reading invoice: %v", err) 686 687 case <-timeout: 688 require.Fail(ht.t, "No invoice msg received in time") 689 } 690 691 // Let's wait for at least a whole ping/pong cycle to happen, so 692 // we can be sure the read/write deadlines are set correctly. 693 // We double the pong wait just to add some extra margin. 694 time.Sleep(pingInterval + 2*pongWait) 695 } 696 } 697 698 // invokeGET calls the given URL with the GET method and appropriate macaroon 699 // header fields then tries to unmarshal the response into the given response 700 // proto message. 701 func invokeGET(node *lntest.HarnessNode, url string, resp proto.Message) error { 702 _, rawResp, err := makeRequest(node, url, "GET", nil, nil) 703 if err != nil { 704 return err 705 } 706 707 return jsonpb.Unmarshal(bytes.NewReader(rawResp), resp) 708 } 709 710 // invokePOST calls the given URL with the POST method, request body and 711 // appropriate macaroon header fields then tries to unmarshal the response into 712 // the given response proto message. 713 func invokePOST(node *lntest.HarnessNode, url string, req, 714 resp proto.Message) error { 715 716 // Marshal the request to JSON using the jsonpb marshaler to get correct 717 // field names. 718 var buf bytes.Buffer 719 if err := jsonMarshaler.Marshal(&buf, req); err != nil { 720 return err 721 } 722 723 _, rawResp, err := makeRequest(node, url, "POST", &buf, nil) 724 if err != nil { 725 return err 726 } 727 728 return jsonpb.Unmarshal(bytes.NewReader(rawResp), resp) 729 } 730 731 // makeRequest calls the given URL with the given method, request body and 732 // appropriate macaroon header fields and returns the raw response body. 733 func makeRequest(node *lntest.HarnessNode, url, method string, 734 request io.Reader, additionalHeaders http.Header) (http.Header, []byte, 735 error) { 736 737 // Assemble the full URL from the node's listening address then create 738 // the request so we can set the macaroon on it. 739 fullURL := fmt.Sprintf("https://%s%s", node.Cfg.RESTAddr(), url) 740 req, err := http.NewRequest(method, fullURL, request) 741 if err != nil { 742 return nil, nil, err 743 } 744 if err := addAdminMacaroon(node, req.Header); err != nil { 745 return nil, nil, err 746 } 747 for key, values := range additionalHeaders { 748 for _, value := range values { 749 req.Header.Add(key, value) 750 } 751 } 752 753 // Do the actual call with the completed request object now. 754 resp, err := restClient.Do(req) 755 if err != nil { 756 return nil, nil, err 757 } 758 defer func() { _ = resp.Body.Close() }() 759 760 data, err := ioutil.ReadAll(resp.Body) 761 return resp.Header, data, err 762 } 763 764 // openWebSocket opens a new WebSocket connection to the given URL with the 765 // appropriate macaroon headers and sends the request message over the socket. 766 func openWebSocket(node *lntest.HarnessNode, url, method string, 767 req proto.Message, customHeader http.Header) (*websocket.Conn, error) { 768 769 // Prepare our macaroon headers and assemble the full URL from the 770 // node's listening address. WebSockets always work over GET so we need 771 // to append the target request method as a query parameter. 772 header := customHeader 773 if header == nil { 774 header = make(http.Header) 775 if err := addAdminMacaroon(node, header); err != nil { 776 return nil, err 777 } 778 } 779 fullURL := fmt.Sprintf( 780 "wss://%s%s?method=%s", node.Cfg.RESTAddr(), url, method, 781 ) 782 conn, resp, err := webSocketDialer.Dial(fullURL, header) 783 if err != nil { 784 return nil, err 785 } 786 defer func() { _ = resp.Body.Close() }() 787 788 // Send the given request message as the first message on the socket. 789 reqMsg, err := jsonMarshaler.MarshalToString(req) 790 if err != nil { 791 return nil, err 792 } 793 err = conn.WriteMessage(websocket.TextMessage, []byte(reqMsg)) 794 if err != nil { 795 return nil, err 796 } 797 798 return conn, nil 799 } 800 801 // addAdminMacaroon reads the admin macaroon from the node and appends it to 802 // the HTTP header fields. 803 func addAdminMacaroon(node *lntest.HarnessNode, header http.Header) error { 804 mac, err := node.ReadMacaroon(node.AdminMacPath(), defaultTimeout) 805 if err != nil { 806 return err 807 } 808 macBytes, err := mac.MarshalBinary() 809 if err != nil { 810 return err 811 } 812 813 header.Set("Grpc-Metadata-Macaroon", hex.EncodeToString(macBytes)) 814 815 return nil 816 }