github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/dataexchange/dxhttps/dxhttps.go (about)

     1  // Copyright © 2021 Kaleido, Inc.
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  //
     5  // Licensed under the Apache License, Version 2.0 (the "License");
     6  // you may not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  package dxhttps
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"strings"
    25  
    26  	"github.com/go-resty/resty/v2"
    27  	"github.com/kaleido-io/firefly/internal/config"
    28  	"github.com/kaleido-io/firefly/internal/i18n"
    29  	"github.com/kaleido-io/firefly/internal/log"
    30  	"github.com/kaleido-io/firefly/internal/restclient"
    31  	"github.com/kaleido-io/firefly/internal/wsclient"
    32  	"github.com/kaleido-io/firefly/pkg/dataexchange"
    33  	"github.com/kaleido-io/firefly/pkg/fftypes"
    34  )
    35  
    36  type HTTPS struct {
    37  	ctx          context.Context
    38  	capabilities *dataexchange.Capabilities
    39  	callbacks    dataexchange.Callbacks
    40  	client       *resty.Client
    41  	wsconn       wsclient.WSClient
    42  }
    43  
    44  type wsEvent struct {
    45  	Type      msgType `json:"type"`
    46  	Sender    string  `json:"sender"`
    47  	Recipient string  `json:"recipient"`
    48  	RequestID string  `json:"requestID"`
    49  	Path      string  `json:"path"`
    50  	Message   string  `json:"message"`
    51  	Hash      string  `json:"hash"`
    52  	Error     string  `json:"error"`
    53  }
    54  
    55  type msgType string
    56  
    57  const (
    58  	messageReceived  msgType = "message-received"
    59  	messageDelivered msgType = "message-delivered"
    60  	messageFailed    msgType = "message-failed"
    61  	blobReceived     msgType = "blob-received"
    62  	blobDelivered    msgType = "blob-delivered"
    63  	blobFailed       msgType = "blob-failed"
    64  )
    65  
    66  type responseWithRequestID struct {
    67  	RequestID string `json:"requestID"`
    68  }
    69  
    70  type sendMessage struct {
    71  	Message   string `json:"message"`
    72  	Recipient string `json:"recipient"`
    73  }
    74  
    75  type transferBlob struct {
    76  	Path      string `json:"path"`
    77  	Recipient string `json:"recipient"`
    78  }
    79  
    80  func (h *HTTPS) Name() string {
    81  	return "https"
    82  }
    83  
    84  func (h *HTTPS) Init(ctx context.Context, prefix config.Prefix, callbacks dataexchange.Callbacks) (err error) {
    85  	h.ctx = log.WithLogField(ctx, "dx", "https")
    86  	h.callbacks = callbacks
    87  
    88  	if prefix.GetString(restclient.HTTPConfigURL) == "" {
    89  		return i18n.NewError(ctx, i18n.MsgMissingPluginConfig, "url", "dataexchange.https")
    90  	}
    91  
    92  	h.client = restclient.New(h.ctx, prefix)
    93  	h.capabilities = &dataexchange.Capabilities{}
    94  	h.wsconn, err = wsclient.New(ctx, prefix, nil)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	go h.eventLoop()
    99  	return nil
   100  }
   101  
   102  func (h *HTTPS) Start() error {
   103  	return h.wsconn.Connect()
   104  }
   105  
   106  func (h *HTTPS) Capabilities() *dataexchange.Capabilities {
   107  	return h.capabilities
   108  }
   109  
   110  func (h *HTTPS) GetEndpointInfo(ctx context.Context) (peerID string, endpoint fftypes.JSONObject, err error) {
   111  	res, err := h.client.R().SetContext(ctx).
   112  		SetResult(&endpoint).
   113  		Get("/api/v1/id")
   114  	if err != nil || !res.IsSuccess() {
   115  		return peerID, endpoint, restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   116  	}
   117  	return endpoint.GetString("id"), endpoint, nil
   118  }
   119  
   120  func (h *HTTPS) AddPeer(ctx context.Context, node *fftypes.Node) (err error) {
   121  	res, err := h.client.R().SetContext(ctx).
   122  		SetBody(node.DX.Endpoint).
   123  		Put(fmt.Sprintf("/api/v1/peers/%s", node.DX.Peer))
   124  	if err != nil || !res.IsSuccess() {
   125  		return restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   126  	}
   127  	return nil
   128  }
   129  
   130  func (h *HTTPS) UploadBLOB(ctx context.Context, ns string, id fftypes.UUID, content io.Reader) (err error) {
   131  	res, err := h.client.R().SetContext(ctx).
   132  		SetFileReader("file", id.String(), content).
   133  		Put(fmt.Sprintf("/api/v1/blobs/%s/%s", ns, &id))
   134  	if err != nil || !res.IsSuccess() {
   135  		return restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   136  	}
   137  	return nil
   138  }
   139  
   140  func (h *HTTPS) DownloadBLOB(ctx context.Context, ns string, id fftypes.UUID) (content io.ReadCloser, err error) {
   141  	res, err := h.client.R().SetContext(ctx).
   142  		SetDoNotParseResponse(true).
   143  		Get(fmt.Sprintf("/api/v1/blobs/%s/%s", ns, &id))
   144  	if err != nil || !res.IsSuccess() {
   145  		if err == nil {
   146  			_ = res.RawBody().Close()
   147  		}
   148  		return nil, restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   149  	}
   150  	return res.RawBody(), nil
   151  }
   152  
   153  func (h *HTTPS) SendMessage(ctx context.Context, node *fftypes.Node, data []byte) (trackingID string, err error) {
   154  	var responseData responseWithRequestID
   155  	res, err := h.client.R().SetContext(ctx).
   156  		SetBody(&sendMessage{
   157  			Message:   string(data),
   158  			Recipient: node.DX.Peer,
   159  		}).
   160  		SetResult(&responseData).
   161  		Post("/api/v1/messages")
   162  	if err != nil || !res.IsSuccess() {
   163  		return "", restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   164  	}
   165  	return responseData.RequestID, nil
   166  }
   167  
   168  func (h *HTTPS) TransferBLOB(ctx context.Context, node *fftypes.Node, ns string, id fftypes.UUID) (trackingID string, err error) {
   169  	var responseData responseWithRequestID
   170  	res, err := h.client.R().SetContext(ctx).
   171  		SetBody(&transferBlob{
   172  			Path:      fmt.Sprintf("%s/%s", ns, id),
   173  			Recipient: node.DX.Peer,
   174  		}).
   175  		SetResult(&responseData).
   176  		Post("/api/v1/transfers")
   177  	if err != nil || !res.IsSuccess() {
   178  		return "", restclient.WrapRestErr(ctx, res, err, i18n.MsgDXRESTErr)
   179  	}
   180  	return responseData.RequestID, nil
   181  }
   182  
   183  func (h *HTTPS) extractBlobPath(ctx context.Context, path string) (ns string, id *fftypes.UUID) {
   184  	parts := strings.Split(path, "/")
   185  	if len(parts) != 2 {
   186  		log.L(ctx).Errorf("Invalid blob path: %s", path)
   187  		return "", nil
   188  	}
   189  	ns = parts[0]
   190  	if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil {
   191  		log.L(ctx).Errorf("Invalid blob namespace: %s", path)
   192  		return "", nil
   193  	}
   194  	id, err := fftypes.ParseUUID(ctx, parts[1])
   195  	if err != nil {
   196  		log.L(ctx).Errorf("Invalid blob UUID: %s", path)
   197  		return "", nil
   198  	}
   199  	return ns, id
   200  }
   201  
   202  func (h *HTTPS) eventLoop() {
   203  	l := log.L(h.ctx).WithField("role", "event-loop")
   204  	ctx := log.WithLogger(h.ctx, l)
   205  	ack, _ := json.Marshal(map[string]string{"action": "commit"})
   206  	for {
   207  		select {
   208  		case <-ctx.Done():
   209  			l.Debugf("Event loop exiting (context cancelled)")
   210  			return
   211  		case msgBytes, ok := <-h.wsconn.Receive():
   212  			if !ok {
   213  				l.Debugf("Event loop exiting (receive channel closed)")
   214  				return
   215  			}
   216  
   217  			l.Tracef("DX message: %s", msgBytes)
   218  			var msg wsEvent
   219  			err := json.Unmarshal(msgBytes, &msg)
   220  			if err != nil {
   221  				l.Errorf("Message cannot be parsed as JSON: %s\n%s", err, string(msgBytes))
   222  				continue // Swallow this and move on
   223  			}
   224  			l.Debugf("Received %s event from DX sender=%s", msg.Type, msg.Sender)
   225  			switch msg.Type {
   226  			case messageFailed:
   227  				h.callbacks.TransferResult(msg.RequestID, fftypes.OpStatusFailed, msg.Error, nil)
   228  			case messageDelivered:
   229  				h.callbacks.TransferResult(msg.RequestID, fftypes.OpStatusSucceeded, "", nil)
   230  			case messageReceived:
   231  				h.callbacks.MessageReceived(msg.Sender, fftypes.Byteable(msg.Message))
   232  			case blobFailed:
   233  				h.callbacks.TransferResult(msg.RequestID, fftypes.OpStatusFailed, msg.Error, nil)
   234  			case blobDelivered:
   235  				h.callbacks.TransferResult(msg.RequestID, fftypes.OpStatusSucceeded, "", nil)
   236  			case blobReceived:
   237  				if ns, id := h.extractBlobPath(ctx, msg.Path); id != nil {
   238  					h.callbacks.BLOBReceived(msg.Sender, ns, *id)
   239  				}
   240  			default:
   241  				l.Errorf("Message unexpected: %s", msg.Type)
   242  			}
   243  
   244  			// Send the ack - only fails if shutting down
   245  			err = h.wsconn.Send(ctx, ack)
   246  			if err != nil {
   247  				l.Errorf("Event loop exiting: %s", err)
   248  				return
   249  			}
   250  		}
   251  	}
   252  }