Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(routing/http/client): allow custom User-Agent #31

Merged
merged 2 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions routing/http/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type client struct {
afterSignCallback func(req *types.WriteBitswapProviderRecord)
}

// defaultUserAgent is used as a fallback to inform HTTP server which library
// version sent a request
var defaultUserAgent = moduleVersion()

var _ contentrouter.Client = &client{}

type httpClient interface {
Expand All @@ -60,6 +64,23 @@ func WithHTTPClient(h httpClient) option {
}
}

func WithUserAgent(ua string) option {
return func(c *client) {
if ua == "" {
return
}
httpClient, ok := c.httpClient.(*http.Client)
if !ok {
return
}
transport, ok := httpClient.Transport.(*ResponseBodyLimitedTransport)
if !ok {
return
}
transport.UserAgent = ua
}
}

func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) option {
return func(c *client) {
c.peerID = peerID
Expand All @@ -76,6 +97,7 @@ func New(baseURL string, opts ...option) (*client, error) {
Transport: &ResponseBodyLimitedTransport{
RoundTripper: http.DefaultTransport,
LimitBytes: 1 << 20,
UserAgent: defaultUserAgent,
},
}
client := &client{
Expand Down
18 changes: 17 additions & 1 deletion routing/http/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ type testDeps struct {
}

func makeTestDeps(t *testing.T) testDeps {
const testUserAgent = "testUserAgent"
peerID, addrs, identity := makeProviderAndIdentity()
router := &mockContentRouter{}
server := httptest.NewServer(server.Handler(router))
t.Cleanup(server.Close)
serverAddr := "http://" + server.Listener.Addr().String()
c, err := New(serverAddr, WithProviderInfo(peerID, addrs), WithIdentity(identity))
c, err := New(serverAddr, WithProviderInfo(peerID, addrs), WithIdentity(identity), WithUserAgent(testUserAgent))
if err != nil {
panic(err)
}
assertUserAgentOverride(t, c, testUserAgent)
return testDeps{
router: router,
server: server,
Expand All @@ -66,6 +68,20 @@ func makeTestDeps(t *testing.T) testDeps {
}
}

func assertUserAgentOverride(t *testing.T, c *client, expected string) {
httpClient, ok := c.httpClient.(*http.Client)
if !ok {
t.Error("invalid c.httpClient")
}
transport, ok := httpClient.Transport.(*ResponseBodyLimitedTransport)
if !ok {
t.Error("invalid httpClient.Transport")
}
if transport.UserAgent != expected {
t.Error("invalid httpClient.Transport.UserAgent")
}
}

func makeCID() cid.Cid {
buf := make([]byte, 63)
_, err := rand.Read(buf)
Expand Down
37 changes: 37 additions & 0 deletions routing/http/client/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import (
"fmt"
"io"
"net/http"
"runtime/debug"
)

type ResponseBodyLimitedTransport struct {
http.RoundTripper
LimitBytes int64
UserAgent string
}

func (r *ResponseBodyLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if r.UserAgent != "" {
req.Header.Set("User-Agent", r.UserAgent)
}
resp, err := r.RoundTripper.RoundTrip(req)
if resp != nil && resp.Body != nil {
resp.Body = &limitReadCloser{
Expand All @@ -36,3 +41,35 @@ func (l *limitReadCloser) Read(p []byte) (int, error) {
}
return n, err
}

// ImportPath is the canonical import path that allows us to identify
// official client builds vs modified forks, and use that info in User-Agent header.
const ImportPath = "github.com/ipfs/go-libipfs/routing/http/client"

// moduleVersion returns a useful user agent version string allowing us to
// identify requests coming from officialxl releases of this module.
lidel marked this conversation as resolved.
Show resolved Hide resolved
func moduleVersion() string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test for this? Really I'm interested in making sure a test fails if this code is moved and ImportPath is not updated (or use reflection or something to get the import path dynamically).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guseggert simplified and added test in 4e11e8b

var module *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {
return ""
}

// find this client in the dependency list
// of the app that has it in go.mod
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
}
}

if module == nil {
return ""
}
ua := ImportPath + "/" + module.Version
if module.Sum != "" {
ua += "/" + module.Sum
}
return ua
}