mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-08 21:10:10 +00:00
Fixes #4802. The Go HTTP server has a global panic handler for requests, so it was not as severe as first thought. This fix can still panic, since we try to send a `500` response - if that happens, the Go HTTP server will terminate the connection. Otherwise, the client will get a 200 response, which we should avoid. I'm sort of torn on whether it's even necessary to include this fix, instead of just letting the HTTP server deal with it.
260 lines
7.5 KiB
Go
260 lines
7.5 KiB
Go
// Commons for HTTP handling
|
|
package rpcserver
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime/debug"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/netutil"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
types "github.com/tendermint/tendermint/rpc/lib/types"
|
|
)
|
|
|
|
// Config is a RPC server configuration.
|
|
type Config struct {
|
|
// see netutil.LimitListener
|
|
MaxOpenConnections int
|
|
// mirrors http.Server#ReadTimeout
|
|
ReadTimeout time.Duration
|
|
// mirrors http.Server#WriteTimeout
|
|
WriteTimeout time.Duration
|
|
// MaxBodyBytes controls the maximum number of bytes the
|
|
// server will read parsing the request body.
|
|
MaxBodyBytes int64
|
|
// mirrors http.Server#MaxHeaderBytes
|
|
MaxHeaderBytes int
|
|
}
|
|
|
|
// DefaultConfig returns a default configuration.
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
MaxOpenConnections: 0, // unlimited
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxBodyBytes: int64(1000000), // 1MB
|
|
MaxHeaderBytes: 1 << 20, // same as the net/http default
|
|
}
|
|
}
|
|
|
|
// StartHTTPServer takes a listener and starts an HTTP server with the given handler.
|
|
// It wraps handler with RecoverAndLogHandler.
|
|
// NOTE: This function blocks - you may want to call it in a go-routine.
|
|
func StartHTTPServer(listener net.Listener, handler http.Handler, logger log.Logger, config *Config) error {
|
|
logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr()))
|
|
s := &http.Server{
|
|
Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger),
|
|
ReadTimeout: config.ReadTimeout,
|
|
WriteTimeout: config.WriteTimeout,
|
|
MaxHeaderBytes: config.MaxHeaderBytes,
|
|
}
|
|
err := s.Serve(listener)
|
|
logger.Info("RPC HTTP server stopped", "err", err)
|
|
return err
|
|
}
|
|
|
|
// StartHTTPAndTLSServer takes a listener and starts an HTTPS server with the given handler.
|
|
// It wraps handler with RecoverAndLogHandler.
|
|
// NOTE: This function blocks - you may want to call it in a go-routine.
|
|
func StartHTTPAndTLSServer(
|
|
listener net.Listener,
|
|
handler http.Handler,
|
|
certFile, keyFile string,
|
|
logger log.Logger,
|
|
config *Config,
|
|
) error {
|
|
logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)",
|
|
listener.Addr(), certFile, keyFile))
|
|
s := &http.Server{
|
|
Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger),
|
|
ReadTimeout: config.ReadTimeout,
|
|
WriteTimeout: config.WriteTimeout,
|
|
MaxHeaderBytes: config.MaxHeaderBytes,
|
|
}
|
|
err := s.ServeTLS(listener, certFile, keyFile)
|
|
|
|
logger.Error("RPC HTTPS server stopped", "err", err)
|
|
return err
|
|
}
|
|
|
|
func WriteRPCResponseHTTPError(
|
|
w http.ResponseWriter,
|
|
httpCode int,
|
|
res types.RPCResponse,
|
|
) {
|
|
jsonBytes, err := json.MarshalIndent(res, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(httpCode)
|
|
if _, err := w.Write(jsonBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) {
|
|
jsonBytes, err := json.MarshalIndent(res, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
if _, err := w.Write(jsonBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// WriteRPCResponseArrayHTTP will do the same as WriteRPCResponseHTTP, except it
|
|
// can write arrays of responses for batched request/response interactions via
|
|
// the JSON RPC.
|
|
func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res []types.RPCResponse) {
|
|
if len(res) == 1 {
|
|
WriteRPCResponseHTTP(w, res[0])
|
|
} else {
|
|
jsonBytes, err := json.MarshalIndent(res, "", " ")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(200)
|
|
if _, err := w.Write(jsonBytes); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// RecoverAndLogHandler wraps an HTTP handler, adding error logging.
|
|
// If the inner function panics, the outer function recovers, logs, sends an
|
|
// HTTP 500 error response.
|
|
func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Wrap the ResponseWriter to remember the status
|
|
rww := &ResponseWriterWrapper{-1, w}
|
|
begin := time.Now()
|
|
|
|
rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix()))
|
|
|
|
defer func() {
|
|
// Handle any panics in the panic handler below. Does not use the logger, since we want
|
|
// to avoid any further panics. However, we try to return a 500, since it otherwise
|
|
// defaults to 200 and there is no other way to terminate the connection. If that
|
|
// should panic for whatever reason then the Go HTTP server will handle it and
|
|
// terminate the connection - panicing is the de-facto and only way to get the Go HTTP
|
|
// server to terminate the request and close the connection/stream:
|
|
// https://github.com/golang/go/issues/17790#issuecomment-258481416
|
|
if e := recover(); e != nil {
|
|
fmt.Fprintf(os.Stderr, "Panic during RPC panic recovery: %v\n%v\n", e, string(debug.Stack()))
|
|
w.WriteHeader(500)
|
|
}
|
|
}()
|
|
|
|
defer func() {
|
|
// Send a 500 error if a panic happens during a handler.
|
|
// Without this, Chrome & Firefox were retrying aborted ajax requests,
|
|
// at least to my localhost.
|
|
if e := recover(); e != nil {
|
|
|
|
// If RPCResponse
|
|
if res, ok := e.(types.RPCResponse); ok {
|
|
WriteRPCResponseHTTP(rww, res)
|
|
} else {
|
|
// Panics can contain anything, attempt to normalize it as an error.
|
|
var err error
|
|
switch e := e.(type) {
|
|
case error:
|
|
err = e
|
|
case string:
|
|
err = errors.New(e)
|
|
case fmt.Stringer:
|
|
err = errors.New(e.String())
|
|
default:
|
|
}
|
|
|
|
logger.Error(
|
|
"Panic in RPC HTTP handler", "err", e, "stack",
|
|
string(debug.Stack()),
|
|
)
|
|
WriteRPCResponseHTTPError(
|
|
rww,
|
|
http.StatusInternalServerError,
|
|
types.RPCInternalError(types.JSONRPCIntID(-1), err),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Finally, log.
|
|
durationMS := time.Since(begin).Nanoseconds() / 1000000
|
|
if rww.Status == -1 {
|
|
rww.Status = 200
|
|
}
|
|
logger.Info("Served RPC HTTP response",
|
|
"method", r.Method, "url", r.URL,
|
|
"status", rww.Status, "duration", durationMS,
|
|
"remoteAddr", r.RemoteAddr,
|
|
)
|
|
}()
|
|
|
|
handler.ServeHTTP(rww, r)
|
|
})
|
|
}
|
|
|
|
// Remember the status for logging
|
|
type ResponseWriterWrapper struct {
|
|
Status int
|
|
http.ResponseWriter
|
|
}
|
|
|
|
func (w *ResponseWriterWrapper) WriteHeader(status int) {
|
|
w.Status = status
|
|
w.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
// implements http.Hijacker
|
|
func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return w.ResponseWriter.(http.Hijacker).Hijack()
|
|
}
|
|
|
|
type maxBytesHandler struct {
|
|
h http.Handler
|
|
n int64
|
|
}
|
|
|
|
func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, h.n)
|
|
h.h.ServeHTTP(w, r)
|
|
}
|
|
|
|
// Listen starts a new net.Listener on the given address.
|
|
// It returns an error if the address is invalid or the call to Listen() fails.
|
|
func Listen(addr string, config *Config) (listener net.Listener, err error) {
|
|
parts := strings.SplitN(addr, "://", 2)
|
|
if len(parts) != 2 {
|
|
return nil, errors.Errorf(
|
|
"invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)",
|
|
addr,
|
|
)
|
|
}
|
|
proto, addr := parts[0], parts[1]
|
|
listener, err = net.Listen(proto, addr)
|
|
if err != nil {
|
|
return nil, errors.Errorf("failed to listen on %v: %v", addr, err)
|
|
}
|
|
if config.MaxOpenConnections > 0 {
|
|
listener = netutil.LimitListener(listener, config.MaxOpenConnections)
|
|
}
|
|
|
|
return listener, nil
|
|
}
|