github.com/insight-chain/inb-go@v1.1.3-0.20191221022159-da049980ae38/swarm/api/client/client.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package client
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"mime/multipart"
    29  	"net/http"
    30  	"net/http/httptrace"
    31  	"net/textproto"
    32  	"net/url"
    33  	"os"
    34  	"path/filepath"
    35  	"regexp"
    36  	"strconv"
    37  	"strings"
    38  	"time"
    39  
    40  	"github.com/insight-chain/inb-go/log"
    41  	"github.com/insight-chain/inb-go/metrics"
    42  	"github.com/insight-chain/inb-go/swarm/api"
    43  	"github.com/insight-chain/inb-go/swarm/spancontext"
    44  	"github.com/insight-chain/inb-go/swarm/storage/feed"
    45  	"github.com/pborman/uuid"
    46  )
    47  
    48  var (
    49  	DefaultGateway = "http://localhost:8500"
    50  	DefaultClient  = NewClient(DefaultGateway)
    51  )
    52  
    53  var (
    54  	ErrUnauthorized = errors.New("unauthorized")
    55  )
    56  
    57  func NewClient(gateway string) *Client {
    58  	return &Client{
    59  		Gateway: gateway,
    60  	}
    61  }
    62  
    63  // Client wraps interaction with a swarm HTTP gateway.
    64  type Client struct {
    65  	Gateway string
    66  }
    67  
    68  // UploadRaw uploads raw data to swarm and returns the resulting hash. If toEncrypt is true it
    69  // uploads encrypted data
    70  func (c *Client) UploadRaw(r io.Reader, size int64, toEncrypt bool) (string, error) {
    71  	if size <= 0 {
    72  		return "", errors.New("data size must be greater than zero")
    73  	}
    74  	addr := ""
    75  	if toEncrypt {
    76  		addr = "encrypt"
    77  	}
    78  	req, err := http.NewRequest("POST", c.Gateway+"/bzz-raw:/"+addr, r)
    79  	if err != nil {
    80  		return "", err
    81  	}
    82  	req.ContentLength = size
    83  	res, err := http.DefaultClient.Do(req)
    84  	if err != nil {
    85  		return "", err
    86  	}
    87  	defer res.Body.Close()
    88  	if res.StatusCode != http.StatusOK {
    89  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
    90  	}
    91  	data, err := ioutil.ReadAll(res.Body)
    92  	if err != nil {
    93  		return "", err
    94  	}
    95  	return string(data), nil
    96  }
    97  
    98  // DownloadRaw downloads raw data from swarm and it returns a ReadCloser and a bool whether the
    99  // content was encrypted
   100  func (c *Client) DownloadRaw(hash string) (io.ReadCloser, bool, error) {
   101  	uri := c.Gateway + "/bzz-raw:/" + hash
   102  	res, err := http.DefaultClient.Get(uri)
   103  	if err != nil {
   104  		return nil, false, err
   105  	}
   106  	if res.StatusCode != http.StatusOK {
   107  		res.Body.Close()
   108  		return nil, false, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   109  	}
   110  	isEncrypted := (res.Header.Get("X-Decrypted") == "true")
   111  	return res.Body, isEncrypted, nil
   112  }
   113  
   114  // File represents a file in a swarm manifest and is used for uploading and
   115  // downloading content to and from swarm
   116  type File struct {
   117  	io.ReadCloser
   118  	api.ManifestEntry
   119  }
   120  
   121  // Open opens a local file which can then be passed to client.Upload to upload
   122  // it to swarm
   123  func Open(path string) (*File, error) {
   124  	f, err := os.Open(path)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	stat, err := f.Stat()
   129  	if err != nil {
   130  		f.Close()
   131  		return nil, err
   132  	}
   133  
   134  	contentType, err := api.DetectContentType(f.Name(), f)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	return &File{
   140  		ReadCloser: f,
   141  		ManifestEntry: api.ManifestEntry{
   142  			ContentType: contentType,
   143  			Mode:        int64(stat.Mode()),
   144  			Size:        stat.Size(),
   145  			ModTime:     stat.ModTime(),
   146  		},
   147  	}, nil
   148  }
   149  
   150  // Upload uploads a file to swarm and either adds it to an existing manifest
   151  // (if the manifest argument is non-empty) or creates a new manifest containing
   152  // the file, returning the resulting manifest hash (the file will then be
   153  // available at bzz:/<hash>/<path>)
   154  func (c *Client) Upload(file *File, manifest string, toEncrypt bool) (string, error) {
   155  	if file.Size <= 0 {
   156  		return "", errors.New("file size must be greater than zero")
   157  	}
   158  	return c.TarUpload(manifest, &FileUploader{file}, "", toEncrypt)
   159  }
   160  
   161  // Download downloads a file with the given path from the swarm manifest with
   162  // the given hash (i.e. it gets bzz:/<hash>/<path>)
   163  func (c *Client) Download(hash, path string) (*File, error) {
   164  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   165  	res, err := http.DefaultClient.Get(uri)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	if res.StatusCode != http.StatusOK {
   170  		res.Body.Close()
   171  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   172  	}
   173  	return &File{
   174  		ReadCloser: res.Body,
   175  		ManifestEntry: api.ManifestEntry{
   176  			ContentType: res.Header.Get("Content-Type"),
   177  			Size:        res.ContentLength,
   178  		},
   179  	}, nil
   180  }
   181  
   182  // UploadDirectory uploads a directory tree to swarm and either adds the files
   183  // to an existing manifest (if the manifest argument is non-empty) or creates a
   184  // new manifest, returning the resulting manifest hash (files from the
   185  // directory will then be available at bzz:/<hash>/path/to/file), with
   186  // the file specified in defaultPath being uploaded to the root of the manifest
   187  // (i.e. bzz:/<hash>/)
   188  func (c *Client) UploadDirectory(dir, defaultPath, manifest string, toEncrypt bool) (string, error) {
   189  	stat, err := os.Stat(dir)
   190  	if err != nil {
   191  		return "", err
   192  	} else if !stat.IsDir() {
   193  		return "", fmt.Errorf("not a directory: %s", dir)
   194  	}
   195  	if defaultPath != "" {
   196  		if _, err := os.Stat(filepath.Join(dir, defaultPath)); err != nil {
   197  			if os.IsNotExist(err) {
   198  				return "", fmt.Errorf("the default path %q was not found in the upload directory %q", defaultPath, dir)
   199  			}
   200  			return "", fmt.Errorf("default path: %v", err)
   201  		}
   202  	}
   203  	return c.TarUpload(manifest, &DirectoryUploader{dir}, defaultPath, toEncrypt)
   204  }
   205  
   206  // DownloadDirectory downloads the files contained in a swarm manifest under
   207  // the given path into a local directory (existing files will be overwritten)
   208  func (c *Client) DownloadDirectory(hash, path, destDir, credentials string) error {
   209  	stat, err := os.Stat(destDir)
   210  	if err != nil {
   211  		return err
   212  	} else if !stat.IsDir() {
   213  		return fmt.Errorf("not a directory: %s", destDir)
   214  	}
   215  
   216  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   217  	req, err := http.NewRequest("GET", uri, nil)
   218  	if err != nil {
   219  		return err
   220  	}
   221  	if credentials != "" {
   222  		req.SetBasicAuth("", credentials)
   223  	}
   224  	req.Header.Set("Accept", "application/x-tar")
   225  	res, err := http.DefaultClient.Do(req)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	defer res.Body.Close()
   230  	switch res.StatusCode {
   231  	case http.StatusOK:
   232  	case http.StatusUnauthorized:
   233  		return ErrUnauthorized
   234  	default:
   235  		return fmt.Errorf("unexpected HTTP status: %s", res.Status)
   236  	}
   237  	tr := tar.NewReader(res.Body)
   238  	for {
   239  		hdr, err := tr.Next()
   240  		if err == io.EOF {
   241  			return nil
   242  		} else if err != nil {
   243  			return err
   244  		}
   245  		// ignore the default path file
   246  		if hdr.Name == "" {
   247  			continue
   248  		}
   249  
   250  		dstPath := filepath.Join(destDir, filepath.Clean(strings.TrimPrefix(hdr.Name, path)))
   251  		if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
   252  			return err
   253  		}
   254  		var mode os.FileMode = 0644
   255  		if hdr.Mode > 0 {
   256  			mode = os.FileMode(hdr.Mode)
   257  		}
   258  		dst, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		n, err := io.Copy(dst, tr)
   263  		dst.Close()
   264  		if err != nil {
   265  			return err
   266  		} else if n != hdr.Size {
   267  			return fmt.Errorf("expected %s to be %d bytes but got %d", hdr.Name, hdr.Size, n)
   268  		}
   269  	}
   270  }
   271  
   272  // DownloadFile downloads a single file into the destination directory
   273  // if the manifest entry does not specify a file name - it will fallback
   274  // to the hash of the file as a filename
   275  func (c *Client) DownloadFile(hash, path, dest, credentials string) error {
   276  	hasDestinationFilename := false
   277  	if stat, err := os.Stat(dest); err == nil {
   278  		hasDestinationFilename = !stat.IsDir()
   279  	} else {
   280  		if os.IsNotExist(err) {
   281  			// does not exist - should be created
   282  			hasDestinationFilename = true
   283  		} else {
   284  			return fmt.Errorf("could not stat path: %v", err)
   285  		}
   286  	}
   287  
   288  	manifestList, err := c.List(hash, path, credentials)
   289  	if err != nil {
   290  		return err
   291  	}
   292  
   293  	switch len(manifestList.Entries) {
   294  	case 0:
   295  		return fmt.Errorf("could not find path requested at manifest address. make sure the path you've specified is correct")
   296  	case 1:
   297  		//continue
   298  	default:
   299  		return fmt.Errorf("got too many matches for this path")
   300  	}
   301  
   302  	uri := c.Gateway + "/bzz:/" + hash + "/" + path
   303  	req, err := http.NewRequest("GET", uri, nil)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	if credentials != "" {
   308  		req.SetBasicAuth("", credentials)
   309  	}
   310  	res, err := http.DefaultClient.Do(req)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	defer res.Body.Close()
   315  	switch res.StatusCode {
   316  	case http.StatusOK:
   317  	case http.StatusUnauthorized:
   318  		return ErrUnauthorized
   319  	default:
   320  		return fmt.Errorf("unexpected HTTP status: expected 200 OK, got %d", res.StatusCode)
   321  	}
   322  	filename := ""
   323  	if hasDestinationFilename {
   324  		filename = dest
   325  	} else {
   326  		// try to assert
   327  		re := regexp.MustCompile("[^/]+$") //everything after last slash
   328  
   329  		if results := re.FindAllString(path, -1); len(results) > 0 {
   330  			filename = results[len(results)-1]
   331  		} else {
   332  			if entry := manifestList.Entries[0]; entry.Path != "" && entry.Path != "/" {
   333  				filename = entry.Path
   334  			} else {
   335  				// assume hash as name if there's nothing from the command line
   336  				filename = hash
   337  			}
   338  		}
   339  		filename = filepath.Join(dest, filename)
   340  	}
   341  	filePath, err := filepath.Abs(filename)
   342  	if err != nil {
   343  		return err
   344  	}
   345  
   346  	if err := os.MkdirAll(filepath.Dir(filePath), 0777); err != nil {
   347  		return err
   348  	}
   349  
   350  	dst, err := os.Create(filename)
   351  	if err != nil {
   352  		return err
   353  	}
   354  	defer dst.Close()
   355  
   356  	_, err = io.Copy(dst, res.Body)
   357  	return err
   358  }
   359  
   360  // UploadManifest uploads the given manifest to swarm
   361  func (c *Client) UploadManifest(m *api.Manifest, toEncrypt bool) (string, error) {
   362  	data, err := json.Marshal(m)
   363  	if err != nil {
   364  		return "", err
   365  	}
   366  	return c.UploadRaw(bytes.NewReader(data), int64(len(data)), toEncrypt)
   367  }
   368  
   369  // DownloadManifest downloads a swarm manifest
   370  func (c *Client) DownloadManifest(hash string) (*api.Manifest, bool, error) {
   371  	res, isEncrypted, err := c.DownloadRaw(hash)
   372  	if err != nil {
   373  		return nil, isEncrypted, err
   374  	}
   375  	defer res.Close()
   376  	var manifest api.Manifest
   377  	if err := json.NewDecoder(res).Decode(&manifest); err != nil {
   378  		return nil, isEncrypted, err
   379  	}
   380  	return &manifest, isEncrypted, nil
   381  }
   382  
   383  // List list files in a swarm manifest which have the given prefix, grouping
   384  // common prefixes using "/" as a delimiter.
   385  //
   386  // For example, if the manifest represents the following directory structure:
   387  //
   388  // file1.txt
   389  // file2.txt
   390  // dir1/file3.txt
   391  // dir1/dir2/file4.txt
   392  //
   393  // Then:
   394  //
   395  // - a prefix of ""      would return [dir1/, file1.txt, file2.txt]
   396  // - a prefix of "file"  would return [file1.txt, file2.txt]
   397  // - a prefix of "dir1/" would return [dir1/dir2/, dir1/file3.txt]
   398  //
   399  // where entries ending with "/" are common prefixes.
   400  func (c *Client) List(hash, prefix, credentials string) (*api.ManifestList, error) {
   401  	req, err := http.NewRequest(http.MethodGet, c.Gateway+"/bzz-list:/"+hash+"/"+prefix, nil)
   402  	if err != nil {
   403  		return nil, err
   404  	}
   405  	if credentials != "" {
   406  		req.SetBasicAuth("", credentials)
   407  	}
   408  	res, err := http.DefaultClient.Do(req)
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	defer res.Body.Close()
   413  	switch res.StatusCode {
   414  	case http.StatusOK:
   415  	case http.StatusUnauthorized:
   416  		return nil, ErrUnauthorized
   417  	default:
   418  		return nil, fmt.Errorf("unexpected HTTP status: %s", res.Status)
   419  	}
   420  	var list api.ManifestList
   421  	if err := json.NewDecoder(res.Body).Decode(&list); err != nil {
   422  		return nil, err
   423  	}
   424  	return &list, nil
   425  }
   426  
   427  // Uploader uploads files to swarm using a provided UploadFn
   428  type Uploader interface {
   429  	Upload(UploadFn) error
   430  }
   431  
   432  type UploaderFunc func(UploadFn) error
   433  
   434  func (u UploaderFunc) Upload(upload UploadFn) error {
   435  	return u(upload)
   436  }
   437  
   438  // DirectoryUploader uploads all files in a directory, optionally uploading
   439  // a file to the default path
   440  type DirectoryUploader struct {
   441  	Dir string
   442  }
   443  
   444  // Upload performs the upload of the directory and default path
   445  func (d *DirectoryUploader) Upload(upload UploadFn) error {
   446  	return filepath.Walk(d.Dir, func(path string, f os.FileInfo, err error) error {
   447  		if err != nil {
   448  			return err
   449  		}
   450  		if f.IsDir() {
   451  			return nil
   452  		}
   453  		file, err := Open(path)
   454  		if err != nil {
   455  			return err
   456  		}
   457  		relPath, err := filepath.Rel(d.Dir, path)
   458  		if err != nil {
   459  			return err
   460  		}
   461  		file.Path = filepath.ToSlash(relPath)
   462  		return upload(file)
   463  	})
   464  }
   465  
   466  // FileUploader uploads a single file
   467  type FileUploader struct {
   468  	File *File
   469  }
   470  
   471  // Upload performs the upload of the file
   472  func (f *FileUploader) Upload(upload UploadFn) error {
   473  	return upload(f.File)
   474  }
   475  
   476  // UploadFn is the type of function passed to an Uploader to perform the upload
   477  // of a single file (for example, a directory uploader would call a provided
   478  // UploadFn for each file in the directory tree)
   479  type UploadFn func(file *File) error
   480  
   481  // TarUpload uses the given Uploader to upload files to swarm as a tar stream,
   482  // returning the resulting manifest hash
   483  func (c *Client) TarUpload(hash string, uploader Uploader, defaultPath string, toEncrypt bool) (string, error) {
   484  	ctx, sp := spancontext.StartSpan(context.Background(), "api.client.tarupload")
   485  	defer sp.Finish()
   486  
   487  	var tn time.Time
   488  
   489  	reqR, reqW := io.Pipe()
   490  	defer reqR.Close()
   491  	addr := hash
   492  
   493  	// If there is a hash already (a manifest), then that manifest will determine if the upload has
   494  	// to be encrypted or not. If there is no manifest then the toEncrypt parameter decides if
   495  	// there is encryption or not.
   496  	if hash == "" && toEncrypt {
   497  		// This is the built-in address for the encrypted upload endpoint
   498  		addr = "encrypt"
   499  	}
   500  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+addr, reqR)
   501  	if err != nil {
   502  		return "", err
   503  	}
   504  
   505  	trace := GetClientTrace("swarm api client - upload tar", "api.client.uploadtar", uuid.New()[:8], &tn)
   506  
   507  	req = req.WithContext(httptrace.WithClientTrace(ctx, trace))
   508  	transport := http.DefaultTransport
   509  
   510  	req.Header.Set("Content-Type", "application/x-tar")
   511  	if defaultPath != "" {
   512  		q := req.URL.Query()
   513  		q.Set("defaultpath", defaultPath)
   514  		req.URL.RawQuery = q.Encode()
   515  	}
   516  
   517  	// use 'Expect: 100-continue' so we don't send the request body if
   518  	// the server refuses the request
   519  	req.Header.Set("Expect", "100-continue")
   520  
   521  	tw := tar.NewWriter(reqW)
   522  
   523  	// define an UploadFn which adds files to the tar stream
   524  	uploadFn := func(file *File) error {
   525  		hdr := &tar.Header{
   526  			Name:    file.Path,
   527  			Mode:    file.Mode,
   528  			Size:    file.Size,
   529  			ModTime: file.ModTime,
   530  			Xattrs: map[string]string{
   531  				"user.swarm.content-type": file.ContentType,
   532  			},
   533  		}
   534  		if err := tw.WriteHeader(hdr); err != nil {
   535  			return err
   536  		}
   537  		_, err = io.Copy(tw, file)
   538  		return err
   539  	}
   540  
   541  	// run the upload in a goroutine so we can send the request headers and
   542  	// wait for a '100 Continue' response before sending the tar stream
   543  	go func() {
   544  		err := uploader.Upload(uploadFn)
   545  		if err == nil {
   546  			err = tw.Close()
   547  		}
   548  		reqW.CloseWithError(err)
   549  	}()
   550  	tn = time.Now()
   551  	res, err := transport.RoundTrip(req)
   552  	if err != nil {
   553  		return "", err
   554  	}
   555  	defer res.Body.Close()
   556  	if res.StatusCode != http.StatusOK {
   557  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   558  	}
   559  	data, err := ioutil.ReadAll(res.Body)
   560  	if err != nil {
   561  		return "", err
   562  	}
   563  	return string(data), nil
   564  }
   565  
   566  // MultipartUpload uses the given Uploader to upload files to swarm as a
   567  // multipart form, returning the resulting manifest hash
   568  func (c *Client) MultipartUpload(hash string, uploader Uploader) (string, error) {
   569  	reqR, reqW := io.Pipe()
   570  	defer reqR.Close()
   571  	req, err := http.NewRequest("POST", c.Gateway+"/bzz:/"+hash, reqR)
   572  	if err != nil {
   573  		return "", err
   574  	}
   575  
   576  	// use 'Expect: 100-continue' so we don't send the request body if
   577  	// the server refuses the request
   578  	req.Header.Set("Expect", "100-continue")
   579  
   580  	mw := multipart.NewWriter(reqW)
   581  	req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%q", mw.Boundary()))
   582  
   583  	// define an UploadFn which adds files to the multipart form
   584  	uploadFn := func(file *File) error {
   585  		hdr := make(textproto.MIMEHeader)
   586  		hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=%q", file.Path))
   587  		hdr.Set("Content-Type", file.ContentType)
   588  		hdr.Set("Content-Length", strconv.FormatInt(file.Size, 10))
   589  		w, err := mw.CreatePart(hdr)
   590  		if err != nil {
   591  			return err
   592  		}
   593  		_, err = io.Copy(w, file)
   594  		return err
   595  	}
   596  
   597  	// run the upload in a goroutine so we can send the request headers and
   598  	// wait for a '100 Continue' response before sending the multipart form
   599  	go func() {
   600  		err := uploader.Upload(uploadFn)
   601  		if err == nil {
   602  			err = mw.Close()
   603  		}
   604  		reqW.CloseWithError(err)
   605  	}()
   606  
   607  	res, err := http.DefaultClient.Do(req)
   608  	if err != nil {
   609  		return "", err
   610  	}
   611  	defer res.Body.Close()
   612  	if res.StatusCode != http.StatusOK {
   613  		return "", fmt.Errorf("unexpected HTTP status: %s", res.Status)
   614  	}
   615  	data, err := ioutil.ReadAll(res.Body)
   616  	if err != nil {
   617  		return "", err
   618  	}
   619  	return string(data), nil
   620  }
   621  
   622  // ErrNoFeedUpdatesFound is returned when Swarm cannot find updates of the given feed
   623  var ErrNoFeedUpdatesFound = errors.New("No updates found for this feed")
   624  
   625  // CreateFeedWithManifest creates a feed manifest, initializing it with the provided
   626  // data
   627  // Returns the resulting feed manifest address that you can use to include in an ENS Resolver (setContent)
   628  // or reference future updates (Client.UpdateFeed)
   629  func (c *Client) CreateFeedWithManifest(request *feed.Request) (string, error) {
   630  	responseStream, err := c.updateFeed(request, true)
   631  	if err != nil {
   632  		return "", err
   633  	}
   634  	defer responseStream.Close()
   635  
   636  	body, err := ioutil.ReadAll(responseStream)
   637  	if err != nil {
   638  		return "", err
   639  	}
   640  
   641  	var manifestAddress string
   642  	if err = json.Unmarshal(body, &manifestAddress); err != nil {
   643  		return "", err
   644  	}
   645  	return manifestAddress, nil
   646  }
   647  
   648  // UpdateFeed allows you to set a new version of your content
   649  func (c *Client) UpdateFeed(request *feed.Request) error {
   650  	_, err := c.updateFeed(request, false)
   651  	return err
   652  }
   653  
   654  func (c *Client) updateFeed(request *feed.Request, createManifest bool) (io.ReadCloser, error) {
   655  	URL, err := url.Parse(c.Gateway)
   656  	if err != nil {
   657  		return nil, err
   658  	}
   659  	URL.Path = "/bzz-feed:/"
   660  	values := URL.Query()
   661  	body := request.AppendValues(values)
   662  	if createManifest {
   663  		values.Set("manifest", "1")
   664  	}
   665  	URL.RawQuery = values.Encode()
   666  
   667  	req, err := http.NewRequest("POST", URL.String(), bytes.NewBuffer(body))
   668  	if err != nil {
   669  		return nil, err
   670  	}
   671  
   672  	res, err := http.DefaultClient.Do(req)
   673  	if err != nil {
   674  		return nil, err
   675  	}
   676  
   677  	return res.Body, nil
   678  }
   679  
   680  // QueryFeed returns a byte stream with the raw content of the feed update
   681  // manifestAddressOrDomain is the address you obtained in CreateFeedWithManifest or an ENS domain whose Resolver
   682  // points to that address
   683  func (c *Client) QueryFeed(query *feed.Query, manifestAddressOrDomain string) (io.ReadCloser, error) {
   684  	return c.queryFeed(query, manifestAddressOrDomain, false)
   685  }
   686  
   687  // queryFeed returns a byte stream with the raw content of the feed update
   688  // manifestAddressOrDomain is the address you obtained in CreateFeedWithManifest or an ENS domain whose Resolver
   689  // points to that address
   690  // meta set to true will instruct the node return feed metainformation instead
   691  func (c *Client) queryFeed(query *feed.Query, manifestAddressOrDomain string, meta bool) (io.ReadCloser, error) {
   692  	URL, err := url.Parse(c.Gateway)
   693  	if err != nil {
   694  		return nil, err
   695  	}
   696  	URL.Path = "/bzz-feed:/" + manifestAddressOrDomain
   697  	values := URL.Query()
   698  	if query != nil {
   699  		query.AppendValues(values) //adds query parameters
   700  	}
   701  	if meta {
   702  		values.Set("meta", "1")
   703  	}
   704  	URL.RawQuery = values.Encode()
   705  	res, err := http.Get(URL.String())
   706  	if err != nil {
   707  		return nil, err
   708  	}
   709  
   710  	if res.StatusCode != http.StatusOK {
   711  		if res.StatusCode == http.StatusNotFound {
   712  			return nil, ErrNoFeedUpdatesFound
   713  		}
   714  		errorMessageBytes, err := ioutil.ReadAll(res.Body)
   715  		var errorMessage string
   716  		if err != nil {
   717  			errorMessage = "cannot retrieve error message: " + err.Error()
   718  		} else {
   719  			errorMessage = string(errorMessageBytes)
   720  		}
   721  		return nil, fmt.Errorf("Error retrieving feed updates: %s", errorMessage)
   722  	}
   723  
   724  	return res.Body, nil
   725  }
   726  
   727  // GetFeedRequest returns a structure that describes the referenced feed status
   728  // manifestAddressOrDomain is the address you obtained in CreateFeedWithManifest or an ENS domain whose Resolver
   729  // points to that address
   730  func (c *Client) GetFeedRequest(query *feed.Query, manifestAddressOrDomain string) (*feed.Request, error) {
   731  
   732  	responseStream, err := c.queryFeed(query, manifestAddressOrDomain, true)
   733  	if err != nil {
   734  		return nil, err
   735  	}
   736  	defer responseStream.Close()
   737  
   738  	body, err := ioutil.ReadAll(responseStream)
   739  	if err != nil {
   740  		return nil, err
   741  	}
   742  
   743  	var metadata feed.Request
   744  	if err := metadata.UnmarshalJSON(body); err != nil {
   745  		return nil, err
   746  	}
   747  	return &metadata, nil
   748  }
   749  
   750  func GetClientTrace(traceMsg, metricPrefix, ruid string, tn *time.Time) *httptrace.ClientTrace {
   751  	trace := &httptrace.ClientTrace{
   752  		GetConn: func(_ string) {
   753  			log.Trace(traceMsg+" - http get", "event", "GetConn", "ruid", ruid)
   754  			metrics.GetOrRegisterResettingTimer(metricPrefix+".getconn", nil).Update(time.Since(*tn))
   755  		},
   756  		GotConn: func(_ httptrace.GotConnInfo) {
   757  			log.Trace(traceMsg+" - http get", "event", "GotConn", "ruid", ruid)
   758  			metrics.GetOrRegisterResettingTimer(metricPrefix+".gotconn", nil).Update(time.Since(*tn))
   759  		},
   760  		PutIdleConn: func(err error) {
   761  			log.Trace(traceMsg+" - http get", "event", "PutIdleConn", "ruid", ruid, "err", err)
   762  			metrics.GetOrRegisterResettingTimer(metricPrefix+".putidle", nil).Update(time.Since(*tn))
   763  		},
   764  		GotFirstResponseByte: func() {
   765  			log.Trace(traceMsg+" - http get", "event", "GotFirstResponseByte", "ruid", ruid)
   766  			metrics.GetOrRegisterResettingTimer(metricPrefix+".firstbyte", nil).Update(time.Since(*tn))
   767  		},
   768  		Got100Continue: func() {
   769  			log.Trace(traceMsg, "event", "Got100Continue", "ruid", ruid)
   770  			metrics.GetOrRegisterResettingTimer(metricPrefix+".got100continue", nil).Update(time.Since(*tn))
   771  		},
   772  		DNSStart: func(_ httptrace.DNSStartInfo) {
   773  			log.Trace(traceMsg, "event", "DNSStart", "ruid", ruid)
   774  			metrics.GetOrRegisterResettingTimer(metricPrefix+".dnsstart", nil).Update(time.Since(*tn))
   775  		},
   776  		DNSDone: func(_ httptrace.DNSDoneInfo) {
   777  			log.Trace(traceMsg, "event", "DNSDone", "ruid", ruid)
   778  			metrics.GetOrRegisterResettingTimer(metricPrefix+".dnsdone", nil).Update(time.Since(*tn))
   779  		},
   780  		ConnectStart: func(network, addr string) {
   781  			log.Trace(traceMsg, "event", "ConnectStart", "ruid", ruid, "network", network, "addr", addr)
   782  			metrics.GetOrRegisterResettingTimer(metricPrefix+".connectstart", nil).Update(time.Since(*tn))
   783  		},
   784  		ConnectDone: func(network, addr string, err error) {
   785  			log.Trace(traceMsg, "event", "ConnectDone", "ruid", ruid, "network", network, "addr", addr, "err", err)
   786  			metrics.GetOrRegisterResettingTimer(metricPrefix+".connectdone", nil).Update(time.Since(*tn))
   787  		},
   788  		WroteHeaders: func() {
   789  			log.Trace(traceMsg, "event", "WroteHeaders(request)", "ruid", ruid)
   790  			metrics.GetOrRegisterResettingTimer(metricPrefix+".wroteheaders", nil).Update(time.Since(*tn))
   791  		},
   792  		Wait100Continue: func() {
   793  			log.Trace(traceMsg, "event", "Wait100Continue", "ruid", ruid)
   794  			metrics.GetOrRegisterResettingTimer(metricPrefix+".wait100continue", nil).Update(time.Since(*tn))
   795  		},
   796  		WroteRequest: func(_ httptrace.WroteRequestInfo) {
   797  			log.Trace(traceMsg, "event", "WroteRequest", "ruid", ruid)
   798  			metrics.GetOrRegisterResettingTimer(metricPrefix+".wroterequest", nil).Update(time.Since(*tn))
   799  		},
   800  	}
   801  	return trace
   802  }