From 49bf4a129c0c54dd8eb0a35e43945fd184d1c316 Mon Sep 17 00:00:00 2001 From: javi11 Date: Tue, 7 Apr 2026 10:34:12 +0200 Subject: [PATCH 1/2] feat: add UserAgentPeerProvider configuration option to Provider Adds a UserAgentPeerProvider interface and field to Provider, mirroring the pattern introduced in nntpcli. The user agent string is threaded through runConnSlot and newNNTPConnectionFromConn into NNTPConnection, making it available for future use (e.g. article POST headers). --- integration_test.go | 24 ++++++++--------- nntp.go | 44 +++++++++++++++++++------------ nntp_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++ post_test.go | 4 +-- 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/integration_test.go b/integration_test.go index 9c1a938..d783446 100644 --- a/integration_test.go +++ b/integration_test.go @@ -24,7 +24,7 @@ func TestNNTPConnection_Greeting(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } @@ -39,7 +39,7 @@ func TestNNTPConnection_GreetingReject(t *testing.T) { }) reqCh := make(chan *Request) - _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err == nil { t.Fatal("expected error for 502 greeting") } @@ -75,7 +75,7 @@ func TestNNTPConnection_Auth(t *testing.T) { nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{ Username: "testuser", Password: "testpass", - }, nil, nil) + }, nil, nil, nil) if err != nil { t.Fatalf("auth error = %v", err) } @@ -100,7 +100,7 @@ func TestNNTPConnection_AuthReject(t *testing.T) { _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{ Username: "testuser", Password: "wrongpass", - }, nil, nil) + }, nil, nil, nil) if err == nil { t.Fatal("expected auth rejection error") } @@ -120,7 +120,7 @@ func TestNNTPConnection_RunSingleRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -162,7 +162,7 @@ func TestNNTPConnection_RunBodyRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -224,7 +224,7 @@ func TestNNTPConnection_RunPipelined(t *testing.T) { }) reqCh := make(chan *Request, 3) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 3, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 3, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -269,7 +269,7 @@ func TestNNTPConnection_CancelledRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -311,7 +311,7 @@ func TestNNTPConnection_IdleTimeout(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -871,7 +871,7 @@ func TestReadOneResponse(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -1266,7 +1266,7 @@ func TestKeepalive_KeepsConnectionAlive(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } @@ -1325,7 +1325,7 @@ func TestKeepalive_DeadConnection(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } diff --git a/nntp.go b/nntp.go index 4581cb3..e019f4c 100644 --- a/nntp.go +++ b/nntp.go @@ -130,6 +130,7 @@ type NNTPConnection struct { keepaliveInterval time.Duration // 0 = no keepalive keepaliveCommand string // NNTP command for keepalive probe (e.g. "DATE") providerName string // set by runConnSlot; used for error context + userAgentProvider UserAgentPeerProvider stats *providerStats // nil for standalone connections @@ -165,7 +166,7 @@ func newNetConn(ctx context.Context, addr string, tlsConfig *tls.Config, keepAli return conn, nil } -func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit int, reqCh <-chan *Request, prioCh <-chan *Request, auth Auth, sharedBuf *readBuffer, stats *providerStats) (*NNTPConnection, error) { +func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit int, reqCh <-chan *Request, prioCh <-chan *Request, auth Auth, userAgentProvider UserAgentPeerProvider, sharedBuf *readBuffer, stats *providerStats) (*NNTPConnection, error) { if ctx == nil { ctx = context.Background() } @@ -180,16 +181,17 @@ func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit } c := &NNTPConnection{ - conn: conn, - ctx: cctx, - cancel: cancel, - reqCh: reqCh, - prioCh: prioCh, - pending: make(chan *Request, inflightLimit), - inflightSem: make(chan struct{}, inflightLimit), - rb: rb, - stats: stats, - done: make(chan struct{}), + conn: conn, + ctx: cctx, + cancel: cancel, + reqCh: reqCh, + prioCh: prioCh, + pending: make(chan *Request, inflightLimit), + inflightSem: make(chan struct{}, inflightLimit), + rb: rb, + stats: stats, + done: make(chan struct{}), + userAgentProvider: userAgentProvider, } // Server greeting is sent immediately upon connect. @@ -216,13 +218,13 @@ func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit return c, nil } -func NewNNTPConnection(ctx context.Context, addr string, tlsConfig *tls.Config, inflightLimit int, reqCh <-chan *Request, auth Auth) (*NNTPConnection, error) { +func NewNNTPConnection(ctx context.Context, addr string, tlsConfig *tls.Config, inflightLimit int, reqCh <-chan *Request, auth Auth, userAgentProvider UserAgentPeerProvider) (*NNTPConnection, error) { conn, err := newNetConn(ctx, addr, tlsConfig, 0) if err != nil { return nil, err } - c, err := newNNTPConnectionFromConn(ctx, conn, inflightLimit, reqCh, nil, auth, nil, nil) + c, err := newNNTPConnectionFromConn(ctx, conn, inflightLimit, reqCh, nil, auth, userAgentProvider, nil, nil) if err != nil { _ = conn.Close() return nil, err @@ -473,7 +475,7 @@ func (g *connGate) snapshot() (maxSlots, running int) { // runConnSlot is the slot goroutine that manages the lifecycle of a single // connection: IDLE → CONNECTING → ACTIVE → (death/idle) → IDLE. -func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Request, hotReqCh <-chan *Request, hotPrioCh <-chan *Request, factory ConnFactory, inflight int, auth Auth, idleTimeout time.Duration, keepaliveInterval time.Duration, keepaliveCommand string, gate *connGate, stats *providerStats, providerName string, wg *sync.WaitGroup) { +func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Request, hotReqCh <-chan *Request, hotPrioCh <-chan *Request, factory ConnFactory, inflight int, auth Auth, userAgentProvider UserAgentPeerProvider, idleTimeout time.Duration, keepaliveInterval time.Duration, keepaliveCommand string, gate *connGate, stats *providerStats, providerName string, wg *sync.WaitGroup) { defer wg.Done() // Shared read buffer persists across reconnections to avoid re-growing. @@ -533,7 +535,7 @@ func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Requ continue } - nc, err := newNNTPConnectionFromConn(ctx, conn, inflight, reqCh, prioCh, auth, &sharedBuf, stats) + nc, err := newNNTPConnectionFromConn(ctx, conn, inflight, reqCh, prioCh, auth, userAgentProvider, &sharedBuf, stats) if err != nil { _ = conn.Close() failRequest(firstReq.RespCh, fmt.Errorf("%s: %w", providerName, err)) @@ -1132,6 +1134,12 @@ func WithDispatchStrategy(s DispatchStrategy) ClientOption { return func(cfg *clientConfig) { cfg.dispatch = s } } +// UserAgentPeerProvider provides the user agent string used to identify the +// connecting client peer to the NNTP server. +type UserAgentPeerProvider interface { + GetUserAgent() string +} + // Provider describes a single NNTP server with its own credentials and connection count. type Provider struct { Host string @@ -1158,6 +1166,10 @@ type Provider struct { // "CAPABILITIES" (response 101) for providers that do not support DATE. // Ignored when KeepaliveInterval is 0. KeepaliveCommand string + + // UserAgentPeerProvider provides the user agent string for identifying + // this client to the NNTP server. If nil, no user agent is set. + UserAgentPeerProvider UserAgentPeerProvider } type providerGroup struct { @@ -1345,7 +1357,7 @@ func (c *Client) startProviderGroup(p Provider, index int) *providerGroup { for range p.Connections { c.wg.Add(1) - go runConnSlot(gctx, g.reqCh, g.prioCh, g.hotReqCh, g.hotPrioCh, factory, inflight, p.Auth, p.IdleTimeout, kaInterval, kaCmd, gate, &g.stats, name, &c.wg) + go runConnSlot(gctx, g.reqCh, g.prioCh, g.hotReqCh, g.hotPrioCh, factory, inflight, p.Auth, p.UserAgentPeerProvider, p.IdleTimeout, kaInterval, kaCmd, gate, &g.stats, name, &c.wg) } return g diff --git a/nntp_test.go b/nntp_test.go index 52e5568..5d4cce9 100644 --- a/nntp_test.go +++ b/nntp_test.go @@ -1345,3 +1345,67 @@ func TestDynamicWeights_LargeN(t *testing.T) { t.Errorf("provider 0 percentage = %.2f%%, want ~83.33%%", pct0) } } + +// mockUserAgentProvider is a test implementation of UserAgentPeerProvider. +type mockUserAgentProvider struct { + userAgent string +} + +func (m *mockUserAgentProvider) GetUserAgent() string { return m.userAgent } + +func TestUserAgentPeerProvider_StoredOnConnection(t *testing.T) { + srv, cli := net.Pipe() + defer func() { _ = srv.Close() }() + defer func() { _ = cli.Close() }() + + go func() { + _, _ = srv.Write([]byte("200 server ready\r\n")) + // Drain any bytes the client sends so the pipe doesn't block. + buf := make([]byte, 256) + for { + if _, err := srv.Read(buf); err != nil { + return + } + } + }() + + reqCh := make(chan *Request) + uap := &mockUserAgentProvider{userAgent: "TestAgent/1.0"} + + nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, uap, nil, nil) + if err != nil { + t.Fatalf("newNNTPConnectionFromConn() error = %v", err) + } + + if nc.userAgentProvider == nil { + t.Fatal("userAgentProvider is nil, want non-nil") + } + if got := nc.userAgentProvider.GetUserAgent(); got != "TestAgent/1.0" { + t.Errorf("GetUserAgent() = %q, want %q", got, "TestAgent/1.0") + } +} + +func TestUserAgentPeerProvider_NilIsAccepted(t *testing.T) { + srv, cli := net.Pipe() + defer func() { _ = srv.Close() }() + defer func() { _ = cli.Close() }() + + go func() { + _, _ = srv.Write([]byte("200 server ready\r\n")) + buf := make([]byte, 256) + for { + if _, err := srv.Read(buf); err != nil { + return + } + } + }() + + reqCh := make(chan *Request) + nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, nil, nil, nil) + if err != nil { + t.Fatalf("newNNTPConnectionFromConn() error = %v", err) + } + if nc.userAgentProvider != nil { + t.Errorf("userAgentProvider = %v, want nil", nc.userAgentProvider) + } +} diff --git a/post_test.go b/post_test.go index bf2664e..830a4e8 100644 --- a/post_test.go +++ b/post_test.go @@ -322,7 +322,7 @@ func TestNNTPConnection_PostTwoPhase(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -521,7 +521,7 @@ func TestNNTPConnection_PostRejected(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } From 37bd0a52008e8df5d60b19be75cf73c061808233 Mon Sep 17 00:00:00 2001 From: javi11 Date: Tue, 7 Apr 2026 10:45:02 +0200 Subject: [PATCH 2/2] refactor: simplify UserAgent config to plain string on Provider Replaces the UserAgentPeerProvider interface with a plain string field Provider.UserAgent, removing unnecessary indirection since the user agent value is static at configuration time. --- integration_test.go | 24 ++++++++++++------------ nntp.go | 27 ++++++++++----------------- nntp_test.go | 29 ++++++++--------------------- post_test.go | 4 ++-- 4 files changed, 32 insertions(+), 52 deletions(-) diff --git a/integration_test.go b/integration_test.go index d783446..f0887bd 100644 --- a/integration_test.go +++ b/integration_test.go @@ -24,7 +24,7 @@ func TestNNTPConnection_Greeting(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } @@ -39,7 +39,7 @@ func TestNNTPConnection_GreetingReject(t *testing.T) { }) reqCh := make(chan *Request) - _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err == nil { t.Fatal("expected error for 502 greeting") } @@ -75,7 +75,7 @@ func TestNNTPConnection_Auth(t *testing.T) { nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{ Username: "testuser", Password: "testpass", - }, nil, nil, nil) + }, "", nil, nil) if err != nil { t.Fatalf("auth error = %v", err) } @@ -100,7 +100,7 @@ func TestNNTPConnection_AuthReject(t *testing.T) { _, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{ Username: "testuser", Password: "wrongpass", - }, nil, nil, nil) + }, "", nil, nil) if err == nil { t.Fatal("expected auth rejection error") } @@ -120,7 +120,7 @@ func TestNNTPConnection_RunSingleRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -162,7 +162,7 @@ func TestNNTPConnection_RunBodyRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -224,7 +224,7 @@ func TestNNTPConnection_RunPipelined(t *testing.T) { }) reqCh := make(chan *Request, 3) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 3, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 3, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -269,7 +269,7 @@ func TestNNTPConnection_CancelledRequest(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -311,7 +311,7 @@ func TestNNTPConnection_IdleTimeout(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -871,7 +871,7 @@ func TestReadOneResponse(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -1266,7 +1266,7 @@ func TestKeepalive_KeepsConnectionAlive(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } @@ -1325,7 +1325,7 @@ func TestKeepalive_DeadConnection(t *testing.T) { }) reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } diff --git a/nntp.go b/nntp.go index e019f4c..304c10c 100644 --- a/nntp.go +++ b/nntp.go @@ -130,7 +130,7 @@ type NNTPConnection struct { keepaliveInterval time.Duration // 0 = no keepalive keepaliveCommand string // NNTP command for keepalive probe (e.g. "DATE") providerName string // set by runConnSlot; used for error context - userAgentProvider UserAgentPeerProvider + userAgent string stats *providerStats // nil for standalone connections @@ -166,7 +166,7 @@ func newNetConn(ctx context.Context, addr string, tlsConfig *tls.Config, keepAli return conn, nil } -func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit int, reqCh <-chan *Request, prioCh <-chan *Request, auth Auth, userAgentProvider UserAgentPeerProvider, sharedBuf *readBuffer, stats *providerStats) (*NNTPConnection, error) { +func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit int, reqCh <-chan *Request, prioCh <-chan *Request, auth Auth, userAgent string, sharedBuf *readBuffer, stats *providerStats) (*NNTPConnection, error) { if ctx == nil { ctx = context.Background() } @@ -191,7 +191,7 @@ func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit rb: rb, stats: stats, done: make(chan struct{}), - userAgentProvider: userAgentProvider, + userAgent: userAgent, } // Server greeting is sent immediately upon connect. @@ -218,13 +218,13 @@ func newNNTPConnectionFromConn(ctx context.Context, conn net.Conn, inflightLimit return c, nil } -func NewNNTPConnection(ctx context.Context, addr string, tlsConfig *tls.Config, inflightLimit int, reqCh <-chan *Request, auth Auth, userAgentProvider UserAgentPeerProvider) (*NNTPConnection, error) { +func NewNNTPConnection(ctx context.Context, addr string, tlsConfig *tls.Config, inflightLimit int, reqCh <-chan *Request, auth Auth, userAgent string) (*NNTPConnection, error) { conn, err := newNetConn(ctx, addr, tlsConfig, 0) if err != nil { return nil, err } - c, err := newNNTPConnectionFromConn(ctx, conn, inflightLimit, reqCh, nil, auth, userAgentProvider, nil, nil) + c, err := newNNTPConnectionFromConn(ctx, conn, inflightLimit, reqCh, nil, auth, userAgent, nil, nil) if err != nil { _ = conn.Close() return nil, err @@ -475,7 +475,7 @@ func (g *connGate) snapshot() (maxSlots, running int) { // runConnSlot is the slot goroutine that manages the lifecycle of a single // connection: IDLE → CONNECTING → ACTIVE → (death/idle) → IDLE. -func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Request, hotReqCh <-chan *Request, hotPrioCh <-chan *Request, factory ConnFactory, inflight int, auth Auth, userAgentProvider UserAgentPeerProvider, idleTimeout time.Duration, keepaliveInterval time.Duration, keepaliveCommand string, gate *connGate, stats *providerStats, providerName string, wg *sync.WaitGroup) { +func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Request, hotReqCh <-chan *Request, hotPrioCh <-chan *Request, factory ConnFactory, inflight int, auth Auth, userAgent string, idleTimeout time.Duration, keepaliveInterval time.Duration, keepaliveCommand string, gate *connGate, stats *providerStats, providerName string, wg *sync.WaitGroup) { defer wg.Done() // Shared read buffer persists across reconnections to avoid re-growing. @@ -535,7 +535,7 @@ func runConnSlot(ctx context.Context, reqCh <-chan *Request, prioCh <-chan *Requ continue } - nc, err := newNNTPConnectionFromConn(ctx, conn, inflight, reqCh, prioCh, auth, userAgentProvider, &sharedBuf, stats) + nc, err := newNNTPConnectionFromConn(ctx, conn, inflight, reqCh, prioCh, auth, userAgent, &sharedBuf, stats) if err != nil { _ = conn.Close() failRequest(firstReq.RespCh, fmt.Errorf("%s: %w", providerName, err)) @@ -1134,12 +1134,6 @@ func WithDispatchStrategy(s DispatchStrategy) ClientOption { return func(cfg *clientConfig) { cfg.dispatch = s } } -// UserAgentPeerProvider provides the user agent string used to identify the -// connecting client peer to the NNTP server. -type UserAgentPeerProvider interface { - GetUserAgent() string -} - // Provider describes a single NNTP server with its own credentials and connection count. type Provider struct { Host string @@ -1167,9 +1161,8 @@ type Provider struct { // Ignored when KeepaliveInterval is 0. KeepaliveCommand string - // UserAgentPeerProvider provides the user agent string for identifying - // this client to the NNTP server. If nil, no user agent is set. - UserAgentPeerProvider UserAgentPeerProvider + // UserAgent identifies this client to the NNTP server. Empty string disables it. + UserAgent string } type providerGroup struct { @@ -1357,7 +1350,7 @@ func (c *Client) startProviderGroup(p Provider, index int) *providerGroup { for range p.Connections { c.wg.Add(1) - go runConnSlot(gctx, g.reqCh, g.prioCh, g.hotReqCh, g.hotPrioCh, factory, inflight, p.Auth, p.UserAgentPeerProvider, p.IdleTimeout, kaInterval, kaCmd, gate, &g.stats, name, &c.wg) + go runConnSlot(gctx, g.reqCh, g.prioCh, g.hotReqCh, g.hotPrioCh, factory, inflight, p.Auth, p.UserAgent, p.IdleTimeout, kaInterval, kaCmd, gate, &g.stats, name, &c.wg) } return g diff --git a/nntp_test.go b/nntp_test.go index 5d4cce9..2a96bcc 100644 --- a/nntp_test.go +++ b/nntp_test.go @@ -1346,21 +1346,13 @@ func TestDynamicWeights_LargeN(t *testing.T) { } } -// mockUserAgentProvider is a test implementation of UserAgentPeerProvider. -type mockUserAgentProvider struct { - userAgent string -} - -func (m *mockUserAgentProvider) GetUserAgent() string { return m.userAgent } - -func TestUserAgentPeerProvider_StoredOnConnection(t *testing.T) { +func TestUserAgent_StoredOnConnection(t *testing.T) { srv, cli := net.Pipe() defer func() { _ = srv.Close() }() defer func() { _ = cli.Close() }() go func() { _, _ = srv.Write([]byte("200 server ready\r\n")) - // Drain any bytes the client sends so the pipe doesn't block. buf := make([]byte, 256) for { if _, err := srv.Read(buf); err != nil { @@ -1370,22 +1362,17 @@ func TestUserAgentPeerProvider_StoredOnConnection(t *testing.T) { }() reqCh := make(chan *Request) - uap := &mockUserAgentProvider{userAgent: "TestAgent/1.0"} - - nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, uap, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, "TestAgent/1.0", nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } - if nc.userAgentProvider == nil { - t.Fatal("userAgentProvider is nil, want non-nil") - } - if got := nc.userAgentProvider.GetUserAgent(); got != "TestAgent/1.0" { - t.Errorf("GetUserAgent() = %q, want %q", got, "TestAgent/1.0") + if nc.userAgent != "TestAgent/1.0" { + t.Errorf("userAgent = %q, want %q", nc.userAgent, "TestAgent/1.0") } } -func TestUserAgentPeerProvider_NilIsAccepted(t *testing.T) { +func TestUserAgent_EmptyIsAccepted(t *testing.T) { srv, cli := net.Pipe() defer func() { _ = srv.Close() }() defer func() { _ = cli.Close() }() @@ -1401,11 +1388,11 @@ func TestUserAgentPeerProvider_NilIsAccepted(t *testing.T) { }() reqCh := make(chan *Request) - nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), cli, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("newNNTPConnectionFromConn() error = %v", err) } - if nc.userAgentProvider != nil { - t.Errorf("userAgentProvider = %v, want nil", nc.userAgentProvider) + if nc.userAgent != "" { + t.Errorf("userAgent = %q, want empty", nc.userAgent) } } diff --git a/post_test.go b/post_test.go index 830a4e8..feec605 100644 --- a/post_test.go +++ b/post_test.go @@ -322,7 +322,7 @@ func TestNNTPConnection_PostTwoPhase(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) } @@ -521,7 +521,7 @@ func TestNNTPConnection_PostRejected(t *testing.T) { }) reqCh := make(chan *Request, 1) - nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, nil, nil, nil) + nc, err := newNNTPConnectionFromConn(context.Background(), conn, 1, reqCh, nil, Auth{}, "", nil, nil) if err != nil { t.Fatalf("connection error = %v", err) }