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