diff options
author | Determinant <[email protected]> | 2020-06-28 14:47:41 -0400 |
---|---|---|
committer | Determinant <[email protected]> | 2020-06-28 14:47:41 -0400 |
commit | d235e2c6a5788ec4a6cff15a16f56b38a3876a0d (patch) | |
tree | 5f2727f7a50ee5840f889c82776d3a30a88dd59b /rpc/http.go | |
parent | 13ebd8bd9468e9d769d598b0ca2afb72ba78cb97 (diff) |
...
Diffstat (limited to 'rpc/http.go')
-rw-r--r-- | rpc/http.go | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/rpc/http.go b/rpc/http.go new file mode 100644 index 0000000..2dffc5d --- /dev/null +++ b/rpc/http.go @@ -0,0 +1,359 @@ +// Copyright 2015 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 rpc + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/ava-labs/go-ethereum/log" + "github.com/rs/cors" +) + +const ( + maxRequestContentLength = 1024 * 1024 * 5 + contentType = "application/json" +) + +// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13 +var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"} + +type httpConn struct { + client *http.Client + req *http.Request + closeOnce sync.Once + closed chan interface{} +} + +// httpConn is treated specially by Client. +func (hc *httpConn) Write(context.Context, interface{}) error { + panic("Write called on httpConn") +} + +func (hc *httpConn) RemoteAddr() string { + return hc.req.URL.String() +} + +func (hc *httpConn) Read() ([]*jsonrpcMessage, bool, error) { + <-hc.closed + return nil, false, io.EOF +} + +func (hc *httpConn) Close() { + hc.closeOnce.Do(func() { close(hc.closed) }) +} + +func (hc *httpConn) Closed() <-chan interface{} { + return hc.closed +} + +// HTTPTimeouts represents the configuration params for the HTTP RPC server. +type HTTPTimeouts struct { + // ReadTimeout is the maximum duration for reading the entire + // request, including the body. + // + // Because ReadTimeout does not let Handlers make per-request + // decisions on each request body's acceptable deadline or + // upload rate, most users will prefer to use + // ReadHeaderTimeout. It is valid to use them both. + ReadTimeout time.Duration + + // WriteTimeout is the maximum duration before timing out + // writes of the response. It is reset whenever a new + // request's header is read. Like ReadTimeout, it does not + // let Handlers make decisions on a per-request basis. + WriteTimeout time.Duration + + // IdleTimeout is the maximum amount of time to wait for the + // next request when keep-alives are enabled. If IdleTimeout + // is zero, the value of ReadTimeout is used. If both are + // zero, ReadHeaderTimeout is used. + IdleTimeout time.Duration +} + +// DefaultHTTPTimeouts represents the default timeout values used if further +// configuration is not provided. +var DefaultHTTPTimeouts = HTTPTimeouts{ + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, +} + +// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP +// using the provided HTTP Client. +func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) { + req, err := http.NewRequest(http.MethodPost, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + req.Header.Set("Accept", contentType) + + initctx := context.Background() + return newClient(initctx, func(context.Context) (ServerCodec, error) { + return &httpConn{client: client, req: req, closed: make(chan interface{})}, nil + }) +} + +// DialHTTP creates a new RPC client that connects to an RPC server over HTTP. +func DialHTTP(endpoint string) (*Client, error) { + return DialHTTPWithClient(endpoint, new(http.Client)) +} + +func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error { + hc := c.writeConn.(*httpConn) + respBody, err := hc.doRequest(ctx, msg) + if respBody != nil { + defer respBody.Close() + } + + if err != nil { + if respBody != nil { + buf := new(bytes.Buffer) + if _, err2 := buf.ReadFrom(respBody); err2 == nil { + return fmt.Errorf("%v %v", err, buf.String()) + } + } + return err + } + var respmsg jsonrpcMessage + if err := json.NewDecoder(respBody).Decode(&respmsg); err != nil { + return err + } + op.resp <- &respmsg + return nil +} + +func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error { + hc := c.writeConn.(*httpConn) + respBody, err := hc.doRequest(ctx, msgs) + if err != nil { + return err + } + defer respBody.Close() + var respmsgs []jsonrpcMessage + if err := json.NewDecoder(respBody).Decode(&respmsgs); err != nil { + return err + } + for i := 0; i < len(respmsgs); i++ { + op.resp <- &respmsgs[i] + } + return nil +} + +func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadCloser, error) { + body, err := json.Marshal(msg) + if err != nil { + return nil, err + } + req := hc.req.WithContext(ctx) + req.Body = ioutil.NopCloser(bytes.NewReader(body)) + req.ContentLength = int64(len(body)) + + resp, err := hc.client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return resp.Body, errors.New(resp.Status) + } + return resp.Body, nil +} + +// httpServerConn turns a HTTP connection into a Conn. +type httpServerConn struct { + io.Reader + io.Writer + r *http.Request +} + +func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec { + body := io.LimitReader(r.Body, maxRequestContentLength) + conn := &httpServerConn{Reader: body, Writer: w, r: r} + return NewJSONCodec(conn) +} + +// Close does nothing and always returns nil. +func (t *httpServerConn) Close() error { return nil } + +// RemoteAddr returns the peer address of the underlying connection. +func (t *httpServerConn) RemoteAddr() string { + return t.r.RemoteAddr +} + +// SetWriteDeadline does nothing and always returns nil. +func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil } + +// NewHTTPServer creates a new HTTP RPC server around an API provider. +// +// Deprecated: Server implements http.Handler +func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv http.Handler) *http.Server { + // Wrap the CORS-handler within a host-handler + handler := newCorsHandler(srv, cors) + handler = newVHostHandler(vhosts, handler) + handler = newGzipHandler(handler) + + // Make sure timeout values are meaningful + if timeouts.ReadTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", DefaultHTTPTimeouts.ReadTimeout) + timeouts.ReadTimeout = DefaultHTTPTimeouts.ReadTimeout + } + if timeouts.WriteTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", DefaultHTTPTimeouts.WriteTimeout) + timeouts.WriteTimeout = DefaultHTTPTimeouts.WriteTimeout + } + if timeouts.IdleTimeout < time.Second { + log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", DefaultHTTPTimeouts.IdleTimeout) + timeouts.IdleTimeout = DefaultHTTPTimeouts.IdleTimeout + } + // Bundle and start the HTTP server + return &http.Server{ + Handler: handler, + ReadTimeout: timeouts.ReadTimeout, + WriteTimeout: timeouts.WriteTimeout, + IdleTimeout: timeouts.IdleTimeout, + } +} + +// ServeHTTP serves JSON-RPC requests over HTTP. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Permit dumb empty requests for remote health-checks (AWS) + if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" { + return + } + if code, err := validateRequest(r); err != nil { + http.Error(w, err.Error(), code) + return + } + // All checks passed, create a codec that reads direct from the request body + // untilEOF and writes the response to w and order the server to process a + // single request. + ctx := r.Context() + ctx = context.WithValue(ctx, "remote", r.RemoteAddr) + ctx = context.WithValue(ctx, "scheme", r.Proto) + ctx = context.WithValue(ctx, "local", r.Host) + if ua := r.Header.Get("User-Agent"); ua != "" { + ctx = context.WithValue(ctx, "User-Agent", ua) + } + if origin := r.Header.Get("Origin"); origin != "" { + ctx = context.WithValue(ctx, "Origin", origin) + } + + w.Header().Set("content-type", contentType) + codec := newHTTPServerConn(r, w) + defer codec.Close() + s.serveSingleRequest(ctx, codec) +} + +// validateRequest returns a non-zero response code and error message if the +// request is invalid. +func validateRequest(r *http.Request) (int, error) { + if r.Method == http.MethodPut || r.Method == http.MethodDelete { + return http.StatusMethodNotAllowed, errors.New("method not allowed") + } + if r.ContentLength > maxRequestContentLength { + err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, maxRequestContentLength) + return http.StatusRequestEntityTooLarge, err + } + // Allow OPTIONS (regardless of content-type) + if r.Method == http.MethodOptions { + return 0, nil + } + // Check content-type + if mt, _, err := mime.ParseMediaType(r.Header.Get("content-type")); err == nil { + for _, accepted := range acceptedContentTypes { + if accepted == mt { + return 0, nil + } + } + } + // Invalid content-type + err := fmt.Errorf("invalid content type, only %s is supported", contentType) + return http.StatusUnsupportedMediaType, err +} + +func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler { + // disable CORS support if user has not specified a custom CORS configuration + if len(allowedOrigins) == 0 { + return srv + } + c := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{http.MethodPost, http.MethodGet}, + MaxAge: 600, + AllowedHeaders: []string{"*"}, + }) + return c.Handler(srv) +} + +// virtualHostHandler is a handler which validates the Host-header of incoming requests. +// The virtualHostHandler can prevent DNS rebinding attacks, which do not utilize CORS-headers, +// since they do in-domain requests against the RPC api. Instead, we can see on the Host-header +// which domain was used, and validate that against a whitelist. +type virtualHostHandler struct { + vhosts map[string]struct{} + next http.Handler +} + +// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler +func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // if r.Host is not set, we can continue serving since a browser would set the Host header + if r.Host == "" { + h.next.ServeHTTP(w, r) + return + } + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + // Either invalid (too many colons) or no port specified + host = r.Host + } + if ipAddr := net.ParseIP(host); ipAddr != nil { + // It's an IP address, we can serve that + h.next.ServeHTTP(w, r) + return + + } + // Not an ip address, but a hostname. Need to validate + if _, exist := h.vhosts["*"]; exist { + h.next.ServeHTTP(w, r) + return + } + if _, exist := h.vhosts[host]; exist { + h.next.ServeHTTP(w, r) + return + } + http.Error(w, "invalid host specified", http.StatusForbidden) +} + +func newVHostHandler(vhosts []string, next http.Handler) http.Handler { + vhostMap := make(map[string]struct{}) + for _, allowedHost := range vhosts { + vhostMap[strings.ToLower(allowedHost)] = struct{}{} + } + return &virtualHostHandler{vhostMap, next} +} |