diff --git a/swarm/api/api.go b/swarm/api/api.go index 3a3e4a81d6..8f73cfbdee 100644 --- a/swarm/api/api.go +++ b/swarm/api/api.go @@ -37,6 +37,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) } @@ -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 { diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go index 3ad8503314..335d6e15e8 100644 --- a/swarm/api/http/server.go +++ b/swarm/api/http/server.go @@ -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 { @@ -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 { @@ -304,7 +310,21 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { s.translateResourceError(w, r, "Resource creation fail", err) return } - outdata = key.Hex() + m, err := s.api.NewResourceManifest(r.uri.Addr) + if err != nil { + s.Error(w, r, fmt.Errorf("Failed to create resource manifest: %v", err)) + return + } + rsrcResponse := &resourceResponse{ + Manifest: m, + Resource: r.uri.Addr, + Update: key, + } + outdata, err = json.Marshal(rsrcResponse) + if err != nil { + s.Error(w, r, fmt.Errorf("Failed to create json response for %v: error was: %v", r, err)) + return + } } data, err := ioutil.ReadAll(r.Body) @@ -318,40 +338,15 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { 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) } -func (s *Server) translateResourceError(w http.ResponseWriter, r *Request, supErr string, err error) { - code := 0 - defaulterr := fmt.Errorf("%s: %v", supErr, err) - rsrcerr, ok := err.(*storage.ResourceError) - if !ok { - code = rsrcerr.Code() - } - switch code { - case storage.ErrInval: - s.BadRequest(w, r, defaulterr.Error()) - case storage.ErrNoent, storage.ErrSync, storage.ErrNodata: - s.NotFound(w, r, defaulterr) - return - case storage.ErrAcces, storage.ErrNokey: - ShowError(w, &r.Request, defaulterr.Error(), http.StatusUnauthorized) - return - case storage.ErrFbig: - ShowError(w, &r.Request, defaulterr.Error(), http.StatusRequestEntityTooLarge) - return - } - - s.Error(w, r, defaulterr) - return -} - // Retrieve mutable resource updates: // bzz-resource:// - get latest update // bzz-resource:/// - get latest update on period n @@ -405,6 +400,31 @@ func (s *Server) handleGetResource(w http.ResponseWriter, r *Request, name strin http.ServeContent(w, &r.Request, "", now, bytes.NewReader(data)) } +func (s *Server) translateResourceError(w http.ResponseWriter, r *Request, supErr string, err error) { + code := 0 + defaulterr := fmt.Errorf("%s: %v", supErr, err) + rsrcerr, ok := err.(*storage.ResourceError) + if !ok { + code = rsrcerr.Code() + } + switch code { + case storage.ErrInval: + s.BadRequest(w, r, defaulterr.Error()) + case storage.ErrNoent, storage.ErrSync, storage.ErrNodata: + s.NotFound(w, r, defaulterr) + return + case storage.ErrAcces, storage.ErrNokey: + ShowError(w, &r.Request, defaulterr.Error(), http.StatusUnauthorized) + return + case storage.ErrFbig: + ShowError(w, &r.Request, defaulterr.Error(), http.StatusRequestEntityTooLarge) + return + } + + s.Error(w, r, defaulterr) + return +} + // HandleGet handles a GET request to // - bzz-raw:// and responds with the raw content stored at the // given storage key @@ -665,7 +685,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: s.NotFound(w, r, err) diff --git a/swarm/api/http/server_test.go b/swarm/api/http/server_test.go index 9a80405ffc..3e695fc1d4 100644 --- a/swarm/api/http/server_test.go +++ b/swarm/api/http/server_test.go @@ -19,28 +19,45 @@ package http_test import ( "bytes" "crypto/rand" + "encoding/json" "errors" + "flag" "fmt" "io/ioutil" "net/http" + "os" "strings" "sync" "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)) @@ -66,10 +83,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) diff --git a/swarm/api/manifest.go b/swarm/api/manifest.go index fde086b7ac..660e5131fa 100644 --- a/swarm/api/manifest.go +++ b/swarm/api/manifest.go @@ -69,6 +69,22 @@ func (a *Api) NewManifest() (storage.Key, error) { return a.Store(bytes.NewReader(data), int64(len(data)), &sync.WaitGroup{}) } +// 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 + } + return a.Store(bytes.NewReader(data), int64(len(data)), &sync.WaitGroup{}) +} + // ManifestWriter is used to add and remove entries from an underlying manifest type ManifestWriter struct { api *Api diff --git a/swarm/storage/resource.go b/swarm/storage/resource.go index 22e2ffe56e..3606533e09 100644 --- a/swarm/storage/resource.go +++ b/swarm/storage/resource.go @@ -571,7 +571,7 @@ func (self *ResourceHandler) Update(ctx context.Context, name string, data []byt } // get our blockheight at this time and the next block of the update period - currentblock, err := self.GetBlock(ctx) + currentblock, err := self.getBlock(ctx) if err != nil { return nil, NewResourceError(ErrIO, fmt.Sprintf("Could not get block height: %v", err)) }