Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Connection options support:
- Environment defaults: `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGAPPNAME`, `PGCONNECT_TIMEOUT`, `PGOPTIONS`, TLS-related `PGSSL*` variables, and `PGGSSENCMODE`, `PGKRBSRVNAME`, `PGGSSDELEGATION`.
- `sslmode` values: `disable`, `prefer` (the default), `require`, `verify-full`. Only `verify-full` verifies the server's certificate; `require` encrypts without authenticating the server, and the default `prefer` falls back to an unencrypted connection if the server declines TLS. Use `verify-full` with `sslrootcert` when the connection needs to be authenticated.
- TLS files: `sslrootcert`, `sslcert`, `sslkey`, and `sslcapath` (`sslcapath` is a fallback CA bundle or directory, used only when `sslrootcert` is unset and ignored otherwise). `sslservername` overrides the TLS server name when connecting to a pre-resolved address; under `verify-full` it is also the name the certificate is verified against, so it must name the server you intend to authenticate.
- `gssencmode` values: `disable` (the default), `prefer`, `require`. GSSAPI (Kerberos) encryption is negotiated before TLS, as in libpq, through the operating system's Kerberos library and ticket cache (`kinit`); `prefer` only tries it when a ticket is available and otherwise follows `sslmode`. `krbsrvname` sets the Kerberos service name (default `postgres`; Active Directory servers often need `POSTGRES`) and `gssdelegation` forwards the ticket to the server. GSSAPI authentication (`gss` in `pg_hba.conf`) is answered automatically with the same library.
- `gssencmode` values: `disable` (the default), `prefer`, `require`. GSSAPI (Kerberos) encryption is negotiated before TLS, as in libpq, through the operating system's Kerberos library and ticket cache (`kinit`); `prefer` only tries it when a ticket is available and otherwise follows `sslmode`. `krbsrvname` sets the Kerberos service name (default `postgres`; Active Directory servers often need `POSTGRES`) and `gssdelegation` forwards the ticket to the server. GSSAPI authentication (`gss` in `pg_hba.conf`) is answered automatically with the same library. Both `sslmode` and `gssencmode` are case-insensitive, and an invalid value of either is rejected with an `ArgumentError` when the connection string is parsed or the connection is constructed, not when connecting.
- `connect_timeout` (seconds) and `statement_timeout` (milliseconds).
- `application_name` and `statement_cache_maxsize`.
- `options`: server command-line options applied when the session starts, as in libpq (`PGOPTIONS`). For example `options='-c search_path=myschema'` sets the default schema. The value is sent in the startup packet, so it also applies after an automatic reconnect.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Postgres.jl accepts DSN strings or PostgreSQL URIs and supports:
- Environment defaults from `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGAPPNAME`, `PGCONNECT_TIMEOUT`, `PGOPTIONS`, TLS-related `PGSSL*` variables, and `PGGSSENCMODE`, `PGKRBSRVNAME`, `PGGSSDELEGATION`.
- `sslmode` values: `disable`, `prefer` (the default), `require`, `verify-full`. Only `verify-full` verifies the server's certificate; `require` encrypts without authenticating the server, and the default `prefer` falls back to an unencrypted connection if the server declines TLS. Use `verify-full` with `sslrootcert` when the connection needs to be authenticated.
- TLS files: `sslrootcert`, `sslcert`, `sslkey`, and `sslcapath` (`sslcapath` is a fallback CA bundle or directory, used only when `sslrootcert` is unset and ignored otherwise). `sslservername` overrides the TLS server name when connecting to a pre-resolved address; under `verify-full` it is also the name the certificate is verified against, so it must name the server you intend to authenticate.
- `gssencmode` values: `disable` (the default), `prefer`, `require`. GSSAPI (Kerberos) encryption is negotiated before TLS, as in libpq, through the operating system's Kerberos library and ticket cache (`kinit`); `prefer` only tries it when a ticket is available and otherwise follows `sslmode`. `krbsrvname` sets the Kerberos service name (default `postgres`; Active Directory servers often need `POSTGRES`) and `gssdelegation` forwards the ticket to the server. GSSAPI authentication (`gss` in `pg_hba.conf`) is answered automatically with the same library.
- `gssencmode` values: `disable` (the default), `prefer`, `require`. GSSAPI (Kerberos) encryption is negotiated before TLS, as in libpq, through the operating system's Kerberos library and ticket cache (`kinit`); `prefer` only tries it when a ticket is available and otherwise follows `sslmode`. `krbsrvname` sets the Kerberos service name (default `postgres`; Active Directory servers often need `POSTGRES`) and `gssdelegation` forwards the ticket to the server. GSSAPI authentication (`gss` in `pg_hba.conf`) is answered automatically with the same library. Both `sslmode` and `gssencmode` are case-insensitive, and an invalid value of either is rejected with an `ArgumentError` when the connection string is parsed or the connection is constructed, not when connecting.
- `connect_timeout` (seconds), `statement_timeout` (milliseconds).
- `application_name` and `statement_cache_maxsize`.
- `options`: server command-line options applied when the session starts, as in libpq (`PGOPTIONS`). For example `options='-c search_path=myschema'` sets the default schema. The value is sent in the startup packet, so it also applies after an automatic reconnect.
Expand Down
15 changes: 15 additions & 0 deletions src/Postgres.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ Base.showerror(io::IO, e::PostgresInterfaceError) = print(io, e.msg)

@noinline _reject_nul(what::String) = throw(PostgresInterfaceError("$what cannot contain a NUL byte"))

# The values accepted for sslmode and gssencmode, compared case-insensitively.
# The connection-string parser, the constructors, and the connect and cancel
# paths all read these, so the lists cannot drift apart.
const SSLMODES = ("disable", "prefer", "require", "verify-full")
const GSSENCMODES = ("disable", "prefer", "require")

include("api/API.jl")
using .API
include("connection_string.jl")
Expand Down Expand Up @@ -82,6 +88,9 @@ Supported keyword arguments. All are also available as DSN/URI options except
forwards the ticket to the server (default `false`). Kerberos comes from the
operating system library (`kinit` tickets, `krb5.conf`), with no bundled
Kerberos; see the support policy.
- `sslmode` and `gssencmode` values are case-insensitive. Any other value
throws an `ArgumentError` when the connection is constructed, before
connecting, with the message `Postgres.parse_dsn` gives for it.
- `statement_cache_maxsize`: LRU backend cache size for explicit named prepared
statements (default 100; `0` disables)
- `reconnect`: automatically reconnect and re-prepare statements if the
Expand Down Expand Up @@ -180,6 +189,12 @@ mutable struct Connection{T, S <: API.AbstractPostgresStyle} <: DBInterface.Conn
krbsrvname_val !== nothing && occursin('\0', krbsrvname_val) && _reject_nul("krbsrvname")
xor(sslcert_val === nothing, sslkey_val === nothing) &&
throw(PostgresInterfaceError("sslcert and sslkey must be provided together"))
# checked here, not only when connecting, so that no negotiation path
# can skip the check and the error matches parse_dsn's
err = ConnectionString.check_mode("sslmode", sslmode_val, SSLMODES)
err === nothing || throw(ArgumentError(err.msg))
err = ConnectionString.check_mode("gssencmode", gssencmode_val, GSSENCMODES)
err === nothing || throw(ArgumentError(err.msg))
maxsize = max(0, Int(statement_cache_maxsize))
socket, pid, skey, server_params = API.connect(host, port, dbname, user, password, debug, app_name, options_val, timeout, sslmode_val, sslrootcert_val, sslcert_val, sslkey_val, sslcapath_val, sslservername_val, statement_timeout_val, gssencmode_val, krbsrvname_val, gssdelegation, style)
registry = Dict(API.DEFAULT_TYPE_REGISTRY)
Expand Down
15 changes: 12 additions & 3 deletions src/api/API.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module API

using UUIDs, Dates, Reseau, SASLAuth, MD5, Parsers, StructUtils, JSON, Random
import ..PostgresInterfaceError
import ..PostgresInterfaceError, ..SSLMODES, ..GSSENCMODES

import Durations, DataDecimals

Expand Down Expand Up @@ -758,8 +758,13 @@ function connect(host::String, port::Integer, dbname::String, user::String, @nos
sslcapath_v = sslcapath::Union{String, Nothing}
sslservername_v = sslservername::Union{String, Nothing}
statement_timeout_v = statement_timeout::Union{Int, Nothing}
# The public constructors reject invalid modes with an ArgumentError before
# getting here, so these checks are a backstop for direct callers. They run
# before any dial, so no negotiation path (GSS included) skips either one.
gssencmode_str = gssencmode === nothing ? "disable" : lowercase(gssencmode::String)
gssencmode_str == "disable" || gssencmode_str == "prefer" || gssencmode_str == "require" || throw(Error("invalid gssencmode: $gssencmode_str"))
gssencmode_str in GSSENCMODES || throw(Error("invalid gssencmode: $gssencmode_str"))
sslmode_str = sslmode_v === nothing ? "prefer" : lowercase(sslmode_v::String)
sslmode_str in SSLMODES || throw(Error("invalid sslmode: $sslmode_str"))
krbsrvname_str = krbsrvname === nothing ? "postgres" : krbsrvname::String
# GSS encryption is only attempted when initiator credentials (a ticket)
# can be acquired; `require` without them fails before any dial, as in libpq
Expand Down Expand Up @@ -813,8 +818,8 @@ function _connect_tls!(socket::Reseau.TCP.Conn, debug::Bool, user::String, dbnam
sslcapath_v = sslcapath::Union{String, Nothing}
sslservername_v = sslservername::Union{String, Nothing}
statement_timeout_v = statement_timeout::Union{Int, Nothing}
# validated by connect
sslmode_str = sslmode_v === nothing ? "prefer" : lowercase(String(sslmode_v))
sslmode_str == "disable" || sslmode_str == "prefer" || sslmode_str == "require" || sslmode_str == "verify-full" || throw(Error("invalid sslmode: $sslmode_str"))
if sslmode_str != "disable"
# send SSLRequest
writemessage(socket, debug, '\0', Int32(80877103))
Expand Down Expand Up @@ -1649,6 +1654,10 @@ function cancel_request(host::String, port::Int, pid::Int32, skey::Int32, debug:
tls_required = sslmode_str == "require" || sslmode_str == "verify-full"
gssencmode_str = gssencmode === nothing ? "disable" : lowercase(gssencmode::String)
gss_required = gssencmode_str == "require"
# a backstop, as in connect: an unknown mode would fall through to the
# branches that may send the cancel key in cleartext
gssencmode_str in GSSENCMODES || throw(Error("invalid gssencmode: $gssencmode_str"))
sslmode_str in SSLMODES || throw(Error("invalid sslmode: $sslmode_str"))
krbsrvname_str = krbsrvname === nothing ? "postgres" : krbsrvname::String
socket = connectsocket(host, port, connect_timeout_v)
stream = socket
Expand Down
35 changes: 27 additions & 8 deletions src/connection_string.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module ConnectionString

using URIs
import ..SSLMODES, ..GSSENCMODES

"""
Postgres.ConnectionParams(; host="localhost", port=5432, user="", password=nothing,
Expand All @@ -17,7 +18,10 @@ arguments mirror the connection keywords: `application_name`, `options`,
`sslservername`, `gssencmode`, `krbsrvname`, `gssdelegation`,
`statement_timeout`, `statement_cache_maxsize`, `debug`, and `reconnect`. `numeric_overflow` accepts `:warn` (return out-of-range numeric
values as text with a warning) or `:error` (throw). DSN strings use `warn` or
`error` without the colon.
`error` without the colon. `sslmode` accepts `disable`, `prefer`, `require`, or
`verify-full`, and `gssencmode` accepts `disable`, `prefer`, or `require`, in
any letter case; any other value throws an `ArgumentError` here rather than
when connecting.
"""
struct ConnectionParams
host::String
Expand Down Expand Up @@ -46,6 +50,10 @@ end

function ConnectionParams(; host::String="localhost", port::Int=5432, user::String="", password::Union{String, Nothing}=nothing, dbname::String="", application_name::Union{String, Nothing}=nothing, options::Union{String, Nothing}=nothing, connect_timeout::Union{Int, Nothing}=nothing, sslmode::Union{String, Nothing}=nothing, sslrootcert::Union{String, Nothing}=nothing, sslcert::Union{String, Nothing}=nothing, sslkey::Union{String, Nothing}=nothing, sslcapath::Union{String, Nothing}=nothing, sslservername::Union{String, Nothing}=nothing, gssencmode::Union{String, Nothing}=nothing, krbsrvname::Union{String, Nothing}=nothing, gssdelegation::Bool=false, statement_timeout::Union{Int, Nothing}=nothing, statement_cache_maxsize::Int=100, debug::Bool=false, reconnect::Bool=false, numeric_overflow::Symbol=:warn)
numeric_overflow in (:warn, :error) || throw(ArgumentError("numeric_overflow must be :warn or :error"))
err = check_mode("sslmode", sslmode, SSLMODES)
err === nothing || throw(ArgumentError(err.msg))
err = check_mode("gssencmode", gssencmode, GSSENCMODES)
err === nothing || throw(ArgumentError(err.msg))
return ConnectionParams(host, port, user, password, dbname, application_name, options, connect_timeout, sslmode, sslrootcert, sslcert, sslkey, sslcapath, sslservername, gssencmode, krbsrvname, gssdelegation, statement_timeout, statement_cache_maxsize, debug, reconnect, numeric_overflow)
end

Expand Down Expand Up @@ -152,6 +160,14 @@ const IGNORED_PARAMS = Set([
# valid UTF-8 either, but lowercase accepts them, so they parse as before.
has_malformed_char(s::String) = any(c -> Base.ismalformed(c) || Base.isoverlong(c), s)

# sslmode and gssencmode are case-insensitive, as in libpq. A malformed value
# can never match and cannot be lowercased.
function check_mode(key::String, value::Union{String, Nothing}, accepted::Tuple{Vararg{String}})
value === nothing && return nothing
!has_malformed_char(value) && lowercase(value) in accepted && return nothing
return ConnectionParameterError(key, :invalid_value, "invalid value \"$(escape_string(value))\" for connection parameter \"$key\"; expected one of $(join(accepted, ", "))")
end

function parse_bool_param(value::Union{String, Nothing}, default::Bool, key::String)
value === nothing && return default
# an empty value means "unset", as it does for the integer parameters
Expand Down Expand Up @@ -230,13 +246,10 @@ function params_from_values(values::Dict{String, String})
port isa ConnectionParameterError && return port
connect_timeout = parse_optional_int(get(merged, "connect_timeout", nothing), "connect_timeout")
connect_timeout isa ConnectionParameterError && return connect_timeout
# other sslmode and gssencmode values are checked when connecting, but a
# malformed one can never match and cannot be lowercased
for (key, accepted) in (("sslmode", "disable, prefer, require, verify-full"), ("gssencmode", "disable, prefer, require"))
value = get(merged, key, nothing)
(value === nothing || !has_malformed_char(value)) ||
return ConnectionParameterError(key, :invalid_value, "invalid value \"$(escape_string(value))\" for connection parameter \"$key\"; expected one of $accepted")
end
err = check_mode("sslmode", get(merged, "sslmode", nothing), SSLMODES)
err === nothing || return err
err = check_mode("gssencmode", get(merged, "gssencmode", nothing), GSSENCMODES)
err === nothing || return err
gssdelegation = parse_bool_param(get(merged, "gssdelegation", nothing), false, "gssdelegation")
gssdelegation isa ConnectionParameterError && return gssdelegation
statement_timeout = parse_optional_int(get(merged, "statement_timeout", nothing), "statement_timeout")
Expand Down Expand Up @@ -369,6 +382,12 @@ to the `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGAPPNAME`,
`PGCONNECT_TIMEOUT`, `PGSSL*`, `PGGSSENCMODE`, `PGKRBSRVNAME`, and
`PGGSSDELEGATION` environment variables, then to defaults.

An invalid value throws an `ArgumentError`. This includes an `sslmode` or
`gssencmode` that is not one of the accepted values, compared
case-insensitively, also when it comes from `PGSSLMODE` or `PGGSSENCMODE`:
`invalid value "verfy-full" for connection parameter "sslmode"; expected one of
disable, prefer, require, verify-full`.

Invalid URI syntax is reported as `ArgumentError` without retaining the
underlying parser's input-bearing exception. To find out which parameter is
invalid without parsing the message, use `Postgres.validate_dsn`.
Expand Down
Loading
Loading