ad6c39012f
swarm/api: integrate tags to count chunks being split and stored swarm/api/http: integrate tags in middleware for HTTP `POST` calls and assert chunks being calculated and counted correctly swarm: remove deprecated and unused code, add swarm hash to DoneSplit signature, remove calls to the api client from the http package
577 lines
16 KiB
Go
577 lines
16 KiB
Go
// Copyright 2016 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
crand "crypto/rand"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/swarm/chunk"
|
|
"github.com/ethereum/go-ethereum/swarm/sctx"
|
|
"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 testAPI(t *testing.T, f func(*API, *chunk.Tags, bool)) {
|
|
for _, v := range []bool{true, false} {
|
|
datadir, err := ioutil.TempDir("", "bzz-test")
|
|
if err != nil {
|
|
t.Fatalf("unable to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(datadir)
|
|
tags := chunk.NewTags()
|
|
fileStore, err := storage.NewLocalFileStore(datadir, make([]byte, 32), tags)
|
|
if err != nil {
|
|
return
|
|
}
|
|
api := NewAPI(fileStore, nil, nil, nil, tags)
|
|
f(api, tags, v)
|
|
}
|
|
}
|
|
|
|
type testResponse struct {
|
|
reader storage.LazySectionReader
|
|
*Response
|
|
}
|
|
|
|
type Response struct {
|
|
MimeType string
|
|
Status int
|
|
Size int64
|
|
Content string
|
|
}
|
|
|
|
func checkResponse(t *testing.T, resp *testResponse, exp *Response) {
|
|
|
|
if resp.MimeType != exp.MimeType {
|
|
t.Errorf("incorrect mimeType. expected '%s', got '%s'", exp.MimeType, resp.MimeType)
|
|
}
|
|
if resp.Status != exp.Status {
|
|
t.Errorf("incorrect status. expected '%d', got '%d'", exp.Status, resp.Status)
|
|
}
|
|
if resp.Size != exp.Size {
|
|
t.Errorf("incorrect size. expected '%d', got '%d'", exp.Size, resp.Size)
|
|
}
|
|
if resp.reader != nil {
|
|
content := make([]byte, resp.Size)
|
|
read, _ := resp.reader.Read(content)
|
|
if int64(read) != exp.Size {
|
|
t.Errorf("incorrect content length. expected '%d...', got '%d...'", read, exp.Size)
|
|
}
|
|
resp.Content = string(content)
|
|
}
|
|
if resp.Content != exp.Content {
|
|
// if !bytes.Equal(resp.Content, exp.Content)
|
|
t.Errorf("incorrect content. expected '%s...', got '%s...'", string(exp.Content), string(resp.Content))
|
|
}
|
|
}
|
|
|
|
// func expResponse(content []byte, mimeType string, status int) *Response {
|
|
func expResponse(content string, mimeType string, status int) *Response {
|
|
log.Trace(fmt.Sprintf("expected content (%v): %v ", len(content), content))
|
|
return &Response{mimeType, status, int64(len(content)), content}
|
|
}
|
|
|
|
func testGet(t *testing.T, api *API, bzzhash, path string) *testResponse {
|
|
addr := storage.Address(common.Hex2Bytes(bzzhash))
|
|
reader, mimeType, status, _, err := api.Get(context.TODO(), NOOPDecrypt, addr, path)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
quitC := make(chan bool)
|
|
size, err := reader.Size(context.TODO(), quitC)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
log.Trace(fmt.Sprintf("reader size: %v ", size))
|
|
s := make([]byte, size)
|
|
_, err = reader.Read(s)
|
|
if err != io.EOF {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
reader.Seek(0, 0)
|
|
return &testResponse{reader, &Response{mimeType, status, size, string(s)}}
|
|
}
|
|
|
|
func TestApiPut(t *testing.T) {
|
|
testAPI(t, func(api *API, tags *chunk.Tags, toEncrypt bool) {
|
|
content := "hello"
|
|
exp := expResponse(content, "text/plain", 0)
|
|
ctx := context.TODO()
|
|
addr, wait, err := putString(ctx, api, content, exp.MimeType, toEncrypt)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
err = wait(ctx)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
resp := testGet(t, api, addr.Hex(), "")
|
|
checkResponse(t, resp, exp)
|
|
tag := tags.All()[0]
|
|
testutil.CheckTag(t, tag, 2, 2, 0, 2) //1 chunk data, 1 chunk manifest
|
|
})
|
|
}
|
|
|
|
// TestApiTagLarge tests that the the number of chunks counted is larger for a larger input
|
|
func TestApiTagLarge(t *testing.T) {
|
|
const contentLength = 4096 * 4095
|
|
testAPI(t, func(api *API, tags *chunk.Tags, toEncrypt bool) {
|
|
randomContentReader := io.LimitReader(crand.Reader, int64(contentLength))
|
|
tag, err := api.Tags.New("unnamed-tag", 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := sctx.SetTag(context.Background(), tag.Uid)
|
|
key, waitContent, err := api.Store(ctx, randomContentReader, int64(contentLength), toEncrypt)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = waitContent(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tag.DoneSplit(key)
|
|
|
|
if toEncrypt {
|
|
tag := tags.All()[0]
|
|
expect := int64(4095 + 64 + 1)
|
|
testutil.CheckTag(t, tag, expect, expect, 0, expect)
|
|
} else {
|
|
tag := tags.All()[0]
|
|
expect := int64(4095 + 32 + 1)
|
|
testutil.CheckTag(t, tag, expect, expect, 0, expect)
|
|
}
|
|
})
|
|
}
|
|
|
|
// testResolver implements the Resolver interface and either returns the given
|
|
// hash if it is set, or returns a "name not found" error
|
|
type testResolveValidator struct {
|
|
hash *common.Hash
|
|
}
|
|
|
|
func newTestResolveValidator(addr string) *testResolveValidator {
|
|
r := &testResolveValidator{}
|
|
if addr != "" {
|
|
hash := common.HexToHash(addr)
|
|
r.hash = &hash
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (t *testResolveValidator) Resolve(addr string) (common.Hash, error) {
|
|
if t.hash == nil {
|
|
return common.Hash{}, fmt.Errorf("DNS name not found: %q", addr)
|
|
}
|
|
return *t.hash, nil
|
|
}
|
|
|
|
func (t *testResolveValidator) Owner(node [32]byte) (addr common.Address, err error) {
|
|
return
|
|
}
|
|
func (t *testResolveValidator) HeaderByNumber(context.Context, *big.Int) (header *types.Header, err error) {
|
|
return
|
|
}
|
|
|
|
// TestAPIResolve tests resolving URIs which can either contain content hashes
|
|
// or ENS names
|
|
func TestAPIResolve(t *testing.T) {
|
|
ensAddr := "swarm.eth"
|
|
hashAddr := "1111111111111111111111111111111111111111111111111111111111111111"
|
|
resolvedAddr := "2222222222222222222222222222222222222222222222222222222222222222"
|
|
doesResolve := newTestResolveValidator(resolvedAddr)
|
|
doesntResolve := newTestResolveValidator("")
|
|
|
|
type test struct {
|
|
desc string
|
|
dns Resolver
|
|
addr string
|
|
immutable bool
|
|
result string
|
|
expectErr error
|
|
}
|
|
|
|
tests := []*test{
|
|
{
|
|
desc: "DNS not configured, hash address, returns hash address",
|
|
dns: nil,
|
|
addr: hashAddr,
|
|
result: hashAddr,
|
|
},
|
|
{
|
|
desc: "DNS not configured, ENS address, returns error",
|
|
dns: nil,
|
|
addr: ensAddr,
|
|
expectErr: errors.New(`no DNS to resolve name: "swarm.eth"`),
|
|
},
|
|
{
|
|
desc: "DNS configured, hash address, hash resolves, returns resolved address",
|
|
dns: doesResolve,
|
|
addr: hashAddr,
|
|
result: resolvedAddr,
|
|
},
|
|
{
|
|
desc: "DNS configured, immutable hash address, hash resolves, returns hash address",
|
|
dns: doesResolve,
|
|
addr: hashAddr,
|
|
immutable: true,
|
|
result: hashAddr,
|
|
},
|
|
{
|
|
desc: "DNS configured, hash address, hash doesn't resolve, returns hash address",
|
|
dns: doesntResolve,
|
|
addr: hashAddr,
|
|
result: hashAddr,
|
|
},
|
|
{
|
|
desc: "DNS configured, ENS address, name resolves, returns resolved address",
|
|
dns: doesResolve,
|
|
addr: ensAddr,
|
|
result: resolvedAddr,
|
|
},
|
|
{
|
|
desc: "DNS configured, immutable ENS address, name resolves, returns error",
|
|
dns: doesResolve,
|
|
addr: ensAddr,
|
|
immutable: true,
|
|
expectErr: errors.New(`immutable address not a content hash: "swarm.eth"`),
|
|
},
|
|
{
|
|
desc: "DNS configured, ENS address, name doesn't resolve, returns error",
|
|
dns: doesntResolve,
|
|
addr: ensAddr,
|
|
expectErr: errors.New(`DNS name not found: "swarm.eth"`),
|
|
},
|
|
}
|
|
for _, x := range tests {
|
|
t.Run(x.desc, func(t *testing.T) {
|
|
api := &API{dns: x.dns}
|
|
uri := &URI{Addr: x.addr, Scheme: "bzz"}
|
|
if x.immutable {
|
|
uri.Scheme = "bzz-immutable"
|
|
}
|
|
res, err := api.ResolveURI(context.TODO(), uri, "")
|
|
if err == nil {
|
|
if x.expectErr != nil {
|
|
t.Fatalf("expected error %q, got result %q", x.expectErr, res)
|
|
}
|
|
if res.String() != x.result {
|
|
t.Fatalf("expected result %q, got %q", x.result, res)
|
|
}
|
|
} else {
|
|
if x.expectErr == nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if err.Error() != x.expectErr.Error() {
|
|
t.Fatalf("expected error %q, got %q", x.expectErr, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiResolver(t *testing.T) {
|
|
doesntResolve := newTestResolveValidator("")
|
|
|
|
ethAddr := "swarm.eth"
|
|
ethHash := "0x2222222222222222222222222222222222222222222222222222222222222222"
|
|
ethResolve := newTestResolveValidator(ethHash)
|
|
|
|
testAddr := "swarm.test"
|
|
testHash := "0x1111111111111111111111111111111111111111111111111111111111111111"
|
|
testResolve := newTestResolveValidator(testHash)
|
|
|
|
tests := []struct {
|
|
desc string
|
|
r Resolver
|
|
addr string
|
|
result string
|
|
err error
|
|
}{
|
|
{
|
|
desc: "No resolvers, returns error",
|
|
r: NewMultiResolver(),
|
|
err: NewNoResolverError(""),
|
|
},
|
|
{
|
|
desc: "One default resolver, returns resolved address",
|
|
r: NewMultiResolver(MultiResolverOptionWithResolver(ethResolve, "")),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "Two default resolvers, returns resolved address",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(ethResolve, ""),
|
|
MultiResolverOptionWithResolver(ethResolve, ""),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "Two default resolvers, first doesn't resolve, returns resolved address",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(doesntResolve, ""),
|
|
MultiResolverOptionWithResolver(ethResolve, ""),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "Default resolver doesn't resolve, tld resolver resolve, returns resolved address",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(doesntResolve, ""),
|
|
MultiResolverOptionWithResolver(ethResolve, "eth"),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "Three TLD resolvers, third resolves, returns resolved address",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(doesntResolve, "eth"),
|
|
MultiResolverOptionWithResolver(doesntResolve, "eth"),
|
|
MultiResolverOptionWithResolver(ethResolve, "eth"),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "One TLD resolver doesn't resolve, returns error",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(doesntResolve, ""),
|
|
MultiResolverOptionWithResolver(ethResolve, "eth"),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
},
|
|
{
|
|
desc: "One defautl and one TLD resolver, all doesn't resolve, returns error",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(doesntResolve, ""),
|
|
MultiResolverOptionWithResolver(doesntResolve, "eth"),
|
|
),
|
|
addr: ethAddr,
|
|
result: ethHash,
|
|
err: errors.New(`DNS name not found: "swarm.eth"`),
|
|
},
|
|
{
|
|
desc: "Two TLD resolvers, both resolve, returns resolved address",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(ethResolve, "eth"),
|
|
MultiResolverOptionWithResolver(testResolve, "test"),
|
|
),
|
|
addr: testAddr,
|
|
result: testHash,
|
|
},
|
|
{
|
|
desc: "One TLD resolver, no default resolver, returns error for different TLD",
|
|
r: NewMultiResolver(
|
|
MultiResolverOptionWithResolver(ethResolve, "eth"),
|
|
),
|
|
addr: testAddr,
|
|
err: NewNoResolverError("test"),
|
|
},
|
|
}
|
|
for _, x := range tests {
|
|
t.Run(x.desc, func(t *testing.T) {
|
|
res, err := x.r.Resolve(x.addr)
|
|
if err == nil {
|
|
if x.err != nil {
|
|
t.Fatalf("expected error %q, got result %q", x.err, res.Hex())
|
|
}
|
|
if res.Hex() != x.result {
|
|
t.Fatalf("expected result %q, got %q", x.result, res.Hex())
|
|
}
|
|
} else {
|
|
if x.err == nil {
|
|
t.Fatalf("expected no error, got %q", err)
|
|
}
|
|
if err.Error() != x.err.Error() {
|
|
t.Fatalf("expected error %q, got %q", x.err, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecryptOriginForbidden(t *testing.T) {
|
|
ctx := context.TODO()
|
|
ctx = sctx.SetHost(ctx, "swarm-gateways.net")
|
|
|
|
me := &ManifestEntry{
|
|
Access: &AccessEntry{Type: AccessTypePass},
|
|
}
|
|
|
|
api := NewAPI(nil, nil, nil, nil, chunk.NewTags())
|
|
|
|
f := api.Decryptor(ctx, "")
|
|
err := f(me)
|
|
if err != ErrDecryptDomainForbidden {
|
|
t.Fatalf("should fail with ErrDecryptDomainForbidden, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecryptOrigin(t *testing.T) {
|
|
for _, v := range []struct {
|
|
host string
|
|
expectError error
|
|
}{
|
|
{
|
|
host: "localhost",
|
|
expectError: ErrDecrypt,
|
|
},
|
|
{
|
|
host: "127.0.0.1",
|
|
expectError: ErrDecrypt,
|
|
},
|
|
{
|
|
host: "swarm-gateways.net",
|
|
expectError: ErrDecryptDomainForbidden,
|
|
},
|
|
} {
|
|
ctx := context.TODO()
|
|
ctx = sctx.SetHost(ctx, v.host)
|
|
|
|
me := &ManifestEntry{
|
|
Access: &AccessEntry{Type: AccessTypePass},
|
|
}
|
|
|
|
api := NewAPI(nil, nil, nil, nil, chunk.NewTags())
|
|
|
|
f := api.Decryptor(ctx, "")
|
|
err := f(me)
|
|
if err != v.expectError {
|
|
t.Fatalf("should fail with %v, got %v", v.expectError, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectContentType(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
file string
|
|
content string
|
|
expectedContentType string
|
|
}{
|
|
{
|
|
file: "file-with-correct-css.css",
|
|
content: "body {background-color: orange}",
|
|
expectedContentType: "text/css; charset=utf-8",
|
|
},
|
|
{
|
|
file: "empty-file.css",
|
|
content: "",
|
|
expectedContentType: "text/css; charset=utf-8",
|
|
},
|
|
{
|
|
file: "empty-file.pdf",
|
|
content: "",
|
|
expectedContentType: "application/pdf",
|
|
},
|
|
{
|
|
file: "empty-file.md",
|
|
content: "",
|
|
expectedContentType: "text/markdown; charset=utf-8",
|
|
},
|
|
{
|
|
file: "empty-file-with-unknown-content.strangeext",
|
|
content: "",
|
|
expectedContentType: "text/plain; charset=utf-8",
|
|
},
|
|
{
|
|
file: "file-with-unknown-extension-and-content.strangeext",
|
|
content: "Lorem Ipsum",
|
|
expectedContentType: "text/plain; charset=utf-8",
|
|
},
|
|
{
|
|
file: "file-no-extension",
|
|
content: "Lorem Ipsum",
|
|
expectedContentType: "text/plain; charset=utf-8",
|
|
},
|
|
{
|
|
file: "file-no-extension-no-content",
|
|
content: "",
|
|
expectedContentType: "text/plain; charset=utf-8",
|
|
},
|
|
{
|
|
file: "css-file-with-html-inside.css",
|
|
content: "<!doctype html><html><head></head><body></body></html>",
|
|
expectedContentType: "text/css; charset=utf-8",
|
|
},
|
|
} {
|
|
t.Run(tc.file, func(t *testing.T) {
|
|
detected, err := DetectContentType(tc.file, bytes.NewReader([]byte(tc.content)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if detected != tc.expectedContentType {
|
|
t.Fatalf("File: %s, Expected mime type %s, got %s", tc.file, tc.expectedContentType, detected)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
// putString provides singleton manifest creation on top of api.API
|
|
func putString(ctx context.Context, a *API, content string, contentType string, toEncrypt bool) (k storage.Address, wait func(context.Context) error, err error) {
|
|
r := strings.NewReader(content)
|
|
tag, err := a.Tags.New("unnamed-tag", 0)
|
|
|
|
log.Trace("created new tag", "uid", tag.Uid)
|
|
|
|
cCtx := sctx.SetTag(ctx, tag.Uid)
|
|
key, waitContent, err := a.Store(cCtx, r, int64(len(content)), toEncrypt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
manifest := fmt.Sprintf(`{"entries":[{"hash":"%v","contentType":"%s"}]}`, key, contentType)
|
|
r = strings.NewReader(manifest)
|
|
key, waitManifest, err := a.Store(cCtx, r, int64(len(manifest)), toEncrypt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
tag.DoneSplit(key)
|
|
return key, func(ctx context.Context) error {
|
|
err := waitContent(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return waitManifest(ctx)
|
|
}, nil
|
|
}
|