1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package rpc
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"time"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
optls "github.com/ethereum-optimism/optimism/op-service/tls"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/rpc"
)
var wildcardHosts = []string{"*"}
type Server struct {
endpoint string
apis []rpc.API
appVersion string
healthzHandler http.Handler
corsHosts []string
vHosts []string
jwtSecret []byte
rpcPath string
healthzPath string
httpRecorder opmetrics.HTTPRecorder
httpServer *http.Server
log log.Logger
tls *ServerTLSConfig
middlewares []Middleware
}
type ServerTLSConfig struct {
Config *tls.Config
CLIConfig *optls.CLIConfig // paths to certificate and key files
}
type ServerOption func(b *Server)
type Middleware func(next http.Handler) http.Handler
func WithAPIs(apis []rpc.API) ServerOption {
return func(b *Server) {
b.apis = apis
}
}
func WithHealthzHandler(hdlr http.Handler) ServerOption {
return func(b *Server) {
b.healthzHandler = hdlr
}
}
func WithCORSHosts(hosts []string) ServerOption {
return func(b *Server) {
b.corsHosts = hosts
}
}
func WithVHosts(hosts []string) ServerOption {
return func(b *Server) {
b.vHosts = hosts
}
}
func WithJWTSecret(secret []byte) ServerOption {
return func(b *Server) {
b.jwtSecret = secret
}
}
func WithRPCPath(path string) ServerOption {
return func(b *Server) {
b.rpcPath = path
}
}
func WithHealthzPath(path string) ServerOption {
return func(b *Server) {
b.healthzPath = path
}
}
func WithHTTPRecorder(recorder opmetrics.HTTPRecorder) ServerOption {
return func(b *Server) {
b.httpRecorder = recorder
}
}
func WithLogger(lgr log.Logger) ServerOption {
return func(b *Server) {
b.log = lgr
}
}
// WithTLSConfig configures TLS for the RPC server
// If this option is passed, the server will use ListenAndServeTLS
func WithTLSConfig(tls *ServerTLSConfig) ServerOption {
return func(b *Server) {
b.tls = tls
}
}
// WithMiddleware adds an http.Handler to the rpc server handler stack
// The added middleware is invoked directly before the RPC callback
func WithMiddleware(middleware func(http.Handler) (hdlr http.Handler)) ServerOption {
return func(b *Server) {
b.middlewares = append(b.middlewares, middleware)
}
}
func NewServer(host string, port int, appVersion string, opts ...ServerOption) *Server {
endpoint := net.JoinHostPort(host, strconv.Itoa(port))
bs := &Server{
endpoint: endpoint,
appVersion: appVersion,
healthzHandler: defaultHealthzHandler(appVersion),
corsHosts: wildcardHosts,
vHosts: wildcardHosts,
rpcPath: "/",
healthzPath: "/healthz",
httpRecorder: opmetrics.NoopHTTPRecorder,
httpServer: &http.Server{
Addr: endpoint,
},
log: log.Root(),
}
for _, opt := range opts {
opt(bs)
}
if bs.tls != nil {
bs.httpServer.TLSConfig = bs.tls.Config
}
bs.AddAPI(rpc.API{
Namespace: "health",
Service: &healthzAPI{
appVersion: appVersion,
},
})
return bs
}
func (b *Server) Endpoint() string {
return b.endpoint
}
func (b *Server) AddAPI(api rpc.API) {
b.apis = append(b.apis, api)
}
func (b *Server) Start() error {
srv := rpc.NewServer()
if err := node.RegisterApis(b.apis, nil, srv); err != nil {
return fmt.Errorf("error registering APIs: %w", err)
}
// rpc middleware
var nodeHdlr http.Handler = srv
for _, middleware := range b.middlewares {
nodeHdlr = middleware(nodeHdlr)
}
nodeHdlr = node.NewHTTPHandlerStack(nodeHdlr, b.corsHosts, b.vHosts, b.jwtSecret)
mux := http.NewServeMux()
mux.Handle(b.rpcPath, nodeHdlr)
mux.Handle(b.healthzPath, b.healthzHandler)
// http middleware
var handler http.Handler = mux
handler = optls.NewPeerTLSMiddleware(handler)
handler = opmetrics.NewHTTPRecordingMiddleware(b.httpRecorder, handler)
handler = oplog.NewLoggingMiddleware(b.log, handler)
b.httpServer.Handler = handler
errCh := make(chan error, 1)
go func() {
if b.tls != nil {
if err := b.httpServer.ListenAndServeTLS("", ""); err != nil {
errCh <- err
}
} else {
if err := b.httpServer.ListenAndServe(); err != nil {
errCh <- err
}
}
}()
// verify that the server comes up
tick := time.NewTimer(10 * time.Millisecond)
select {
case err := <-errCh:
return fmt.Errorf("http server failed: %w", err)
case <-tick.C:
return nil
}
}
func (b *Server) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = b.httpServer.Shutdown(ctx)
return nil
}
type HealthzResponse struct {
Version string `json:"version"`
}
func defaultHealthzHandler(appVersion string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
_ = enc.Encode(&HealthzResponse{Version: appVersion})
}
}
type healthzAPI struct {
appVersion string
}
func (h *healthzAPI) Status() string {
return h.appVersion
}