123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799 |
- package itest
- import (
- "bytes"
- "crypto/tls"
- "encoding/base64"
- "encoding/hex"
- "fmt"
- "io"
- "net/http"
- "regexp"
- "strings"
- "testing"
- "time"
- "github.com/gorilla/websocket"
- "github.com/lightningnetwork/lnd/lnrpc"
- "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
- "github.com/lightningnetwork/lnd/lnrpc/chainrpc"
- "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
- "github.com/lightningnetwork/lnd/lnrpc/verrpc"
- "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
- "github.com/lightningnetwork/lnd/lntest"
- "github.com/lightningnetwork/lnd/lntest/node"
- "github.com/stretchr/testify/require"
- "google.golang.org/protobuf/proto"
- )
- var (
- insecureTransport = &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- }
- restClient = &http.Client{
- Transport: insecureTransport,
- }
- urlEnc = base64.URLEncoding
- webSocketDialer = &websocket.Dialer{
- HandshakeTimeout: time.Second,
- TLSClientConfig: insecureTransport.TLSClientConfig,
- }
- resultPattern = regexp.MustCompile("{\"result\":(.*)}")
- closeMsg = websocket.FormatCloseMessage(
- websocket.CloseNormalClosure, "done",
- )
- pingInterval = time.Millisecond * 200
- pongWait = time.Millisecond * 50
- )
- // testRestAPI tests that the most important features of the REST API work
- // correctly.
- func testRestAPI(ht *lntest.HarnessTest) {
- testCases := []struct {
- name string
- run func(*testing.T, *node.HarnessNode, *node.HarnessNode)
- }{{
- name: "simple GET",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- // Check that the parsing into the response proto
- // message works.
- resp := &lnrpc.GetInfoResponse{}
- err := invokeGET(a, "/v1/getinfo", resp)
- require.Nil(t, err, "getinfo")
- require.Equal(t, "#3399ff", resp.Color, "node color")
- // Make sure we get the correct field names (snake
- // case).
- _, resp2, err := makeRequest(
- a, "/v1/getinfo", "GET", nil, nil,
- )
- require.Nil(t, err, "getinfo")
- require.Contains(
- t, string(resp2), "best_header_timestamp",
- "getinfo",
- )
- },
- }, {
- name: "simple POST and GET with query param",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- // Add an invoice, testing POST in the process.
- req := &lnrpc.Invoice{Value: 1234}
- resp := &lnrpc.AddInvoiceResponse{}
- err := invokePOST(a, "/v1/invoices", req, resp)
- require.Nil(t, err, "add invoice")
- require.Equal(t, 32, len(resp.RHash), "invoice rhash")
- // Make sure we can call a GET endpoint with a hex
- // encoded URL part.
- url := fmt.Sprintf("/v1/invoice/%x", resp.RHash)
- resp2 := &lnrpc.Invoice{}
- err = invokeGET(a, url, resp2)
- require.Nil(t, err, "query invoice")
- require.Equal(t, int64(1234), resp2.Value,
- "invoice amt")
- },
- }, {
- name: "GET with base64 encoded byte slice in path",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- url := "/v2/router/mc/probability/%s/%s/%d"
- url = fmt.Sprintf(
- url, urlEnc.EncodeToString(a.PubKey[:]),
- urlEnc.EncodeToString(b.PubKey[:]), 1234,
- )
- resp := &routerrpc.QueryProbabilityResponse{}
- err := invokeGET(a, url, resp)
- require.Nil(t, err, "query probability")
- require.Zero(t, resp.Probability)
- },
- }, {
- name: "GET with map type query param",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- // Use a fake address.
- addr := "bcrt1qlutnwklt4u2548cufrjmsjclewugr9lcpnkzag"
- // Create the full URL with the map query param.
- url := "/v1/transactions/fee?target_conf=%d&" +
- "AddrToAmount[%s]=%d"
- url = fmt.Sprintf(url, 2, addr, 50000)
- resp := &lnrpc.EstimateFeeResponse{}
- err := invokeGET(a, url, resp)
- require.Nil(t, err, "estimate fee")
- require.Greater(t, resp.FeeSat, int64(253), "fee")
- },
- }, {
- name: "sub RPC servers REST support",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- // Query autopilot status.
- res1 := &autopilotrpc.StatusResponse{}
- err := invokeGET(a, "/v2/autopilot/status", res1)
- require.Nil(t, err, "autopilot status")
- require.Equal(t, false, res1.Active, "autopilot status")
- // Query the version RPC.
- res2 := &verrpc.Version{}
- err = invokeGET(a, "/v2/versioner/version", res2)
- require.Nil(t, err, "version")
- require.Greater(
- t, res2.AppMinor, uint32(0), "lnd minor version",
- )
- // Request a new external address from the wallet kit.
- req1 := &walletrpc.AddrRequest{}
- res3 := &walletrpc.AddrResponse{}
- err = invokePOST(
- a, "/v2/wallet/address/next", req1, res3,
- )
- require.Nil(t, err, "address")
- require.NotEmpty(t, res3.Addr, "address")
- },
- }, {
- name: "CORS headers",
- run: func(t *testing.T, a, b *node.HarnessNode) {
- t.Helper()
- // Alice allows all origins. Make sure we get the same
- // value back in the CORS header that we send in the
- // Origin header.
- reqHeaders := make(http.Header)
- reqHeaders.Add("Origin", "https://foo.bar:9999")
- resHeaders, body, err := makeRequest(
- a, "/v1/getinfo", "OPTIONS", nil, reqHeaders,
- )
- require.Nil(t, err, "getinfo")
- require.Equal(
- t, "https://foo.bar:9999",
- resHeaders.Get("Access-Control-Allow-Origin"),
- "CORS header",
- )
- require.Equal(t, 0, len(body))
- // Make sure that we don't get a value set for Bob which
- // doesn't allow any CORS origin.
- resHeaders, body, err = makeRequest(
- b, "/v1/getinfo", "OPTIONS", nil, reqHeaders,
- )
- require.Nil(t, err, "getinfo")
- require.Equal(
- t, "",
- resHeaders.Get("Access-Control-Allow-Origin"),
- "CORS header",
- )
- require.Equal(t, 0, len(body))
- },
- }}
- wsTestCases := []struct {
- name string
- run func(ht *lntest.HarnessTest)
- }{{
- name: "websocket subscription",
- run: wsTestCaseSubscription,
- }, {
- name: "websocket subscription with macaroon in protocol",
- run: wsTestCaseSubscriptionMacaroon,
- }, {
- name: "websocket bi-directional subscription",
- run: wsTestCaseBiDirectionalSubscription,
- }, {
- name: "websocket ping and pong timeout",
- run: wsTestPingPongTimeout,
- }}
- // Make sure Alice allows all CORS origins. Bob will keep the default.
- // We also make sure the ping/pong messages are sent very often, so we
- // can test them without waiting half a minute.
- alice, bob := ht.Alice, ht.Bob
- alice.Cfg.ExtraArgs = append(
- alice.Cfg.ExtraArgs, "--restcors=\"*\"",
- fmt.Sprintf("--ws-ping-interval=%s", pingInterval),
- fmt.Sprintf("--ws-pong-wait=%s", pongWait),
- )
- ht.RestartNode(alice)
- for _, tc := range testCases {
- tc := tc
- ht.Run(tc.name, func(t *testing.T) {
- tc.run(t, alice, bob)
- })
- }
- for _, tc := range wsTestCases {
- tc := tc
- ht.Run(tc.name, func(t *testing.T) {
- st := ht.Subtest(t)
- tc.run(st)
- })
- }
- }
- func wsTestCaseSubscription(ht *lntest.HarnessTest) {
- // Find out the current best block so we can subscribe to the next one.
- hash, height := ht.Miner.GetBestBlock()
- // Create a new subscription to get block epoch events.
- req := &chainrpc.BlockEpoch{
- Hash: hash.CloneBytes(),
- Height: uint32(height),
- }
- url := "/v2/chainnotifier/register/blocks"
- c, err := openWebSocket(ht.Alice, url, "POST", req, nil)
- require.NoError(ht, err, "websocket")
- defer func() {
- err := c.WriteMessage(websocket.CloseMessage, closeMsg)
- require.NoError(ht, err)
- _ = c.Close()
- }()
- msgChan := make(chan *chainrpc.BlockEpoch)
- errChan := make(chan error)
- timeout := time.After(defaultTimeout)
- // We want to read exactly one message.
- go func() {
- defer close(msgChan)
- _, msg, err := c.ReadMessage()
- if err != nil {
- errChan <- err
- return
- }
- // The chunked/streamed responses come wrapped in either a
- // {"result":{}} or {"error":{}} wrapper which we'll get rid of
- // here.
- msgStr := string(msg)
- if !strings.Contains(msgStr, "\"result\":") {
- errChan <- fmt.Errorf("invalid msg: %s", msgStr)
- return
- }
- msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
- // Make sure we can parse the unwrapped message into the
- // expected proto message.
- protoMsg := &chainrpc.BlockEpoch{}
- err = lnrpc.RESTJsonUnmarshalOpts.Unmarshal(
- []byte(msgStr), protoMsg,
- )
- if err != nil {
- errChan <- err
- return
- }
- select {
- case msgChan <- protoMsg:
- case <-timeout:
- }
- }()
- // Mine a block and make sure we get a message for it.
- blockHashes := ht.Miner.GenerateBlocks(1)
- select {
- case msg := <-msgChan:
- require.Equal(
- ht, blockHashes[0].CloneBytes(), msg.Hash,
- "block hash",
- )
- case err := <-errChan:
- ht.Fatalf("Received error from WS: %v", err)
- case <-timeout:
- ht.Fatalf("Timeout before message was received")
- }
- }
- func wsTestCaseSubscriptionMacaroon(ht *lntest.HarnessTest) {
- // Find out the current best block so we can subscribe to the next one.
- hash, height := ht.Miner.GetBestBlock()
- // Create a new subscription to get block epoch events.
- req := &chainrpc.BlockEpoch{
- Hash: hash.CloneBytes(),
- Height: uint32(height),
- }
- url := "/v2/chainnotifier/register/blocks"
- // This time we send the macaroon in the special header
- // Sec-Websocket-Protocol which is the only header field available to
- // browsers when opening a WebSocket.
- alice := ht.Alice
- mac, err := alice.ReadMacaroon(
- alice.Cfg.AdminMacPath, defaultTimeout,
- )
- require.NoError(ht, err, "read admin mac")
- macBytes, err := mac.MarshalBinary()
- require.NoError(ht, err, "marshal admin mac")
- customHeader := make(http.Header)
- customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
- "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
- ))
- c, err := openWebSocket(alice, url, "POST", req, customHeader)
- require.Nil(ht, err, "websocket")
- defer func() {
- err := c.WriteMessage(websocket.CloseMessage, closeMsg)
- require.NoError(ht, err)
- _ = c.Close()
- }()
- msgChan := make(chan *chainrpc.BlockEpoch)
- errChan := make(chan error)
- timeout := time.After(defaultTimeout)
- // We want to read exactly one message.
- go func() {
- defer close(msgChan)
- _, msg, err := c.ReadMessage()
- if err != nil {
- errChan <- err
- return
- }
- // The chunked/streamed responses come wrapped in either a
- // {"result":{}} or {"error":{}} wrapper which we'll get rid of
- // here.
- msgStr := string(msg)
- if !strings.Contains(msgStr, "\"result\":") {
- errChan <- fmt.Errorf("invalid msg: %s", msgStr)
- return
- }
- msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
- // Make sure we can parse the unwrapped message into the
- // expected proto message.
- protoMsg := &chainrpc.BlockEpoch{}
- err = lnrpc.RESTJsonUnmarshalOpts.Unmarshal(
- []byte(msgStr), protoMsg,
- )
- if err != nil {
- errChan <- err
- return
- }
- select {
- case msgChan <- protoMsg:
- case <-timeout:
- }
- }()
- // Mine a block and make sure we get a message for it.
- blockHashes := ht.Miner.GenerateBlocks(1)
- select {
- case msg := <-msgChan:
- require.Equal(
- ht, blockHashes[0].CloneBytes(), msg.Hash,
- "block hash",
- )
- case err := <-errChan:
- ht.Fatalf("Received error from WS: %v", err)
- case <-timeout:
- ht.Fatalf("Timeout before message was received")
- }
- }
- func wsTestCaseBiDirectionalSubscription(ht *lntest.HarnessTest) {
- initialRequest := &lnrpc.ChannelAcceptResponse{}
- url := "/v1/channels/acceptor"
- // This time we send the macaroon in the special header
- // Sec-Websocket-Protocol which is the only header field available to
- // browsers when opening a WebSocket.
- alice := ht.Alice
- mac, err := alice.ReadMacaroon(
- alice.Cfg.AdminMacPath, defaultTimeout,
- )
- require.NoError(ht, err, "read admin mac")
- macBytes, err := mac.MarshalBinary()
- require.NoError(ht, err, "marshal admin mac")
- customHeader := make(http.Header)
- customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
- "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
- ))
- conn, err := openWebSocket(
- alice, url, "POST", initialRequest, customHeader,
- )
- require.Nil(ht, err, "websocket")
- defer func() {
- err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
- _ = conn.Close()
- require.NoError(ht, err)
- }()
- // Buffer the message channel to make sure we're always blocking on
- // conn.ReadMessage() to allow the ping/pong mechanism to work.
- msgChan := make(chan *lnrpc.ChannelAcceptResponse, 1)
- errChan := make(chan error)
- done := make(chan struct{})
- timeout := time.After(defaultTimeout)
- // We want to read messages over and over again. We just accept any
- // channels that are opened.
- defer close(done)
- go func() {
- for {
- _, msg, err := conn.ReadMessage()
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- // The chunked/streamed responses come wrapped in either
- // a {"result":{}} or {"error":{}} wrapper which we'll
- // get rid of here.
- msgStr := string(msg)
- if !strings.Contains(msgStr, "\"result\":") {
- select {
- case errChan <- fmt.Errorf("invalid msg: %s",
- msgStr):
- case <-done:
- }
- return
- }
- msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
- // Make sure we can parse the unwrapped message into the
- // expected proto message.
- protoMsg := &lnrpc.ChannelAcceptRequest{}
- err = lnrpc.RESTJsonUnmarshalOpts.Unmarshal(
- []byte(msgStr), protoMsg,
- )
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- // Send the response that we accept the channel.
- res := &lnrpc.ChannelAcceptResponse{
- Accept: true,
- PendingChanId: protoMsg.PendingChanId,
- }
- resMsg, err := lnrpc.RESTJsonMarshalOpts.Marshal(res)
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- err = conn.WriteMessage(websocket.TextMessage, resMsg)
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- // Also send the message on our message channel to make
- // sure we count it as successful.
- msgChan <- res
- // Are we done or should there be more messages?
- select {
- case <-done:
- return
- default:
- }
- }
- }()
- // Before we start opening channels, make sure the two nodes are
- // connected.
- bob := ht.Bob
- ht.EnsureConnected(alice, bob)
- // Open 3 channels to make sure multiple requests and responses can be
- // sent over the web socket.
- const numChannels = 3
- for i := 0; i < numChannels; i++ {
- chanPoint := ht.OpenChannel(
- bob, alice, lntest.OpenChannelParams{Amt: 500000},
- )
- defer ht.CloseChannel(bob, chanPoint)
- select {
- case <-msgChan:
- case err := <-errChan:
- ht.Fatalf("Received error from WS: %v", err)
- case <-timeout:
- ht.Fatalf("Timeout before message was received")
- }
- }
- }
- func wsTestPingPongTimeout(ht *lntest.HarnessTest) {
- initialRequest := &lnrpc.InvoiceSubscription{
- AddIndex: 1, SettleIndex: 1,
- }
- url := "/v1/invoices/subscribe"
- // This time we send the macaroon in the special header
- // Sec-Websocket-Protocol which is the only header field available to
- // browsers when opening a WebSocket.
- alice := ht.Alice
- mac, err := alice.ReadMacaroon(
- alice.Cfg.AdminMacPath, defaultTimeout,
- )
- require.NoError(ht, err, "read admin mac")
- macBytes, err := mac.MarshalBinary()
- require.NoError(ht, err, "marshal admin mac")
- customHeader := make(http.Header)
- customHeader.Set(lnrpc.HeaderWebSocketProtocol, fmt.Sprintf(
- "Grpc-Metadata-Macaroon+%s", hex.EncodeToString(macBytes),
- ))
- conn, err := openWebSocket(
- alice, url, "GET", initialRequest, customHeader,
- )
- require.Nil(ht, err, "websocket")
- defer func() {
- err := conn.WriteMessage(websocket.CloseMessage, closeMsg)
- _ = conn.Close()
- require.NoError(ht, err)
- }()
- // We want to be able to read invoices for a long time, making sure we
- // can continue to read even after we've gone through several ping/pong
- // cycles.
- invoices := make(chan *lnrpc.Invoice, 1)
- errChan := make(chan error)
- done := make(chan struct{})
- timeout := time.After(defaultTimeout)
- defer close(done)
- go func() {
- for {
- _, msg, err := conn.ReadMessage()
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- // The chunked/streamed responses come wrapped in either
- // a {"result":{}} or {"error":{}} wrapper which we'll
- // get rid of here.
- msgStr := string(msg)
- if !strings.Contains(msgStr, "\"result\":") {
- select {
- case errChan <- fmt.Errorf("invalid msg: %s",
- msgStr):
- case <-done:
- }
- return
- }
- msgStr = resultPattern.ReplaceAllString(msgStr, "${1}")
- // Make sure we can parse the unwrapped message into the
- // expected proto message.
- protoMsg := &lnrpc.Invoice{}
- err = lnrpc.RESTJsonUnmarshalOpts.Unmarshal(
- []byte(msgStr), protoMsg,
- )
- if err != nil {
- select {
- case errChan <- err:
- case <-done:
- }
- return
- }
- invoices <- protoMsg
- // Make sure we exit the loop once we've sent through
- // all expected test messages.
- select {
- case <-done:
- return
- default:
- }
- }
- }()
- // The SubscribeInvoices call returns immediately after the gRPC/REST
- // connection is established. But it can happen that the goroutine in
- // lnd that actually registers the subscriber in the invoice backend
- // didn't get any CPU time just yet. So we can run into the situation
- // where we add our first invoice _before_ the subscription client is
- // registered. If that happens, we'll never get notified about the
- // invoice in question. So all we really can do is wait a bit here to
- // make sure the subscription is registered correctly.
- time.Sleep(500 * time.Millisecond)
- // Let's create five invoices and wait for them to arrive. We'll wait
- // for at least one ping/pong cycle between each invoice.
- const numInvoices = 5
- const value = 123
- const memo = "websocket"
- for i := 0; i < numInvoices; i++ {
- invoice := &lnrpc.Invoice{
- Value: value,
- Memo: memo,
- }
- alice.RPC.AddInvoice(invoice)
- select {
- case streamMsg := <-invoices:
- require.Equal(ht, int64(value), streamMsg.Value)
- require.Equal(ht, memo, streamMsg.Memo)
- case err := <-errChan:
- require.Fail(ht, "Error reading invoice: %v", err)
- case <-timeout:
- require.Fail(ht, "No invoice msg received in time")
- }
- // Let's wait for at least a whole ping/pong cycle to happen, so
- // we can be sure the read/write deadlines are set correctly.
- // We double the pong wait just to add some extra margin.
- time.Sleep(pingInterval + 2*pongWait)
- }
- }
- // invokeGET calls the given URL with the GET method and appropriate macaroon
- // header fields then tries to unmarshal the response into the given response
- // proto message.
- func invokeGET(node *node.HarnessNode, url string, resp proto.Message) error {
- _, rawResp, err := makeRequest(node, url, "GET", nil, nil)
- if err != nil {
- return err
- }
- return lnrpc.RESTJsonUnmarshalOpts.Unmarshal(rawResp, resp)
- }
- // invokePOST calls the given URL with the POST method, request body and
- // appropriate macaroon header fields then tries to unmarshal the response into
- // the given response proto message.
- func invokePOST(node *node.HarnessNode, url string, req,
- resp proto.Message) error {
- // Marshal the request to JSON using the REST marshaler to get correct
- // field names.
- reqBytes, err := lnrpc.RESTJsonMarshalOpts.Marshal(req)
- if err != nil {
- return err
- }
- _, rawResp, err := makeRequest(
- node, url, "POST", bytes.NewReader(reqBytes), nil,
- )
- if err != nil {
- return err
- }
- return lnrpc.RESTJsonUnmarshalOpts.Unmarshal(rawResp, resp)
- }
- // makeRequest calls the given URL with the given method, request body and
- // appropriate macaroon header fields and returns the raw response body.
- func makeRequest(node *node.HarnessNode, url, method string,
- request io.Reader, additionalHeaders http.Header) (http.Header, []byte,
- error) {
- // Assemble the full URL from the node's listening address then create
- // the request so we can set the macaroon on it.
- fullURL := fmt.Sprintf("https://%s%s", node.Cfg.RESTAddr(), url)
- req, err := http.NewRequest(method, fullURL, request)
- if err != nil {
- return nil, nil, err
- }
- if err := addAdminMacaroon(node, req.Header); err != nil {
- return nil, nil, err
- }
- for key, values := range additionalHeaders {
- for _, value := range values {
- req.Header.Add(key, value)
- }
- }
- // Do the actual call with the completed request object now.
- resp, err := restClient.Do(req)
- if err != nil {
- return nil, nil, err
- }
- defer func() { _ = resp.Body.Close() }()
- data, err := io.ReadAll(resp.Body)
- return resp.Header, data, err
- }
- // openWebSocket opens a new WebSocket connection to the given URL with the
- // appropriate macaroon headers and sends the request message over the socket.
- func openWebSocket(node *node.HarnessNode, url, method string,
- req proto.Message, customHeader http.Header) (*websocket.Conn, error) {
- // Prepare our macaroon headers and assemble the full URL from the
- // node's listening address. WebSockets always work over GET so we need
- // to append the target request method as a query parameter.
- header := customHeader
- if header == nil {
- header = make(http.Header)
- if err := addAdminMacaroon(node, header); err != nil {
- return nil, err
- }
- }
- fullURL := fmt.Sprintf(
- "wss://%s%s?method=%s", node.Cfg.RESTAddr(), url, method,
- )
- conn, resp, err := webSocketDialer.Dial(fullURL, header)
- if err != nil {
- return nil, err
- }
- defer func() { _ = resp.Body.Close() }()
- // Send the given request message as the first message on the socket.
- reqMsg, err := lnrpc.RESTJsonMarshalOpts.Marshal(req)
- if err != nil {
- return nil, err
- }
- err = conn.WriteMessage(websocket.TextMessage, reqMsg)
- if err != nil {
- return nil, err
- }
- return conn, nil
- }
- // addAdminMacaroon reads the admin macaroon from the node and appends it to
- // the HTTP header fields.
- func addAdminMacaroon(node *node.HarnessNode, header http.Header) error {
- mac, err := node.ReadMacaroon(node.Cfg.AdminMacPath, defaultTimeout)
- if err != nil {
- return err
- }
- macBytes, err := mac.MarshalBinary()
- if err != nil {
- return err
- }
- header.Set("Grpc-Metadata-Macaroon", hex.EncodeToString(macBytes))
- return nil
- }
|