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