aboutsummaryrefslogtreecommitdiff
path: root/rpc/http.go
diff options
context:
space:
mode:
authorDeterminant <tederminant@gmail.com>2020-06-28 14:47:41 -0400
committerDeterminant <tederminant@gmail.com>2020-06-28 14:47:41 -0400
commitd235e2c6a5788ec4a6cff15a16f56b38a3876a0d (patch)
tree5f2727f7a50ee5840f889c82776d3a30a88dd59b /rpc/http.go
parent13ebd8bd9468e9d769d598b0ca2afb72ba78cb97 (diff)
...
Diffstat (limited to 'rpc/http.go')
-rw-r--r--rpc/http.go359
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}
+}