swarm/api/http: refactored http package (#17309)

This commit is contained in:
Elad 2018-08-07 11:56:55 +02:00 committed by Balint Gabor
parent 64a4e89504
commit 93fe16b0a5
11 changed files with 822 additions and 1270 deletions

View File

@ -339,8 +339,7 @@ func (a *API) Get(ctx context.Context, manifestAddr storage.Address, path string
if err != nil { if err != nil {
apiGetNotFound.Inc(1) apiGetNotFound.Inc(1)
status = http.StatusNotFound status = http.StatusNotFound
log.Warn(fmt.Sprintf("loadManifestTrie error: %v", err)) return nil, "", http.StatusNotFound, nil, err
return
} }
log.Debug("trie getting entry", "key", manifestAddr, "path", path) log.Debug("trie getting entry", "key", manifestAddr, "path", path)

View File

@ -1,208 +0,0 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
Show nicely (but simple) formatted HTML error pages (or respond with JSON
if the appropriate `Accept` header is set)) for the http package.
*/
package http
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/swarm/api"
l "github.com/ethereum/go-ethereum/swarm/log"
)
//templateMap holds a mapping of an HTTP error code to a template
var templateMap map[int]*template.Template
var caseErrors []CaseError
//metrics variables
var (
htmlCounter = metrics.NewRegisteredCounter("api.http.errorpage.html.count", nil)
jsonCounter = metrics.NewRegisteredCounter("api.http.errorpage.json.count", nil)
)
//parameters needed for formatting the correct HTML page
type ResponseParams struct {
Msg string
Code int
Timestamp string
template *template.Template
Details template.HTML
}
//a custom error case struct that would be used to store validators and
//additional error info to display with client responses.
type CaseError struct {
Validator func(*Request) bool
Msg func(*Request) string
}
//we init the error handling right on boot time, so lookup and http response is fast
func init() {
initErrHandling()
}
func initErrHandling() {
//pages are saved as strings - get these strings
genErrPage := GetGenericErrorPage()
notFoundPage := GetNotFoundErrorPage()
multipleChoicesPage := GetMultipleChoicesErrorPage()
//map the codes to the available pages
tnames := map[int]string{
0: genErrPage, //default
http.StatusBadRequest: genErrPage,
http.StatusNotFound: notFoundPage,
http.StatusMultipleChoices: multipleChoicesPage,
http.StatusInternalServerError: genErrPage,
}
templateMap = make(map[int]*template.Template)
for code, tname := range tnames {
//assign formatted HTML to the code
templateMap[code] = template.Must(template.New(fmt.Sprintf("%d", code)).Parse(tname))
}
caseErrors = []CaseError{
{
Validator: func(r *Request) bool { return r.uri != nil && r.uri.Addr != "" && strings.HasPrefix(r.uri.Addr, "0x") },
Msg: func(r *Request) string {
uriCopy := r.uri
uriCopy.Addr = strings.TrimPrefix(uriCopy.Addr, "0x")
return fmt.Sprintf(`The requested hash seems to be prefixed with '0x'. You will be redirected to the correct URL within 5 seconds.<br/>
Please click <a href='%[1]s'>here</a> if your browser does not redirect you.<script>setTimeout("location.href='%[1]s';",5000);</script>`, "/"+uriCopy.String())
},
}}
}
//ValidateCaseErrors is a method that process the request object through certain validators
//that assert if certain conditions are met for further information to log as an error
func ValidateCaseErrors(r *Request) string {
for _, err := range caseErrors {
if err.Validator(r) {
return err.Msg(r)
}
}
return ""
}
//ShowMultipeChoices is used when a user requests a resource in a manifest which results
//in ambiguous results. It returns a HTML page with clickable links of each of the entry
//in the manifest which fits the request URI ambiguity.
//For example, if the user requests bzz:/<hash>/read and that manifest contains entries
//"readme.md" and "readinglist.txt", a HTML page is returned with this two links.
//This only applies if the manifest has no default entry
func ShowMultipleChoices(w http.ResponseWriter, req *Request, list api.ManifestList) {
msg := ""
if list.Entries == nil {
Respond(w, req, "Could not resolve", http.StatusInternalServerError)
return
}
//make links relative
//requestURI comes with the prefix of the ambiguous path, e.g. "read" for "readme.md" and "readinglist.txt"
//to get clickable links, need to remove the ambiguous path, i.e. "read"
idx := strings.LastIndex(req.RequestURI, "/")
if idx == -1 {
Respond(w, req, "Internal Server Error", http.StatusInternalServerError)
return
}
//remove ambiguous part
base := req.RequestURI[:idx+1]
for _, e := range list.Entries {
//create clickable link for each entry
msg += "<a href='" + base + e.Path + "'>" + e.Path + "</a><br/>"
}
Respond(w, req, msg, http.StatusMultipleChoices)
}
//Respond is used to show an HTML page to a client.
//If there is an `Accept` header of `application/json`, JSON will be returned instead
//The function just takes a string message which will be displayed in the error page.
//The code is used to evaluate which template will be displayed
//(and return the correct HTTP status code)
func Respond(w http.ResponseWriter, req *Request, msg string, code int) {
additionalMessage := ValidateCaseErrors(req)
switch code {
case http.StatusInternalServerError:
log.Output(msg, log.LvlError, l.CallDepth, "ruid", req.ruid, "code", code)
case http.StatusMultipleChoices:
log.Output(msg, log.LvlDebug, l.CallDepth, "ruid", req.ruid, "code", code)
listURI := api.URI{
Scheme: "bzz-list",
Addr: req.uri.Addr,
Path: req.uri.Path,
}
additionalMessage = fmt.Sprintf(`<a href="/%s">multiple choices</a>`, listURI.String())
default:
log.Output(msg, log.LvlDebug, l.CallDepth, "ruid", req.ruid, "code", code)
}
if code >= 400 {
w.Header().Del("Cache-Control") //avoid sending cache headers for errors!
w.Header().Del("ETag")
}
respond(w, &req.Request, &ResponseParams{
Code: code,
Msg: msg,
Details: template.HTML(additionalMessage),
Timestamp: time.Now().Format(time.RFC1123),
template: getTemplate(code),
})
}
//evaluate if client accepts html or json response
func respond(w http.ResponseWriter, r *http.Request, params *ResponseParams) {
w.WriteHeader(params.Code)
if r.Header.Get("Accept") == "application/json" {
respondJSON(w, params)
} else {
respondHTML(w, params)
}
}
//return a HTML page
func respondHTML(w http.ResponseWriter, params *ResponseParams) {
htmlCounter.Inc(1)
err := params.template.Execute(w, params)
if err != nil {
log.Error(err.Error())
}
}
//return JSON
func respondJSON(w http.ResponseWriter, params *ResponseParams) {
jsonCounter.Inc(1)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(params)
}
//get the HTML template for a given code
func getTemplate(code int) *template.Template {
if val, tmpl := templateMap[code]; tmpl {
return val
}
return templateMap[0]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,95 @@
package http
import (
"fmt"
"net/http"
"runtime/debug"
"strings"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/log"
"github.com/ethereum/go-ethereum/swarm/spancontext"
"github.com/pborman/uuid"
)
// Adapt chains h (main request handler) main handler to adapters (middleware handlers)
// Please note that the order of execution for `adapters` is FIFO (adapters[0] will be executed first)
func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
for i := range adapters {
adapter := adapters[len(adapters)-1-i]
h = adapter(h)
}
return h
}
type Adapter func(http.Handler) http.Handler
func SetRequestID(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(SetRUID(r.Context(), uuid.New()[:8]))
metrics.GetOrRegisterCounter(fmt.Sprintf("http.request.%s", r.Method), nil).Inc(1)
log.Info("created ruid for request", "ruid", GetRUID(r.Context()), "method", r.Method, "url", r.RequestURI)
h.ServeHTTP(w, r)
})
}
func ParseURI(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uri, err := api.Parse(strings.TrimLeft(r.URL.Path, "/"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
RespondError(w, r, fmt.Sprintf("invalid URI %q", r.URL.Path), http.StatusBadRequest)
return
}
if uri.Addr != "" && strings.HasPrefix(uri.Addr, "0x") {
uri.Addr = strings.TrimPrefix(uri.Addr, "0x")
msg := fmt.Sprintf(`The requested hash seems to be prefixed with '0x'. You will be redirected to the correct URL within 5 seconds.<br/>
Please click <a href='%[1]s'>here</a> if your browser does not redirect you within 5 seconds.<script>setTimeout("location.href='%[1]s';",5000);</script>`, "/"+uri.String())
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(msg))
return
}
ctx := r.Context()
r = r.WithContext(SetURI(ctx, uri))
log.Debug("parsed request path", "ruid", GetRUID(r.Context()), "method", r.Method, "uri.Addr", uri.Addr, "uri.Path", uri.Path, "uri.Scheme", uri.Scheme)
h.ServeHTTP(w, r)
})
}
func InitLoggingResponseWriter(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := newLoggingResponseWriter(w)
h.ServeHTTP(writer, r)
})
}
func InstrumentOpenTracing(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uri := GetURI(r.Context())
if uri == nil || r.Method == "" || (uri != nil && uri.Scheme == "") {
h.ServeHTTP(w, r) // soft fail
return
}
spanName := fmt.Sprintf("http.%s.%s", r.Method, uri.Scheme)
ctx, sp := spancontext.StartSpan(r.Context(), spanName)
defer sp.Finish()
h.ServeHTTP(w, r.WithContext(ctx))
})
}
func RecoverPanic(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Error("panic recovery!", "stack trace", debug.Stack(), "url", r.URL.String(), "headers", r.Header)
}
}()
h.ServeHTTP(w, r)
})
}

139
swarm/api/http/response.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package http
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/swarm/api"
)
//metrics variables
var (
htmlCounter = metrics.NewRegisteredCounter("api.http.errorpage.html.count", nil)
jsonCounter = metrics.NewRegisteredCounter("api.http.errorpage.json.count", nil)
plaintextCounter = metrics.NewRegisteredCounter("api.http.errorpage.plaintext.count", nil)
)
//parameters needed for formatting the correct HTML page
type ResponseParams struct {
Msg template.HTML
Code int
Timestamp string
template *template.Template
Details template.HTML
}
//ShowMultipeChoices is used when a user requests a resource in a manifest which results
//in ambiguous results. It returns a HTML page with clickable links of each of the entry
//in the manifest which fits the request URI ambiguity.
//For example, if the user requests bzz:/<hash>/read and that manifest contains entries
//"readme.md" and "readinglist.txt", a HTML page is returned with this two links.
//This only applies if the manifest has no default entry
func ShowMultipleChoices(w http.ResponseWriter, r *http.Request, list api.ManifestList) {
log.Debug("ShowMultipleChoices", "ruid", GetRUID(r.Context()), "uri", GetURI(r.Context()))
msg := ""
if list.Entries == nil {
RespondError(w, r, "Could not resolve", http.StatusInternalServerError)
return
}
requestUri := strings.TrimPrefix(r.RequestURI, "/")
uri, err := api.Parse(requestUri)
if err != nil {
RespondError(w, r, "Bad Request", http.StatusBadRequest)
}
uri.Scheme = "bzz-list"
//request the same url just with bzz-list
msg += fmt.Sprintf("Disambiguation:<br/>Your request may refer to multiple choices.<br/>Click <a class=\"orange\" href='"+"/"+uri.String()+"'>here</a> if your browser does not redirect you within 5 seconds.<script>setTimeout(\"location.href='%s';\",5000);</script><br/>", "/"+uri.String())
RespondTemplate(w, r, "error", msg, http.StatusMultipleChoices)
}
func RespondTemplate(w http.ResponseWriter, r *http.Request, templateName, msg string, code int) {
log.Debug("RespondTemplate", "ruid", GetRUID(r.Context()), "uri", GetURI(r.Context()))
respond(w, r, &ResponseParams{
Code: code,
Msg: template.HTML(msg),
Timestamp: time.Now().Format(time.RFC1123),
template: TemplatesMap[templateName],
})
}
func RespondError(w http.ResponseWriter, r *http.Request, msg string, code int) {
log.Debug("RespondError", "ruid", GetRUID(r.Context()), "uri", GetURI(r.Context()))
RespondTemplate(w, r, "error", msg, code)
}
//evaluate if client accepts html or json response
func respond(w http.ResponseWriter, r *http.Request, params *ResponseParams) {
w.WriteHeader(params.Code)
if params.Code >= 400 {
w.Header().Del("Cache-Control") //avoid sending cache headers for errors!
w.Header().Del("ETag")
}
acceptHeader := r.Header.Get("Accept")
// this cannot be in a switch form since an Accept header can be in the form of "Accept: */*, text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8"
if strings.Contains(acceptHeader, "application/json") {
if err := respondJSON(w, r, params); err != nil {
RespondError(w, r, "Internal server error", http.StatusInternalServerError)
}
} else if strings.Contains(acceptHeader, "text/html") {
respondHTML(w, r, params)
} else {
respondPlaintext(w, r, params) //returns nice errors for curl
}
}
//return a HTML page
func respondHTML(w http.ResponseWriter, r *http.Request, params *ResponseParams) {
htmlCounter.Inc(1)
log.Debug("respondHTML", "ruid", GetRUID(r.Context()))
err := params.template.Execute(w, params)
if err != nil {
log.Error(err.Error())
}
}
//return JSON
func respondJSON(w http.ResponseWriter, r *http.Request, params *ResponseParams) error {
jsonCounter.Inc(1)
log.Debug("respondJSON", "ruid", GetRUID(r.Context()))
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(params)
}
//return plaintext
func respondPlaintext(w http.ResponseWriter, r *http.Request, params *ResponseParams) error {
plaintextCounter.Inc(1)
log.Debug("respondPlaintext", "ruid", GetRUID(r.Context()))
w.Header().Set("Content-Type", "text/plain")
strToWrite := "Code: " + fmt.Sprintf("%d", params.Code) + "\n"
strToWrite += "Message: " + string(params.Msg) + "\n"
strToWrite += "Timestamp: " + params.Timestamp + "\n"
_, err := w.Write([]byte(strToWrite))
return err
}

View File

@ -44,7 +44,7 @@ func TestError(t *testing.T) {
defer resp.Body.Close() defer resp.Body.Close()
respbody, err = ioutil.ReadAll(resp.Body) respbody, err = ioutil.ReadAll(resp.Body)
if resp.StatusCode != 400 && !strings.Contains(string(respbody), "Invalid URI &#34;/this_should_fail_as_no_bzz_protocol_present&#34;: unknown scheme") { if resp.StatusCode != 404 && !strings.Contains(string(respbody), "Invalid URI &#34;/this_should_fail_as_no_bzz_protocol_present&#34;: unknown scheme") {
t.Fatalf("Response body does not match, expected: %v, to contain: %v; received code %d, expected code: %d", string(respbody), "Invalid bzz URI: unknown scheme", 400, resp.StatusCode) t.Fatalf("Response body does not match, expected: %v, to contain: %v; received code %d, expected code: %d", string(respbody), "Invalid bzz URI: unknown scheme", 400, resp.StatusCode)
} }

38
swarm/api/http/sctx.go Normal file
View File

@ -0,0 +1,38 @@
package http
import (
"context"
"github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/sctx"
)
type contextKey int
const (
uriKey contextKey = iota
)
func GetRUID(ctx context.Context) string {
v, ok := ctx.Value(sctx.HTTPRequestIDKey).(string)
if ok {
return v
}
return "xxxxxxxx"
}
func SetRUID(ctx context.Context, ruid string) context.Context {
return context.WithValue(ctx, sctx.HTTPRequestIDKey, ruid)
}
func GetURI(ctx context.Context) *api.URI {
v, ok := ctx.Value(uriKey).(*api.URI)
if ok {
return v
}
return nil
}
func SetURI(ctx context.Context, uri *api.URI) context.Context {
return context.WithValue(ctx, uriKey, uri)
}

View File

@ -41,12 +41,9 @@ import (
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/swarm/api" "github.com/ethereum/go-ethereum/swarm/api"
"github.com/ethereum/go-ethereum/swarm/log" "github.com/ethereum/go-ethereum/swarm/log"
"github.com/ethereum/go-ethereum/swarm/spancontext"
"github.com/ethereum/go-ethereum/swarm/storage" "github.com/ethereum/go-ethereum/swarm/storage"
"github.com/ethereum/go-ethereum/swarm/storage/mru" "github.com/ethereum/go-ethereum/swarm/storage/mru"
opentracing "github.com/opentracing/opentracing-go"
"github.com/pborman/uuid"
"github.com/rs/cors" "github.com/rs/cors"
) )
@ -72,6 +69,17 @@ var (
getListFail = metrics.NewRegisteredCounter("api.http.get.list.fail", nil) getListFail = metrics.NewRegisteredCounter("api.http.get.list.fail", nil)
) )
type methodHandler map[string]http.Handler
func (m methodHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
v, ok := m[r.Method]
if ok {
v.ServeHTTP(rw, r)
return
}
rw.WriteHeader(http.StatusMethodNotAllowed)
}
func NewServer(api *api.API, corsString string) *Server { func NewServer(api *api.API, corsString string) *Server {
var allowedOrigins []string var allowedOrigins []string
for _, domain := range strings.Split(corsString, ",") { for _, domain := range strings.Split(corsString, ",") {
@ -84,20 +92,79 @@ func NewServer(api *api.API, corsString string) *Server {
AllowedHeaders: []string{"*"}, AllowedHeaders: []string{"*"},
}) })
mux := http.NewServeMux()
server := &Server{api: api} server := &Server{api: api}
mux.HandleFunc("/bzz:/", server.WrapHandler(true, server.HandleBzz))
mux.HandleFunc("/bzz-raw:/", server.WrapHandler(true, server.HandleBzzRaw))
mux.HandleFunc("/bzz-immutable:/", server.WrapHandler(true, server.HandleBzzImmutable))
mux.HandleFunc("/bzz-hash:/", server.WrapHandler(true, server.HandleBzzHash))
mux.HandleFunc("/bzz-list:/", server.WrapHandler(true, server.HandleBzzList))
mux.HandleFunc("/bzz-resource:/", server.WrapHandler(true, server.HandleBzzResource))
mux.HandleFunc("/", server.WrapHandler(false, server.HandleRootPaths)) defaultMiddlewares := []Adapter{
mux.HandleFunc("/robots.txt", server.WrapHandler(false, server.HandleRootPaths)) RecoverPanic,
mux.HandleFunc("/favicon.ico", server.WrapHandler(false, server.HandleRootPaths)) SetRequestID,
InitLoggingResponseWriter,
ParseURI,
InstrumentOpenTracing,
}
mux := http.NewServeMux()
mux.Handle("/bzz:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleBzzGet),
defaultMiddlewares...,
),
"POST": Adapt(
http.HandlerFunc(server.HandlePostFiles),
defaultMiddlewares...,
),
"DELETE": Adapt(
http.HandlerFunc(server.HandleDelete),
defaultMiddlewares...,
),
})
mux.Handle("/bzz-raw:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleGet),
defaultMiddlewares...,
),
"POST": Adapt(
http.HandlerFunc(server.HandlePostRaw),
defaultMiddlewares...,
),
})
mux.Handle("/bzz-immutable:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleGet),
defaultMiddlewares...,
),
})
mux.Handle("/bzz-hash:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleGet),
defaultMiddlewares...,
),
})
mux.Handle("/bzz-list:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleGetList),
defaultMiddlewares...,
),
})
mux.Handle("/bzz-resource:/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleGetResource),
defaultMiddlewares...,
),
"POST": Adapt(
http.HandlerFunc(server.HandlePostResource),
defaultMiddlewares...,
),
})
mux.Handle("/", methodHandler{
"GET": Adapt(
http.HandlerFunc(server.HandleRootPaths),
SetRequestID,
InitLoggingResponseWriter,
),
})
server.Handler = c.Handler(mux) server.Handler = c.Handler(mux)
return server return server
} }
@ -105,139 +172,6 @@ func (s *Server) ListenAndServe(addr string) error {
return http.ListenAndServe(addr, s) return http.ListenAndServe(addr, s)
} }
func (s *Server) HandleRootPaths(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
if r.RequestURI == "/" {
if strings.Contains(r.Header.Get("Accept"), "text/html") {
err := landingPageTemplate.Execute(w, nil)
if err != nil {
log.Error(fmt.Sprintf("error rendering landing page: %s", err))
}
return
}
if strings.Contains(r.Header.Get("Accept"), "application/json") {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode("Welcome to Swarm!")
return
}
}
if r.URL.Path == "/robots.txt" {
w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat))
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
return
}
Respond(w, r, "Bad Request", http.StatusBadRequest)
default:
Respond(w, r, "Not Found", http.StatusNotFound)
}
}
func (s *Server) HandleBzz(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetBzz")
if r.Header.Get("Accept") == "application/x-tar" {
reader, err := s.api.GetDirectoryTar(r.Context(), r.uri)
if err != nil {
Respond(w, r, fmt.Sprintf("Had an error building the tarball: %v", err), http.StatusInternalServerError)
}
defer reader.Close()
w.Header().Set("Content-Type", "application/x-tar")
w.WriteHeader(http.StatusOK)
io.Copy(w, reader)
return
}
s.HandleGetFile(w, r)
case http.MethodPost:
log.Debug("handlePostFiles")
s.HandlePostFiles(w, r)
case http.MethodDelete:
log.Debug("handleBzzDelete")
s.HandleDelete(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) HandleBzzRaw(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetRaw")
s.HandleGet(w, r)
case http.MethodPost:
log.Debug("handlePostRaw")
s.HandlePostRaw(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) HandleBzzImmutable(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetHash")
s.HandleGetList(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) HandleBzzHash(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetHash")
s.HandleGet(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) HandleBzzList(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetHash")
s.HandleGetList(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) HandleBzzResource(w http.ResponseWriter, r *Request) {
switch r.Method {
case http.MethodGet:
log.Debug("handleGetResource")
s.HandleGetResource(w, r)
case http.MethodPost:
log.Debug("handlePostResource")
s.HandlePostResource(w, r)
default:
Respond(w, r, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) WrapHandler(parseBzzUri bool, h func(http.ResponseWriter, *Request)) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer metrics.GetOrRegisterResettingTimer(fmt.Sprintf("http.request.%s.time", r.Method), nil).UpdateSince(time.Now())
req := &Request{Request: *r, ruid: uuid.New()[:8]}
metrics.GetOrRegisterCounter(fmt.Sprintf("http.request.%s", r.Method), nil).Inc(1)
log.Info("serving request", "ruid", req.ruid, "method", r.Method, "url", r.RequestURI)
// wrapping the ResponseWriter, so that we get the response code set by http.ServeContent
w := newLoggingResponseWriter(rw)
if parseBzzUri {
uri, err := api.Parse(strings.TrimLeft(r.URL.Path, "/"))
if err != nil {
Respond(w, req, fmt.Sprintf("invalid URI %q", r.URL.Path), http.StatusBadRequest)
return
}
req.uri = uri
log.Debug("parsed request path", "ruid", req.ruid, "method", req.Method, "uri.Addr", req.uri.Addr, "uri.Path", req.uri.Path, "uri.Scheme", req.uri.Scheme)
}
h(w, req) // call original
log.Info("served response", "ruid", req.ruid, "code", w.statusCode)
})
}
// browser API for registering bzz url scheme handlers: // browser API for registering bzz url scheme handlers:
// https://developer.mozilla.org/en/docs/Web-based_protocol_handlers // https://developer.mozilla.org/en/docs/Web-based_protocol_handlers
// electron (chromium) api for registering bzz url scheme handlers: // electron (chromium) api for registering bzz url scheme handlers:
@ -247,59 +181,81 @@ type Server struct {
api *api.API api *api.API
} }
// Request wraps http.Request and also includes the parsed bzz URI func (s *Server) HandleBzzGet(w http.ResponseWriter, r *http.Request) {
type Request struct { log.Debug("handleBzzGet", "ruid", GetRUID(r.Context()))
http.Request if r.Header.Get("Accept") == "application/x-tar" {
uri := GetURI(r.Context())
reader, err := s.api.GetDirectoryTar(r.Context(), uri)
if err != nil {
RespondError(w, r, fmt.Sprintf("Had an error building the tarball: %v", err), http.StatusInternalServerError)
}
defer reader.Close()
uri *api.URI w.Header().Set("Content-Type", "application/x-tar")
ruid string // request unique id w.WriteHeader(http.StatusOK)
io.Copy(w, reader)
return
}
s.HandleGetFile(w, r)
}
func (s *Server) HandleRootPaths(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/":
RespondTemplate(w, r, "landing-page", "Swarm: Please request a valid ENS or swarm hash with the appropriate bzz scheme", 200)
return
case "/robots.txt":
w.Header().Set("Last-Modified", time.Now().Format(http.TimeFormat))
fmt.Fprintf(w, "User-agent: *\nDisallow: /")
case "/favicon.ico":
w.WriteHeader(http.StatusOK)
w.Write(faviconBytes)
default:
RespondError(w, r, "Not Found", http.StatusNotFound)
}
} }
// HandlePostRaw handles a POST request to a raw bzz-raw:/ URI, stores the request // HandlePostRaw handles a POST request to a raw bzz-raw:/ URI, stores the request
// body in swarm and returns the resulting storage address as a text/plain response // body in swarm and returns the resulting storage address as a text/plain response
func (s *Server) HandlePostRaw(w http.ResponseWriter, r *Request) { func (s *Server) HandlePostRaw(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.post.raw", "ruid", r.ruid) ruid := GetRUID(r.Context())
log.Debug("handle.post.raw", "ruid", ruid)
postRawCount.Inc(1) postRawCount.Inc(1)
ctx := r.Context()
var sp opentracing.Span
ctx, sp = spancontext.StartSpan(
ctx,
"http.post.raw")
defer sp.Finish()
toEncrypt := false toEncrypt := false
if r.uri.Addr == "encrypt" { uri := GetURI(r.Context())
if uri.Addr == "encrypt" {
toEncrypt = true toEncrypt = true
} }
if r.uri.Path != "" { if uri.Path != "" {
postRawFail.Inc(1) postRawFail.Inc(1)
Respond(w, r, "raw POST request cannot contain a path", http.StatusBadRequest) RespondError(w, r, "raw POST request cannot contain a path", http.StatusBadRequest)
return return
} }
if r.uri.Addr != "" && r.uri.Addr != "encrypt" { if uri.Addr != "" && uri.Addr != "encrypt" {
postRawFail.Inc(1) postRawFail.Inc(1)
Respond(w, r, "raw POST request addr can only be empty or \"encrypt\"", http.StatusBadRequest) RespondError(w, r, "raw POST request addr can only be empty or \"encrypt\"", http.StatusBadRequest)
return return
} }
if r.Header.Get("Content-Length") == "" { if r.Header.Get("Content-Length") == "" {
postRawFail.Inc(1) postRawFail.Inc(1)
Respond(w, r, "missing Content-Length header in request", http.StatusBadRequest) RespondError(w, r, "missing Content-Length header in request", http.StatusBadRequest)
return return
} }
addr, _, err := s.api.Store(ctx, r.Body, r.ContentLength, toEncrypt) addr, _, err := s.api.Store(r.Context(), r.Body, r.ContentLength, toEncrypt)
if err != nil { if err != nil {
postRawFail.Inc(1) postRawFail.Inc(1)
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
log.Debug("stored content", "ruid", r.ruid, "key", addr) log.Debug("stored content", "ruid", ruid, "key", addr)
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -311,55 +267,49 @@ func (s *Server) HandlePostRaw(w http.ResponseWriter, r *Request) {
// (either a tar archive or multipart form), adds those files either to an // (either a tar archive or multipart form), adds those files either to an
// existing manifest or to a new manifest under <path> and returns the // existing manifest or to a new manifest under <path> and returns the
// resulting manifest hash as a text/plain response // resulting manifest hash as a text/plain response
func (s *Server) HandlePostFiles(w http.ResponseWriter, r *Request) { func (s *Server) HandlePostFiles(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.post.files", "ruid", r.ruid) ruid := GetRUID(r.Context())
log.Debug("handle.post.files", "ruid", ruid)
postFilesCount.Inc(1) postFilesCount.Inc(1)
var sp opentracing.Span
ctx := r.Context()
ctx, sp = spancontext.StartSpan(
ctx,
"http.post.files")
defer sp.Finish()
contentType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) contentType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
postFilesFail.Inc(1) postFilesFail.Inc(1)
Respond(w, r, err.Error(), http.StatusBadRequest) RespondError(w, r, err.Error(), http.StatusBadRequest)
return return
} }
toEncrypt := false toEncrypt := false
if r.uri.Addr == "encrypt" { uri := GetURI(r.Context())
if uri.Addr == "encrypt" {
toEncrypt = true toEncrypt = true
} }
var addr storage.Address var addr storage.Address
if r.uri.Addr != "" && r.uri.Addr != "encrypt" { if uri.Addr != "" && uri.Addr != "encrypt" {
addr, err = s.api.Resolve(r.Context(), r.uri) addr, err = s.api.Resolve(r.Context(), uri)
if err != nil { if err != nil {
postFilesFail.Inc(1) postFilesFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusInternalServerError)
return return
} }
log.Debug("resolved key", "ruid", r.ruid, "key", addr) log.Debug("resolved key", "ruid", ruid, "key", addr)
} else { } else {
addr, err = s.api.NewManifest(r.Context(), toEncrypt) addr, err = s.api.NewManifest(r.Context(), toEncrypt)
if err != nil { if err != nil {
postFilesFail.Inc(1) postFilesFail.Inc(1)
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
log.Debug("new manifest", "ruid", r.ruid, "key", addr) log.Debug("new manifest", "ruid", ruid, "key", addr)
} }
newAddr, err := s.api.UpdateManifest(ctx, addr, func(mw *api.ManifestWriter) error { newAddr, err := s.api.UpdateManifest(r.Context(), addr, func(mw *api.ManifestWriter) error {
switch contentType { switch contentType {
case "application/x-tar": case "application/x-tar":
_, err := s.handleTarUpload(r, mw) _, err := s.handleTarUpload(r, mw)
if err != nil { if err != nil {
Respond(w, r, fmt.Sprintf("error uploading tarball: %v", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("error uploading tarball: %v", err), http.StatusInternalServerError)
return err return err
} }
return nil return nil
@ -372,30 +322,31 @@ func (s *Server) HandlePostFiles(w http.ResponseWriter, r *Request) {
}) })
if err != nil { if err != nil {
postFilesFail.Inc(1) postFilesFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot create manifest: %s", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("cannot create manifest: %s", err), http.StatusInternalServerError)
return return
} }
log.Debug("stored content", "ruid", r.ruid, "key", newAddr) log.Debug("stored content", "ruid", ruid, "key", newAddr)
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprint(w, newAddr) fmt.Fprint(w, newAddr)
} }
func (s *Server) handleTarUpload(r *Request, mw *api.ManifestWriter) (storage.Address, error) { func (s *Server) handleTarUpload(r *http.Request, mw *api.ManifestWriter) (storage.Address, error) {
log.Debug("handle.tar.upload", "ruid", r.ruid) log.Debug("handle.tar.upload", "ruid", GetRUID(r.Context()))
key, err := s.api.UploadTar(r.Context(), r.Body, r.uri.Path, mw) key, err := s.api.UploadTar(r.Context(), r.Body, GetURI(r.Context()).Path, mw)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return key, nil return key, nil
} }
func (s *Server) handleMultipartUpload(req *Request, boundary string, mw *api.ManifestWriter) error { func (s *Server) handleMultipartUpload(r *http.Request, boundary string, mw *api.ManifestWriter) error {
log.Debug("handle.multipart.upload", "ruid", req.ruid) ruid := GetRUID(r.Context())
mr := multipart.NewReader(req.Body, boundary) log.Debug("handle.multipart.upload", "ruid", ruid)
mr := multipart.NewReader(r.Body, boundary)
for { for {
part, err := mr.NextPart() part, err := mr.NextPart()
if err == io.EOF { if err == io.EOF {
@ -435,48 +386,52 @@ func (s *Server) handleMultipartUpload(req *Request, boundary string, mw *api.Ma
if name == "" { if name == "" {
name = part.FormName() name = part.FormName()
} }
path := path.Join(req.uri.Path, name) uri := GetURI(r.Context())
path := path.Join(uri.Path, name)
entry := &api.ManifestEntry{ entry := &api.ManifestEntry{
Path: path, Path: path,
ContentType: part.Header.Get("Content-Type"), ContentType: part.Header.Get("Content-Type"),
Size: size, Size: size,
ModTime: time.Now(), ModTime: time.Now(),
} }
log.Debug("adding path to new manifest", "ruid", req.ruid, "bytes", entry.Size, "path", entry.Path) log.Debug("adding path to new manifest", "ruid", ruid, "bytes", entry.Size, "path", entry.Path)
contentKey, err := mw.AddEntry(req.Context(), reader, entry) contentKey, err := mw.AddEntry(r.Context(), reader, entry)
if err != nil { if err != nil {
return fmt.Errorf("error adding manifest entry from multipart form: %s", err) return fmt.Errorf("error adding manifest entry from multipart form: %s", err)
} }
log.Debug("stored content", "ruid", req.ruid, "key", contentKey) log.Debug("stored content", "ruid", ruid, "key", contentKey)
} }
} }
func (s *Server) handleDirectUpload(req *Request, mw *api.ManifestWriter) error { func (s *Server) handleDirectUpload(r *http.Request, mw *api.ManifestWriter) error {
log.Debug("handle.direct.upload", "ruid", req.ruid) ruid := GetRUID(r.Context())
key, err := mw.AddEntry(req.Context(), req.Body, &api.ManifestEntry{ log.Debug("handle.direct.upload", "ruid", ruid)
Path: req.uri.Path, key, err := mw.AddEntry(r.Context(), r.Body, &api.ManifestEntry{
ContentType: req.Header.Get("Content-Type"), Path: GetURI(r.Context()).Path,
ContentType: r.Header.Get("Content-Type"),
Mode: 0644, Mode: 0644,
Size: req.ContentLength, Size: r.ContentLength,
ModTime: time.Now(), ModTime: time.Now(),
}) })
if err != nil { if err != nil {
return err return err
} }
log.Debug("stored content", "ruid", req.ruid, "key", key) log.Debug("stored content", "ruid", ruid, "key", key)
return nil return nil
} }
// HandleDelete handles a DELETE request to bzz:/<manifest>/<path>, removes // HandleDelete handles a DELETE request to bzz:/<manifest>/<path>, removes
// <path> from <manifest> and returns the resulting manifest hash as a // <path> from <manifest> and returns the resulting manifest hash as a
// text/plain response // text/plain response
func (s *Server) HandleDelete(w http.ResponseWriter, r *Request) { func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.delete", "ruid", r.ruid) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.delete", "ruid", ruid)
deleteCount.Inc(1) deleteCount.Inc(1)
newKey, err := s.api.Delete(r.Context(), r.uri.Addr, r.uri.Path) newKey, err := s.api.Delete(r.Context(), uri.Addr, uri.Path)
if err != nil { if err != nil {
deleteFail.Inc(1) deleteFail.Inc(1)
Respond(w, r, fmt.Sprintf("could not delete from manifest: %v", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("could not delete from manifest: %v", err), http.StatusInternalServerError)
return return
} }
@ -519,27 +474,20 @@ func resourcePostMode(path string) (isRaw bool, frequency uint64, err error) {
// //
// The POST request admits a JSON structure as defined in the mru package: `mru.updateRequestJSON` // The POST request admits a JSON structure as defined in the mru package: `mru.updateRequestJSON`
// 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 // 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
func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) { func (s *Server) HandlePostResource(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.post.resource", "ruid", r.ruid) ruid := GetRUID(r.Context())
log.Debug("handle.post.resource", "ruid", ruid)
var sp opentracing.Span
ctx := r.Context()
ctx, sp = spancontext.StartSpan(
ctx,
"http.post.resource")
defer sp.Finish()
var err error var err error
// Creation and update must send mru.updateRequestJSON JSON structure // Creation and update must send mru.updateRequestJSON JSON structure
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
var updateRequest mru.Request var updateRequest mru.Request
if err := updateRequest.UnmarshalJSON(body); err != nil { // decodes request JSON if err := updateRequest.UnmarshalJSON(body); err != nil { // decodes request JSON
Respond(w, r, err.Error(), http.StatusBadRequest) //TODO: send different status response depending on error RespondError(w, r, err.Error(), http.StatusBadRequest) //TODO: send different status response depending on error
return return
} }
@ -548,7 +496,7 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
// to update this resource // to update this resource
// Check this early, to avoid creating a resource and then not being able to set its first update. // Check this early, to avoid creating a resource and then not being able to set its first update.
if err = updateRequest.Verify(); err != nil { if err = updateRequest.Verify(); err != nil {
Respond(w, r, err.Error(), http.StatusForbidden) RespondError(w, r, err.Error(), http.StatusForbidden)
return return
} }
} }
@ -557,7 +505,7 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
err = s.api.ResourceCreate(r.Context(), &updateRequest) err = s.api.ResourceCreate(r.Context(), &updateRequest)
if err != nil { if err != nil {
code, err2 := s.translateResourceError(w, r, "resource creation fail", err) code, err2 := s.translateResourceError(w, r, "resource creation fail", err)
Respond(w, r, err2.Error(), code) RespondError(w, r, err2.Error(), code)
return return
} }
} }
@ -565,7 +513,7 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
if updateRequest.IsUpdate() { if updateRequest.IsUpdate() {
_, err = s.api.ResourceUpdate(r.Context(), &updateRequest.SignedResourceUpdate) _, err = s.api.ResourceUpdate(r.Context(), &updateRequest.SignedResourceUpdate)
if err != nil { if err != nil {
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
} }
@ -579,7 +527,7 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
// metadata chunk (rootAddr) // metadata chunk (rootAddr)
m, err := s.api.NewResourceManifest(r.Context(), updateRequest.RootAddr().Hex()) m, err := s.api.NewResourceManifest(r.Context(), updateRequest.RootAddr().Hex())
if err != nil { if err != nil {
Respond(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("failed to create resource manifest: %v", err), http.StatusInternalServerError)
return return
} }
@ -589,7 +537,7 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
// \TODO update manifest key automatically in ENS // \TODO update manifest key automatically in ENS
outdata, err := json.Marshal(m) outdata, err := json.Marshal(m)
if err != nil { if err != nil {
Respond(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("failed to create json response: %s", err), http.StatusInternalServerError)
return return
} }
fmt.Fprint(w, string(outdata)) fmt.Fprint(w, string(outdata))
@ -604,17 +552,19 @@ func (s *Server) HandlePostResource(w http.ResponseWriter, r *Request) {
// bzz-resource://<id>/meta - get metadata and next version information // bzz-resource://<id>/meta - get metadata and next version information
// <id> = ens name or hash // <id> = ens name or hash
// TODO: Enable pass maxPeriod parameter // TODO: Enable pass maxPeriod parameter
func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) { func (s *Server) HandleGetResource(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.get.resource", "ruid", r.ruid) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.get.resource", "ruid", ruid)
var err error var err error
// resolve the content key. // resolve the content key.
manifestAddr := r.uri.Address() manifestAddr := uri.Address()
if manifestAddr == nil { if manifestAddr == nil {
manifestAddr, err = s.api.Resolve(r.Context(), r.uri) manifestAddr, err = s.api.Resolve(r.Context(), uri)
if err != nil { if err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound)
return return
} }
} else { } else {
@ -625,25 +575,25 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) {
rootAddr, err := s.api.ResolveResourceManifest(r.Context(), manifestAddr) rootAddr, err := s.api.ResolveResourceManifest(r.Context(), manifestAddr)
if err != nil { if err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", r.uri.Addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("error resolving resource root chunk for %s: %s", uri.Addr, err), http.StatusNotFound)
return return
} }
log.Debug("handle.get.resource: resolved", "ruid", r.ruid, "manifestkey", manifestAddr, "rootchunk addr", rootAddr) log.Debug("handle.get.resource: resolved", "ruid", ruid, "manifestkey", manifestAddr, "rootchunk addr", rootAddr)
// determine if the query specifies period and version or it is a metadata query // determine if the query specifies period and version or it is a metadata query
var params []string var params []string
if len(r.uri.Path) > 0 { if len(uri.Path) > 0 {
if r.uri.Path == "meta" { if uri.Path == "meta" {
unsignedUpdateRequest, err := s.api.ResourceNewRequest(r.Context(), rootAddr) unsignedUpdateRequest, err := s.api.ResourceNewRequest(r.Context(), rootAddr)
if err != nil { if err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot retrieve resource metadata for rootAddr=%s: %s", rootAddr.Hex(), err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("cannot retrieve resource metadata for rootAddr=%s: %s", rootAddr.Hex(), err), http.StatusNotFound)
return return
} }
rawResponse, err := unsignedUpdateRequest.MarshalJSON() rawResponse, err := unsignedUpdateRequest.MarshalJSON()
if err != nil { if err != nil {
Respond(w, r, fmt.Sprintf("cannot encode unsigned UpdateRequest: %v", err), http.StatusInternalServerError) RespondError(w, r, fmt.Sprintf("cannot encode unsigned UpdateRequest: %v", err), http.StatusInternalServerError)
return return
} }
w.Header().Add("Content-type", "application/json") w.Header().Add("Content-type", "application/json")
@ -653,7 +603,7 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) {
} }
params = strings.Split(r.uri.Path, "/") params = strings.Split(uri.Path, "/")
} }
var name string var name string
@ -689,17 +639,17 @@ func (s *Server) HandleGetResource(w http.ResponseWriter, r *Request) {
// any error from the switch statement will end up here // any error from the switch statement will end up here
if err != nil { if err != nil {
code, err2 := s.translateResourceError(w, r, "mutable resource lookup fail", err) code, err2 := s.translateResourceError(w, r, "mutable resource lookup fail", err)
Respond(w, r, err2.Error(), code) RespondError(w, r, err2.Error(), code)
return return
} }
// All ok, serve the retrieved update // All ok, serve the retrieved update
log.Debug("Found update", "name", name, "ruid", r.ruid) log.Debug("Found update", "name", name, "ruid", ruid)
w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Type", "application/octet-stream")
http.ServeContent(w, &r.Request, "", now, bytes.NewReader(data)) http.ServeContent(w, r, "", now, bytes.NewReader(data))
} }
func (s *Server) translateResourceError(w http.ResponseWriter, r *Request, supErr string, err error) (int, error) { func (s *Server) translateResourceError(w http.ResponseWriter, r *http.Request, supErr string, err error) (int, error) {
code := 0 code := 0
defaultErr := fmt.Errorf("%s: %v", supErr, err) defaultErr := fmt.Errorf("%s: %v", supErr, err)
rsrcErr, ok := err.(*mru.Error) rsrcErr, ok := err.(*mru.Error)
@ -725,46 +675,41 @@ func (s *Server) translateResourceError(w http.ResponseWriter, r *Request, supEr
// given storage key // given storage key
// - bzz-hash://<key> and responds with the hash of the content stored // - bzz-hash://<key> and responds with the hash of the content stored
// at the given storage key as a text/plain response // at the given storage key as a text/plain response
func (s *Server) HandleGet(w http.ResponseWriter, r *Request) { func (s *Server) HandleGet(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.get", "ruid", r.ruid, "uri", r.uri) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.get", "ruid", ruid, "uri", uri)
getCount.Inc(1) getCount.Inc(1)
var sp opentracing.Span
ctx := r.Context()
ctx, sp = spancontext.StartSpan(
ctx,
"http.get")
defer sp.Finish()
var err error var err error
addr := r.uri.Address() addr := uri.Address()
if addr == nil { if addr == nil {
addr, err = s.api.Resolve(r.Context(), r.uri) addr, err = s.api.Resolve(r.Context(), uri)
if err != nil { if err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound)
return return
} }
} else { } else {
w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable.
} }
log.Debug("handle.get: resolved", "ruid", r.ruid, "key", addr) log.Debug("handle.get: resolved", "ruid", ruid, "key", addr)
// if path is set, interpret <key> as a manifest and return the // if path is set, interpret <key> as a manifest and return the
// raw entry at the given path // raw entry at the given path
if r.uri.Path != "" { if uri.Path != "" {
walker, err := s.api.NewManifestWalker(r.Context(), addr, nil) walker, err := s.api.NewManifestWalker(r.Context(), addr, nil)
if err != nil { if err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("%s is not a manifest", addr), http.StatusBadRequest) RespondError(w, r, fmt.Sprintf("%s is not a manifest", addr), http.StatusBadRequest)
return return
} }
var entry *api.ManifestEntry var entry *api.ManifestEntry
walker.Walk(func(e *api.ManifestEntry) error { walker.Walk(func(e *api.ManifestEntry) error {
// if the entry matches the path, set entry and stop // if the entry matches the path, set entry and stop
// the walk // the walk
if e.Path == r.uri.Path { if e.Path == uri.Path {
entry = e entry = e
// return an error to cancel the walk // return an error to cancel the walk
return errors.New("found") return errors.New("found")
@ -778,7 +723,7 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *Request) {
// if the manifest's path is a prefix of the // if the manifest's path is a prefix of the
// requested path, recurse into it by returning // requested path, recurse into it by returning
// nil and continuing the walk // nil and continuing the walk
if strings.HasPrefix(r.uri.Path, e.Path) { if strings.HasPrefix(uri.Path, e.Path) {
return nil return nil
} }
@ -786,7 +731,7 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *Request) {
}) })
if entry == nil { if entry == nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("manifest entry could not be loaded"), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("manifest entry could not be loaded"), http.StatusNotFound)
return return
} }
addr = storage.Address(common.Hex2Bytes(entry.Hash)) addr = storage.Address(common.Hex2Bytes(entry.Hash))
@ -796,23 +741,23 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *Request) {
w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to manifest key or raw entry key. w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to manifest key or raw entry key.
if noneMatchEtag != "" { if noneMatchEtag != "" {
if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), addr) { if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), addr) {
Respond(w, r, "Not Modified", http.StatusNotModified) w.WriteHeader(http.StatusNotModified)
return return
} }
} }
// check the root chunk exists by retrieving the file's size // check the root chunk exists by retrieving the file's size
reader, isEncrypted := s.api.Retrieve(ctx, addr) reader, isEncrypted := s.api.Retrieve(r.Context(), addr)
if _, err := reader.Size(ctx, nil); err != nil { if _, err := reader.Size(r.Context(), nil); err != nil {
getFail.Inc(1) getFail.Inc(1)
Respond(w, r, fmt.Sprintf("root chunk not found %s: %s", addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("root chunk not found %s: %s", addr, err), http.StatusNotFound)
return return
} }
w.Header().Set("X-Decrypted", fmt.Sprintf("%v", isEncrypted)) w.Header().Set("X-Decrypted", fmt.Sprintf("%v", isEncrypted))
switch { switch {
case r.uri.Raw(): case uri.Raw():
// allow the request to overwrite the content type using a query // allow the request to overwrite the content type using a query
// parameter // parameter
contentType := "application/octet-stream" contentType := "application/octet-stream"
@ -820,8 +765,8 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *Request) {
contentType = typ contentType = typ
} }
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
http.ServeContent(w, &r.Request, "", time.Now(), reader) http.ServeContent(w, r, "", time.Now(), reader)
case r.uri.Hash(): case uri.Hash():
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
fmt.Fprint(w, addr) fmt.Fprint(w, addr)
@ -831,35 +776,30 @@ func (s *Server) HandleGet(w http.ResponseWriter, r *Request) {
// HandleGetList handles a GET request to bzz-list:/<manifest>/<path> and returns // HandleGetList handles a GET request to bzz-list:/<manifest>/<path> and returns
// a list of all files contained in <manifest> under <path> grouped into // a list of all files contained in <manifest> under <path> grouped into
// common prefixes using "/" as a delimiter // common prefixes using "/" as a delimiter
func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) { func (s *Server) HandleGetList(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.get.list", "ruid", r.ruid, "uri", r.uri) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.get.list", "ruid", ruid, "uri", uri)
getListCount.Inc(1) getListCount.Inc(1)
var sp opentracing.Span
ctx := r.Context()
ctx, sp = spancontext.StartSpan(
ctx,
"http.get.list")
defer sp.Finish()
// ensure the root path has a trailing slash so that relative URLs work // ensure the root path has a trailing slash so that relative URLs work
if r.uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, &r.Request, r.URL.Path+"/", http.StatusMovedPermanently) http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently)
return return
} }
addr, err := s.api.Resolve(r.Context(), r.uri) addr, err := s.api.Resolve(r.Context(), uri)
if err != nil { if err != nil {
getListFail.Inc(1) getListFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound)
return return
} }
log.Debug("handle.get.list: resolved", "ruid", r.ruid, "key", addr) log.Debug("handle.get.list: resolved", "ruid", ruid, "key", addr)
list, err := s.api.GetManifestList(ctx, addr, r.uri.Path) list, err := s.api.GetManifestList(r.Context(), addr, uri.Path)
if err != nil { if err != nil {
getListFail.Inc(1) getListFail.Inc(1)
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
@ -867,11 +807,11 @@ func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) {
// HTML index with relative URLs // HTML index with relative URLs
if strings.Contains(r.Header.Get("Accept"), "text/html") { if strings.Contains(r.Header.Get("Accept"), "text/html") {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err := htmlListTemplate.Execute(w, &htmlListData{ err := TemplatesMap["bzz-list"].Execute(w, &htmlListData{
URI: &api.URI{ URI: &api.URI{
Scheme: "bzz", Scheme: "bzz",
Addr: r.uri.Addr, Addr: uri.Addr,
Path: r.uri.Path, Path: uri.Path,
}, },
List: &list, List: &list,
}) })
@ -888,45 +828,40 @@ func (s *Server) HandleGetList(w http.ResponseWriter, r *Request) {
// HandleGetFile handles a GET request to bzz://<manifest>/<path> and responds // HandleGetFile handles a GET request to bzz://<manifest>/<path> and responds
// with the content of the file at <path> from the given <manifest> // with the content of the file at <path> from the given <manifest>
func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) { func (s *Server) HandleGetFile(w http.ResponseWriter, r *http.Request) {
log.Debug("handle.get.file", "ruid", r.ruid) ruid := GetRUID(r.Context())
uri := GetURI(r.Context())
log.Debug("handle.get.file", "ruid", ruid)
getFileCount.Inc(1) getFileCount.Inc(1)
var sp opentracing.Span
ctx := r.Context()
ctx, sp = spancontext.StartSpan(
ctx,
"http.get.file")
defer sp.Finish()
// ensure the root path has a trailing slash so that relative URLs work // ensure the root path has a trailing slash so that relative URLs work
if r.uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") { if uri.Path == "" && !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, &r.Request, r.URL.Path+"/", http.StatusMovedPermanently) http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently)
return return
} }
var err error var err error
manifestAddr := r.uri.Address() manifestAddr := uri.Address()
if manifestAddr == nil { if manifestAddr == nil {
manifestAddr, err = s.api.Resolve(r.Context(), r.uri) manifestAddr, err = s.api.Resolve(r.Context(), uri)
if err != nil { if err != nil {
getFileFail.Inc(1) getFileFail.Inc(1)
Respond(w, r, fmt.Sprintf("cannot resolve %s: %s", r.uri.Addr, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("cannot resolve %s: %s", uri.Addr, err), http.StatusNotFound)
return return
} }
} else { } else {
w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable. w.Header().Set("Cache-Control", "max-age=2147483648, immutable") // url was of type bzz://<hex key>/path, so we are sure it is immutable.
} }
log.Debug("handle.get.file: resolved", "ruid", r.ruid, "key", manifestAddr) log.Debug("handle.get.file: resolved", "ruid", ruid, "key", manifestAddr)
reader, contentType, status, contentKey, err := s.api.Get(r.Context(), manifestAddr, r.uri.Path) reader, contentType, status, contentKey, err := s.api.Get(r.Context(), manifestAddr, uri.Path)
etag := common.Bytes2Hex(contentKey) etag := common.Bytes2Hex(contentKey)
noneMatchEtag := r.Header.Get("If-None-Match") noneMatchEtag := r.Header.Get("If-None-Match")
w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to actual content key. w.Header().Set("ETag", fmt.Sprintf("%q", etag)) // set etag to actual content key.
if noneMatchEtag != "" { if noneMatchEtag != "" {
if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), contentKey) { if bytes.Equal(storage.Address(common.Hex2Bytes(noneMatchEtag)), contentKey) {
Respond(w, r, "Not Modified", http.StatusNotModified) w.WriteHeader(http.StatusNotModified)
return return
} }
} }
@ -935,10 +870,10 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) {
switch status { switch status {
case http.StatusNotFound: case http.StatusNotFound:
getFileNotFound.Inc(1) getFileNotFound.Inc(1)
Respond(w, r, err.Error(), http.StatusNotFound) RespondError(w, r, err.Error(), http.StatusNotFound)
default: default:
getFileFail.Inc(1) getFileFail.Inc(1)
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
} }
return return
} }
@ -946,28 +881,28 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) {
//the request results in ambiguous files //the request results in ambiguous files
//e.g. /read with readme.md and readinglist.txt available in manifest //e.g. /read with readme.md and readinglist.txt available in manifest
if status == http.StatusMultipleChoices { if status == http.StatusMultipleChoices {
list, err := s.api.GetManifestList(ctx, manifestAddr, r.uri.Path) list, err := s.api.GetManifestList(r.Context(), manifestAddr, uri.Path)
if err != nil { if err != nil {
getFileFail.Inc(1) getFileFail.Inc(1)
Respond(w, r, err.Error(), http.StatusInternalServerError) RespondError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
log.Debug(fmt.Sprintf("Multiple choices! --> %v", list), "ruid", r.ruid) log.Debug(fmt.Sprintf("Multiple choices! --> %v", list), "ruid", ruid)
//show a nice page links to available entries //show a nice page links to available entries
ShowMultipleChoices(w, r, list) ShowMultipleChoices(w, r, list)
return return
} }
// check the root chunk exists by retrieving the file's size // check the root chunk exists by retrieving the file's size
if _, err := reader.Size(ctx, nil); err != nil { if _, err := reader.Size(r.Context(), nil); err != nil {
getFileNotFound.Inc(1) getFileNotFound.Inc(1)
Respond(w, r, fmt.Sprintf("file not found %s: %s", r.uri, err), http.StatusNotFound) RespondError(w, r, fmt.Sprintf("file not found %s: %s", uri, err), http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
http.ServeContent(w, &r.Request, "", time.Now(), newBufferedReadSeeker(reader, getFileBufferSize)) http.ServeContent(w, r, "", time.Now(), newBufferedReadSeeker(reader, getFileBufferSize))
} }
// The size of buffer used for bufio.Reader on LazyChunkReader passed to // The size of buffer used for bufio.Reader on LazyChunkReader passed to

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

7
swarm/sctx/sctx.go Normal file
View File

@ -0,0 +1,7 @@
package sctx
type ContextKey int
const (
HTTPRequestIDKey ContextKey = iota
)