diff options
author | Bas van Kervel <bas@ethdev.com> | 2016-02-24 18:19:00 +0800 |
---|---|---|
committer | Bas van Kervel <bas@ethdev.com> | 2016-03-23 18:27:08 +0800 |
commit | a7bae3b2a645653a149b9bcbb9bdc857e27027e2 (patch) | |
tree | 72a0014c4f4c3e6f64eabe6e7d541890b437246d /rpc/http.go | |
parent | 6d3cd03a03167ccac851676a912ce31c76d5f75c (diff) | |
download | go-tangerine-a7bae3b2a645653a149b9bcbb9bdc857e27027e2.tar.gz go-tangerine-a7bae3b2a645653a149b9bcbb9bdc857e27027e2.tar.zst go-tangerine-a7bae3b2a645653a149b9bcbb9bdc857e27027e2.zip |
rpc/http: improve request handling
Diffstat (limited to 'rpc/http.go')
-rw-r--r-- | rpc/http.go | 277 |
1 files changed, 56 insertions, 221 deletions
diff --git a/rpc/http.go b/rpc/http.go index d9053b003..af3d29014 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -17,240 +17,23 @@ package rpc import ( - "bufio" "bytes" "encoding/json" "fmt" - "io" "io/ioutil" - "net" "net/http" "net/url" - "strconv" "strings" - "time" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "gopkg.in/fatih/set.v0" + "io" + + "github.com/rs/cors" ) const ( - httpReadDeadLine = 60 * time.Second // wait max httpReadDeadeline for next request + maxHTTPRequestContentLength = 1024 * 128 ) -// httpMessageStream is the glue between a HTTP connection which is message based -// and the RPC codecs that expect json requests to be read from a stream. It will -// parse HTTP messages and offer the bodies of these requests as a stream through -// the Read method. This will require full control of the connection and thus need -// a "hijacked" HTTP connection. -type httpMessageStream struct { - conn net.Conn // TCP connection - rw *bufio.ReadWriter // buffered where HTTP requests/responses are read/written from/to - currentReq *http.Request // pending request, codec can pass in a too small buffer for a single read we need to keep track of the current requests if it was not read at once - payloadBytesRead int64 // number of bytes which are read from the current request - allowedOrigins *set.Set // allowed CORS domains - origin string // origin of this connection/request -} - -// NewHttpMessageStream will create a new http message stream parser that can be -// used by the codes in the RPC package. It will take full control of the given -// connection and thus needs to be hijacked. It will read and write HTTP messages -// from the passed rwbuf. The allowed origins are the RPC CORS domains the user has supplied. -func NewHTTPMessageStream(c net.Conn, rwbuf *bufio.ReadWriter, initialReq *http.Request, allowdOrigins []string) *httpMessageStream { - r := &httpMessageStream{conn: c, rw: rwbuf, currentReq: initialReq, allowedOrigins: set.New()} - for _, origin := range allowdOrigins { - r.allowedOrigins.Add(origin) - } - return r -} - -// handleOptionsRequest handles the HTTP preflight requests (OPTIONS) that browsers -// make to enforce CORS rules. Only the POST method is allowed and the origin must -// be on the rpccorsdomain list the user has specified. -func (h *httpMessageStream) handleOptionsRequest(req *http.Request) error { - headers := req.Header - - if !strings.EqualFold(req.Method, "OPTIONS") { - return fmt.Errorf("preflight aborted: %s!=OPTIONS", req.Method) - } - - origin := headers.Get("Origin") - if origin == "" { - return fmt.Errorf("preflight aborted: empty origin") - } - - responseHeaders := make(http.Header) - responseHeaders.Set("Access-Control-Allow-Methods", "POST") - if h.allowedOrigins.Has(origin) || h.allowedOrigins.Has("*") { - responseHeaders.Set("Access-Control-Allow-Origin", origin) - } else { - glog.V(logger.Info).Infof("origin '%s' not allowed", origin) - } - responseHeaders.Set("Access-Control-Allow-Headers", "Content-Type") - responseHeaders.Set("Date", string(httpTimestamp(time.Now()))) - responseHeaders.Set("Content-Type", "text/plain; charset=utf-8") - responseHeaders.Set("Content-Length", "0") - responseHeaders.Set("Vary", "Origin") - - defer h.rw.Flush() - - if _, err := h.rw.WriteString("HTTP/1.1 200 OK\r\n"); err != nil { - glog.V(logger.Error).Infof("unable to write OPTIONS response: %v\n", err) - return err - } - if err := responseHeaders.Write(h.rw); err != nil { - glog.V(logger.Error).Infof("unable to write OPTIONS headers: %v\n", err) - } - if _, err := h.rw.WriteString("\r\n"); err != nil { - glog.V(logger.Error).Infof("unable to write OPTIONS response: %v\n", err) - } - - return nil -} - -// Read will read incoming HTTP requests and reads the body data from these requests -// as an endless stream of data. -func (h *httpMessageStream) Read(buf []byte) (n int, err error) { - h.conn.SetReadDeadline(time.Now().Add(httpReadDeadLine)) - for { - // if the last request was read completely try to read the next request - if h.currentReq == nil { - if h.currentReq, err = http.ReadRequest(bufio.NewReader(h.rw)); err != nil { - return 0, err - } - } - - // The "options" method is http specific and not interested for the RPC server. - // Handle it internally and wait for the next request. - if strings.EqualFold(h.currentReq.Method, "OPTIONS") { - if err = h.handleOptionsRequest(h.currentReq); err != nil { - glog.V(logger.Info).Infof("RPC/HTTP OPTIONS error: %v\n", err) - h.currentReq = nil - return 0, err - } - - // processed valid request -> reset deadline - h.conn.SetReadDeadline(time.Now().Add(httpReadDeadLine)) - h.currentReq = nil - continue - } - - if strings.EqualFold(h.currentReq.Method, "GET") || strings.EqualFold(h.currentReq.Method, "POST") { - n, err := h.currentReq.Body.Read(buf) - h.payloadBytesRead += int64(n) - - // entire payload read, read new request next time - if err == io.EOF || h.payloadBytesRead >= h.currentReq.ContentLength { - h.origin = h.currentReq.Header.Get("origin") - h.payloadBytesRead = 0 - h.currentReq.Body.Close() - h.currentReq = nil - err = nil // io.EOF is not an error - } else if err != nil { - // unable to read body - h.currentReq.Body.Close() - h.currentReq = nil - h.payloadBytesRead = 0 - } - // partial read of body - return n, err - } - return 0, fmt.Errorf("unsupported HTTP method '%s'", h.currentReq.Method) - } -} - -// Write will create a HTTP response with the given payload and send it to the peer. -func (h *httpMessageStream) Write(payload []byte) (int, error) { - defer h.rw.Flush() - - responseHeaders := make(http.Header) - responseHeaders.Set("Content-Type", "application/json") - responseHeaders.Set("Content-Length", strconv.Itoa(len(payload))) - if h.origin != "" { - responseHeaders.Set("Access-Control-Allow-Origin", h.origin) - } - - h.rw.WriteString("HTTP/1.1 200 OK\r\n") - responseHeaders.Write(h.rw) - h.rw.WriteString("\r\n") - - return h.rw.Write(payload) -} - -// Close will close the underlying TCP connection this instance has taken ownership over. -func (h *httpMessageStream) Close() error { - h.rw.Flush() - return h.conn.Close() -} - -// TimeFormat is the time format to use with time.Parse and time.Time.Format when -// parsing or generating times in HTTP headers. It is like time.RFC1123 but hard -// codes GMT as the time zone. -const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" - -// httpTimestamp formats the given t as specified in RFC1123. -func httpTimestamp(t time.Time) []byte { - const days = "SunMonTueWedThuFriSat" - const months = "JanFebMarAprMayJunJulAugSepOctNovDec" - - b := make([]byte, 0) - t = t.UTC() - yy, mm, dd := t.Date() - hh, mn, ss := t.Clock() - day := days[3*t.Weekday():] - mon := months[3*(mm-1):] - - return append(b, - day[0], day[1], day[2], ',', ' ', - byte('0'+dd/10), byte('0'+dd%10), ' ', - mon[0], mon[1], mon[2], ' ', - byte('0'+yy/1000), byte('0'+(yy/100)%10), byte('0'+(yy/10)%10), byte('0'+yy%10), ' ', - byte('0'+hh/10), byte('0'+hh%10), ':', - byte('0'+mn/10), byte('0'+mn%10), ':', - byte('0'+ss/10), byte('0'+ss%10), ' ', - 'G', 'M', 'T') -} - -// httpConnHijacker is a http.Handler implementation that will hijack the HTTP -// connection, wraps it in a HttpMessageStream that is then wrapped in a JSON -// codec which will be served on the rpcServer. -type httpConnHijacker struct { - corsdomains []string - rpcServer *Server -} - -// ServeHTTP will hijack the connection, wraps the captured connection in a -// HttpMessageStream which is then used as codec. -func (h *httpConnHijacker) ServeHTTP(w http.ResponseWriter, req *http.Request) { - hj, ok := w.(http.Hijacker) - if !ok { - http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) - return - } - - conn, rwbuf, err := hj.Hijack() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - httpRequestStream := NewHTTPMessageStream(conn, rwbuf, req, h.corsdomains) - - codec := NewJSONCodec(httpRequestStream) - go h.rpcServer.ServeCodec(codec) -} - -// NewHTTPServer creates a new HTTP RPC server around an API provider. -func NewHTTPServer(cors string, handler *Server) *http.Server { - return &http.Server{ - Handler: &httpConnHijacker{ - corsdomains: strings.Split(cors, ","), - rpcServer: handler, - }, - } -} - // httpClient connects to a geth RPC server over HTTP. type httpClient struct { endpoint *url.URL // HTTP-RPC server endpoint @@ -313,3 +96,55 @@ func (client *httpClient) Close() { func (client *httpClient) SupportedModules() (map[string]string, error) { return SupportedModules(client) } + +// httpReadWriteNopCloser wraps a io.Reader and io.Writer with a NOP Close method. +type httpReadWriteNopCloser struct { + io.Reader + io.Writer +} + +// Close does nothing and returns always nil +func (t *httpReadWriteNopCloser) Close() error { + return nil +} + +// newJSONHTTPHandler creates a HTTP handler that will parse incoming JSON requests, +// send the request to the given API provider and sends the response back to the caller. +func newJSONHTTPHandler(srv *Server) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.ContentLength > maxHTTPRequestContentLength { + http.Error(w, + fmt.Sprintf("content length too large (%d>%d)", r.ContentLength, maxHTTPRequestContentLength), + http.StatusRequestEntityTooLarge) + return + } + + w.Header().Set("content-type", "application/json") + + // create a codec that reads direct from the request body until + // EOF and writes the response to w and order the server to process + // a single request. + codec := NewJSONCodec(&httpReadWriteNopCloser{r.Body, w}) + defer codec.Close() + srv.ServeSingleRequest(codec) + } +} + +// NewHTTPServer creates a new HTTP RPC server around an API provider. +func NewHTTPServer(corsString string, srv *Server) *http.Server { + var allowedOrigins []string + for _, domain := range strings.Split(corsString, ",") { + allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain)) + } + + c := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"POST", "GET"}, + }) + + handler := c.Handler(newJSONHTTPHandler(srv)) + + return &http.Server{ + Handler: handler, + } +} |