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
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,33 @@ jobs:
- name: Check on MSRV
run: cargo check --features all

# The `rustls-webpki-roots` feature (and the rustls trust-store code paths in
# general) are excluded from `--features=all` because the three TLS backends
# are mutually exclusive, so neither the clippy nor the MSRV job above builds
# them. This server-less lane keeps them clippy-clean, buildable, and unit-
# tested so a regression (e.g. a `webpki-roots` major bump changing
# `TLS_SERVER_ROOTS`) is caught in CI.
rustls-features:
name: rustls trust-store (unit)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y openssl libkrb5-dev
# Build + unit-test both rustls feature sets. `--lib` needs no live server.
# (Strict `-D warnings` clippy is deferred repo-wide until the baseline
# lints land, matching the advisory `clippy` job above.)
- name: Unit tests (rustls)
run: cargo test --no-default-features --features rustls,chrono,time,tds73 --lib
- name: Unit tests (rustls-webpki-roots)
run: cargo test --no-default-features --features rustls-webpki-roots,chrono,time,tds73 --lib

semver:
name: semver-checks (advisory)
if: github.event_name == 'pull_request'
Expand Down
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@

## Version 0.13.0

- feat: TLS trust configuration is revamped around two orthogonal axes plus a
bypass, unifying community PRs #330 and #290:
- `Config::trust_cert_ca_bundle(bytes)` (and the `ConfigBuilder` mirror) trusts
additional CA certificates supplied as in-memory bytes, without writing them
to a temporary file. The bytes are auto-detected: a `-----BEGIN` marker is
parsed as a multi-certificate PEM bundle (e.g. the AWS RDS root bundle),
otherwise they are treated as a single DER certificate.
- `Config::trust_webpki_roots()` (and the `ConfigBuilder` mirror) bases trust
on a compiled-in snapshot of Mozilla's root CA store instead of the OS trust
store. rustls-only, behind the new `rustls-webpki-roots` feature. Note: the
bundled roots are a pinned snapshot that goes stale (missing newly added or
newly distrusted CAs) unless the dependency is updated and the app rebuilt.
- `Config::trust_cert_ca(path)` now accepts **multi-certificate** files (the
previous "exactly one certificate" restriction is lifted); every certificate
in the file is trusted, on all three TLS backends.
- BREAKING: repeated `trust_cert_ca` calls now **accumulate** rather than
replace-last-wins. `trust_cert_ca(a); trust_cert_ca(b)` (and any mix with
`trust_cert_ca_bundle`) trusts every supplied CA, layered on top of the base
trust anchors. Code that relied on a later call overriding an earlier one must
now set the CA only once.
- fix: a CA source (file or in-memory bundle) that yields zero usable
certificates is now a hard error naming the source, on every backend, instead
of silently degrading to base-roots-only trust. The `native-tls` and
`vendored-openssl` backends also now load *all* certificates from a
multi-certificate CA file/bundle (previously only the first was used) and
preserve the path plus underlying I/O error in load failures, matching rustls.
- BREAKING: the connection-string `encrypt` default is now `Required` (was
`Off`) when a TLS backend is enabled, matching modern ADO.NET; without a TLS
backend it remains `NotSupported`.
Expand All @@ -17,6 +43,36 @@
- feat: client-certificate authentication, including PEM/DER key files
(`Config::client_certificate`) and PKCS#12 bundles
(`Config::client_certificate_pkcs12`).
- BREAKING: credentials (SQL Server / Windows passwords, the AAD bearer token
and the PKCS#12 password) are now stored as `secrecy::SecretString` instead of
`zeroize::Zeroizing<String>`. They are still zeroized on drop, and their
`Debug` now renders as `SecretBox<str>([REDACTED])` (was `<HIDDEN>`). The
`AuthMethod::AADToken` tuple variant consequently holds a `SecretString`: code
that pattern-matched it and read the token via `Deref`/`Display` must now call
`secrecy::ExposeSecret::expose_secret`. Constructing auth via
`AuthMethod::aad_token`/`sql_server`/`windows` is unchanged.
- feat: connection & command timeouts (closes #375 and #360), matching
ADO.NET's two-knob model and backed by a runtime-agnostic timer so they apply
under any async runtime:
- `Config::handshake_timeout` bounds the whole post-TCP handshake (prelogin,
TLS negotiation and login), surfacing a `TimedOut` error instead of hanging
forever when a server accepts the TCP connection and then stalls
mid-handshake — the reported failure against `azure-sql-edge` on macOS
(#375) and the stalled-peer case in #360. Defaults to 15s (ADO.NET
`Connect Timeout` parity). The handshake also emits per-stage `tracing`
DEBUG events so a stall can be pinpointed.
- `Config::command_timeout` bounds each server round-trip while reading
command results (`query`/`execute`/`simple_query`, the `bulk_insert`
acknowledgement and `column_metadata`). It measures per-round-trip stall,
not total enumeration: the deadline resets on every delivered token, so a
slow consumer never trips it — only a stalled server does. Defaults to 30s
(ADO.NET `Command Timeout` parity).
- BREAKING: both knobs now default to a bounded value (15s handshake / 30s
command) where pre-0.13 they were effectively unbounded. A command that
legitimately runs longer than 30s between server round-trips (e.g. a long
`WAITFOR`, a big sort/aggregate or a slow stored procedure) will now fail
with a `TimedOut` error unless you raise or disable `command_timeout`. Pass
`None` to either knob to restore the pre-0.13 wait-indefinitely behaviour.
- chore: upgraded the rustls stack to 0.23 (tokio-rustls 0.26) and resolved the
associated advisories.
- fix: numerous decode-path hardening fixes (protocol errors instead of panics
Expand Down
34 changes: 31 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ connection-string = "0.2"
num-traits = "0.2"
uuid = "1.0"
zeroize = "1.8.2"
secrecy = "0.10"

[target.'cfg(windows)'.dependencies]
winauth = { version = "0.0.5", optional = true }
Expand Down Expand Up @@ -98,6 +99,13 @@ version = "0.3"
default-features = false
features = ["io", "sink"]

# Runtime-agnostic timer used to bound the connection handshake (see
# `Config::handshake_timeout`). Its `Delay` future works under any executor, so
# the generic, runtime-agnostic `Connection::connect` path can enforce a timeout
# without depending on tokio/smol timers.
[dependencies.futures-timer]
version = "3"

[dependencies.tracing]
features = ["log"]
version = "0.1"
Expand Down Expand Up @@ -142,6 +150,22 @@ version = "0.103.13"
optional = true
default-features = false

# Shared certificate DER/PEM types used by the backend-agnostic CA loader
# (`src/client/tls_stream/certs.rs`). Enabled by every TLS backend so all three
# parse trust anchors through one code path. This is the same `rustls-pki-types`
# crate `tokio-rustls` re-exports, so the `CertificateDer` handed to rustls is
# the exact same type.
[dependencies.rustls-pki-types]
version = "1"
optional = true

# Compiled-in Mozilla root CA bundle for the `rustls-webpki-roots` feature.
# Pinned to its current major (1.x). Note: this is a *pinned snapshot* that goes
# stale unless the dependency is updated.
[dependencies.webpki-roots]
version = "1"
optional = true

[dependencies.opentls]
version = "0.2.1"
optional = true
Expand Down Expand Up @@ -224,9 +248,13 @@ sql-browser-tokio = ["tokio", "tokio-util"]
sql-browser-smol = ["async-io", "async-net", "futures-lite"]
integrated-auth-gssapi = ["libgssapi"]
bigdecimal = ["bigdecimal_"]
rustls = ["tokio-rustls", "tokio-util", "rustls-native-certs", "rustls-webpki"]
native-tls = ["async-native-tls"]
vendored-openssl = ["opentls"]
rustls = ["tokio-rustls", "tokio-util", "rustls-native-certs", "rustls-webpki", "rustls-pki-types"]
# Bundle a compiled-in snapshot of Mozilla's root CA store, selectable via
# `Config::trust_webpki_roots()` (rustls only). Implies `rustls`; the bundled
# roots are a pinned snapshot that goes stale without dependency updates.
rustls-webpki-roots = ["rustls", "dep:webpki-roots"]
native-tls = ["async-native-tls", "rustls-pki-types"]
vendored-openssl = ["opentls", "rustls-pki-types"]
# Enables Windows-style SSPI/NTLM authentication (`AuthMethod::Windows`) on Unix
# platforms without requiring Kerberos, via the pure-Rust `sspi` crate. On
# Windows the same authentication is provided by the `winauth` feature.
Expand Down
64 changes: 64 additions & 0 deletions src/bulk_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Options controlling the behaviour of a bulk-insert (`INSERT BULK`)
//! operation, mirroring the knobs exposed by `SqlBulkCopyOptions` in
//! ADO.NET's `SqlBulkCopy`.

use enumflags2::{bitflags, BitFlags};

/// A single bulk-copy option. Combine several with the `|` operator to build a
/// [`SqlBulkCopyOptions`] set, e.g.
/// `SqlBulkCopyOption::KeepIdentity | SqlBulkCopyOption::TableLock`.
///
/// See the MS docs for `SqlBulkCopyOptions`:
/// <https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlbulkcopyoptions>
#[bitflags]
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SqlBulkCopyOption {
/// Preserve source identity values. When not specified, identity values are
/// assigned by the destination. Implemented by keeping the identity column
/// in the bulk column list (the `INSERT BULK` `WITH (...)` grammar has no
/// `KEEP_IDENTITY` keyword), matching ADO.NET's `SqlBulkCopy`.
KeepIdentity = 1 << 0,
/// Check constraints while data is being inserted. By default, constraints
/// are not checked. Emits `CHECK_CONSTRAINTS`.
CheckConstraints = 1 << 1,
/// Obtain a bulk update lock for the duration of the bulk-copy operation.
/// When not specified, row locks are used. Emits `TABLOCK`.
TableLock = 1 << 2,
/// Preserve null values in the destination table regardless of the settings
/// for default values. When not specified, null values are replaced by
/// default values where applicable. Emits `KEEP_NULLS`.
KeepNulls = 1 << 3,
/// Cause the server to fire the insert triggers for the rows being inserted
/// into the database. Emits `FIRE_TRIGGERS`.
FireTriggers = 1 << 4,
}

/// A set of [`SqlBulkCopyOption`] flags controlling an `INSERT BULK`.
///
/// Build one by combining flags with `|`, or use [`SqlBulkCopyOptions::empty`]
/// (also the [`Default`]) for no options:
///
/// ```
/// # use tiberius::{SqlBulkCopyOption, SqlBulkCopyOptions};
/// let opts: SqlBulkCopyOptions =
/// SqlBulkCopyOption::KeepIdentity | SqlBulkCopyOption::TableLock;
/// assert!(opts.contains(SqlBulkCopyOption::KeepIdentity));
/// assert!(SqlBulkCopyOptions::empty().is_empty());
/// ```
pub type SqlBulkCopyOptions = BitFlags<SqlBulkCopyOption>;

/// The sort order of a column, used as a bulk-insert `ORDER (...)` hint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SortOrder {
/// Ascending order (`ASC`).
Ascending,
/// Descending order (`DESC`).
Descending,
}

/// An order hint for a bulk insert: the column name and the [`SortOrder`] the
/// incoming rows are already sorted by. Passed as a slice, e.g.
/// `&[("id", SortOrder::Ascending)]`, these become the `ORDER (...)` clause of
/// the `INSERT BULK` statement.
pub type ColumnOrderHint<'a> = (&'a str, SortOrder);
Loading
Loading