diff options
Diffstat (limited to 'swarm/api/http/server.go')
-rw-r--r-- | swarm/api/http/server.go | 937 |
1 files changed, 0 insertions, 937 deletions
diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go deleted file mode 100644 index 88f2e4db9..000000000 --- a/swarm/api/http/server.go +++ /dev/null @@ -1,937 +0,0 @@ -// 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/>. - -/* -A simple http server interface to Swarm -*/ -package http - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "math" - "mime" - "mime/multipart" - "net/http" - "os" - "path" - "strconv" - "strings" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/swarm/api" - "github.com/ethereum/go-ethereum/swarm/chunk" - "github.com/ethereum/go-ethereum/swarm/log" - "github.com/ethereum/go-ethereum/swarm/sctx" - "github.com/ethereum/go-ethereum/swarm/storage" - "github.com/ethereum/go-ethereum/swarm/storage/feed" - "github.com/rs/cors" -) - -var ( - postRawCount = metrics.NewRegisteredCounter("api.http.post.raw.count", nil) - postRawFail = metrics.NewRegisteredCounter("api.http.post.raw.fail", nil) - postFilesCount = metrics.NewRegisteredCounter("api.http.post.files.count", nil) - postFilesFail = metrics.NewRegisteredCounter("api.http.post.files.fail", nil) - deleteCount = metrics.NewRegisteredCounter("api.http.delete.count", nil) - deleteFail = metrics.NewRegisteredCounter("api.http.delete.fail", nil) - getCount = metrics.NewRegisteredCounter("api.http.get.count", nil) - getFail = metrics.NewRegisteredCounter("api.http.get.fail", nil) - getFileCount = metrics.NewRegisteredCounter("api.http.get.file.count", nil) - getFileNotFound = metrics.NewRegisteredCounter("api.http.get.file.notfound", nil) - getFileFail = metrics.NewRegisteredCounter("api.http.get.file.fail", nil) - getListCount = metrics.NewRegisteredCounter("api.http.get.list.count", nil) - getListFail = metrics.NewRegisteredCounter("api.http.get.list.fail", nil) -) - -const SwarmTagHeaderName = "x-swarm-tag" - -type methodHandler map[string]http.Handler - -func (m methodHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - v, ok := m[r.Method] - if ok { - v.ServeHTTP(rw, r) - return - } - rw.WriteHeader(http.StatusMethodNotAllowed) -} - -func NewServer(api *api.API, corsString string) *Server { - var allowedOrigins []string - for _, domain := range strings.Split(corsString, ",") { - allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain)) - } - c := cors.New(cors.Options{ - AllowedOrigins: allowedOrigins, - AllowedMethods: []string{http.MethodPost, http.MethodGet, http.MethodDelete, http.MethodPatch, http.MethodPut}, - MaxAge: 600, - AllowedHeaders: []string{"*"}, - }) - - server := &Server{api: api} - - defaultMiddlewares := []Adapter{ - RecoverPanic, - SetRequestID, - SetRequestHost, - InitLoggingResponseWriter, - ParseURI, - InstrumentOpenTracing, - } - - tagAdapter := Adapter(func(h http.Handler) http.Handler { - return InitUploadTag(h, api.Tags) - }) - - defaultPostMiddlewares := append(defaultMiddlewares, tagAdapter) - - mux := http.NewServeMux() - mux.Handle("/bzz:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleBzzGet), - defaultMiddlewares..., - ), - "POST": Adapt( - http.HandlerFunc(server.HandlePostFiles), - defaultPostMiddlewares..., - ), - "DELETE": Adapt( - http.HandlerFunc(server.HandleDelete), - defaultMiddlewares..., - ), - }) - mux.Handle("/bzz-raw:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleGet), - defaultMiddlewares..., - ), - "POST": Adapt( - http.HandlerFunc(server.HandlePostRaw), - defaultPostMiddlewares..., - ), - }) - mux.Handle("/bzz-immutable:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleBzzGet), - defaultMiddlewares..., - ), - }) - mux.Handle("/bzz-hash:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleGet), - defaultMiddlewares..., - ), - }) - mux.Handle("/bzz-list:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleGetList), - defaultMiddlewares..., - ), - }) - mux.Handle("/bzz-feed:/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleGetFeed), - defaultMiddlewares..., - ), - "POST": Adapt( - http.HandlerFunc(server.HandlePostFeed), - defaultMiddlewares..., - ), - }) - - mux.Handle("/", methodHandler{ - "GET": Adapt( - http.HandlerFunc(server.HandleRootPaths), - SetRequestID, - InitLoggingResponseWriter, - ), - }) - server.Handler = c.Handler(mux) - - return server -} - -func (s *Server) ListenAndServe(addr string) error { - s.listenAddr = addr - return http.ListenAndServe(addr, s) -} - -// browser API for registering bzz url scheme handlers: -// https://developer.mozilla.org/en/docs/Web-based_protocol_handlers -// electron (chromium) api for registering bzz url scheme handlers: -// https://github.com/atom/electron/blob/master/docs/api/protocol.md -type Server struct { - http.Handler - api *api.API - listenAddr string -} - -func (s *Server) HandleBzzGet(w http.ResponseWriter, r *http.Request) { - log.Debug("handleBzzGet", "ruid", GetRUID(r.Context()), "uri", r.RequestURI) - if r.Header.Get("Accept") == "application/x-tar" { - uri := GetURI(r.Context()) - _, credentials, _ := r.BasicAuth() - reader, err := s.api.GetDirectoryTar(r.Context(), s.api.Decryptor(r.Context(), credentials), uri) - if err != nil { - if isDecryptError(err) { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", uri.Address().String())) - respondError(w, r, err.Error(), http.StatusUnauthorized) - return - } - respondError(w, r, fmt.Sprintf("Had an error building the tarball: %v", err), http.StatusInternalServerError) - return - } - defer reader.Close() - - w.Header().Set("Content-Type", "application/x-tar") - - fileName := uri.Addr - if found := path.Base(uri.Path); found != "" && found != "." && found != "/" { - fileName = found - } - w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.tar\"", fileName)) - - w.WriteHeader(http.StatusOK) - io.Copy(w, reader) - return - } - - s.HandleGetFile(w, r) -} - -func (s *Server) HandleRootPaths(w http.ResponseWriter, r *http.Request) { - switch r.RequestURI { - case "/": - respondTemplate(w, r, "landing-page", "Swarm: Please request a valid ENS or swarm hash with the appropriate bzz scheme", 200) - return - case "/robots.txt": - w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat)) - fmt.Fprintf(w, "User-agent: *\nDisallow: /") - case "/favicon.ico": - w.WriteHeader(http.StatusOK) - w.Write(faviconBytes) - default: - respondError(w, r, "Not Found", http.StatusNotFound) - } -} - -// HandlePostRaw handles a POST request to a raw bzz-raw:/ URI, stores the request -// body in swarm and returns the resulting storage address as a text/plain response -func (s *Server) HandlePostRaw(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - log.Debug("handle.post.raw", "ruid", ruid) - - tagUid := sctx.GetTag(r.Context()) - tag, err := s.api.Tags.Get(tagUid) - if err != nil { - log.Error("handle post raw got an error retrieving tag for DoneSplit", "tagUid", tagUid, "err", err) - } - - postRawCount.Inc(1) - - toEncrypt := false - uri := GetURI(r.Context()) - if uri.Addr == "encrypt" { - toEncrypt = true - } - - if uri.Path != "" { - postRawFail.Inc(1) - respondError(w, r, "raw POST request cannot contain a path", http.StatusBadRequest) - return - } - - if uri.Addr != "" && uri.Addr != "encrypt" { - postRawFail.Inc(1) - respondError(w, r, "raw POST request addr can only be empty or \"encrypt\"", http.StatusBadRequest) - return - } - - if r.Header.Get("Content-Length") == "" { - postRawFail.Inc(1) - respondError(w, r, "missing Content-Length header in request", http.StatusBadRequest) - return - } - - addr, wait, err := s.api.Store(r.Context(), r.Body, r.ContentLength, toEncrypt) - if err != nil { - postRawFail.Inc(1) - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - - wait(r.Context()) - tag.DoneSplit(addr) - - log.Debug("stored content", "ruid", ruid, "key", addr) - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, addr) -} - -// HandlePostFiles handles a POST request to -// bzz:/<hash>/<path> which contains either a single file or multiple files -// (either a tar archive or multipart form), adds those files either to an -// existing manifest or to a new manifest under <path> and returns the -// resulting manifest hash as a text/plain response -func (s *Server) HandlePostFiles(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - log.Debug("handle.post.files", "ruid", ruid) - postFilesCount.Inc(1) - - contentType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - postFilesFail.Inc(1) - respondError(w, r, err.Error(), http.StatusBadRequest) - return - } - - toEncrypt := false - uri := GetURI(r.Context()) - if uri.Addr == "encrypt" { - toEncrypt = true - } - - var addr storage.Address - if uri.Addr != "" && uri.Addr != "encrypt" { - addr, err = s.api.Resolve(r.Context(), uri.Addr) - if err != nil { - postFilesFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusInternalServerError) - return - } - log.Debug("resolved key", "ruid", ruid, "key", addr) - } else { - addr, err = s.api.NewManifest(r.Context(), toEncrypt) - if err != nil { - postFilesFail.Inc(1) - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - log.Debug("new manifest", "ruid", ruid, "key", addr) - } - newAddr, err := s.api.UpdateManifest(r.Context(), addr, func(mw *api.ManifestWriter) error { - switch contentType { - case "application/x-tar": - _, err := s.handleTarUpload(r, mw) - if err != nil { - respondError(w, r, fmt.Sprintf("error uploading tarball: %v", err), http.StatusInternalServerError) - return err - } - return nil - case "multipart/form-data": - return s.handleMultipartUpload(r, params["boundary"], mw) - - default: - return s.handleDirectUpload(r, mw) - } - }) - if err != nil { - postFilesFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot create manifest: %s", err), http.StatusInternalServerError) - return - } - - tagUid := sctx.GetTag(r.Context()) - tag, err := s.api.Tags.Get(tagUid) - if err != nil { - log.Error("got an error retrieving tag for DoneSplit", "tagUid", tagUid, "err", err) - } - - log.Debug("done splitting, setting tag total", "SPLIT", tag.Get(chunk.StateSplit), "TOTAL", tag.Total()) - tag.DoneSplit(newAddr) - - log.Debug("stored content", "ruid", ruid, "key", newAddr) - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, newAddr) -} - -func (s *Server) handleTarUpload(r *http.Request, mw *api.ManifestWriter) (storage.Address, error) { - log.Debug("handle.tar.upload", "ruid", GetRUID(r.Context()), "tag", sctx.GetTag(r.Context())) - - defaultPath := r.URL.Query().Get("defaultpath") - - key, err := s.api.UploadTar(r.Context(), r.Body, GetURI(r.Context()).Path, defaultPath, mw) - if err != nil { - return nil, err - } - return key, nil -} - -func (s *Server) handleMultipartUpload(r *http.Request, boundary string, mw *api.ManifestWriter) error { - ruid := GetRUID(r.Context()) - log.Debug("handle.multipart.upload", "ruid", ruid) - mr := multipart.NewReader(r.Body, boundary) - for { - part, err := mr.NextPart() - if err == io.EOF { - return nil - } else if err != nil { - return fmt.Errorf("error reading multipart form: %s", err) - } - - var size int64 - var reader io.Reader - if contentLength := part.Header.Get("Content-Length"); contentLength != "" { - size, err = strconv.ParseInt(contentLength, 10, 64) - if err != nil { - return fmt.Errorf("error parsing multipart content length: %s", err) - } - reader = part - } else { - // copy the part to a tmp file to get its size - tmp, err := ioutil.TempFile("", "swarm-multipart") - if err != nil { - return err - } - defer os.Remove(tmp.Name()) - defer tmp.Close() - size, err = io.Copy(tmp, part) - if err != nil { - return fmt.Errorf("error copying multipart content: %s", err) - } - if _, err := tmp.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("error copying multipart content: %s", err) - } - reader = tmp - } - - // add the entry under the path from the request - name := part.FileName() - if name == "" { - name = part.FormName() - } - uri := GetURI(r.Context()) - path := path.Join(uri.Path, name) - entry := &api.ManifestEntry{ - Path: path, - ContentType: part.Header.Get("Content-Type"), - Size: size, - } - log.Debug("adding path to new manifest", "ruid", ruid, "bytes", entry.Size, "path", entry.Path) - contentKey, err := mw.AddEntry(r.Context(), reader, entry) - if err != nil { - return fmt.Errorf("error adding manifest entry from multipart form: %s", err) - } - log.Debug("stored content", "ruid", ruid, "key", contentKey) - } -} - -func (s *Server) handleDirectUpload(r *http.Request, mw *api.ManifestWriter) error { - ruid := GetRUID(r.Context()) - log.Debug("handle.direct.upload", "ruid", ruid) - key, err := mw.AddEntry(r.Context(), r.Body, &api.ManifestEntry{ - Path: GetURI(r.Context()).Path, - ContentType: r.Header.Get("Content-Type"), - Mode: 0644, - Size: r.ContentLength, - }) - if err != nil { - return err - } - log.Debug("stored content", "ruid", ruid, "key", key) - return nil -} - -// HandleDelete handles a DELETE request to bzz:/<manifest>/<path>, removes -// <path> from <manifest> and returns the resulting manifest hash as a -// text/plain response -func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - log.Debug("handle.delete", "ruid", ruid) - deleteCount.Inc(1) - newKey, err := s.api.Delete(r.Context(), uri.Addr, uri.Path) - if err != nil { - deleteFail.Inc(1) - respondError(w, r, fmt.Sprintf("could not delete from manifest: %v", err), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, newKey) -} - -// Handles feed manifest creation and feed updates -// The POST request admits a JSON structure as defined in the feeds package: `feed.updateRequestJSON` -// The requests can be to a) create a feed manifest, b) update a feed or c) both a+b: create a feed manifest and publish a first update -func (s *Server) HandlePostFeed(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - log.Debug("handle.post.feed", "ruid", ruid) - var err error - - // Creation and update must send feed.updateRequestJSON JSON structure - body, err := ioutil.ReadAll(r.Body) - if err != nil { - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - - fd, err := s.api.ResolveFeed(r.Context(), uri, r.URL.Query()) - if err != nil { // couldn't parse query string or retrieve manifest - getFail.Inc(1) - httpStatus := http.StatusBadRequest - if err == api.ErrCannotLoadFeedManifest || err == api.ErrCannotResolveFeedURI { - httpStatus = http.StatusNotFound - } - respondError(w, r, fmt.Sprintf("cannot retrieve feed from manifest: %s", err), httpStatus) - return - } - - var updateRequest feed.Request - updateRequest.Feed = *fd - query := r.URL.Query() - - if err := updateRequest.FromValues(query, body); err != nil { // decodes request from query parameters - respondError(w, r, err.Error(), http.StatusBadRequest) - return - } - - switch { - case updateRequest.IsUpdate(): - // Verify that the signature is intact and that the signer is authorized - // to update this feed - // Check this early, to avoid creating a feed and then not being able to set its first update. - if err = updateRequest.Verify(); err != nil { - respondError(w, r, err.Error(), http.StatusForbidden) - return - } - _, err = s.api.FeedsUpdate(r.Context(), &updateRequest) - if err != nil { - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - fallthrough - case query.Get("manifest") == "1": - // we create a manifest so we can retrieve feed updates with bzz:// later - // this manifest has a special "feed type" manifest, and saves the - // feed identification used to retrieve feed updates later - m, err := s.api.NewFeedManifest(r.Context(), &updateRequest.Feed) - if err != nil { - respondError(w, r, fmt.Sprintf("failed to create feed manifest: %v", err), http.StatusInternalServerError) - return - } - // the key to the manifest will be passed back to the client - // the client can access the feed directly through its Feed member - // the manifest key can be set as content in the resolver of the ENS name - outdata, err := json.Marshal(m) - if err != nil { - respondError(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) - return - } - fmt.Fprint(w, string(outdata)) - - w.Header().Add("Content-type", "application/json") - default: - respondError(w, r, "Missing signature in feed update request", http.StatusBadRequest) - } -} - -// HandleGetFeed retrieves Swarm feeds updates: -// bzz-feed://<manifest address or ENS name> - get latest feed update, given a manifest address -// - or - -// specify user + topic (optional), subtopic name (optional) directly, without manifest: -// bzz-feed://?user=0x...&topic=0x...&name=subtopic name -// topic defaults to 0x000... if not specified. -// name defaults to empty string if not specified. -// thus, empty name and topic refers to the user's default feed. -// -// Optional parameters: -// time=xx - get the latest update before time (in epoch seconds) -// hint.time=xx - hint the lookup algorithm looking for updates at around that time -// hint.level=xx - hint the lookup algorithm looking for updates at around this frequency level -// meta=1 - get feed metadata and status information instead of performing a feed query -// NOTE: meta=1 will be deprecated in the near future -func (s *Server) HandleGetFeed(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - log.Debug("handle.get.feed", "ruid", ruid) - var err error - - fd, err := s.api.ResolveFeed(r.Context(), uri, r.URL.Query()) - if err != nil { // couldn't parse query string or retrieve manifest - getFail.Inc(1) - httpStatus := http.StatusBadRequest - if err == api.ErrCannotLoadFeedManifest || err == api.ErrCannotResolveFeedURI { - httpStatus = http.StatusNotFound - } - respondError(w, r, fmt.Sprintf("cannot retrieve feed information from manifest: %s", err), httpStatus) - return - } - - // determine if the query specifies period and version or it is a metadata query - if r.URL.Query().Get("meta") == "1" { - unsignedUpdateRequest, err := s.api.FeedsNewRequest(r.Context(), fd) - if err != nil { - getFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot retrieve feed metadata for feed=%s: %s", fd.Hex(), err), http.StatusNotFound) - return - } - rawResponse, err := unsignedUpdateRequest.MarshalJSON() - if err != nil { - respondError(w, r, fmt.Sprintf("cannot encode unsigned feed update request: %v", err), http.StatusInternalServerError) - return - } - w.Header().Add("Content-type", "application/json") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, string(rawResponse)) - return - } - - lookupParams := &feed.Query{Feed: *fd} - if err = lookupParams.FromValues(r.URL.Query()); err != nil { // parse period, version - respondError(w, r, fmt.Sprintf("invalid feed update request:%s", err), http.StatusBadRequest) - return - } - - data, err := s.api.FeedsLookup(r.Context(), lookupParams) - - // any error from the switch statement will end up here - if err != nil { - code, err2 := s.translateFeedError(w, r, "feed lookup fail", err) - respondError(w, r, err2.Error(), code) - return - } - - // All ok, serve the retrieved update - log.Debug("Found update", "feed", fd.Hex(), "ruid", ruid) - w.Header().Set("Content-Type", api.MimeOctetStream) - http.ServeContent(w, r, "", time.Now(), bytes.NewReader(data)) -} - -func (s *Server) translateFeedError(w http.ResponseWriter, r *http.Request, supErr string, err error) (int, error) { - code := 0 - defaultErr := fmt.Errorf("%s: %v", supErr, err) - rsrcErr, ok := err.(*feed.Error) - if !ok && rsrcErr != nil { - code = rsrcErr.Code() - } - switch code { - case storage.ErrInvalidValue: - return http.StatusBadRequest, defaultErr - case storage.ErrNotFound, storage.ErrNotSynced, storage.ErrNothingToReturn, storage.ErrInit: - return http.StatusNotFound, defaultErr - case storage.ErrUnauthorized, storage.ErrInvalidSignature: - return http.StatusUnauthorized, defaultErr - case storage.ErrDataOverflow: - return http.StatusRequestEntityTooLarge, defaultErr - } - - return http.StatusInternalServerError, defaultErr -} - -// HandleGet handles a GET request to -// - bzz-raw://<key> and responds with the raw content stored at the -// given storage key -// - bzz-hash://<key> and responds with the hash of the content stored -// at the given storage key as a text/plain response -func (s *Server) HandleGet(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - log.Debug("handle.get", "ruid", ruid, "uri", uri) - getCount.Inc(1) - _, pass, _ := r.BasicAuth() - - addr, err := s.api.ResolveURI(r.Context(), uri, pass) - if err != nil { - getFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) - return - } - w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. - - log.Debug("handle.get: resolved", "ruid", ruid, "key", addr) - - // if path is set, interpret <key> as a manifest and return the - // raw entry at the given path - etag := common.Bytes2Hex(addr) - noneMatchEtag := r.Header.Get("If-None-Match") - w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to manifest key or raw entry key. - if noneMatchEtag != "" { - if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), addr) { - w.WriteHeader(http.StatusNotModified) - return - } - } - - switch { - case uri.Raw(): - // check the root chunk exists by retrieving the file's size - reader, isEncrypted := s.api.Retrieve(r.Context(), addr) - if _, err := reader.Size(r.Context(), nil); err != nil { - getFail.Inc(1) - respondError(w, r, fmt.Sprintf("root chunk not found %s: %s", addr, err), http.StatusNotFound) - return - } - - w.Header().Set("X-Decrypted", fmt.Sprintf("%v", isEncrypted)) - - // allow the request to overwrite the content type using a query - // parameter - if typ := r.URL.Query().Get("content_type"); typ != "" { - w.Header().Set("Content-Type", typ) - } - http.ServeContent(w, r, "", time.Now(), reader) - case uri.Hash(): - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, addr) - } - -} - -// HandleGetList handles a GET request to bzz-list:/<manifest>/<path> and returns -// a list of all files contained in <manifest> under <path> grouped into -// common prefixes using "/" as a delimiter -func (s *Server) HandleGetList(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - _, credentials, _ := r.BasicAuth() - log.Debug("handle.get.list", "ruid", ruid, "uri", uri) - getListCount.Inc(1) - - // ensure the root path has a trailing slash so that relative URLs work - if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) - return - } - - addr, err := s.api.Resolve(r.Context(), uri.Addr) - if err != nil { - getListFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) - return - } - log.Debug("handle.get.list: resolved", "ruid", ruid, "key", addr) - - list, err := s.api.GetManifestList(r.Context(), s.api.Decryptor(r.Context(), credentials), addr, uri.Path) - if err != nil { - getListFail.Inc(1) - if isDecryptError(err) { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", addr.String())) - respondError(w, r, err.Error(), http.StatusUnauthorized) - return - } - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - - // if the client wants HTML (e.g. a browser) then render the list as a - // HTML index with relative URLs - if strings.Contains(r.Header.Get("Accept"), "text/html") { - w.Header().Set("Content-Type", "text/html") - err := TemplatesMap["bzz-list"].Execute(w, &htmlListData{ - URI: &api.URI{ - Scheme: "bzz", - Addr: uri.Addr, - Path: uri.Path, - }, - List: &list, - }) - if err != nil { - getListFail.Inc(1) - log.Error(fmt.Sprintf("error rendering list HTML: %s", err)) - } - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(&list) -} - -// HandleGetFile handles a GET request to bzz://<manifest>/<path> and responds -// with the content of the file at <path> from the given <manifest> -func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) { - ruid := GetRUID(r.Context()) - uri := GetURI(r.Context()) - _, credentials, _ := r.BasicAuth() - log.Debug("handle.get.file", "ruid", ruid, "uri", r.RequestURI) - getFileCount.Inc(1) - - // ensure the root path has a trailing slash so that relative URLs work - if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) - return - } - var err error - manifestAddr := uri.Address() - - if manifestAddr == nil { - manifestAddr, err = s.api.Resolve(r.Context(), uri.Addr) - if err != nil { - getFileFail.Inc(1) - respondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) - return - } - } else { - w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. - } - - log.Debug("handle.get.file: resolved", "ruid", ruid, "key", manifestAddr) - - reader, contentType, status, contentKey, err := s.api.Get(r.Context(), s.api.Decryptor(r.Context(), credentials), manifestAddr, uri.Path) - - etag := common.Bytes2Hex(contentKey) - noneMatchEtag := r.Header.Get("If-None-Match") - w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to actual content key. - if noneMatchEtag != "" { - if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), contentKey) { - w.WriteHeader(http.StatusNotModified) - return - } - } - - if err != nil { - if isDecryptError(err) { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", manifestAddr)) - respondError(w, r, err.Error(), http.StatusUnauthorized) - return - } - - switch status { - case http.StatusNotFound: - getFileNotFound.Inc(1) - respondError(w, r, err.Error(), http.StatusNotFound) - default: - getFileFail.Inc(1) - respondError(w, r, err.Error(), http.StatusInternalServerError) - } - return - } - - //the request results in ambiguous files - //e.g. /read with readme.md and readinglist.txt available in manifest - if status == http.StatusMultipleChoices { - list, err := s.api.GetManifestList(r.Context(), s.api.Decryptor(r.Context(), credentials), manifestAddr, uri.Path) - if err != nil { - getFileFail.Inc(1) - if isDecryptError(err) { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", manifestAddr)) - respondError(w, r, err.Error(), http.StatusUnauthorized) - return - } - respondError(w, r, err.Error(), http.StatusInternalServerError) - return - } - - log.Debug(fmt.Sprintf("Multiple choices! --> %v", list), "ruid", ruid) - //show a nice page links to available entries - ShowMultipleChoices(w, r, list) - return - } - - // check the root chunk exists by retrieving the file's size - if _, err := reader.Size(r.Context(), nil); err != nil { - getFileNotFound.Inc(1) - respondError(w, r, fmt.Sprintf("file not found %s: %s", uri, err), http.StatusNotFound) - return - } - - if contentType != "" { - w.Header().Set("Content-Type", contentType) - } - - fileName := uri.Addr - if found := path.Base(uri.Path); found != "" && found != "." && found != "/" { - fileName = found - } - w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) - - http.ServeContent(w, r, fileName, time.Now(), newBufferedReadSeeker(reader, getFileBufferSize)) -} - -// calculateNumberOfChunks calculates the number of chunks in an arbitrary content length -func calculateNumberOfChunks(contentLength int64, isEncrypted bool) int64 { - if contentLength < 4096 { - return 1 - } - branchingFactor := 128 - if isEncrypted { - branchingFactor = 64 - } - - dataChunks := math.Ceil(float64(contentLength) / float64(4096)) - totalChunks := dataChunks - intermediate := dataChunks / float64(branchingFactor) - - for intermediate > 1 { - totalChunks += math.Ceil(intermediate) - intermediate = intermediate / float64(branchingFactor) - } - - return int64(totalChunks) + 1 -} - -// The size of buffer used for bufio.Reader on LazyChunkReader passed to -// http.ServeContent in HandleGetFile. -// Warning: This value influences the number of chunk requests and chunker join goroutines -// per file request. -// Recommended value is 4 times the io.Copy default buffer value which is 32kB. -const getFileBufferSize = 4 * 32 * 1024 - -// bufferedReadSeeker wraps bufio.Reader to expose Seek method -// from the provied io.ReadSeeker in newBufferedReadSeeker. -type bufferedReadSeeker struct { - r io.Reader - s io.Seeker -} - -// newBufferedReadSeeker creates a new instance of bufferedReadSeeker, -// out of io.ReadSeeker. Argument `size` is the size of the read buffer. -func newBufferedReadSeeker(readSeeker io.ReadSeeker, size int) bufferedReadSeeker { - return bufferedReadSeeker{ - r: bufio.NewReaderSize(readSeeker, size), - s: readSeeker, - } -} - -func (b bufferedReadSeeker) Read(p []byte) (n int, err error) { - return b.r.Read(p) -} - -func (b bufferedReadSeeker) Seek(offset int64, whence int) (int64, error) { - return b.s.Seek(offset, whence) -} - -type loggingResponseWriter struct { - http.ResponseWriter - statusCode int -} - -func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { - return &loggingResponseWriter{w, http.StatusOK} -} - -func (lrw *loggingResponseWriter) WriteHeader(code int) { - lrw.statusCode = code - lrw.ResponseWriter.WriteHeader(code) -} - -func isDecryptError(err error) bool { - return strings.Contains(err.Error(), api.ErrDecrypt.Error()) -} |