Skip to content

Commit ba14957

Browse files
committed
rpc/comms: use ConnState to track HTTP connections
The JSON-RPC server wraps TCP connections in order to be able to shut down them down when RPC is stopped. This is rather scary code. Go 1.3 introduced the http.Server.ConnState hook for such purposes. We can use this facility now that we depend on Go 1.4. There are multiple reasons for the switch apart from making the code less scary: * the TCP listener no longer ticks every second to check a channel * pending requests are allowed to finish after stopping the server * we can time out idle keep-alive connections
1 parent 97cdf84 commit ba14957

File tree

2 files changed

+162
-205
lines changed

2 files changed

+162
-205
lines changed

rpc/comms/http.go

Lines changed: 162 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
package comms
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
22+
"net"
2123
"net/http"
2224
"strings"
25+
"sync"
26+
"time"
2327

2428
"bytes"
29+
"io"
2530
"io/ioutil"
2631

2732
"github.com/ethereum/go-ethereum/logger"
@@ -31,10 +36,15 @@ import (
3136
"github.com/rs/cors"
3237
)
3338

39+
const (
40+
serverIdleTimeout = 10 * time.Second // idle keep-alive connections
41+
serverReadTimeout = 15 * time.Second // per-request read timeout
42+
serverWriteTimeout = 15 * time.Second // per-request read timeout
43+
)
44+
3445
var (
35-
// main HTTP rpc listener
36-
httpListener *stoppableTCPListener
37-
listenerStoppedError = fmt.Errorf("Listener has stopped")
46+
httpServerMu sync.Mutex
47+
httpServer *stopServer
3848
)
3949

4050
type HttpConfig struct {
@@ -43,42 +53,171 @@ type HttpConfig struct {
4353
CorsDomain string
4454
}
4555

56+
// stopServer augments http.Server with idle connection tracking.
57+
// Idle keep-alive connections are shut down when Close is called.
58+
type stopServer struct {
59+
*http.Server
60+
l net.Listener
61+
// connection tracking state
62+
mu sync.Mutex
63+
shutdown bool // true when Stop has returned
64+
idle map[net.Conn]struct{}
65+
}
66+
67+
type handler struct {
68+
codec codec.Codec
69+
api shared.EthereumApi
70+
}
71+
72+
// StartHTTP starts listening for RPC requests sent via HTTP.
4673
func StartHttp(cfg HttpConfig, codec codec.Codec, api shared.EthereumApi) error {
47-
if httpListener != nil {
48-
if fmt.Sprintf("%s:%d", cfg.ListenAddress, cfg.ListenPort) != httpListener.Addr().String() {
49-
return fmt.Errorf("RPC service already running on %s ", httpListener.Addr().String())
74+
httpServerMu.Lock()
75+
defer httpServerMu.Unlock()
76+
77+
addr := fmt.Sprintf("%s:%d", cfg.ListenAddress, cfg.ListenPort)
78+
if httpServer != nil {
79+
if addr != httpServer.Addr {
80+
return fmt.Errorf("RPC service already running on %s ", httpServer.Addr)
5081
}
5182
return nil // RPC service already running on given host/port
5283
}
53-
54-
l, err := newStoppableTCPListener(fmt.Sprintf("%s:%d", cfg.ListenAddress, cfg.ListenPort))
84+
// Set up the request handler, wrapping it with CORS headers if configured.
85+
handler := http.Handler(&handler{codec, api})
86+
if len(cfg.CorsDomain) > 0 {
87+
opts := cors.Options{
88+
AllowedMethods: []string{"POST"},
89+
AllowedOrigins: strings.Split(cfg.CorsDomain, " "),
90+
}
91+
handler = cors.New(opts).Handler(handler)
92+
}
93+
// Start the server.
94+
s, err := listenHTTP(addr, handler)
5595
if err != nil {
5696
glog.V(logger.Error).Infof("Can't listen on %s:%d: %v", cfg.ListenAddress, cfg.ListenPort, err)
5797
return err
5898
}
59-
httpListener = l
99+
httpServer = s
100+
return nil
101+
}
60102

61-
var handler http.Handler
62-
if len(cfg.CorsDomain) > 0 {
63-
var opts cors.Options
64-
opts.AllowedMethods = []string{"POST"}
65-
opts.AllowedOrigins = strings.Split(cfg.CorsDomain, " ")
103+
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
104+
w.Header().Set("Content-Type", "application/json")
66105

67-
c := cors.New(opts)
68-
handler = newStoppableHandler(c.Handler(gethHttpHandler(codec, api)), l.stop)
69-
} else {
70-
handler = newStoppableHandler(gethHttpHandler(codec, api), l.stop)
106+
// Limit request size to resist DoS
107+
if req.ContentLength > maxHttpSizeReqLength {
108+
err := fmt.Errorf("Request too large")
109+
response := shared.NewRpcErrorResponse(-1, shared.JsonRpcVersion, -32700, err)
110+
sendJSON(w, &response)
111+
return
71112
}
72113

73-
go http.Serve(l, handler)
114+
defer req.Body.Close()
115+
payload, err := ioutil.ReadAll(req.Body)
116+
if err != nil {
117+
err := fmt.Errorf("Could not read request body")
118+
response := shared.NewRpcErrorResponse(-1, shared.JsonRpcVersion, -32700, err)
119+
sendJSON(w, &response)
120+
return
121+
}
74122

75-
return nil
123+
c := h.codec.New(nil)
124+
var rpcReq shared.Request
125+
if err = c.Decode(payload, &rpcReq); err == nil {
126+
reply, err := h.api.Execute(&rpcReq)
127+
res := shared.NewRpcResponse(rpcReq.Id, rpcReq.Jsonrpc, reply, err)
128+
sendJSON(w, &res)
129+
return
130+
}
131+
132+
var reqBatch []shared.Request
133+
if err = c.Decode(payload, &reqBatch); err == nil {
134+
resBatch := make([]*interface{}, len(reqBatch))
135+
resCount := 0
136+
for i, rpcReq := range reqBatch {
137+
reply, err := h.api.Execute(&rpcReq)
138+
if rpcReq.Id != nil { // this leaves nil entries in the response batch for later removal
139+
resBatch[i] = shared.NewRpcResponse(rpcReq.Id, rpcReq.Jsonrpc, reply, err)
140+
resCount += 1
141+
}
142+
}
143+
// make response omitting nil entries
144+
sendJSON(w, resBatch[:resCount])
145+
return
146+
}
147+
148+
// invalid request
149+
err = fmt.Errorf("Could not decode request")
150+
res := shared.NewRpcErrorResponse(-1, shared.JsonRpcVersion, -32600, err)
151+
sendJSON(w, res)
76152
}
77153

154+
func sendJSON(w io.Writer, v interface{}) {
155+
if glog.V(logger.Detail) {
156+
if payload, err := json.MarshalIndent(v, "", "\t"); err == nil {
157+
glog.Infof("Sending payload: %s", payload)
158+
}
159+
}
160+
if err := json.NewEncoder(w).Encode(v); err != nil {
161+
glog.V(logger.Error).Infoln("Error sending JSON:", err)
162+
}
163+
}
164+
165+
// Stop closes all active HTTP connections and shuts down the server.
78166
func StopHttp() {
79-
if httpListener != nil {
80-
httpListener.Stop()
81-
httpListener = nil
167+
httpServerMu.Lock()
168+
defer httpServerMu.Unlock()
169+
if httpServer != nil {
170+
httpServer.Close()
171+
httpServer = nil
172+
}
173+
}
174+
175+
func listenHTTP(addr string, h http.Handler) (*stopServer, error) {
176+
l, err := net.Listen("tcp", addr)
177+
if err != nil {
178+
return nil, err
179+
}
180+
s := &stopServer{l: l, idle: make(map[net.Conn]struct{})}
181+
s.Server = &http.Server{
182+
Addr: addr,
183+
Handler: h,
184+
ReadTimeout: serverReadTimeout,
185+
WriteTimeout: serverWriteTimeout,
186+
ConnState: s.connState,
187+
}
188+
go s.Serve(l)
189+
return s, nil
190+
}
191+
192+
func (s *stopServer) connState(c net.Conn, state http.ConnState) {
193+
s.mu.Lock()
194+
defer s.mu.Unlock()
195+
// Close c immediately if we're past shutdown.
196+
if s.shutdown {
197+
if state != http.StateClosed {
198+
c.Close()
199+
}
200+
return
201+
}
202+
if state == http.StateIdle {
203+
s.idle[c] = struct{}{}
204+
} else {
205+
delete(s.idle, c)
206+
}
207+
}
208+
209+
func (s *stopServer) Close() {
210+
s.mu.Lock()
211+
defer s.mu.Unlock()
212+
// Shut down the acceptor. No new connections can be created.
213+
s.l.Close()
214+
// Drop all idle connections. Non-idle connections will be
215+
// closed by connState as soon as they become idle.
216+
s.shutdown = true
217+
for c := range s.idle {
218+
glog.V(logger.Detail).Infof("closing idle connection %v", c.RemoteAddr())
219+
c.Close()
220+
delete(s.idle, c)
82221
}
83222
}
84223

0 commit comments

Comments
 (0)