github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/rpc/lib/server/http_server.go (about) 1 // Commons for HTTP handling 2 package rpcserver 3 4 import ( 5 "bufio" 6 "encoding/json" 7 "fmt" 8 "log/slog" 9 "net" 10 "net/http" 11 "runtime/debug" 12 "strings" 13 "time" 14 15 "golang.org/x/net/netutil" 16 17 types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" 18 "github.com/gnolang/gno/tm2/pkg/errors" 19 ) 20 21 // Config is a RPC server configuration. 22 type Config struct { 23 // see netutil.LimitListener 24 MaxOpenConnections int 25 // mirrors http.Server#ReadTimeout 26 ReadTimeout time.Duration 27 // mirrors http.Server#WriteTimeout 28 WriteTimeout time.Duration 29 // MaxBodyBytes controls the maximum number of bytes the 30 // server will read parsing the request body. 31 MaxBodyBytes int64 32 // mirrors http.Server#MaxHeaderBytes 33 MaxHeaderBytes int 34 } 35 36 // DefaultConfig returns a default configuration. 37 func DefaultConfig() *Config { 38 return &Config{ 39 MaxOpenConnections: 0, // unlimited 40 ReadTimeout: 10 * time.Second, 41 WriteTimeout: 10 * time.Second, 42 MaxBodyBytes: int64(5000000), // 5MB 43 MaxHeaderBytes: 1 << 20, // same as the net/http default 44 } 45 } 46 47 // StartHTTPServer takes a listener and starts an HTTP server with the given handler. 48 // It wraps handler with RecoverAndLogHandler. 49 // NOTE: This function blocks - you may want to call it in a go-routine. 50 func StartHTTPServer(listener net.Listener, handler http.Handler, logger *slog.Logger, config *Config) error { 51 logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr())) 52 s := &http.Server{ 53 Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), 54 ReadTimeout: config.ReadTimeout, 55 ReadHeaderTimeout: 60 * time.Second, 56 WriteTimeout: config.WriteTimeout, 57 MaxHeaderBytes: config.MaxHeaderBytes, 58 } 59 err := s.Serve(listener) 60 logger.Info("RPC HTTP server stopped", "err", err) 61 return err 62 } 63 64 // StartHTTPAndTLSServer takes a listener and starts an HTTPS server with the given handler. 65 // It wraps handler with RecoverAndLogHandler. 66 // NOTE: This function blocks - you may want to call it in a go-routine. 67 func StartHTTPAndTLSServer( 68 listener net.Listener, 69 handler http.Handler, 70 certFile, keyFile string, 71 logger *slog.Logger, 72 config *Config, 73 ) error { 74 logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)", 75 listener.Addr(), certFile, keyFile)) 76 s := &http.Server{ 77 Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), 78 ReadTimeout: config.ReadTimeout, 79 ReadHeaderTimeout: 60 * time.Second, 80 WriteTimeout: config.WriteTimeout, 81 MaxHeaderBytes: config.MaxHeaderBytes, 82 } 83 err := s.ServeTLS(listener, certFile, keyFile) 84 85 logger.Error("RPC HTTPS server stopped", "err", err) 86 return err 87 } 88 89 func WriteRPCResponseHTTPError( 90 w http.ResponseWriter, 91 httpCode int, 92 res types.RPCResponse, 93 ) { 94 jsonBytes, err := json.MarshalIndent(res, "", " ") 95 if err != nil { 96 panic(err) 97 } 98 99 w.Header().Set("Content-Type", "application/json") 100 w.WriteHeader(httpCode) 101 if _, err := w.Write(jsonBytes); err != nil { 102 panic(err) 103 } 104 } 105 106 func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { 107 jsonBytes, err := json.MarshalIndent(res, "", " ") 108 if err != nil { 109 panic(err) 110 } 111 w.Header().Set("Content-Type", "application/json") 112 w.WriteHeader(200) 113 if _, err := w.Write(jsonBytes); err != nil { 114 panic(err) 115 } 116 } 117 118 // WriteRPCResponseArrayHTTP will do the same as WriteRPCResponseHTTP, except it 119 // can write arrays of responses for batched request/response interactions via 120 // the JSON RPC. 121 func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res types.RPCResponses) { 122 if len(res) == 1 { 123 WriteRPCResponseHTTP(w, res[0]) 124 } else { 125 jsonBytes, err := json.MarshalIndent(res, "", " ") 126 if err != nil { 127 panic(err) 128 } 129 w.Header().Set("Content-Type", "application/json") 130 w.WriteHeader(200) 131 if _, err := w.Write(jsonBytes); err != nil { 132 panic(err) 133 } 134 } 135 } 136 137 // ----------------------------------------------------------------------------- 138 139 // RecoverAndLogHandler wraps an HTTP handler, adding error logging. 140 // If the inner function panics, the outer function recovers, logs, sends an 141 // HTTP 500 error response. 142 func RecoverAndLogHandler(handler http.Handler, logger *slog.Logger) http.Handler { 143 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 // Wrap the ResponseWriter to remember the status 145 rww := &ResponseWriterWrapper{-1, w} 146 begin := time.Now() 147 148 rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) 149 150 defer func() { 151 // Send a 500 error if a panic happens during a handler. 152 // Without this, Chrome & Firefox were retrying aborted ajax requests, 153 // at least to my localhost. 154 if e := recover(); e != nil { 155 switch e := e.(type) { 156 case types.RPCResponse: 157 WriteRPCResponseHTTP(rww, e) 158 159 case error: 160 logger.Error( 161 "Panic in RPC HTTP handler", "err", e, "stack", 162 string(debug.Stack()), 163 ) 164 WriteRPCResponseHTTPError(rww, http.StatusInternalServerError, 165 types.RPCInternalError(types.JSONRPCStringID(""), e)) 166 167 default: // handle string type and any other types 168 logger.Error( 169 "Panic in RPC HTTP handler", "err", e, "stack", 170 string(debug.Stack()), 171 ) 172 WriteRPCResponseHTTPError(rww, http.StatusInternalServerError, 173 types.RPCInternalError(types.JSONRPCStringID(""), fmt.Errorf("%v", e))) 174 } 175 } 176 177 // Finally, log. 178 durationMS := time.Since(begin).Nanoseconds() / 1000000 179 if rww.Status == -1 { 180 rww.Status = 200 181 } 182 logger.Info("Served RPC HTTP response", 183 "method", r.Method, "url", r.URL, 184 "status", rww.Status, "duration", durationMS, 185 "remoteAddr", r.RemoteAddr, 186 ) 187 }() 188 189 handler.ServeHTTP(rww, r) 190 }) 191 } 192 193 // Remember the status for logging 194 type ResponseWriterWrapper struct { 195 Status int 196 http.ResponseWriter 197 } 198 199 func (w *ResponseWriterWrapper) WriteHeader(status int) { 200 w.Status = status 201 w.ResponseWriter.WriteHeader(status) 202 } 203 204 // implements http.Hijacker 205 func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { 206 return w.ResponseWriter.(http.Hijacker).Hijack() 207 } 208 209 type maxBytesHandler struct { 210 h http.Handler 211 n int64 212 } 213 214 func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 215 r.Body = http.MaxBytesReader(w, r.Body, h.n) 216 h.h.ServeHTTP(w, r) 217 } 218 219 // Listen starts a new net.Listener on the given address. 220 // It returns an error if the address is invalid or the call to Listen() fails. 221 func Listen(addr string, config *Config) (listener net.Listener, err error) { 222 parts := strings.SplitN(addr, "://", 2) 223 if len(parts) != 2 { 224 return nil, errors.New( 225 "invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", 226 addr, 227 ) 228 } 229 proto, addr := parts[0], parts[1] 230 listener, err = net.Listen(proto, addr) 231 if err != nil { 232 return nil, errors.New("failed to listen on %v: %v", addr, err) 233 } 234 if config.MaxOpenConnections > 0 { 235 listener = netutil.LimitListener(listener, config.MaxOpenConnections) 236 } 237 238 return listener, nil 239 }