// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
package http
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/swarm/api"
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
"github.com/ethereum/go-ethereum/swarm/multihash"
"github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/testutil"
)
func init() {
loglevel := flag.Int("loglevel", 2, "loglevel")
flag.Parse()
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
}
func TestResourcePostMode(t *testing.T) {
path := ""
errstr := "resourcePostMode for '%s' should be raw %v frequency %d, was raw %v, frequency %d"
r, f, err := resourcePostMode(path)
if err != nil {
t.Fatal(err)
} else if r || f != 0 {
t.Fatalf(errstr, path, false, 0, r, f)
}
path = "raw"
r, f, err = resourcePostMode(path)
if err != nil {
t.Fatal(err)
} else if !r || f != 0 {
t.Fatalf(errstr, path, true, 0, r, f)
}
path = "13"
r, f, err = resourcePostMode(path)
if err != nil {
t.Fatal(err)
} else if r || f == 0 {
t.Fatalf(errstr, path, false, 13, r, f)
}
path = "raw/13"
r, f, err = resourcePostMode(path)
if err != nil {
t.Fatal(err)
} else if !r || f == 0 {
t.Fatalf(errstr, path, true, 13, r, f)
}
path = "foo/13"
r, f, err = resourcePostMode(path)
if err == nil {
t.Fatal("resourcePostMode for 'foo/13' should fail, returned error nil")
}
}
func serverFunc(api *api.API) testutil.TestServer {
return NewServer(api)
}
// test the transparent resolving of multihash resource types with bzz:// scheme
//
// first upload data, and store the multihash to the resulting manifest in a resource update
// retrieving the update with the multihash should return the manifest pointing directly to the data
// and raw retrieve of that hash should return the data
func TestBzzResourceMultihash(t *testing.T) {
srv := testutil.NewTestSwarmServer(t, serverFunc)
defer srv.Close()
// add the data our multihash aliased manifest will point to
databytes := "bar"
url := fmt.Sprintf("%s/bzz:/", srv.URL)
resp, err := http.Post(url, "text/plain", bytes.NewReader([]byte(databytes)))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
s := common.FromHex(string(b))
mh := multihash.ToMultihash(s)
mhHex := hexutil.Encode(mh)
log.Info("added data", "manifest", string(b), "data", common.ToHex(mh))
// our mutable resource "name"
keybytes := "foo.eth"
// create the multihash update
url = fmt.Sprintf("%s/bzz-resource:/%s/13", srv.URL, keybytes)
resp, err = http.Post(url, "application/octet-stream", bytes.NewReader([]byte(mhHex)))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
rsrcResp := &storage.Address{}
err = json.Unmarshal(b, rsrcResp)
if err != nil {
t.Fatalf("data %s could not be unmarshaled: %v", b, err)
}
correctManifestAddrHex := "d689648fb9e00ddc7ebcf474112d5881c5bf7dbc6e394681b1d224b11b59b5e0"
if rsrcResp.Hex() != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp)
}
// get bzz manifest transparent resource resolve
url = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, []byte(databytes)) {
t.Fatalf("retrieved data mismatch, expected %x, got %x", databytes, b)
}
}
// Test resource updates using the raw update methods
func TestBzzResource(t *testing.T) {
srv := testutil.NewTestSwarmServer(t, serverFunc)
defer srv.Close()
// our mutable resource "name"
keybytes := "foo.eth"
// data of update 1
databytes := make([]byte, 666)
_, err := rand.Read(databytes)
if err != nil {
t.Fatal(err)
}
// creates resource and sets update 1
url := fmt.Sprintf("%s/bzz-resource:/%s/raw/13", srv.URL, []byte(keybytes))
resp, err := http.Post(url, "application/octet-stream", bytes.NewReader(databytes))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
rsrcResp := &storage.Address{}
err = json.Unmarshal(b, rsrcResp)
if err != nil {
t.Fatalf("data %s could not be unmarshaled: %v", b, err)
}
correctManifestAddrHex := "d689648fb9e00ddc7ebcf474112d5881c5bf7dbc6e394681b1d224b11b59b5e0"
if rsrcResp.Hex() != correctManifestAddrHex {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", correctManifestAddrHex, rsrcResp.Hex())
}
// get the manifest
url = fmt.Sprintf("%s/bzz-raw:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
manifest := &api.Manifest{}
err = json.Unmarshal(b, manifest)
if err != nil {
t.Fatal(err)
}
if len(manifest.Entries) != 1 {
t.Fatalf("Manifest has %d entries", len(manifest.Entries))
}
correctRootKeyHex := "f667277e004e8486c7a3631fd226802430e84e9a81b6085d31f512a591ae0065"
if manifest.Entries[0].Hash != correctRootKeyHex {
t.Fatalf("Expected manifest path '%s', got '%s'", correctRootKeyHex, manifest.Entries[0].Hash)
}
// get bzz manifest transparent resource resolve
url = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
// get non-existent name, should fail
url = fmt.Sprintf("%s/bzz-resource:/bar", srv.URL)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
// get latest update (1.1) through resource directly
log.Info("get update latest = 1.1", "addr", correctManifestAddrHex)
url = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(databytes, b) {
t.Fatalf("Expected body '%x', got '%x'", databytes, b)
}
// update 2
log.Info("update 2")
url = fmt.Sprintf("%s/bzz-resource:/%s/raw", srv.URL, correctManifestAddrHex)
data := []byte("foo")
resp, err = http.Post(url, "application/octet-stream", bytes.NewReader(data))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Update returned %s", resp.Status)
}
// get latest update (1.2) through resource directly
log.Info("get update 1.2")
url = fmt.Sprintf("%s/bzz-resource:/%s", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, b) {
t.Fatalf("Expected body '%x', got '%x'", data, b)
}
// get latest update (1.2) with specified period
log.Info("get update latest = 1.2")
url = fmt.Sprintf("%s/bzz-resource:/%s/1", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(data, b) {
t.Fatalf("Expected body '%x', got '%x'", data, b)
}
// get first update (1.1) with specified period and version
log.Info("get first update 1.1")
url = fmt.Sprintf("%s/bzz-resource:/%s/1/1", srv.URL, correctManifestAddrHex)
resp, err = http.Get(url)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("err %s", resp.Status)
}
b, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(databytes, b) {
t.Fatalf("Expected body '%x', got '%x'", databytes, b)
}
}
func TestBzzGetPath(t *testing.T) {
testBzzGetPath(false, t)
testBzzGetPath(true, t)
}
func testBzzGetPath(encrypted bool, t *testing.T) {
var err error
testmanifest := []string{
`{"entries":[{"path":"b","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0},{"path":"c","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0}]}`,
`{"entries":[{"path":"a","hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","contentType":"","status":0},{"path":"b/","hash":"","contentType":"application/bzz-manifest+json","status":0}]}`,
`{"entries":[{"path":"a/","hash":"","contentType":"application/bzz-manifest+json","status":0}]}`,
}
testrequests := make(map[string]int)
testrequests["/"] = 2
testrequests["/a/"] = 1
testrequests["/a/b/"] = 0
testrequests["/x"] = 0
testrequests[""] = 0
expectedfailrequests := []string{"", "/x"}
reader := [3]*bytes.Reader{}
addr := [3]storage.Address{}
srv := testutil.NewTestSwarmServer(t, serverFunc)
defer srv.Close()
for i, mf := range testmanifest {
reader[i] = bytes.NewReader([]byte(mf))
var wait func(context.Context) error
ctx := context.TODO()
addr[i], wait, err = srv.FileStore.Store(ctx, reader[i], int64(len(mf)), encrypted)
for j := i + 1; j < len(testmanifest); j++ {
testmanifest[j] = strings.Replace(testmanifest[j], fmt.Sprintf("", i), addr[i].Hex(), -1)
}
if err != nil {
t.Fatal(err)
}
err = wait(ctx)
if err != nil {
t.Fatal(err)
}
}
rootRef := addr[2].Hex()
_, err = http.Get(srv.URL + "/bzz-raw:/" + rootRef + "/a")
if err != nil {
t.Fatalf("Failed to connect to proxy: %v", err)
}
for k, v := range testrequests {
var resp *http.Response
var respbody []byte
url := srv.URL + "/bzz-raw:/"
if k[:] != "" {
url += rootRef + "/" + k[1:] + "?content_type=text/plain"
}
resp, err = http.Get(url)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
respbody, err = ioutil.ReadAll(resp.Body)
if string(respbody) != testmanifest[v] {
isexpectedfailrequest := false
for _, r := range expectedfailrequests {
if k[:] == r {
isexpectedfailrequest = true
}
}
if !isexpectedfailrequest {
t.Fatalf("Response body does not match, expected: %v, got %v", testmanifest[v], string(respbody))
}
}
}
for k, v := range testrequests {
var resp *http.Response
var respbody []byte
url := srv.URL + "/bzz-hash:/"
if k[:] != "" {
url += rootRef + "/" + k[1:]
}
resp, err = http.Get(url)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
respbody, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Read request body: %v", err)
}
if string(respbody) != addr[v].Hex() {
isexpectedfailrequest := false
for _, r := range expectedfailrequests {
if k[:] == r {
isexpectedfailrequest = true
}
}
if !isexpectedfailrequest {
t.Fatalf("Response body does not match, expected: %v, got %v", addr[v], string(respbody))
}
}
}
ref := addr[2].Hex()
for _, c := range []struct {
path string
json string
html string
}{
{
path: "/",
json: `{"common_prefixes":["a/"]}`,
html: fmt.Sprintf("\n\n\n \n \n\t\t\n\tSwarm index of bzz:/%s/\n\n\n\n Swarm index of bzz:/%s/
\n
\n \n \n \n\tPath | \n\tType | \n\tSize | \n
\n \n\n \n \n\t\n\t a/ | \n\t DIR | \n\t - | \n\t
\n \n\n \n
\n
\n\n", ref, ref),
},
{
path: "/a/",
json: `{"common_prefixes":["a/b/"],"entries":[{"hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","path":"a/a","mod_time":"0001-01-01T00:00:00Z"}]}`,
html: fmt.Sprintf("\n\n\n \n \n\t\t\n\tSwarm index of bzz:/%s/a/\n\n\n\n Swarm index of bzz:/%s/a/
\n
\n \n \n \n\tPath | \n\tType | \n\tSize | \n
\n \n\n \n \n\t\n\t b/ | \n\t DIR | \n\t - | \n\t
\n \n\n \n\t\n\t a | \n\t | \n\t 0 | \n\t
\n \n
\n
\n\n", ref, ref),
},
{
path: "/a/b/",
json: `{"entries":[{"hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","path":"a/b/b","mod_time":"0001-01-01T00:00:00Z"},{"hash":"011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce","path":"a/b/c","mod_time":"0001-01-01T00:00:00Z"}]}`,
html: fmt.Sprintf("\n\n\n \n \n\t\t\n\tSwarm index of bzz:/%s/a/b/\n\n\n\n Swarm index of bzz:/%s/a/b/
\n
\n \n \n \n\tPath | \n\tType | \n\tSize | \n
\n \n\n \n \n\n \n\t\n\t b | \n\t | \n\t 0 | \n\t
\n \n\t\n\t c | \n\t | \n\t 0 | \n\t
\n \n
\n
\n\n", ref, ref),
},
{
path: "/x",
},
{
path: "",
},
} {
k := c.path
url := srv.URL + "/bzz-list:/"
if k[:] != "" {
url += rootRef + "/" + k[1:]
}
t.Run("json list "+c.path, func(t *testing.T) {
resp, err := http.Get(url)
if err != nil {
t.Fatalf("HTTP request: %v", err)
}
defer resp.Body.Close()
respbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Read response body: %v", err)
}
body := strings.TrimSpace(string(respbody))
if body != c.json {
isexpectedfailrequest := false
for _, r := range expectedfailrequests {
if k[:] == r {
isexpectedfailrequest = true
}
}
if !isexpectedfailrequest {
t.Errorf("Response list body %q does not match, expected: %v, got %v", k, c.json, body)
}
}
})
t.Run("html list "+c.path, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
t.Fatalf("New request: %v", err)
}
req.Header.Set("Accept", "text/html")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("HTTP request: %v", err)
}
defer resp.Body.Close()
respbody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Read response body: %v", err)
}
if string(respbody) != c.html {
isexpectedfailrequest := false
for _, r := range expectedfailrequests {
if k[:] == r {
isexpectedfailrequest = true
}
}
if !isexpectedfailrequest {
t.Errorf("Response list body %q does not match, expected: %q, got %q", k, c.html, string(respbody))
}
}
})
}
nonhashtests := []string{
srv.URL + "/bzz:/name",
srv.URL + "/bzz-immutable:/nonhash",
srv.URL + "/bzz-raw:/nonhash",
srv.URL + "/bzz-list:/nonhash",
srv.URL + "/bzz-hash:/nonhash",
}
nonhashresponses := []string{
"cannot resolve name: no DNS to resolve name: "name"",
"cannot resolve nonhash: immutable address not a content hash: "nonhash"",
"cannot resolve nonhash: no DNS to resolve name: "nonhash"",
"cannot resolve nonhash: no DNS to resolve name: "nonhash"",
"cannot resolve nonhash: no DNS to resolve name: "nonhash"",
}
for i, url := range nonhashtests {
var resp *http.Response
var respbody []byte
resp, err = http.Get(url)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
defer resp.Body.Close()
respbody, err = ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("ReadAll failed: %v", err)
}
if !strings.Contains(string(respbody), nonhashresponses[i]) {
t.Fatalf("Non-Hash response body does not match, expected: %v, got: %v", nonhashresponses[i], string(respbody))
}
}
}
// TestBzzRootRedirect tests that getting the root path of a manifest without
// a trailing slash gets redirected to include the trailing slash so that
// relative URLs work as expected.
func TestBzzRootRedirect(t *testing.T) {
testBzzRootRedirect(false, t)
}
func TestBzzRootRedirectEncrypted(t *testing.T) {
testBzzRootRedirect(true, t)
}
func testBzzRootRedirect(toEncrypt bool, t *testing.T) {
srv := testutil.NewTestSwarmServer(t, serverFunc)
defer srv.Close()
// create a manifest with some data at the root path
client := swarm.NewClient(srv.URL)
data := []byte("data")
file := &swarm.File{
ReadCloser: ioutil.NopCloser(bytes.NewReader(data)),
ManifestEntry: api.ManifestEntry{
Path: "",
ContentType: "text/plain",
Size: int64(len(data)),
},
}
hash, err := client.Upload(file, "", toEncrypt)
if err != nil {
t.Fatal(err)
}
// define a CheckRedirect hook which ensures there is only a single
// redirect to the correct URL
redirected := false
httpClient := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if redirected {
return errors.New("too many redirects")
}
redirected = true
expectedPath := "/bzz:/" + hash + "/"
if req.URL.Path != expectedPath {
return fmt.Errorf("expected redirect to %q, got %q", expectedPath, req.URL.Path)
}
return nil
},
}
// perform the GET request and assert the response
res, err := httpClient.Get(srv.URL + "/bzz:/" + hash)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if !redirected {
t.Fatal("expected GET /bzz:/ to redirect to /bzz:// but it didn't")
}
gotData, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(gotData, data) {
t.Fatalf("expected response to equal %q, got %q", data, gotData)
}
}
func TestMethodsNotAllowed(t *testing.T) {
srv := testutil.NewTestSwarmServer(t, serverFunc)
defer srv.Close()
databytes := "bar"
for _, c := range []struct {
url string
code int
}{
{
url: fmt.Sprintf("%s/bzz-list:/", srv.URL),
code: 405,
}, {
url: fmt.Sprintf("%s/bzz-hash:/", srv.URL),
code: 405,
},
{
url: fmt.Sprintf("%s/bzz-immutable:/", srv.URL),
code: 405,
},
} {
res, _ := http.Post(c.url, "text/plain", bytes.NewReader([]byte(databytes)))
if res.StatusCode != c.code {
t.Fatal("should have failed")
}
}
}