Resolve IRC client hostnames off the accept loop (closes #156) #172
@@ -70,7 +70,6 @@ type Conn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newConn(
|
func newConn(
|
||||||
ctx context.Context,
|
|
||||||
tcpConn net.Conn,
|
tcpConn net.Conn,
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
database *db.Database,
|
database *db.Database,
|
||||||
@@ -94,7 +93,6 @@ func newConn(
|
|||||||
svc: svc,
|
svc: svc,
|
||||||
serverSfx: srvName,
|
serverSfx: srvName,
|
||||||
remoteIP: host,
|
remoteIP: host,
|
||||||
hostname: resolveHost(ctx, host),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.commands = conn.buildCommandMap()
|
conn.commands = conn.buildCommandMap()
|
||||||
@@ -212,12 +210,15 @@ func resolveHost(ctx context.Context, addr string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// serve is the main loop for a single IRC client connection.
|
// serve is the main loop for a single IRC client connection.
|
||||||
|
// It first sets the client's hostname to what resolve returns
|
||||||
|
// for the client's address.
|
||||||
// Cancelling ctx when it returns stops the relay goroutine.
|
// Cancelling ctx when it returns stops the relay goroutine.
|
||||||
// When nothing has been read for interval the client is sent
|
// When nothing has been read for interval the client is sent
|
||||||
// a PING, and when nothing is read within deadline after that
|
// a PING, and when nothing is read within deadline after that
|
||||||
// the connection is closed.
|
// the connection is closed.
|
||||||
func (c *Conn) serve(
|
func (c *Conn) serve(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
resolve func(ctx context.Context, addr string) string,
|
||||||
interval, deadline time.Duration,
|
interval, deadline time.Duration,
|
||||||
) {
|
) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
@@ -226,6 +227,11 @@ func (c *Conn) serve(
|
|||||||
// still end the session then.
|
// still end the session then.
|
||||||
defer c.cleanup(context.WithoutCancel(ctx))
|
defer c.cleanup(context.WithoutCancel(ctx))
|
||||||
|
|
||||||
|
// The hostname is looked up here, on this connection's
|
||||||
|
// goroutine, and not when the connection is accepted, so
|
||||||
|
// that a slow lookup holds up only this client.
|
||||||
|
c.hostname = resolve(ctx, c.remoteIP)
|
||||||
|
|
||||||
ping := time.AfterFunc(interval, func() {
|
ping := time.AfterFunc(interval, func() {
|
||||||
c.send("PING :" + c.serverSfx)
|
c.send("PING :" + c.serverSfx)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import (
|
|||||||
|
|
||||||
// NewTestServer creates a Server suitable for testing, whose
|
// NewTestServer creates a Server suitable for testing, whose
|
||||||
// connections use interval and deadline in place of
|
// connections use interval and deadline in place of
|
||||||
// pingInterval and pongDeadline.
|
// pingInterval and pongDeadline, and resolve in place of
|
||||||
|
// resolveHost.
|
||||||
// The caller must call Stop() when finished.
|
// The caller must call Stop() when finished.
|
||||||
func NewTestServer(
|
func NewTestServer(
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
@@ -23,6 +24,7 @@ func NewTestServer(
|
|||||||
database *db.Database,
|
database *db.Database,
|
||||||
brk *broker.Broker,
|
brk *broker.Broker,
|
||||||
interval, deadline time.Duration,
|
interval, deadline time.Duration,
|
||||||
|
resolve func(ctx context.Context, addr string) string,
|
||||||
) *Server {
|
) *Server {
|
||||||
globs := &globals.Globals{
|
globs := &globals.Globals{
|
||||||
Appname: "neoirc",
|
Appname: "neoirc",
|
||||||
@@ -44,9 +46,16 @@ func NewTestServer(
|
|||||||
|
|
||||||
pingInterval: interval,
|
pingInterval: interval,
|
||||||
pongDeadline: deadline,
|
pongDeadline: deadline,
|
||||||
|
resolveHost: resolve,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveHost is the reverse DNS lookup that a server made by
|
||||||
|
// New does for each client.
|
||||||
|
func ResolveHost(ctx context.Context, addr string) string {
|
||||||
|
return resolveHost(ctx, addr)
|
||||||
|
}
|
||||||
|
|
||||||
// Start exposes the unexported start method for tests.
|
// Start exposes the unexported start method for tests.
|
||||||
func (s *Server) Start(addr string) error {
|
func (s *Server) Start(addr string) error {
|
||||||
return s.start(context.Background(), addr)
|
return s.start(context.Background(), addr)
|
||||||
@@ -97,10 +106,7 @@ func NewTestConn(
|
|||||||
tcpConn net.Conn,
|
tcpConn net.Conn,
|
||||||
nick string,
|
nick string,
|
||||||
) *Conn {
|
) *Conn {
|
||||||
conn := newConn(
|
conn := newConn(tcpConn, log, nil, nil, cfg, nil)
|
||||||
context.Background(), tcpConn, log,
|
|
||||||
nil, nil, cfg, nil,
|
|
||||||
)
|
|
||||||
conn.nick = nick
|
conn.nick = nick
|
||||||
|
|
||||||
return conn
|
return conn
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ type Server struct {
|
|||||||
// same names, except in tests, which shorten them.
|
// same names, except in tests, which shorten them.
|
||||||
pingInterval time.Duration
|
pingInterval time.Duration
|
||||||
pongDeadline time.Duration
|
pongDeadline time.Duration
|
||||||
|
|
||||||
|
// resolveHost is the function of the same name, except in
|
||||||
|
// tests, which replace it with one that blocks.
|
||||||
|
resolveHost func(ctx context.Context, addr string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new IRC Server and registers its lifecycle
|
// New creates a new IRC Server and registers its lifecycle
|
||||||
@@ -75,6 +79,7 @@ func New(
|
|||||||
|
|
||||||
pingInterval: pingInterval,
|
pingInterval: pingInterval,
|
||||||
pongDeadline: pongDeadline,
|
pongDeadline: pongDeadline,
|
||||||
|
resolveHost: resolveHost,
|
||||||
}
|
}
|
||||||
|
|
||||||
listenAddr := params.Config.IRCListenAddr
|
listenAddr := params.Config.IRCListenAddr
|
||||||
@@ -175,7 +180,7 @@ func (s *Server) acceptLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := newConn(
|
client := newConn(
|
||||||
ctx, tcpConn, s.log,
|
tcpConn, s.log,
|
||||||
s.database, s.brk, s.cfg, s.svc,
|
s.database, s.brk, s.cfg, s.svc,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -204,7 +209,10 @@ func (s *Server) acceptLoop(ctx context.Context) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
client.serve(ctx, s.pingInterval, s.pongDeadline)
|
client.serve(
|
||||||
|
ctx, s.resolveHost,
|
||||||
|
s.pingInterval, s.pongDeadline,
|
||||||
|
)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ircserver_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -90,6 +92,21 @@ func newTestEnvWithPingTimes(
|
|||||||
) *testEnv {
|
) *testEnv {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
return newTestEnvWithResolver(
|
||||||
|
t, cfg, interval, deadline, ircserver.ResolveHost,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestEnvWithResolver is newTestEnvWithPingTimes, except
|
||||||
|
// that the server looks up a client's hostname with resolve.
|
||||||
|
func newTestEnvWithResolver(
|
||||||
|
t *testing.T,
|
||||||
|
cfg *config.Config,
|
||||||
|
interval, deadline time.Duration,
|
||||||
|
resolve func(ctx context.Context, addr string) string,
|
||||||
|
) *testEnv {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
// A random name, so that no other test environment, not
|
// A random name, so that no other test environment, not
|
||||||
// even an earlier run of the same test, can share it.
|
// even an earlier run of the same test, can share it.
|
||||||
dsn := fmt.Sprintf(
|
dsn := fmt.Sprintf(
|
||||||
@@ -147,7 +164,7 @@ func newTestEnvWithPingTimes(
|
|||||||
))
|
))
|
||||||
|
|
||||||
srv := ircserver.NewTestServer(
|
srv := ircserver.NewTestServer(
|
||||||
log, cfg, database, brk, interval, deadline,
|
log, cfg, database, brk, interval, deadline, resolve,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = srv.Start(addr)
|
err = srv.Start(addr)
|
||||||
@@ -415,6 +432,49 @@ func TestRegistration(t *testing.T) {
|
|||||||
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
assertContains(t, lines, " 001 ", "RPL_WELCOME")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRegistrationWhileAnotherLookupIsPending checks that a
|
||||||
|
// client is accepted and registered while the hostname lookup
|
||||||
|
// of an earlier client has not returned, and that it
|
||||||
|
// registers with the hostname its own lookup returned.
|
||||||
|
func TestRegistrationWhileAnotherLookupIsPending(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const lookedUpHost = "client.example"
|
||||||
|
|
||||||
|
var lookups atomic.Int32
|
||||||
|
|
||||||
|
firstLookupStarted := make(chan struct{})
|
||||||
|
|
||||||
|
// The first lookup returns only when the server stops.
|
||||||
|
resolve := func(ctx context.Context, _ string) string {
|
||||||
|
if lookups.Add(1) == 1 {
|
||||||
|
close(firstLookupStarted)
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookedUpHost
|
||||||
|
}
|
||||||
|
|
||||||
|
env := newTestEnvWithResolver(
|
||||||
|
t, &config.Config{ServerName: testServerName},
|
||||||
|
ircserver.PingInterval, ircserver.PongDeadline, resolve,
|
||||||
|
)
|
||||||
|
|
||||||
|
env.dial(t)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-firstLookupStarted:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatal("the first client's lookup did not start")
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := env.dial(t).register("second")
|
||||||
|
assertContains(
|
||||||
|
t, lines, "second!second@"+lookedUpHost,
|
||||||
|
"RPL_WELCOME with the looked-up hostname",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TestEachTestEnvHasItsOwnDatabase checks that a test
|
// TestEachTestEnvHasItsOwnDatabase checks that a test
|
||||||
// environment starts on an empty database while another one
|
// environment starts on an empty database while another one
|
||||||
// is still open.
|
// is still open.
|
||||||
|
|||||||
Reference in New Issue
Block a user