github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/logsync/http.go (about)

     1  package logsync
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  
    12  	"github.com/qri-io/qri/auth/key"
    13  	"github.com/qri-io/qri/dsref"
    14  	"github.com/qri-io/qri/logbook"
    15  	"github.com/qri-io/qri/profile"
    16  	"github.com/qri-io/qri/repo"
    17  	reporef "github.com/qri-io/qri/repo/ref"
    18  )
    19  
    20  // httpClient is the request side of doing dsync over HTTP
    21  type httpClient struct {
    22  	URL string
    23  }
    24  
    25  // compile time assertion that httpClient is a remote
    26  // httpClient exists to satisfy the Remote interface on the client side
    27  var _ remote = (*httpClient)(nil)
    28  
    29  func (c *httpClient) addr() string {
    30  	return c.URL
    31  }
    32  
    33  func (c *httpClient) put(ctx context.Context, author profile.Author, ref dsref.Ref, r io.Reader) error {
    34  	log.Debugw("httpClient.put", "ref", ref)
    35  	u, err := url.Parse(c.URL)
    36  	if err != nil {
    37  		return fmt.Errorf("invalid logsync client url: %w", err)
    38  	}
    39  	q := u.Query()
    40  	// TODO(B5): we need the old serialization format here b/c logsync checks the
    41  	// profileID matches the author. Migrate to a new system for validating who-can-push-what
    42  	// using keystore & UCANs, then switch this to standard reference string serialization
    43  	q.Set("ref", ref.LegacyProfileIDString())
    44  	u.RawQuery = q.Encode()
    45  
    46  	req, err := http.NewRequest("PUT", u.String(), r)
    47  	if err != nil {
    48  		return err
    49  	}
    50  	req = req.WithContext(ctx)
    51  
    52  	if err := addAuthorHTTPHeaders(req.Header, author); err != nil {
    53  		return err
    54  	}
    55  
    56  	res, err := http.DefaultClient.Do(req)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	if res.StatusCode != http.StatusOK {
    61  		if errmsg, err := ioutil.ReadAll(res.Body); err == nil {
    62  			return fmt.Errorf(string(errmsg))
    63  		}
    64  		return err
    65  	}
    66  
    67  	return nil
    68  }
    69  
    70  func (c *httpClient) get(ctx context.Context, author profile.Author, ref dsref.Ref) (profile.Author, io.Reader, error) {
    71  	log.Debugw("httpClient.get", "ref", ref)
    72  	u, err := url.Parse(c.URL)
    73  	if err != nil {
    74  		return nil, nil, fmt.Errorf("invalid logsync client url: %w", err)
    75  	}
    76  	q := u.Query()
    77  	// TODO(b5): remove initID for backwards compatiblity. get doesn't rely on the
    78  	// ProfileID field, but the other end of the wire may error if we send an InitID
    79  	// field
    80  	ref.InitID = ""
    81  	q.Set("ref", ref.String())
    82  	u.RawQuery = q.Encode()
    83  
    84  	req, err := http.NewRequest("GET", u.String(), nil)
    85  	if err != nil {
    86  		return nil, nil, err
    87  	}
    88  	req = req.WithContext(ctx)
    89  
    90  	if err := addAuthorHTTPHeaders(req.Header, author); err != nil {
    91  		log.Debugf("addAuthorHTTPHeaders error=%q", err)
    92  		return nil, nil, err
    93  	}
    94  
    95  	res, err := http.DefaultClient.Do(req)
    96  	if err != nil {
    97  		log.Debugf("http.DefaultClient.Do error=%q", err)
    98  		return nil, nil, err
    99  	}
   100  
   101  	if res.StatusCode != http.StatusOK {
   102  		log.Debugf("httpClient.get statusCode=%d", res.StatusCode)
   103  		if errmsg, err := ioutil.ReadAll(res.Body); err == nil {
   104  			return nil, nil, fmt.Errorf(string(errmsg))
   105  		}
   106  		return nil, nil, err
   107  	}
   108  
   109  	sender, err := senderFromHTTPHeaders(res.Header)
   110  	if err != nil {
   111  		return nil, nil, err
   112  	}
   113  
   114  	return sender, res.Body, nil
   115  }
   116  
   117  func (c *httpClient) del(ctx context.Context, author profile.Author, ref dsref.Ref) error {
   118  	req, err := http.NewRequest("DELETE", fmt.Sprintf("%s?ref=%s", c.URL, ref), nil)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	req = req.WithContext(ctx)
   123  
   124  	if err := addAuthorHTTPHeaders(req.Header, author); err != nil {
   125  		return err
   126  	}
   127  
   128  	res, err := http.DefaultClient.Do(req)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	if res.StatusCode != http.StatusOK {
   133  		if errmsg, err := ioutil.ReadAll(res.Body); err == nil {
   134  			return fmt.Errorf(string(errmsg))
   135  		}
   136  	}
   137  	return err
   138  }
   139  
   140  func addAuthorHTTPHeaders(h http.Header, author profile.Author) error {
   141  	h.Set("ID", author.AuthorID())
   142  	h.Set("username", author.Username())
   143  	pubKey, err := key.EncodePubKeyB64(author.AuthorPubKey())
   144  	if err != nil {
   145  		return err
   146  	}
   147  	h.Set("PubKey", pubKey)
   148  	return nil
   149  }
   150  
   151  func senderFromHTTPHeaders(h http.Header) (profile.Author, error) {
   152  	pub, err := key.DecodeB64PubKey(h.Get("PubKey"))
   153  	if err != nil {
   154  		return nil, fmt.Errorf("decoding public key: %s", err)
   155  	}
   156  
   157  	return profile.NewAuthor(h.Get("ID"), pub, h.Get("username")), nil
   158  }
   159  
   160  // HTTPHandler exposes a Dsync remote over HTTP by exposing a HTTP handler
   161  // that interlocks with methods exposed by httpClient
   162  func HTTPHandler(lsync *Logsync) http.HandlerFunc {
   163  	return func(w http.ResponseWriter, r *http.Request) {
   164  		sender, err := senderFromHTTPHeaders(r.Header)
   165  		if err != nil {
   166  			log.Debugf("senderFromHTTPHeaders error=%q", err)
   167  			w.WriteHeader(http.StatusBadRequest)
   168  			w.Write([]byte(err.Error()))
   169  			return
   170  		}
   171  
   172  		switch r.Method {
   173  		case "PUT":
   174  			ref, err := dsref.Parse(r.FormValue("ref"))
   175  			if err != nil {
   176  				log.Debugf("PUT dsref.Parse error=%q", err)
   177  				w.WriteHeader(http.StatusBadRequest)
   178  				w.Write([]byte(err.Error()))
   179  				return
   180  			}
   181  			if err := lsync.put(r.Context(), sender, ref, r.Body); err != nil {
   182  				w.WriteHeader(http.StatusBadRequest)
   183  				w.Write([]byte(err.Error()))
   184  				return
   185  			}
   186  			r.Body.Close()
   187  
   188  			addAuthorHTTPHeaders(w.Header(), lsync.Author())
   189  			return
   190  		case "GET":
   191  			ref, err := dsref.Parse(r.FormValue("ref"))
   192  			if err != nil {
   193  				log.Debugf("GET dsref.Parse error=%q", err)
   194  				w.WriteHeader(http.StatusBadRequest)
   195  				w.Write([]byte(err.Error()))
   196  				return
   197  			}
   198  
   199  			receiver, r, err := lsync.get(r.Context(), sender, ref)
   200  			if err != nil {
   201  				log.Debugf("GET error=%q ref=%q", err, ref)
   202  				// TODO (ramfox): implement a robust error response strategy
   203  				if errors.Is(err, logbook.ErrNotFound) {
   204  					w.WriteHeader(http.StatusNotFound)
   205  					w.Write([]byte(err.Error()))
   206  					return
   207  				}
   208  				w.WriteHeader(http.StatusBadRequest)
   209  				w.Write([]byte(err.Error()))
   210  				return
   211  			}
   212  			addAuthorHTTPHeaders(w.Header(), receiver)
   213  			io.Copy(w, r)
   214  			return
   215  		case "DELETE":
   216  			ref, err := repo.ParseDatasetRef(r.FormValue("ref"))
   217  			if err != nil {
   218  				w.WriteHeader(http.StatusBadRequest)
   219  				w.Write([]byte(err.Error()))
   220  				return
   221  			}
   222  
   223  			if err = lsync.del(r.Context(), sender, reporef.ConvertToDsref(ref)); err != nil {
   224  				w.WriteHeader(http.StatusBadRequest)
   225  				w.Write([]byte(err.Error()))
   226  				return
   227  			}
   228  
   229  			addAuthorHTTPHeaders(w.Header(), lsync.Author())
   230  			return
   231  		default:
   232  			w.WriteHeader(http.StatusNotFound)
   233  			w.Write([]byte(`not found`))
   234  			return
   235  		}
   236  	}
   237  }