github.com/oskarth/go-ethereum@v1.6.8-0.20191013093314-dac24a9d3494/swarm/api/http/server.go (about) 1 // Copyright 2016 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 /* 18 A simple http server interface to Swarm 19 */ 20 package http 21 22 import ( 23 "bufio" 24 "bytes" 25 "encoding/json" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "mime" 30 "mime/multipart" 31 "net/http" 32 "os" 33 "path" 34 "strconv" 35 "strings" 36 "time" 37 38 "github.com/ethereum/go-ethereum/common" 39 "github.com/ethereum/go-ethereum/metrics" 40 "github.com/ethereum/go-ethereum/swarm/api" 41 "github.com/ethereum/go-ethereum/swarm/log" 42 "github.com/ethereum/go-ethereum/swarm/storage" 43 "github.com/ethereum/go-ethereum/swarm/storage/feed" 44 "github.com/rs/cors" 45 ) 46 47 var ( 48 postRawCount = metrics.NewRegisteredCounter("api.http.post.raw.count", nil) 49 postRawFail = metrics.NewRegisteredCounter("api.http.post.raw.fail", nil) 50 postFilesCount = metrics.NewRegisteredCounter("api.http.post.files.count", nil) 51 postFilesFail = metrics.NewRegisteredCounter("api.http.post.files.fail", nil) 52 deleteCount = metrics.NewRegisteredCounter("api.http.delete.count", nil) 53 deleteFail = metrics.NewRegisteredCounter("api.http.delete.fail", nil) 54 getCount = metrics.NewRegisteredCounter("api.http.get.count", nil) 55 getFail = metrics.NewRegisteredCounter("api.http.get.fail", nil) 56 getFileCount = metrics.NewRegisteredCounter("api.http.get.file.count", nil) 57 getFileNotFound = metrics.NewRegisteredCounter("api.http.get.file.notfound", nil) 58 getFileFail = metrics.NewRegisteredCounter("api.http.get.file.fail", nil) 59 getListCount = metrics.NewRegisteredCounter("api.http.get.list.count", nil) 60 getListFail = metrics.NewRegisteredCounter("api.http.get.list.fail", nil) 61 ) 62 63 type methodHandler map[string]http.Handler 64 65 func (m methodHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 66 v, ok := m[r.Method] 67 if ok { 68 v.ServeHTTP(rw, r) 69 return 70 } 71 rw.WriteHeader(http.StatusMethodNotAllowed) 72 } 73 74 func NewServer(api *api.API, corsString string) *Server { 75 var allowedOrigins []string 76 for _, domain := range strings.Split(corsString, ",") { 77 allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain)) 78 } 79 c := cors.New(cors.Options{ 80 AllowedOrigins: allowedOrigins, 81 AllowedMethods: []string{http.MethodPost, http.MethodGet, http.MethodDelete, http.MethodPatch, http.MethodPut}, 82 MaxAge: 600, 83 AllowedHeaders: []string{"*"}, 84 }) 85 86 server := &Server{api: api} 87 88 defaultMiddlewares := []Adapter{ 89 RecoverPanic, 90 SetRequestID, 91 SetRequestHost, 92 InitLoggingResponseWriter, 93 ParseURI, 94 InstrumentOpenTracing, 95 } 96 97 mux := http.NewServeMux() 98 mux.Handle("/bzz:/", methodHandler{ 99 "GET": Adapt( 100 http.HandlerFunc(server.HandleBzzGet), 101 defaultMiddlewares..., 102 ), 103 "POST": Adapt( 104 http.HandlerFunc(server.HandlePostFiles), 105 defaultMiddlewares..., 106 ), 107 "DELETE": Adapt( 108 http.HandlerFunc(server.HandleDelete), 109 defaultMiddlewares..., 110 ), 111 }) 112 mux.Handle("/bzz-raw:/", methodHandler{ 113 "GET": Adapt( 114 http.HandlerFunc(server.HandleGet), 115 defaultMiddlewares..., 116 ), 117 "POST": Adapt( 118 http.HandlerFunc(server.HandlePostRaw), 119 defaultMiddlewares..., 120 ), 121 }) 122 mux.Handle("/bzz-immutable:/", methodHandler{ 123 "GET": Adapt( 124 http.HandlerFunc(server.HandleBzzGet), 125 defaultMiddlewares..., 126 ), 127 }) 128 mux.Handle("/bzz-hash:/", methodHandler{ 129 "GET": Adapt( 130 http.HandlerFunc(server.HandleGet), 131 defaultMiddlewares..., 132 ), 133 }) 134 mux.Handle("/bzz-list:/", methodHandler{ 135 "GET": Adapt( 136 http.HandlerFunc(server.HandleGetList), 137 defaultMiddlewares..., 138 ), 139 }) 140 mux.Handle("/bzz-feed:/", methodHandler{ 141 "GET": Adapt( 142 http.HandlerFunc(server.HandleGetFeed), 143 defaultMiddlewares..., 144 ), 145 "POST": Adapt( 146 http.HandlerFunc(server.HandlePostFeed), 147 defaultMiddlewares..., 148 ), 149 }) 150 151 mux.Handle("/", methodHandler{ 152 "GET": Adapt( 153 http.HandlerFunc(server.HandleRootPaths), 154 SetRequestID, 155 InitLoggingResponseWriter, 156 ), 157 }) 158 server.Handler = c.Handler(mux) 159 160 return server 161 } 162 163 func (s *Server) ListenAndServe(addr string) error { 164 s.listenAddr = addr 165 return http.ListenAndServe(addr, s) 166 } 167 168 // browser API for registering bzz url scheme handlers: 169 // https://developer.mozilla.org/en/docs/Web-based_protocol_handlers 170 // electron (chromium) api for registering bzz url scheme handlers: 171 // https://github.com/atom/electron/blob/master/docs/api/protocol.md 172 type Server struct { 173 http.Handler 174 api *api.API 175 listenAddr string 176 } 177 178 func (s *Server) HandleBzzGet(w http.ResponseWriter, r *http.Request) { 179 log.Debug("handleBzzGet", "ruid", GetRUID(r.Context()), "uri", r.RequestURI) 180 if r.Header.Get("Accept") == "application/x-tar" { 181 uri := GetURI(r.Context()) 182 _, credentials, _ := r.BasicAuth() 183 reader, err := s.api.GetDirectoryTar(r.Context(), s.api.Decryptor(r.Context(), credentials), uri) 184 if err != nil { 185 if isDecryptError(err) { 186 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", uri.Address().String())) 187 RespondError(w, r, err.Error(), http.StatusUnauthorized) 188 return 189 } 190 RespondError(w, r, fmt.Sprintf("Had an error building the tarball: %v", err), http.StatusInternalServerError) 191 return 192 } 193 defer reader.Close() 194 195 w.Header().Set("Content-Type", "application/x-tar") 196 197 fileName := uri.Addr 198 if found := path.Base(uri.Path); found != "" && found != "." && found != "/" { 199 fileName = found 200 } 201 w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.tar\"", fileName)) 202 203 w.WriteHeader(http.StatusOK) 204 io.Copy(w, reader) 205 return 206 } 207 208 s.HandleGetFile(w, r) 209 } 210 211 func (s *Server) HandleRootPaths(w http.ResponseWriter, r *http.Request) { 212 switch r.RequestURI { 213 case "/": 214 RespondTemplate(w, r, "landing-page", "Swarm: Please request a valid ENS or swarm hash with the appropriate bzz scheme", 200) 215 return 216 case "/robots.txt": 217 w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat)) 218 fmt.Fprintf(w, "User-agent: *\nDisallow: /") 219 case "/favicon.ico": 220 w.WriteHeader(http.StatusOK) 221 w.Write(faviconBytes) 222 default: 223 RespondError(w, r, "Not Found", http.StatusNotFound) 224 } 225 } 226 227 // HandlePostRaw handles a POST request to a raw bzz-raw:/ URI, stores the request 228 // body in swarm and returns the resulting storage address as a text/plain response 229 func (s *Server) HandlePostRaw(w http.ResponseWriter, r *http.Request) { 230 ruid := GetRUID(r.Context()) 231 log.Debug("handle.post.raw", "ruid", ruid) 232 233 postRawCount.Inc(1) 234 235 toEncrypt := false 236 uri := GetURI(r.Context()) 237 if uri.Addr == "encrypt" { 238 toEncrypt = true 239 } 240 241 if uri.Path != "" { 242 postRawFail.Inc(1) 243 RespondError(w, r, "raw POST request cannot contain a path", http.StatusBadRequest) 244 return 245 } 246 247 if uri.Addr != "" && uri.Addr != "encrypt" { 248 postRawFail.Inc(1) 249 RespondError(w, r, "raw POST request addr can only be empty or \"encrypt\"", http.StatusBadRequest) 250 return 251 } 252 253 if r.Header.Get("Content-Length") == "" { 254 postRawFail.Inc(1) 255 RespondError(w, r, "missing Content-Length header in request", http.StatusBadRequest) 256 return 257 } 258 259 addr, _, err := s.api.Store(r.Context(), r.Body, r.ContentLength, toEncrypt) 260 if err != nil { 261 postRawFail.Inc(1) 262 RespondError(w, r, err.Error(), http.StatusInternalServerError) 263 return 264 } 265 266 log.Debug("stored content", "ruid", ruid, "key", addr) 267 268 w.Header().Set("Content-Type", "text/plain") 269 w.WriteHeader(http.StatusOK) 270 fmt.Fprint(w, addr) 271 } 272 273 // HandlePostFiles handles a POST request to 274 // bzz:/<hash>/<path> which contains either a single file or multiple files 275 // (either a tar archive or multipart form), adds those files either to an 276 // existing manifest or to a new manifest under <path> and returns the 277 // resulting manifest hash as a text/plain response 278 func (s *Server) HandlePostFiles(w http.ResponseWriter, r *http.Request) { 279 ruid := GetRUID(r.Context()) 280 log.Debug("handle.post.files", "ruid", ruid) 281 postFilesCount.Inc(1) 282 283 contentType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 284 if err != nil { 285 postFilesFail.Inc(1) 286 RespondError(w, r, err.Error(), http.StatusBadRequest) 287 return 288 } 289 290 toEncrypt := false 291 uri := GetURI(r.Context()) 292 if uri.Addr == "encrypt" { 293 toEncrypt = true 294 } 295 296 var addr storage.Address 297 if uri.Addr != "" && uri.Addr != "encrypt" { 298 addr, err = s.api.Resolve(r.Context(), uri.Addr) 299 if err != nil { 300 postFilesFail.Inc(1) 301 RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusInternalServerError) 302 return 303 } 304 log.Debug("resolved key", "ruid", ruid, "key", addr) 305 } else { 306 addr, err = s.api.NewManifest(r.Context(), toEncrypt) 307 if err != nil { 308 postFilesFail.Inc(1) 309 RespondError(w, r, err.Error(), http.StatusInternalServerError) 310 return 311 } 312 log.Debug("new manifest", "ruid", ruid, "key", addr) 313 } 314 315 newAddr, err := s.api.UpdateManifest(r.Context(), addr, func(mw *api.ManifestWriter) error { 316 switch contentType { 317 case "application/x-tar": 318 _, err := s.handleTarUpload(r, mw) 319 if err != nil { 320 RespondError(w, r, fmt.Sprintf("error uploading tarball: %v", err), http.StatusInternalServerError) 321 return err 322 } 323 return nil 324 case "multipart/form-data": 325 return s.handleMultipartUpload(r, params["boundary"], mw) 326 327 default: 328 return s.handleDirectUpload(r, mw) 329 } 330 }) 331 if err != nil { 332 postFilesFail.Inc(1) 333 RespondError(w, r, fmt.Sprintf("cannot create manifest: %s", err), http.StatusInternalServerError) 334 return 335 } 336 337 log.Debug("stored content", "ruid", ruid, "key", newAddr) 338 339 w.Header().Set("Content-Type", "text/plain") 340 w.WriteHeader(http.StatusOK) 341 fmt.Fprint(w, newAddr) 342 } 343 344 func (s *Server) handleTarUpload(r *http.Request, mw *api.ManifestWriter) (storage.Address, error) { 345 log.Debug("handle.tar.upload", "ruid", GetRUID(r.Context())) 346 347 defaultPath := r.URL.Query().Get("defaultpath") 348 349 key, err := s.api.UploadTar(r.Context(), r.Body, GetURI(r.Context()).Path, defaultPath, mw) 350 if err != nil { 351 return nil, err 352 } 353 return key, nil 354 } 355 356 func (s *Server) handleMultipartUpload(r *http.Request, boundary string, mw *api.ManifestWriter) error { 357 ruid := GetRUID(r.Context()) 358 log.Debug("handle.multipart.upload", "ruid", ruid) 359 mr := multipart.NewReader(r.Body, boundary) 360 for { 361 part, err := mr.NextPart() 362 if err == io.EOF { 363 return nil 364 } else if err != nil { 365 return fmt.Errorf("error reading multipart form: %s", err) 366 } 367 368 var size int64 369 var reader io.Reader = part 370 if contentLength := part.Header.Get("Content-Length"); contentLength != "" { 371 size, err = strconv.ParseInt(contentLength, 10, 64) 372 if err != nil { 373 return fmt.Errorf("error parsing multipart content length: %s", err) 374 } 375 reader = part 376 } else { 377 // copy the part to a tmp file to get its size 378 tmp, err := ioutil.TempFile("", "swarm-multipart") 379 if err != nil { 380 return err 381 } 382 defer os.Remove(tmp.Name()) 383 defer tmp.Close() 384 size, err = io.Copy(tmp, part) 385 if err != nil { 386 return fmt.Errorf("error copying multipart content: %s", err) 387 } 388 if _, err := tmp.Seek(0, io.SeekStart); err != nil { 389 return fmt.Errorf("error copying multipart content: %s", err) 390 } 391 reader = tmp 392 } 393 394 // add the entry under the path from the request 395 name := part.FileName() 396 if name == "" { 397 name = part.FormName() 398 } 399 uri := GetURI(r.Context()) 400 path := path.Join(uri.Path, name) 401 entry := &api.ManifestEntry{ 402 Path: path, 403 ContentType: part.Header.Get("Content-Type"), 404 Size: size, 405 } 406 log.Debug("adding path to new manifest", "ruid", ruid, "bytes", entry.Size, "path", entry.Path) 407 contentKey, err := mw.AddEntry(r.Context(), reader, entry) 408 if err != nil { 409 return fmt.Errorf("error adding manifest entry from multipart form: %s", err) 410 } 411 log.Debug("stored content", "ruid", ruid, "key", contentKey) 412 } 413 } 414 415 func (s *Server) handleDirectUpload(r *http.Request, mw *api.ManifestWriter) error { 416 ruid := GetRUID(r.Context()) 417 log.Debug("handle.direct.upload", "ruid", ruid) 418 key, err := mw.AddEntry(r.Context(), r.Body, &api.ManifestEntry{ 419 Path: GetURI(r.Context()).Path, 420 ContentType: r.Header.Get("Content-Type"), 421 Mode: 0644, 422 Size: r.ContentLength, 423 }) 424 if err != nil { 425 return err 426 } 427 log.Debug("stored content", "ruid", ruid, "key", key) 428 return nil 429 } 430 431 // HandleDelete handles a DELETE request to bzz:/<manifest>/<path>, removes 432 // <path> from <manifest> and returns the resulting manifest hash as a 433 // text/plain response 434 func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) { 435 ruid := GetRUID(r.Context()) 436 uri := GetURI(r.Context()) 437 log.Debug("handle.delete", "ruid", ruid) 438 deleteCount.Inc(1) 439 newKey, err := s.api.Delete(r.Context(), uri.Addr, uri.Path) 440 if err != nil { 441 deleteFail.Inc(1) 442 RespondError(w, r, fmt.Sprintf("could not delete from manifest: %v", err), http.StatusInternalServerError) 443 return 444 } 445 446 w.Header().Set("Content-Type", "text/plain") 447 w.WriteHeader(http.StatusOK) 448 fmt.Fprint(w, newKey) 449 } 450 451 // Handles feed manifest creation and feed updates 452 // The POST request admits a JSON structure as defined in the feeds package: `feed.updateRequestJSON` 453 // 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 454 func (s *Server) HandlePostFeed(w http.ResponseWriter, r *http.Request) { 455 ruid := GetRUID(r.Context()) 456 uri := GetURI(r.Context()) 457 log.Debug("handle.post.feed", "ruid", ruid) 458 var err error 459 460 // Creation and update must send feed.updateRequestJSON JSON structure 461 body, err := ioutil.ReadAll(r.Body) 462 if err != nil { 463 RespondError(w, r, err.Error(), http.StatusInternalServerError) 464 return 465 } 466 467 fd, err := s.api.ResolveFeed(r.Context(), uri, r.URL.Query()) 468 if err != nil { // couldn't parse query string or retrieve manifest 469 getFail.Inc(1) 470 httpStatus := http.StatusBadRequest 471 if err == api.ErrCannotLoadFeedManifest || err == api.ErrCannotResolveFeedURI { 472 httpStatus = http.StatusNotFound 473 } 474 RespondError(w, r, fmt.Sprintf("cannot retrieve feed from manifest: %s", err), httpStatus) 475 return 476 } 477 478 var updateRequest feed.Request 479 updateRequest.Feed = *fd 480 query := r.URL.Query() 481 482 if err := updateRequest.FromValues(query, body); err != nil { // decodes request from query parameters 483 RespondError(w, r, err.Error(), http.StatusBadRequest) 484 return 485 } 486 487 if updateRequest.IsUpdate() { 488 // Verify that the signature is intact and that the signer is authorized 489 // to update this feed 490 // Check this early, to avoid creating a feed and then not being able to set its first update. 491 if err = updateRequest.Verify(); err != nil { 492 RespondError(w, r, err.Error(), http.StatusForbidden) 493 return 494 } 495 _, err = s.api.FeedsUpdate(r.Context(), &updateRequest) 496 if err != nil { 497 RespondError(w, r, err.Error(), http.StatusInternalServerError) 498 return 499 } 500 } 501 502 if query.Get("manifest") == "1" { 503 // we create a manifest so we can retrieve feed updates with bzz:// later 504 // this manifest has a special "feed type" manifest, and saves the 505 // feed identification used to retrieve feed updates later 506 m, err := s.api.NewFeedManifest(r.Context(), &updateRequest.Feed) 507 if err != nil { 508 RespondError(w, r, fmt.Sprintf("failed to create feed manifest: %v", err), http.StatusInternalServerError) 509 return 510 } 511 // the key to the manifest will be passed back to the client 512 // the client can access the feed directly through its Feed member 513 // the manifest key can be set as content in the resolver of the ENS name 514 outdata, err := json.Marshal(m) 515 if err != nil { 516 RespondError(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) 517 return 518 } 519 fmt.Fprint(w, string(outdata)) 520 521 w.Header().Add("Content-type", "application/json") 522 } 523 } 524 525 // HandleGetFeed retrieves Swarm feeds updates: 526 // bzz-feed://<manifest address or ENS name> - get latest feed update, given a manifest address 527 // - or - 528 // specify user + topic (optional), subtopic name (optional) directly, without manifest: 529 // bzz-feed://?user=0x...&topic=0x...&name=subtopic name 530 // topic defaults to 0x000... if not specified. 531 // name defaults to empty string if not specified. 532 // thus, empty name and topic refers to the user's default feed. 533 // 534 // Optional parameters: 535 // time=xx - get the latest update before time (in epoch seconds) 536 // hint.time=xx - hint the lookup algorithm looking for updates at around that time 537 // hint.level=xx - hint the lookup algorithm looking for updates at around this frequency level 538 // meta=1 - get feed metadata and status information instead of performing a feed query 539 // NOTE: meta=1 will be deprecated in the near future 540 func (s *Server) HandleGetFeed(w http.ResponseWriter, r *http.Request) { 541 ruid := GetRUID(r.Context()) 542 uri := GetURI(r.Context()) 543 log.Debug("handle.get.feed", "ruid", ruid) 544 var err error 545 546 fd, err := s.api.ResolveFeed(r.Context(), uri, r.URL.Query()) 547 if err != nil { // couldn't parse query string or retrieve manifest 548 getFail.Inc(1) 549 httpStatus := http.StatusBadRequest 550 if err == api.ErrCannotLoadFeedManifest || err == api.ErrCannotResolveFeedURI { 551 httpStatus = http.StatusNotFound 552 } 553 RespondError(w, r, fmt.Sprintf("cannot retrieve feed information from manifest: %s", err), httpStatus) 554 return 555 } 556 557 // determine if the query specifies period and version or it is a metadata query 558 if r.URL.Query().Get("meta") == "1" { 559 unsignedUpdateRequest, err := s.api.FeedsNewRequest(r.Context(), fd) 560 if err != nil { 561 getFail.Inc(1) 562 RespondError(w, r, fmt.Sprintf("cannot retrieve feed metadata for feed=%s: %s", fd.Hex(), err), http.StatusNotFound) 563 return 564 } 565 rawResponse, err := unsignedUpdateRequest.MarshalJSON() 566 if err != nil { 567 RespondError(w, r, fmt.Sprintf("cannot encode unsigned feed update request: %v", err), http.StatusInternalServerError) 568 return 569 } 570 w.Header().Add("Content-type", "application/json") 571 w.WriteHeader(http.StatusOK) 572 fmt.Fprint(w, string(rawResponse)) 573 return 574 } 575 576 lookupParams := &feed.Query{Feed: *fd} 577 if err = lookupParams.FromValues(r.URL.Query()); err != nil { // parse period, version 578 RespondError(w, r, fmt.Sprintf("invalid feed update request:%s", err), http.StatusBadRequest) 579 return 580 } 581 582 data, err := s.api.FeedsLookup(r.Context(), lookupParams) 583 584 // any error from the switch statement will end up here 585 if err != nil { 586 code, err2 := s.translateFeedError(w, r, "feed lookup fail", err) 587 RespondError(w, r, err2.Error(), code) 588 return 589 } 590 591 // All ok, serve the retrieved update 592 log.Debug("Found update", "feed", fd.Hex(), "ruid", ruid) 593 w.Header().Set("Content-Type", api.MimeOctetStream) 594 http.ServeContent(w, r, "", time.Now(), bytes.NewReader(data)) 595 } 596 597 func (s *Server) translateFeedError(w http.ResponseWriter, r *http.Request, supErr string, err error) (int, error) { 598 code := 0 599 defaultErr := fmt.Errorf("%s: %v", supErr, err) 600 rsrcErr, ok := err.(*feed.Error) 601 if !ok && rsrcErr != nil { 602 code = rsrcErr.Code() 603 } 604 switch code { 605 case storage.ErrInvalidValue: 606 return http.StatusBadRequest, defaultErr 607 case storage.ErrNotFound, storage.ErrNotSynced, storage.ErrNothingToReturn, storage.ErrInit: 608 return http.StatusNotFound, defaultErr 609 case storage.ErrUnauthorized, storage.ErrInvalidSignature: 610 return http.StatusUnauthorized, defaultErr 611 case storage.ErrDataOverflow: 612 return http.StatusRequestEntityTooLarge, defaultErr 613 } 614 615 return http.StatusInternalServerError, defaultErr 616 } 617 618 // HandleGet handles a GET request to 619 // - bzz-raw://<key> and responds with the raw content stored at the 620 // given storage key 621 // - bzz-hash://<key> and responds with the hash of the content stored 622 // at the given storage key as a text/plain response 623 func (s *Server) HandleGet(w http.ResponseWriter, r *http.Request) { 624 ruid := GetRUID(r.Context()) 625 uri := GetURI(r.Context()) 626 log.Debug("handle.get", "ruid", ruid, "uri", uri) 627 getCount.Inc(1) 628 _, pass, _ := r.BasicAuth() 629 630 addr, err := s.api.ResolveURI(r.Context(), uri, pass) 631 if err != nil { 632 getFail.Inc(1) 633 RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) 634 return 635 } 636 w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. 637 638 log.Debug("handle.get: resolved", "ruid", ruid, "key", addr) 639 640 // if path is set, interpret <key> as a manifest and return the 641 // raw entry at the given path 642 643 etag := common.Bytes2Hex(addr) 644 noneMatchEtag := r.Header.Get("If-None-Match") 645 w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to manifest key or raw entry key. 646 if noneMatchEtag != "" { 647 if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), addr) { 648 w.WriteHeader(http.StatusNotModified) 649 return 650 } 651 } 652 653 // check the root chunk exists by retrieving the file's size 654 reader, isEncrypted := s.api.Retrieve(r.Context(), addr) 655 if _, err := reader.Size(r.Context(), nil); err != nil { 656 getFail.Inc(1) 657 RespondError(w, r, fmt.Sprintf("root chunk not found %s: %s", addr, err), http.StatusNotFound) 658 return 659 } 660 661 w.Header().Set("X-Decrypted", fmt.Sprintf("%v", isEncrypted)) 662 663 switch { 664 case uri.Raw(): 665 // allow the request to overwrite the content type using a query 666 // parameter 667 if typ := r.URL.Query().Get("content_type"); typ != "" { 668 w.Header().Set("Content-Type", typ) 669 } 670 http.ServeContent(w, r, "", time.Now(), reader) 671 case uri.Hash(): 672 w.Header().Set("Content-Type", "text/plain") 673 w.WriteHeader(http.StatusOK) 674 fmt.Fprint(w, addr) 675 } 676 } 677 678 // HandleGetList handles a GET request to bzz-list:/<manifest>/<path> and returns 679 // a list of all files contained in <manifest> under <path> grouped into 680 // common prefixes using "/" as a delimiter 681 func (s *Server) HandleGetList(w http.ResponseWriter, r *http.Request) { 682 ruid := GetRUID(r.Context()) 683 uri := GetURI(r.Context()) 684 _, credentials, _ := r.BasicAuth() 685 log.Debug("handle.get.list", "ruid", ruid, "uri", uri) 686 getListCount.Inc(1) 687 688 // ensure the root path has a trailing slash so that relative URLs work 689 if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { 690 http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) 691 return 692 } 693 694 addr, err := s.api.Resolve(r.Context(), uri.Addr) 695 if err != nil { 696 getListFail.Inc(1) 697 RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) 698 return 699 } 700 log.Debug("handle.get.list: resolved", "ruid", ruid, "key", addr) 701 702 list, err := s.api.GetManifestList(r.Context(), s.api.Decryptor(r.Context(), credentials), addr, uri.Path) 703 if err != nil { 704 getListFail.Inc(1) 705 if isDecryptError(err) { 706 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", addr.String())) 707 RespondError(w, r, err.Error(), http.StatusUnauthorized) 708 return 709 } 710 RespondError(w, r, err.Error(), http.StatusInternalServerError) 711 return 712 } 713 714 // if the client wants HTML (e.g. a browser) then render the list as a 715 // HTML index with relative URLs 716 if strings.Contains(r.Header.Get("Accept"), "text/html") { 717 w.Header().Set("Content-Type", "text/html") 718 err := TemplatesMap["bzz-list"].Execute(w, &htmlListData{ 719 URI: &api.URI{ 720 Scheme: "bzz", 721 Addr: uri.Addr, 722 Path: uri.Path, 723 }, 724 List: &list, 725 }) 726 if err != nil { 727 getListFail.Inc(1) 728 log.Error(fmt.Sprintf("error rendering list HTML: %s", err)) 729 } 730 return 731 } 732 733 w.Header().Set("Content-Type", "application/json") 734 json.NewEncoder(w).Encode(&list) 735 } 736 737 // HandleGetFile handles a GET request to bzz://<manifest>/<path> and responds 738 // with the content of the file at <path> from the given <manifest> 739 func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) { 740 ruid := GetRUID(r.Context()) 741 uri := GetURI(r.Context()) 742 _, credentials, _ := r.BasicAuth() 743 log.Debug("handle.get.file", "ruid", ruid, "uri", r.RequestURI) 744 getFileCount.Inc(1) 745 746 // ensure the root path has a trailing slash so that relative URLs work 747 if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { 748 http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) 749 return 750 } 751 var err error 752 manifestAddr := uri.Address() 753 754 if manifestAddr == nil { 755 manifestAddr, err = s.api.Resolve(r.Context(), uri.Addr) 756 if err != nil { 757 getFileFail.Inc(1) 758 RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound) 759 return 760 } 761 } else { 762 w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. 763 } 764 765 log.Debug("handle.get.file: resolved", "ruid", ruid, "key", manifestAddr) 766 767 reader, contentType, status, contentKey, err := s.api.Get(r.Context(), s.api.Decryptor(r.Context(), credentials), manifestAddr, uri.Path) 768 769 etag := common.Bytes2Hex(contentKey) 770 noneMatchEtag := r.Header.Get("If-None-Match") 771 w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to actual content key. 772 if noneMatchEtag != "" { 773 if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), contentKey) { 774 w.WriteHeader(http.StatusNotModified) 775 return 776 } 777 } 778 779 if err != nil { 780 if isDecryptError(err) { 781 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", manifestAddr)) 782 RespondError(w, r, err.Error(), http.StatusUnauthorized) 783 return 784 } 785 786 switch status { 787 case http.StatusNotFound: 788 getFileNotFound.Inc(1) 789 RespondError(w, r, err.Error(), http.StatusNotFound) 790 default: 791 getFileFail.Inc(1) 792 RespondError(w, r, err.Error(), http.StatusInternalServerError) 793 } 794 return 795 } 796 797 //the request results in ambiguous files 798 //e.g. /read with readme.md and readinglist.txt available in manifest 799 if status == http.StatusMultipleChoices { 800 list, err := s.api.GetManifestList(r.Context(), s.api.Decryptor(r.Context(), credentials), manifestAddr, uri.Path) 801 if err != nil { 802 getFileFail.Inc(1) 803 if isDecryptError(err) { 804 w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", manifestAddr)) 805 RespondError(w, r, err.Error(), http.StatusUnauthorized) 806 return 807 } 808 RespondError(w, r, err.Error(), http.StatusInternalServerError) 809 return 810 } 811 812 log.Debug(fmt.Sprintf("Multiple choices! --> %v", list), "ruid", ruid) 813 //show a nice page links to available entries 814 ShowMultipleChoices(w, r, list) 815 return 816 } 817 818 // check the root chunk exists by retrieving the file's size 819 if _, err := reader.Size(r.Context(), nil); err != nil { 820 getFileNotFound.Inc(1) 821 RespondError(w, r, fmt.Sprintf("file not found %s: %s", uri, err), http.StatusNotFound) 822 return 823 } 824 825 if contentType != "" { 826 w.Header().Set("Content-Type", contentType) 827 } 828 829 fileName := uri.Addr 830 if found := path.Base(uri.Path); found != "" && found != "." && found != "/" { 831 fileName = found 832 } 833 w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) 834 835 http.ServeContent(w, r, fileName, time.Now(), newBufferedReadSeeker(reader, getFileBufferSize)) 836 } 837 838 // The size of buffer used for bufio.Reader on LazyChunkReader passed to 839 // http.ServeContent in HandleGetFile. 840 // Warning: This value influences the number of chunk requests and chunker join goroutines 841 // per file request. 842 // Recommended value is 4 times the io.Copy default buffer value which is 32kB. 843 const getFileBufferSize = 4 * 32 * 1024 844 845 // bufferedReadSeeker wraps bufio.Reader to expose Seek method 846 // from the provied io.ReadSeeker in newBufferedReadSeeker. 847 type bufferedReadSeeker struct { 848 r io.Reader 849 s io.Seeker 850 } 851 852 // newBufferedReadSeeker creates a new instance of bufferedReadSeeker, 853 // out of io.ReadSeeker. Argument `size` is the size of the read buffer. 854 func newBufferedReadSeeker(readSeeker io.ReadSeeker, size int) bufferedReadSeeker { 855 return bufferedReadSeeker{ 856 r: bufio.NewReaderSize(readSeeker, size), 857 s: readSeeker, 858 } 859 } 860 861 func (b bufferedReadSeeker) Read(p []byte) (n int, err error) { 862 return b.r.Read(p) 863 } 864 865 func (b bufferedReadSeeker) Seek(offset int64, whence int) (int64, error) { 866 return b.s.Seek(offset, whence) 867 } 868 869 type loggingResponseWriter struct { 870 http.ResponseWriter 871 statusCode int 872 } 873 874 func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { 875 return &loggingResponseWriter{w, http.StatusOK} 876 } 877 878 func (lrw *loggingResponseWriter) WriteHeader(code int) { 879 lrw.statusCode = code 880 lrw.ResponseWriter.WriteHeader(code) 881 } 882 883 func isDecryptError(err error) bool { 884 return strings.Contains(err.Error(), api.ErrDecrypt.Error()) 885 }