Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 13 additions & 7 deletions async_postgres/pg_connection/dsn.nim
Original file line number Diff line number Diff line change
Expand Up @@ -752,8 +752,10 @@ proc parseUriDsn*(dsn: string): ConnConfig =
result.port = result.hosts[0].port
validateClientCertConfig(result)

proc validateConnConfig(config: var ConnConfig) =
## Mirror DSN guards for ``initConnConfig`` (DSN parsers validate inline).
proc validateConnConfig*(config: var ConnConfig) =
## Mirror DSN guards for ``initConnConfig`` and the ``connect`` chokepoint
## (DSN parsers validate inline; hand-built ``ConnConfig`` is re-checked at
## connect time so numeric / hostaddr faults become ``PgConfigError``).
## Negative ``connectTimeout`` becomes ``ZeroDuration``.
if config.connectTimeout < ZeroDuration:
config.connectTimeout = ZeroDuration
Expand All @@ -769,11 +771,15 @@ proc validateConnConfig(config: var ConnConfig) =
"Invalid hostaddr: must be a numeric IP address, not a Unix socket path (use host for Unix sockets)",
)

checkPort(config.port)
checkHostaddr(config.hostaddr)
for entry in config.hosts:
checkPort(entry.port)
checkHostaddr(entry.hostaddr)
# Once `hosts` is populated the scalar host/port pair is an unused back-compat
# mirror, left zeroed by hand-built configs.
if config.hosts.len > 0:
for entry in config.hosts:
checkPort(entry.port)
checkHostaddr(entry.hostaddr)
else:
checkPort(config.port)
checkHostaddr(config.hostaddr)

if config.keepAliveIdle < 0:
raise newException(PgConfigError, "keepalives_idle must be non-negative")
Expand Down
18 changes: 14 additions & 4 deletions async_postgres/pg_connection/lifecycle.nim
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,15 @@ proc connectToHost*(
): Future[PgConnection] {.async.} =
## Connect to single host (dial ``hostaddr`` else ``host``; verify via ``host``).

# Re-check the mTLS pairing here as well: `connect` validates it in `wrapped`,
# but this proc is public and a direct caller would otherwise have the certs
# silently dropped by a successful sslAllow plaintext attempt.
# Local mutable copy: ``validateConnConfig`` may normalize ``connectTimeout``.
var config = config

# Re-check numeric / hostaddr / mTLS pairing here as well: `connect` validates
# them in `wrapped`, but this proc is public and a direct caller would
# otherwise bypass the parsers (port wrap, keepalive ``cint`` RangeDefect,
# negative timeout footgun) or have certs silently dropped by a successful
# sslAllow plaintext attempt.
validateConnConfig(config)
validateClientCertConfig(config)

# Validate before the sslAllow branch rewrites sslMode to sslDisable, which
Expand Down Expand Up @@ -581,6 +587,8 @@ proc orderedHosts*(config: ConnConfig): seq[HostEntry] =
proc connect*(config: ConnConfig): Future[PgConnection] =
## Connect with multi-host failover, ``targetSessionAttrs``, per-host ``connectTimeout``.
## Per-host failures fold into one ``PgConnectionError``; a ``PgConfigError`` escapes the fold.
# Local mutable copy: ``validateConnConfig`` may normalize ``connectTimeout``.
var config = config
proc perform(hosts: seq[HostEntry]): Future[PgConnection] {.async.} =
# `hosts` is already ordered by the caller (shuffled under lbhRandom), so
# both the preferStandby two-pass loop and the single-pass loop below share
Expand Down Expand Up @@ -654,7 +662,9 @@ proc connect*(config: ConnConfig): Future[PgConnection] =

proc wrapped(): Future[PgConnection] {.async.} =
# ConnConfig may be built or mutated without passing through the parsers'
# validation — re-check here so every connect path rejects bad cert config.
# validation — re-check here so every connect path rejects bad numeric /
# hostaddr / cert config (``initConnConfig`` alone is not enough).
validateConnConfig(config)
validateClientCertConfig(config)
if config.channelBinding == cbRequire and config.sslMode == sslDisable:
# Knowable before any dial; the per-host check in selectScramMechanism
Expand Down
1 change: 1 addition & 0 deletions tests/api_surface.golden
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ async_postgres/pg_connection/dsn.nim parseSslMode
async_postgres/pg_connection/dsn.nim parseSslNegotiation
async_postgres/pg_connection/dsn.nim parseTargetSessionAttrs
async_postgres/pg_connection/dsn.nim parseUriDsn
async_postgres/pg_connection/dsn.nim validateConnConfig
async_postgres/pg_connection/lifecycle.nim close
async_postgres/pg_connection/lifecycle.nim closeImpl
async_postgres/pg_connection/lifecycle.nim connect
Expand Down
147 changes: 147 additions & 0 deletions tests/test_ssl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,153 @@ suite "Client certificate config validation":
check raised
check configFault

suite "connect hand-built ConnConfig numeric validation":
# #631 gap: `validateConnConfig` used to run only in `initConnConfig`.
# Hand-built `ConnConfig` must hit the same guards at the `connect` chokepoint
# (no mock server — failure is client-side before dial).
test "out-of-range port is a config fault, not a connection failure":
var raised = false
var configFault = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "127.0.0.1",
port: 99999,
user: "test",
database: "test",
sslMode: sslDisable,
)
try:
let conn = await connect(config)
await conn.close()
except PgError as e:
raised = true
configFault = e of PgConfigError

waitFor testBody()
check raised
check configFault

test "port 0 is a config fault":
var raised = false
var configFault = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "127.0.0.1", port: 0, user: "test", database: "test", sslMode: sslDisable
)
try:
let conn = await connect(config)
await conn.close()
except PgError as e:
raised = true
configFault = e of PgConfigError

waitFor testBody()
check raised
check configFault

test "slash hostaddr is a config fault":
var raised = false
var configFault = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "db",
hostaddr: "/tmp",
port: 5432,
user: "test",
database: "test",
sslMode: sslDisable,
)
try:
let conn = await connect(config)
await conn.close()
except PgError as e:
raised = true
configFault = e of PgConfigError

waitFor testBody()
check raised
check configFault

test "negative keepAliveIdle is a config fault":
var raised = false
var configFault = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "127.0.0.1",
port: 1,
user: "test",
database: "test",
sslMode: sslDisable,
keepAliveIdle: -1,
)
try:
let conn = await connect(config)
await conn.close()
except PgError as e:
raised = true
configFault = e of PgConfigError

waitFor testBody()
check raised
check configFault

test "keepAliveIdle exceeding cint is a config fault":
when sizeof(cint) < sizeof(int):
var raised = false
var configFault = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "127.0.0.1",
port: 1,
user: "test",
database: "test",
sslMode: sslDisable,
keepAliveIdle: int(high(cint)) + 1,
)
try:
let conn = await connect(config)
await conn.close()
except PgError as e:
raised = true
configFault = e of PgConfigError

waitFor testBody()
check raised
check configFault

test "negative connectTimeout normalizes and does not raise PgConfigError":
# Normalization must happen before dial; use an immediately-refused port so
# the attempt finishes without hanging (ZeroDuration = no timeout).
var configFault = false
var connected = false

proc testBody() {.async.} =
let config = ConnConfig(
host: "127.0.0.1",
port: 1,
user: "test",
database: "test",
sslMode: sslDisable,
connectTimeout: seconds(-5),
)
try:
let conn = await connect(config)
connected = true
await conn.close()
except PgConfigError:
configFault = true
except PgError:
discard

waitFor testBody()
check not configFault
check not connected

suite "SSL negotiation - sslAllow":
test "sslAllow connects without SSL when server accepts plaintext":
var connState: PgConnState
Expand Down
Loading