infra/op-txproxy/auth_handler.go

102 lines
2.7 KiB
Go
Raw Normal View History

package op_txproxy
import (
"bytes"
"context"
"io"
"net/http"
"strings"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
var (
defaultBodyLimit = 5 * 1024 * 1024 // default in op-geth
DefaultAuthHeaderKey = "X-Optimism-Signature"
)
type authHandler struct {
headerKey string
next http.Handler
}
// This middleware detects when authentication information is present on the request. If
// so, it will validate and set the caller in the request context. It does not reject
// if authentication information is missing. It is up to the request handler to do so via
// the missing `AuthContext`
// - NOTE: only up to the default body limit (5MB) is read when constructing the text hash
// that is signed over by the caller
func AuthMiddleware(headerKey string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return &authHandler{headerKey, next}
}
}
type authContextKey struct{}
type AuthContext struct {
Caller common.Address
}
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get(h.headerKey)
if authHeader == "" {
h.next.ServeHTTP(w, r)
return
}
authElems := strings.Split(authHeader, ":")
if len(authElems) != 2 {
http.Error(w, "misformatted auth header", http.StatusBadRequest)
return
}
if r.Body == nil {
// edge case from unit tests
r.Body = io.NopCloser(bytes.NewBuffer(nil))
}
// Since this middleware runs prior to the server, we need to manually apply the body limit when reading.
bodyBytes, err := io.ReadAll(io.LimitReader(r.Body, int64(defaultBodyLimit)))
if err != nil {
http.Error(w, "unable to parse request body", http.StatusInternalServerError)
return
}
r.Body = struct {
io.Reader
io.Closer
}{
io.MultiReader(bytes.NewReader(bodyBytes), r.Body),
r.Body,
}
txtHash := accounts.TextHash(bodyBytes)
caller, signature := common.HexToAddress(authElems[0]), common.FromHex(authElems[1])
sigPubKey, err := crypto.SigToPub(txtHash, signature)
if err != nil {
http.Error(w, "invalid authentication signature", http.StatusBadRequest)
return
}
if caller != crypto.PubkeyToAddress(*sigPubKey) {
http.Error(w, "mismatched recovered signer", http.StatusBadRequest)
return
}
// Set the authenticated caller in the context
newCtx := context.WithValue(r.Context(), authContextKey{}, &AuthContext{caller})
h.next.ServeHTTP(w, r.WithContext(newCtx))
}
func AuthFromContext(ctx context.Context) *AuthContext {
auth, ok := ctx.Value(authContextKey{}).(*AuthContext)
if !ok {
return nil
}
return auth
}