// 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 . /* 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:// 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 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://, removes // from 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:// - 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:// and responds with the raw content stored at the // given storage key // - bzz-hash:// 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:///path, so we are sure it is immutable. log.Debug("handle.get: resolved", "ruid", ruid, "key", addr) // if path is set, interpret 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 } } // 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)) switch { case uri.Raw(): // 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:// and returns // a list of all files contained in under 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:/// and responds // with the content of the file at from the given 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:///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()) }