diff --git a/swarm/api/api.go b/swarm/api/api.go index 7d185ab3c..a5941fb5c 100644 --- a/swarm/api/api.go +++ b/swarm/api/api.go @@ -132,6 +132,7 @@ func (self *Api) Put(content, contentType string) (storage.Key, error) { func (self *Api) Get(key storage.Key, path string) (reader storage.LazySectionReader, mimeType string, status int, err error) { trie, err := loadManifest(self.dpa, key, nil) if err != nil { + status = http.StatusNotFound log.Warn(fmt.Sprintf("loadManifestTrie error: %v", err)) return } diff --git a/swarm/api/http/error.go b/swarm/api/http/error.go new file mode 100644 index 000000000..ebb5e3ebe --- /dev/null +++ b/swarm/api/http/error.go @@ -0,0 +1,116 @@ +// 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 . + +/* +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" + "time" + + "github.com/ethereum/go-ethereum/log" +) + +//templateMap holds a mapping of an HTTP error code to a template +var templateMap map[int]*template.Template + +//parameters needed for formatting the correct HTML page +type ErrorParams struct { + Msg string + Code int + Timestamp string + template *template.Template + Details template.HTML +} + +//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() + //map the codes to the available pages + tnames := map[int]string{ + 0: genErrPage, //default + 400: genErrPage, + 404: notFoundPage, + 500: 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)) + } +} + +//ShowError is used to show an HTML error 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 ShowError(w http.ResponseWriter, r *http.Request, msg string, code int) { + if code == http.StatusInternalServerError { + log.Error(msg) + } + respond(w, r, &ErrorParams{ + Code: code, + Msg: msg, + 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 *ErrorParams) { + 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 *ErrorParams) { + err := params.template.Execute(w, params) + if err != nil { + log.Error(err.Error()) + } +} + +//return JSON +func respondJson(w http.ResponseWriter, params *ErrorParams) { + 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 + } else { + return templateMap[0] + } +} diff --git a/swarm/api/http/error_templates.go b/swarm/api/http/error_templates.go new file mode 100644 index 000000000..29bd3bfbb --- /dev/null +++ b/swarm/api/http/error_templates.go @@ -0,0 +1,376 @@ +// 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 . + +/* +We use html templates to handle simple but as informative as possible error pages. + +To eliminate circular dependency in case of an error, we don't store error pages on swarm. +We can't save the error pages as html files on disk, or when deploying compiled binaries +they won't be found. + +For this reason we resort to save the HTML error pages as strings, which then can be +parsed by Go's html/template package +*/ +package http + +//This returns the HTML for generic errors +func GetGenericErrorPage() string { + page := ` + + + + + + + + + + + + Swarm::HTTP Error Page + + + + +
+ +
+
+ +
+
+

There was a problem serving the requested page

+
+
+
{{.Timestamp}}
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ Hmmmmm....Swarm was not able to serve your request! +
+ Error message: +
+ {{.Msg}} +
+ Error code: +
+ {{.Code}} +
+
+
+ +
+

+ Swarm: Serverless Hosting Incentivised Peer-To-Peer Storage And Content Distribution
+ Swarm +

+
+ + +
+ + + +` + return page +} + +//This returns the HTML for a 404 Not Found error +func GetNotFoundErrorPage() string { + page := ` + + + + + + + + + + + + Swarm::404 HTTP Not Found + + + + +
+ +
+
+ +
+
+

Resource Not Found

+
+
+
{{.Timestamp}}
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ Unfortunately, the resource you were trying to access could not be found on swarm. +
+
+ {{.Msg}} +
+ Error code: +
+ {{.Code}} +
+
+
+ +
+

+ Swarm: Serverless Hosting Incentivised Peer-To-Peer Storage And Content Distribution
+ Swarm +

+
+ + +
+ + + +` + return page +} diff --git a/swarm/api/http/error_test.go b/swarm/api/http/error_test.go new file mode 100644 index 000000000..465fbf4ca --- /dev/null +++ b/swarm/api/http/error_test.go @@ -0,0 +1,143 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package http_test + +import ( + "encoding/json" + "golang.org/x/net/html" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/swarm/testutil" +) + +func TestError(t *testing.T) { + + srv := testutil.NewTestSwarmServer(t) + defer srv.Close() + + var resp *http.Response + var respbody []byte + + url := srv.URL + "/this_should_fail_as_no_bzz_protocol_present" + resp, err := http.Get(url) + + if err != nil { + t.Fatalf("Request failed: %v", err) + } + defer resp.Body.Close() + respbody, err = ioutil.ReadAll(resp.Body) + + if resp.StatusCode != 400 && !strings.Contains(string(respbody), "Invalid URI "/this_should_fail_as_no_bzz_protocol_present": 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) + } + + _, err = html.Parse(strings.NewReader(string(respbody))) + if err != nil { + t.Fatalf("HTML validation failed for error page returned!") + } +} + +func Test404Page(t *testing.T) { + srv := testutil.NewTestSwarmServer(t) + defer srv.Close() + + var resp *http.Response + var respbody []byte + + url := srv.URL + "/bzz:/1234567890123456789012345678901234567890123456789012345678901234" + resp, err := http.Get(url) + + if err != nil { + t.Fatalf("Request failed: %v", err) + } + defer resp.Body.Close() + respbody, err = ioutil.ReadAll(resp.Body) + + if resp.StatusCode != 404 || !strings.Contains(string(respbody), "404") { + t.Fatalf("Invalid Status Code received, expected 404, got %d", resp.StatusCode) + } + + _, err = html.Parse(strings.NewReader(string(respbody))) + if err != nil { + t.Fatalf("HTML validation failed for error page returned!") + } +} + +func Test500Page(t *testing.T) { + srv := testutil.NewTestSwarmServer(t) + defer srv.Close() + + var resp *http.Response + var respbody []byte + + url := srv.URL + "/bzz:/thisShouldFailWith500Code" + resp, err := http.Get(url) + + if err != nil { + t.Fatalf("Request failed: %v", err) + } + defer resp.Body.Close() + respbody, err = ioutil.ReadAll(resp.Body) + + if resp.StatusCode != 500 || !strings.Contains(string(respbody), "500") { + t.Fatalf("Invalid Status Code received, expected 500, got %d", resp.StatusCode) + } + + _, err = html.Parse(strings.NewReader(string(respbody))) + if err != nil { + t.Fatalf("HTML validation failed for error page returned!") + } +} + +func TestJsonResponse(t *testing.T) { + srv := testutil.NewTestSwarmServer(t) + defer srv.Close() + + var resp *http.Response + var respbody []byte + + url := srv.URL + "/bzz:/thisShouldFailWith500Code/" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + req.Header.Set("Accept", "application/json") + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + + defer resp.Body.Close() + respbody, err = ioutil.ReadAll(resp.Body) + + if resp.StatusCode != 500 { + t.Fatalf("Invalid Status Code received, expected 500, got %d", resp.StatusCode) + } + + if !isJSON(string(respbody)) { + t.Fatalf("Expected repsonse to be JSON, received invalid JSON: %s", string(respbody)) + } + +} + +func isJSON(s string) bool { + var js map[string]interface{} + return json.Unmarshal([]byte(s), &js) == nil +} diff --git a/swarm/api/http/server.go b/swarm/api/http/server.go index 0b4ec7e18..a637b8735 100644 --- a/swarm/api/http/server.go +++ b/swarm/api/http/server.go @@ -332,7 +332,7 @@ func (s *Server) HandleGetRaw(w http.ResponseWriter, r *Request) { return api.SkipManifest }) if entry == nil { - http.NotFound(w, &r.Request) + s.NotFound(w, r, fmt.Errorf("Manifest entry could not be loaded")) return } key = storage.Key(common.Hex2Bytes(entry.Hash)) @@ -341,8 +341,7 @@ func (s *Server) HandleGetRaw(w http.ResponseWriter, r *Request) { // check the root chunk exists by retrieving the file's size reader := s.api.Retrieve(key) if _, err := reader.Size(nil); err != nil { - s.logDebug("key not found %s: %s", key, err) - http.NotFound(w, &r.Request) + s.NotFound(w, r, fmt.Errorf("Root chunk not found %s: %s", key, err)) return } @@ -534,16 +533,20 @@ func (s *Server) HandleGetFile(w http.ResponseWriter, r *Request) { return } - reader, contentType, _, err := s.api.Get(key, r.uri.Path) + reader, contentType, status, err := s.api.Get(key, r.uri.Path) if err != nil { - s.Error(w, r, err) + switch status { + case http.StatusNotFound: + s.NotFound(w, r, err) + default: + s.Error(w, r, err) + } return } // check the root chunk exists by retrieving the file's size if _, err := reader.Size(nil); err != nil { - s.logDebug("file not found %s: %s", r.uri, err) - http.NotFound(w, &r.Request) + s.NotFound(w, r, fmt.Errorf("File not found %s: %s", r.uri, err)) return } @@ -556,14 +559,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.logDebug("HTTP %s request URL: '%s', Host: '%s', Path: '%s', Referer: '%s', Accept: '%s'", r.Method, r.RequestURI, r.URL.Host, r.URL.Path, r.Referer(), r.Header.Get("Accept")) uri, err := api.Parse(strings.TrimLeft(r.URL.Path, "/")) + req := &Request{Request: *r, uri: uri} if err != nil { s.logError("Invalid URI %q: %s", r.URL.Path, err) - http.Error(w, fmt.Sprintf("Invalid bzz URI: %s", err), http.StatusBadRequest) + s.BadRequest(w, req, fmt.Sprintf("Invalid URI %q: %s", r.URL.Path, err)) return } s.logDebug("%s request received for %s", r.Method, uri) - req := &Request{Request: *r, uri: uri} switch r.Method { case "POST": if uri.Raw() { @@ -579,7 +582,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // strictly a traditional PUT request which replaces content // at a URI, and POST is more ubiquitous) if uri.Raw() { - http.Error(w, fmt.Sprintf("No PUT to %s allowed.", uri), http.StatusBadRequest) + ShowError(w, r, fmt.Sprintf("No PUT to %s allowed.", uri), http.StatusBadRequest) return } else { s.HandlePostFiles(w, req) @@ -587,7 +590,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { case "DELETE": if uri.Raw() { - http.Error(w, fmt.Sprintf("No DELETE to %s allowed.", uri), http.StatusBadRequest) + ShowError(w, r, fmt.Sprintf("No DELETE to %s allowed.", uri), http.StatusBadRequest) return } s.HandleDelete(w, req) @@ -611,7 +614,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.HandleGetFile(w, req) default: - http.Error(w, "Method "+r.Method+" is not supported.", http.StatusMethodNotAllowed) + ShowError(w, r, fmt.Sprintf("Method "+r.Method+" is not supported.", uri), http.StatusMethodNotAllowed) } } @@ -643,11 +646,13 @@ func (s *Server) logError(format string, v ...interface{}) { } func (s *Server) BadRequest(w http.ResponseWriter, r *Request, reason string) { - s.logDebug("bad request %s %s: %s", r.Method, r.uri, reason) - http.Error(w, reason, http.StatusBadRequest) + ShowError(w, &r.Request, fmt.Sprintf("Bad request %s %s: %s", r.Method, r.uri, reason), http.StatusBadRequest) } func (s *Server) Error(w http.ResponseWriter, r *Request, err error) { - s.logError("error serving %s %s: %s", r.Method, r.uri, err) - http.Error(w, err.Error(), http.StatusInternalServerError) + ShowError(w, &r.Request, fmt.Sprintf("Error serving %s %s: %s", r.Method, r.uri, err), http.StatusInternalServerError) +} + +func (s *Server) NotFound(w http.ResponseWriter, r *Request, err error) { + ShowError(w, &r.Request, fmt.Sprintf("NOT FOUND error serving %s %s: %s", r.Method, r.uri, err), http.StatusNotFound) } diff --git a/swarm/api/http/server_test.go b/swarm/api/http/server_test.go index d3374594b..ffeaf6e0d 100644 --- a/swarm/api/http/server_test.go +++ b/swarm/api/http/server_test.go @@ -22,6 +22,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "sync" "testing" @@ -110,9 +111,9 @@ func TestBzzrGetPath(t *testing.T) { } nonhashresponses := []string{ - "error resolving name: no DNS to resolve name: \"name\"\n", - "error resolving nonhash: immutable address not a content hash: \"nonhash\"\n", - "error resolving nonhash: no DNS to resolve name: \"nonhash\"\n", + "error resolving name: no DNS to resolve name: "name"", + "error resolving nonhash: immutable address not a content hash: "nonhash"", + "error resolving nonhash: no DNS to resolve name: "nonhash"", } for i, url := range nonhashtests { @@ -129,7 +130,7 @@ func TestBzzrGetPath(t *testing.T) { if err != nil { t.Fatalf("ReadAll failed: %v", err) } - if string(respbody) != nonhashresponses[i] { + if !strings.Contains(string(respbody), nonhashresponses[i]) { t.Fatalf("Non-Hash response body does not match, expected: %v, got: %v", nonhashresponses[i], string(respbody)) } }