Commit 36245294 authored by Zach Howard's avatar Zach Howard Committed by GitHub

INF-23 tls and custom middleware support for op-service/rpc (#4524)

parent 7f8f0fe2
...@@ -2,6 +2,7 @@ package rpc ...@@ -2,6 +2,7 @@ package rpc
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
...@@ -11,6 +12,7 @@ import ( ...@@ -11,6 +12,7 @@ import (
oplog "github.com/ethereum-optimism/optimism/op-service/log" oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" 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/log"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
...@@ -32,10 +34,19 @@ type Server struct { ...@@ -32,10 +34,19 @@ type Server struct {
httpRecorder opmetrics.HTTPRecorder httpRecorder opmetrics.HTTPRecorder
httpServer *http.Server httpServer *http.Server
log log.Logger 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 ServerOption func(b *Server)
type Middleware func(next http.Handler) http.Handler
func WithAPIs(apis []rpc.API) ServerOption { func WithAPIs(apis []rpc.API) ServerOption {
return func(b *Server) { return func(b *Server) {
b.apis = apis b.apis = apis
...@@ -90,6 +101,22 @@ func WithLogger(lgr log.Logger) ServerOption { ...@@ -90,6 +101,22 @@ func WithLogger(lgr log.Logger) ServerOption {
} }
} }
// 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 { func NewServer(host string, port int, appVersion string, opts ...ServerOption) *Server {
endpoint := net.JoinHostPort(host, strconv.Itoa(port)) endpoint := net.JoinHostPort(host, strconv.Itoa(port))
bs := &Server{ bs := &Server{
...@@ -109,6 +136,9 @@ func NewServer(host string, port int, appVersion string, opts ...ServerOption) * ...@@ -109,6 +136,9 @@ func NewServer(host string, port int, appVersion string, opts ...ServerOption) *
for _, opt := range opts { for _, opt := range opts {
opt(bs) opt(bs)
} }
if bs.tls != nil {
bs.httpServer.TLSConfig = bs.tls.Config
}
bs.AddAPI(rpc.API{ bs.AddAPI(rpc.API{
Namespace: "health", Namespace: "health",
Service: &healthzAPI{ Service: &healthzAPI{
...@@ -132,16 +162,34 @@ func (b *Server) Start() error { ...@@ -132,16 +162,34 @@ func (b *Server) Start() error {
return fmt.Errorf("error registering APIs: %w", err) return fmt.Errorf("error registering APIs: %w", err)
} }
nodeHdlr := node.NewHTTPHandlerStack(srv, b.corsHosts, b.vHosts, b.jwtSecret) // 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 := http.NewServeMux()
mux.Handle(b.rpcPath, nodeHdlr) mux.Handle(b.rpcPath, nodeHdlr)
mux.Handle(b.healthzPath, b.healthzHandler) mux.Handle(b.healthzPath, b.healthzHandler)
metricsMW := oplog.NewLoggingMiddleware(b.log, opmetrics.NewHTTPRecordingMiddleware(b.httpRecorder, mux))
b.httpServer.Handler = metricsMW // 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) errCh := make(chan error, 1)
go func() { go func() {
if err := b.httpServer.ListenAndServe(); err != nil { if b.tls != nil {
errCh <- err if err := b.httpServer.ListenAndServeTLS(b.tls.CLIConfig.TLSCert, b.tls.CLIConfig.TLSKey); err != nil {
errCh <- err
}
} else {
if err := b.httpServer.ListenAndServe(); err != nil {
errCh <- err
}
} }
}() }()
......
// This file contains CLI and env TLS configurations that can be used by clients or servers
package tls
import (
"errors"
"github.com/urfave/cli"
opservice "github.com/ethereum-optimism/optimism/op-service"
)
const (
TLSCaCertFlagName = "tls.ca"
TLSCertFlagName = "tls.cert"
TLSKeyFlagName = "tls.key"
)
func CLIFlags(envPrefix string) []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: TLSCaCertFlagName,
Usage: "tls ca cert path",
Value: "tls/ca.crt",
EnvVar: opservice.PrefixEnvVar(envPrefix, "TLS_CA"),
},
cli.StringFlag{
Name: TLSCertFlagName,
Usage: "tls cert path",
Value: "tls/tls.crt",
EnvVar: opservice.PrefixEnvVar(envPrefix, "TLS_CERT"),
},
cli.StringFlag{
Name: TLSKeyFlagName,
Usage: "tls key",
Value: "tls/tls.key",
EnvVar: opservice.PrefixEnvVar(envPrefix, "TLS_KEY"),
},
}
}
type CLIConfig struct {
TLSCaCert string
TLSCert string
TLSKey string
}
func (c CLIConfig) Check() error {
if c.TLSEnabled() && (c.TLSCaCert == "" || c.TLSCert == "" || c.TLSKey == "") {
return errors.New("all tls flags must be set if at least one is set")
}
return nil
}
func (c CLIConfig) TLSEnabled() bool {
return !(c.TLSCaCert == "" && c.TLSCert == "" && c.TLSKey == "")
}
func ReadCLIConfig(ctx *cli.Context) CLIConfig {
return CLIConfig{
TLSCaCert: ctx.GlobalString(TLSCaCertFlagName),
TLSCert: ctx.GlobalString(TLSCertFlagName),
TLSKey: ctx.GlobalString(TLSKeyFlagName),
}
}
package tls
import (
"context"
"crypto/x509"
"net/http"
)
// PeerTLSInfo contains request-scoped peer certificate data
// It can be used by downstream http.Handlers to authorize access for TLS-authenticated clients
type PeerTLSInfo struct {
LeafCertificate *x509.Certificate
}
type peerTLSInfoContextKey struct{}
// NewPeerTLSMiddleware returns an http.Handler that extracts the peer's certificate data into PeerTLSInfo and attaches it to the request-scoped context.
// PeerTLSInfo will only be populated if the http.Server is listening with ListenAndServeTLS
// This is useful for ethereum-go/rpc endpoints because the http.Request object isn't accessible in the registered service.
func NewPeerTLSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
peerTlsInfo := PeerTLSInfo{}
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
peerTlsInfo.LeafCertificate = r.TLS.PeerCertificates[0]
}
ctx := context.WithValue(r.Context(), peerTLSInfoContextKey{}, peerTlsInfo)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// PeerTLSInfoFromContext extracts PeerTLSInfo from the context
// Result will only be populated if NewPeerTLSMiddleware has been added to the handler stack.
func PeerTLSInfoFromContext(ctx context.Context) PeerTLSInfo {
info, _ := ctx.Value(peerTLSInfoContextKey{}).(PeerTLSInfo)
return info
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment