diff --git a/rpc/context_headers.go b/rpc/context_headers.go new file mode 100644 index 0000000000..29a58150e3 --- /dev/null +++ b/rpc/context_headers.go @@ -0,0 +1,56 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rpc + +import ( + "context" + "net/http" +) + +type mdHeaderKey struct{} + +// NewContextWithHeaders wraps the given context, adding HTTP headers. These headers will +// be applied by Client when making a request using the returned context. +func NewContextWithHeaders(ctx context.Context, h http.Header) context.Context { + if len(h) == 0 { + // This check ensures the header map set in context will never be nil. + return ctx + } + + var ctxh http.Header + prev, ok := ctx.Value(mdHeaderKey{}).(http.Header) + if ok { + ctxh = setHeaders(prev.Clone(), h) + } else { + ctxh = h.Clone() + } + return context.WithValue(ctx, mdHeaderKey{}, ctxh) +} + +// headersFromContext is used to extract http.Header from context. +func headersFromContext(ctx context.Context) http.Header { + source, _ := ctx.Value(mdHeaderKey{}).(http.Header) + return source +} + +// setHeaders sets all headers from src in dst. +func setHeaders(dst http.Header, src http.Header) http.Header { + for key, values := range src { + dst[http.CanonicalHeaderKey(key)] = values + } + return dst +} diff --git a/rpc/http.go b/rpc/http.go index 403a7a73b2..4fe9b6a0c0 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -198,6 +198,7 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos hc.mu.Lock() req.Header = hc.headers.Clone() hc.mu.Unlock() + setHeaders(req.Header, headersFromContext(ctx)) // do request resp, err := hc.client.Do(req) diff --git a/rpc/http_test.go b/rpc/http_test.go index 9ce1587f34..584842a9aa 100644 --- a/rpc/http_test.go +++ b/rpc/http_test.go @@ -17,6 +17,8 @@ package rpc import ( + "context" + "fmt" "net/http" "net/http/httptest" "strings" @@ -125,42 +127,6 @@ func TestHTTPRespBodyUnlimited(t *testing.T) { } } -func TestHTTPPeerInfo(t *testing.T) { - s := newTestServer() - defer s.Stop() - ts := httptest.NewServer(s) - defer ts.Close() - - c, err := Dial(ts.URL) - if err != nil { - t.Fatal(err) - } - c.SetHeader("user-agent", "ua-testing") - c.SetHeader("origin", "origin.example.com") - - // Request peer information. - var info PeerInfo - if err := c.Call(&info, "test_peerInfo"); err != nil { - t.Fatal(err) - } - - if info.RemoteAddr == "" { - t.Error("RemoteAddr not set") - } - if info.Transport != "http" { - t.Errorf("wrong Transport %q", info.Transport) - } - if info.HTTP.Version != "HTTP/1.1" { - t.Errorf("wrong HTTP.Version %q", info.HTTP.Version) - } - if info.HTTP.UserAgent != "ua-testing" { - t.Errorf("wrong HTTP.UserAgent %q", info.HTTP.UserAgent) - } - if info.HTTP.Origin != "origin.example.com" { - t.Errorf("wrong HTTP.Origin %q", info.HTTP.UserAgent) - } -} - // Tests that an HTTP error results in an HTTPError instance // being returned with the expected attributes. func TestHTTPErrorResponse(t *testing.T) { @@ -199,3 +165,79 @@ func TestHTTPErrorResponse(t *testing.T) { t.Error("unexpected error message", errMsg) } } + +func TestHTTPPeerInfo(t *testing.T) { + s := newTestServer() + defer s.Stop() + ts := httptest.NewServer(s) + defer ts.Close() + + c, err := Dial(ts.URL) + if err != nil { + t.Fatal(err) + } + c.SetHeader("user-agent", "ua-testing") + c.SetHeader("origin", "origin.example.com") + + // Request peer information. + var info PeerInfo + if err := c.Call(&info, "test_peerInfo"); err != nil { + t.Fatal(err) + } + + if info.RemoteAddr == "" { + t.Error("RemoteAddr not set") + } + if info.Transport != "http" { + t.Errorf("wrong Transport %q", info.Transport) + } + if info.HTTP.Version != "HTTP/1.1" { + t.Errorf("wrong HTTP.Version %q", info.HTTP.Version) + } + if info.HTTP.UserAgent != "ua-testing" { + t.Errorf("wrong HTTP.UserAgent %q", info.HTTP.UserAgent) + } + if info.HTTP.Origin != "origin.example.com" { + t.Errorf("wrong HTTP.Origin %q", info.HTTP.UserAgent) + } +} + +func TestNewContextWithHeaders(t *testing.T) { + expectedHeaders := 0 + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + for i := 0; i < expectedHeaders; i++ { + key, want := fmt.Sprintf("key-%d", i), fmt.Sprintf("val-%d", i) + if have := request.Header.Get(key); have != want { + t.Errorf("wrong request headers for %s, want: %s, have: %s", key, want, have) + } + } + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write([]byte(`{}`)) + })) + defer server.Close() + + client, err := Dial(server.URL) + if err != nil { + t.Fatalf("failed to dial: %s", err) + } + defer client.Close() + + newHdr := func(k, v string) http.Header { + header := http.Header{} + header.Set(k, v) + return header + } + ctx1 := NewContextWithHeaders(context.Background(), newHdr("key-0", "val-0")) + ctx2 := NewContextWithHeaders(ctx1, newHdr("key-1", "val-1")) + ctx3 := NewContextWithHeaders(ctx2, newHdr("key-2", "val-2")) + + expectedHeaders = 3 + if err := client.CallContext(ctx3, nil, "test"); err != ErrNoResult { + t.Error("call failed", err) + } + + expectedHeaders = 2 + if err := client.CallContext(ctx2, nil, "test"); err != ErrNoResult { + t.Error("call failed:", err) + } +}