04e175b8ec
* rpc: implement websockets with github.com/gorilla/websocket This change makes package rpc use the github.com/gorilla/websocket package for WebSockets instead of golang.org/x/net/websocket. The new library is more robust and supports all WebSocket features including continuation frames. There are new tests for two issues with the previously-used library: - TestWebsocketClientPing checks handling of Ping frames. - TestWebsocketLargeCall checks whether the request size limit is applied correctly. * rpc: raise HTTP/WebSocket request size limit to 5MB * rpc: remove default origin for client connections The client used to put the local hostname into the Origin header because the server wanted an origin to accept the connection, but that's silly: Origin is for browsers/websites. The nobody would whitelist a particular hostname. Now that the server doesn't need Origin anymore, don't bother setting one for clients. Users who need an origin can use DialWebsocket to create a client with arbitrary origin if needed. * vendor: put golang.org/x/net/websocket back * rpc: don't set Origin header for empty (default) origin * rpc: add HTTP status code to handshake error This makes it easier to debug failing connections. * ethstats: use github.com/gorilla/websocket * rpc: fix lint
359 lines
11 KiB
Go
359 lines
11 KiB
Go
// 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/ethereum/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)
|
|
|
|
// 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}
|
|
}
|