Skip to content

Commit

Permalink
Merge pull request ethereum#212 from ethersphere/swarm-mutableresourc…
Browse files Browse the repository at this point in the history
…es-errors

Add granular error reporting in mutable resources
  • Loading branch information
gbalint authored Feb 20, 2018
2 parents 0c46ec4 + 747f63f commit a1a1838
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 63 deletions.
30 changes: 25 additions & 5 deletions swarm/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ import (

var hashMatcher = regexp.MustCompile("^[0-9A-Fa-f]{64}")

type ErrResourceReturn struct {
key string
}

func (e *ErrResourceReturn) Error() string {
return "resourceupdate"
}

func (e *ErrResourceReturn) Key() string {
return e.key
}

type Resolver interface {
Resolve(string) (common.Hash, error)
}
Expand Down Expand Up @@ -145,6 +157,18 @@ func (self *Api) Get(key storage.Key, path string) (reader storage.LazySectionRe
entry, _ := trie.getEntry(path)

if entry != nil {
// we want to be able to serve Mutable Resource Updates transparently using the bzz:// scheme
//
// we use a special manifest hack for this purpose, which is pathless and where the resource root key
// is set as the hash of the manifest (see swarm/api/manifest.go:NewResourceManifest)
//
// to avoid taking a performance hit hacking a storage.LazySectionReader to wrap the resource key,
// we return a typed error instead. Since for all other purposes this is an invalid manifest,
// any normal interfacing code will just see an error fail accordingly.
if entry.ContentType == ResourceContentType {
log.Warn("resource type", "hash", entry.Hash)
return nil, entry.ContentType, http.StatusOK, &ErrResourceReturn{entry.Hash}
}
key = common.Hex2Bytes(entry.Hash)
status = entry.Status
if status == http.StatusMultipleChoices {
Expand Down Expand Up @@ -370,11 +394,7 @@ func (self *Api) ResourceLookup(ctx context.Context, name string, period uint32,
var err error
if version != 0 {
if period == 0 {
currentblocknumber, err := self.resource.GetBlock(ctx)
if err != nil {
return nil, nil, fmt.Errorf("Could not determine latest block: %v", err)
}
period = self.resource.BlockToPeriod(name, currentblocknumber)
return nil, nil, storage.NewResourceError(storage.ErrInvalidValue, "Period can't be 0")
}
_, err = self.resource.LookupVersionByName(ctx, name, period, version, true)
} else if period != 0 {
Expand Down
74 changes: 67 additions & 7 deletions swarm/api/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ import (
"github.com/rs/cors"
)

type resourceResponse struct {
Manifest storage.Key `json:"manifest"`
Resource string `json:"resource"`
Update storage.Key `json:"update"`
}

// ServerConfig is the basic configuration needed for the HTTP server and also
// includes CORS settings.
type ServerConfig struct {
Expand Down Expand Up @@ -292,7 +298,7 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *Request) {
}

func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
var outdata string
var outdata []byte
if r.uri.Path != "" {
frequency, err := strconv.ParseUint(r.uri.Path, 10, 64)
if err != nil {
Expand All @@ -301,10 +307,26 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
}
key, err := s.api.ResourceCreate(r.Context(), r.uri.Addr, frequency)
if err != nil {
ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, fmt.Errorf("Resource creation failed: %v", err)), http.StatusInternalServerError)
code, err2 := s.translateResourceError(w, r, "Resource creation fail", err)

ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, err2), code)
return
}
m, err := s.api.NewResourceManifest(r.uri.Addr)
if err != nil {
ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, fmt.Errorf("Failed to create resource manifest: %v", err)), http.StatusInternalServerError)
return
}
rsrcResponse := &resourceResponse{
Manifest: m,
Resource: r.uri.Addr,
Update: key,
}
outdata, err = json.Marshal(rsrcResponse)
if err != nil {
ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, fmt.Errorf("Failed to create json response for %v: error was: %v", r, err)), http.StatusInternalServerError)
return
}
outdata = key.Hex()
}

data, err := ioutil.ReadAll(r.Body)
Expand All @@ -314,14 +336,16 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
}
_, _, _, err = s.api.ResourceUpdate(r.Context(), r.uri.Addr, data)
if err != nil {
ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, fmt.Errorf("Update resource failed: %v", err)), http.StatusInternalServerError)
code, err2 := s.translateResourceError(w, r, "Mutable resource update fail", err)

ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, err2), code)
return
}

if outdata != "" {
if len(outdata) > 0 {
w.Header().Add("Content-type", "text/plain")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, outdata)
fmt.Fprint(w, string(outdata))
return
}
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -372,14 +396,43 @@ func (s *Server) handleGetResource(w http.ResponseWriter, r *Request, name strin
return
}
if err != nil {
ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, fmt.Errorf("Mutable resource lookup failed: %v", err)), http.StatusInternalServerError)
code, err2 := s.translateResourceError(w, r, "Mutable resource lookup fail", err)

ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, err2), code)
return
}
log.Debug("Found update", "key", updateKey)
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeContent(w, &r.Request, "", now, bytes.NewReader(data))
}

func (s *Server) translateResourceError(w http.ResponseWriter, r *Request, supErr string, err error) (int, error) {
code := 0
defaultErr := fmt.Errorf("%s: %v", supErr, err)
rsrcErr, ok := err.(*storage.ResourceError)
if !ok {
code = rsrcErr.Code()
}
switch code {
case storage.ErrInvalidValue:
//s.BadRequest(w, r, defaultErr.Error())
return http.StatusBadRequest, defaultErr
case storage.ErrNotFound, storage.ErrNotSynced, storage.ErrNothingToReturn:
//s.NotFound(w, r, defaultErr)
return http.StatusNotFound, defaultErr
case storage.ErrUnauthorized, storage.ErrInvalidSignature:
//ShowError(w, &r.Request, defaultErr.Error(), http.StatusUnauthorized)
return http.StatusUnauthorized, defaultErr
case storage.ErrDataOverflow:
//ShowError(w, &r.Request, defaultErr.Error(), http.StatusRequestEntityTooLarge)
return http.StatusRequestEntityTooLarge, defaultErr
}

return http.StatusInternalServerError, defaultErr

//s.Error(w, r, defaultErr)
}

// HandleGet handles a GET request to
// - bzz-raw://<key> and responds with the raw content stored at the
// given storage key
Expand Down Expand Up @@ -640,7 +693,14 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) {
}

reader, contentType, status, err := s.api.Get(key, r.uri.Path)

if err != nil {
// cheeky, cheeky hack. See swarm/api/api.go:Api.Get() for an explanation
if rsrcErr, ok := err.(*api.ErrResourceReturn); ok {
log.Trace("getting resource proxy", "err", rsrcErr.Key())
s.handleGetResource(w, r, rsrcErr.Key())
return
}
switch status {
case http.StatusNotFound:
ShowError(w, &r.Request, fmt.Sprintf("NOT FOUND error serving %s %s: %s", r.Method, r.uri, err), http.StatusNotFound)
Expand Down
72 changes: 67 additions & 5 deletions swarm/api/http/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,44 @@ package http_test
import (
"bytes"
"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/log"
"github.com/ethereum/go-ethereum/swarm/api"
swarm "github.com/ethereum/go-ethereum/swarm/api/client"
"github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/testutil"
)

func init() {
verbose := flag.Bool("v", false, "verbose")
flag.Parse()
if *verbose {
log.Root().SetHandler(log.CallerFileHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))))
}
}

type resourceResponse struct {
Manifest storage.Key `json:"manifest"`
Resource string `json:"resource"`
Update storage.Key `json:"update"`
}

func TestBzzResource(t *testing.T) {
srv := testutil.NewTestSwarmServer(t)
defer srv.Close()

// our mutable resource "name"
keybytes := make([]byte, common.HashLength)
copy(keybytes, []byte{42})
keybytes := []byte("foo")
srv.Hasher.Reset()
srv.Hasher.Write([]byte(fmt.Sprintf("%x", keybytes)))
keybyteshash := fmt.Sprintf("%x", srv.Hasher.Sum(nil))
Expand All @@ -65,10 +82,55 @@ func TestBzzResource(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, []byte(keybyteshash)) {
t.Fatalf("resource update hash mismatch, expected '%s' got '%s'", keybyteshash, b)
rsrcResp := &resourceResponse{}
err = json.Unmarshal(b, rsrcResp)
if err != nil {
t.Fatalf("data %s could not be unmarshaled: %v", b, err)
}
if rsrcResp.Update.Hex() != keybyteshash {
t.Fatalf("Response resource key mismatch, expected '%s', got '%s'", keybyteshash, rsrcResp.Resource)
}

// get manifest
url = fmt.Sprintf("%s/bzz-raw:/%s", srv.URL, rsrcResp.Manifest)
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))
}
if manifest.Entries[0].Hash != rsrcResp.Resource {
t.Fatalf("Expected manifest path '%s', got '%s'", keybyteshash, manifest.Entries[0].Hash)
}

// get bzz manifest transparent resource resolve
url = fmt.Sprintf("%s/bzz:/%s", srv.URL, rsrcResp.Manifest)
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)
}
t.Logf("creatreturn %v / %v", keybyteshash, b)

// get latest update (1.1) through resource directly
url = fmt.Sprintf("%s/bzz-resource:/%x", srv.URL, keybytes)
Expand Down
17 changes: 17 additions & 0 deletions swarm/api/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ func (a *Api) NewManifest() (storage.Key, error) {
return key, err
}

// Manifest hack for supporting Mutable Resource Updates from the bzz: scheme
// see swarm/api/api.go:Api.Get() for more information
func (a *Api) NewResourceManifest(resourceKey string) (storage.Key, error) {
var manifest Manifest
entry := ManifestEntry{
Hash: resourceKey,
ContentType: ResourceContentType,
}
manifest.Entries = append(manifest.Entries, entry)
data, err := json.Marshal(&manifest)
if err != nil {
return nil, err
}
key, _, err := a.Store(bytes.NewReader(data), int64(len(data)))
return key, err
}

// ManifestWriter is used to add and remove entries from an underlying manifest
type ManifestWriter struct {
api *Api
Expand Down
6 changes: 5 additions & 1 deletion swarm/network/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ func newBzzBaseTester(t *testing.T, n int, addr *BzzAddr, spec *protocols.Spec,
cs := make(map[string]chan bool)

srv := func(p *BzzPeer) error {
defer close(cs[p.ID().String()])
defer func() {
if cs[p.ID().String()] != nil {
close(cs[p.ID().String()])
}
}()
return run(p)
}

Expand Down
6 changes: 3 additions & 3 deletions swarm/storage/dbstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ func (s *DbStore) get(key Key) (chunk *Chunk, err error) {
chunk = NewChunk(key, nil)
decodeData(data, chunk)
} else {
err = ErrNotFound
err = ErrChunkNotFound
}

return
Expand All @@ -708,8 +708,8 @@ func newMockGetDataFunc(mockStore *mock.NodeStore) func(key Key) (data []byte, e
return func(key Key) (data []byte, err error) {
data, err = mockStore.Get(key)
if err == mock.ErrNotFound {
// preserve ErrNotFound error
err = ErrNotFound
// preserve ErrChunkNotFound error
err = ErrChunkNotFound
}
return data, err
}
Expand Down
4 changes: 2 additions & 2 deletions swarm/storage/dbstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ func testDbStoreNotFound(t *testing.T, mock bool) {
defer db.close()

_, err = db.Get(ZeroKey)
if err != ErrNotFound {
t.Errorf("Expected ErrNotFound, got %v", err)
if err != ErrChunkNotFound {
t.Errorf("Expected ErrChunkNotFound, got %v", err)
}
}

Expand Down
4 changes: 2 additions & 2 deletions swarm/storage/dpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ const (
)

var (
ErrNotFound = errors.New("not found")
ErrFetching = errors.New("chunk still fetching")
ErrChunkNotFound = errors.New("chunk not found")
ErrFetching = errors.New("chunk still fetching")
// timeout interval before retrieval is timed out
searchTimeout = 3 * time.Second
)
Expand Down
13 changes: 13 additions & 0 deletions swarm/storage/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package storage

const (
ErrNotFound = iota
ErrIO
ErrUnauthorized
ErrInvalidValue
ErrDataOverflow
ErrNothingToReturn
ErrInvalidSignature
ErrNotSynced
ErrCnt
)
4 changes: 2 additions & 2 deletions swarm/storage/memstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (s *MemStore) Get(hash Key) (chunk *Chunk, err error) {
l := hash.bits(bitpos, node.bits)
st := node.subtree[l]
if st == nil {
return nil, ErrNotFound
return nil, ErrChunkNotFound
}
bitpos += node.bits
node = st
Expand All @@ -232,7 +232,7 @@ func (s *MemStore) Get(hash Key) (chunk *Chunk, err error) {
}
}
} else {
err = ErrNotFound
err = ErrChunkNotFound
}

return
Expand Down
Loading

0 comments on commit a1a1838

Please sign in to comment.