Преглед изворни кода

swarm/api/http: add error pages (#14967)

holisticode пре 8 година
родитељ
комит
ac193e36ce

+ 1 - 0
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
 	}

+ 116 - 0
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 <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"
+	"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]
+	}
+}

Разлика између датотеке није приказан због своје велике величине
+ 141 - 0
swarm/api/http/error_templates.go


+ 143 - 0
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 <http://www.gnu.org/licenses/>.
+
+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 &#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)
+	}
+
+	_, 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
+}

+ 21 - 16
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)
 }

+ 5 - 4
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: &#34;name&#34;",
+		"error resolving nonhash: immutable address not a content hash: &#34;nonhash&#34;",
+		"error resolving nonhash: no DNS to resolve name: &#34;nonhash&#34;",
 	}
 
 	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))
 		}
 	}

Неке датотеке нису приказане због велике количине промена