Skip to content

Commit 3180560

Browse files
lovasoacursoragent
andauthored
Discover sql server named instance ports (#43)
* feat: Add SSRP support for MSSQL named instances Co-authored-by: contact <[email protected]> * Add logging to MssqlStream and SSRP instance resolution Co-authored-by: contact <[email protected]> * Remove redundant debug logs from SSRP connection logic Co-authored-by: contact <[email protected]> * Refactor: Use String::from_utf8_lossy for SSRP response parsing Co-authored-by: contact <[email protected]> * Checkpoint before follow-up message Co-authored-by: contact <[email protected]> * Refactor: Improve SSRP error handling and logging Co-authored-by: contact <[email protected]> * Fix: Correctly parse SSRP instance info types Co-authored-by: contact <[email protected]> * Refactor: Rename ssrp parsing functions and improve logging Co-authored-by: contact <[email protected]> * feat: Improve mssql port resolution logic Co-authored-by: contact <[email protected]> * feat: Add SQL Server named instance support with SSRP Co-authored-by: contact <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent 6ed3daa commit 3180560

File tree

17 files changed

+307
-31
lines changed

17 files changed

+307
-31
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.6.50
9+
- Added support for SQL Server named instances with automatic port discovery via SSRP (SQL Server Resolution Protocol). You can now connect using `mssql://user:pass@host/db?instance=SQLEXPRESS` and the port will be automatically discovered.
10+
811
## 0.6.49
912
- Added support for ODBC. SQLx-oldapi can now connect to Oracle, Db2, Snowflake, BigQuery, Databricks, and many other databases, using locally installed ODBC drivers.
1013

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
2626

2727
[package]
2828
name = "sqlx-oldapi"
29-
version = "0.6.49"
29+
version = "0.6.50"
3030
license = "MIT OR Apache-2.0"
3131
readme = "README.md"
3232
repository = "https://github.com/lovasoa/sqlx"
@@ -155,8 +155,8 @@ bstr = ["sqlx-core/bstr"]
155155
git2 = ["sqlx-core/git2"]
156156

157157
[dependencies]
158-
sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.49", path = "sqlx-core", default-features = false }
159-
sqlx-macros = { package = "sqlx-macros-oldapi", version = "0.6.49", path = "sqlx-macros", default-features = false, optional = true }
158+
sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.50", path = "sqlx-core", default-features = false }
159+
sqlx-macros = { package = "sqlx-macros-oldapi", version = "0.6.50", path = "sqlx-macros", default-features = false, optional = true }
160160

161161
[dev-dependencies]
162162
anyhow = "1.0.52"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
> - Multiple bug fixes around string handling, including better support for long strings
1616
> - Support for packet chunking, which fixes a bug where large bound parameters or large queries would fail
1717
> - Support for TLS encrypted connections
18+
> - Support for named instances with automatic port discovery via SSRP
1819
>
1920
> The main use case driving the development of sqlx-oldapi is the [SQLPage](https://sql.datapage.app/) SQL-only rapid application building tool.
2021

examples/postgres/axum-social-with-tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99
[dependencies]
1010
# Primary crates
1111
axum = { version = "0.5.13", features = ["macros"] }
12-
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "../../../", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] }
12+
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "../../../", features = ["runtime-tokio-rustls", "postgres", "time", "uuid"] }
1313
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] }
1414

1515
# Important secondary crates

sqlx-bench/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ sqlite = ["sqlx/sqlite"]
3333
criterion = "0.3.3"
3434
dotenvy = "0.15.0"
3535
once_cell = "1.4"
36-
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "../", default-features = false, features = ["macros"] }
37-
sqlx-rt = { package = "sqlx-rt-oldapi", version = "0.6.49", path = "../sqlx-rt", default-features = false }
36+
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "../", default-features = false, features = ["macros"] }
37+
sqlx-rt = { package = "sqlx-rt-oldapi", version = "0.6.50", path = "../sqlx-rt", default-features = false }
3838

3939
chrono = "0.4.19"
4040

sqlx-cli/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlx-cli"
3-
version = "0.6.49"
3+
version = "0.6.50"
44
description = "Command-line utility for SQLx, the Rust SQL toolkit."
55
edition = "2021"
66
readme = "README.md"
@@ -28,7 +28,7 @@ path = "src/bin/cargo-sqlx.rs"
2828
[dependencies]
2929
dotenvy = "0.15.0"
3030
tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread"] }
31-
sqlx = { package = "sqlx-oldapi", version = "0.6.49", path = "..", default-features = false, features = [
31+
sqlx = { package = "sqlx-oldapi", version = "0.6.50", path = "..", default-features = false, features = [
3232
"migrate",
3333
"any",
3434
"offline",

sqlx-core/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlx-core-oldapi"
3-
version = "0.6.49"
3+
version = "0.6.50"
44
repository = "https://github.com/lovasoa/sqlx"
55
description = "Core of SQLx, the rust SQL toolkit. Not intended to be used directly."
66
license = "MIT OR Apache-2.0"
@@ -104,7 +104,7 @@ offline = ["serde", "either/serde"]
104104
paste = "1.0.6"
105105
ahash = "0.8.3"
106106
atoi = "2.0.0"
107-
sqlx-rt = { path = "../sqlx-rt", version = "0.6.49", package = "sqlx-rt-oldapi" }
107+
sqlx-rt = { path = "../sqlx-rt", version = "0.6.50", package = "sqlx-rt-oldapi" }
108108
base64 = { version = "0.22", default-features = false, optional = true, features = ["std"] }
109109
bigdecimal_ = { version = "0.4.1", optional = true, package = "bigdecimal" }
110110
rust_decimal = { version = "1.19.0", optional = true }

sqlx-core/src/mssql/connection/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::sync::Arc;
1414
mod establish;
1515
mod executor;
1616
mod prepare;
17+
mod ssrp;
1718
mod stream;
1819
mod tls_prelogin_stream_wrapper;
1920

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use crate::error::Error;
2+
use encoding_rs::WINDOWS_1252;
3+
use sqlx_rt::{timeout, UdpSocket};
4+
use std::time::Duration;
5+
6+
const SSRP_PORT: u16 = 1434;
7+
const CLNT_UCAST_INST: u8 = 0x04;
8+
const SVR_RESP: u8 = 0x05;
9+
const SSRP_TIMEOUT: Duration = Duration::from_secs(1);
10+
11+
struct InstanceInfo<'a> {
12+
server_name: Option<&'a str>,
13+
instance_name: Option<&'a str>,
14+
is_clustered: Option<bool>,
15+
version: Option<&'a str>,
16+
tcp_port: Option<u16>,
17+
}
18+
19+
pub(crate) async fn resolve_instance_port(server: &str, instance: &str) -> Result<u16, Error> {
20+
log::debug!(
21+
"resolving SQL Server instance port for '{}' on server '{}'",
22+
instance,
23+
server
24+
);
25+
26+
let mut request = Vec::with_capacity(1 + instance.len() + 1);
27+
request.push(CLNT_UCAST_INST);
28+
request.extend_from_slice(instance.as_bytes());
29+
request.push(0);
30+
31+
let socket = UdpSocket::bind("0.0.0.0:0")
32+
.await
33+
.map_err(|e| err_protocol!("failed to bind UDP socket for SSRP: {}", e))?;
34+
35+
log::debug!(
36+
"sending SSRP CLNT_UCAST_INST request to {}:{} for instance '{}'",
37+
server,
38+
SSRP_PORT,
39+
instance
40+
);
41+
42+
socket
43+
.send_to(&request, (server, SSRP_PORT))
44+
.await
45+
.map_err(|e| {
46+
err_protocol!(
47+
"failed to send SSRP request to {}:{}: {}",
48+
server,
49+
SSRP_PORT,
50+
e
51+
)
52+
})?;
53+
54+
let mut buffer = [0u8; 1024];
55+
let bytes_read = timeout(SSRP_TIMEOUT, socket.recv(&mut buffer))
56+
.await
57+
.map_err(|_| {
58+
err_protocol!(
59+
"SSRP request to {} for instance {} timed out after {:?}",
60+
server,
61+
instance,
62+
SSRP_TIMEOUT
63+
)
64+
})?
65+
.map_err(|e| {
66+
err_protocol!(
67+
"failed to receive SSRP response from {} for instance {}: {}",
68+
server,
69+
instance,
70+
e
71+
)
72+
})?;
73+
74+
log::debug!(
75+
"received SSRP response from {} ({} bytes)",
76+
server,
77+
bytes_read
78+
);
79+
80+
if bytes_read < 3 {
81+
return Err(err_protocol!(
82+
"SSRP response too short: {} bytes",
83+
bytes_read
84+
));
85+
}
86+
87+
if buffer[0] != SVR_RESP {
88+
return Err(err_protocol!(
89+
"invalid SSRP response type: expected 0x05, got 0x{:02x}",
90+
buffer[0]
91+
));
92+
}
93+
94+
let response_size = u16::from_le_bytes([buffer[1], buffer[2]]) as usize;
95+
if response_size + 3 > bytes_read {
96+
return Err(err_protocol!(
97+
"SSRP response size mismatch: expected {} bytes, got {}",
98+
response_size + 3,
99+
bytes_read
100+
));
101+
}
102+
103+
let response_bytes = &buffer[3..(3 + response_size)];
104+
let (response_str, _encoding_used, had_errors) = WINDOWS_1252.decode(response_bytes);
105+
106+
if had_errors {
107+
log::debug!("SSRP response had MBCS decoding errors, continuing anyway");
108+
}
109+
110+
log::debug!("SSRP response data: {}", response_str);
111+
112+
find_instance_tcp_port(&response_str, instance)
113+
}
114+
115+
fn find_instance_tcp_port(data: &str, instance_name: &str) -> Result<u16, Error> {
116+
for instance_data in data.split(";;") {
117+
if instance_data.is_empty() {
118+
continue;
119+
}
120+
121+
let info = parse_instance_info(instance_data);
122+
123+
if let Some(name) = info.instance_name {
124+
log::debug!("found instance '{}' in SSRP response", name);
125+
126+
if name.eq_ignore_ascii_case(instance_name) {
127+
log::debug!(
128+
"instance '{}' matches requested instance '{}'",
129+
name,
130+
instance_name
131+
);
132+
133+
if let Some(port) = info.tcp_port {
134+
log::debug!("resolved instance '{}' to port {}", instance_name, port);
135+
return Ok(port);
136+
} else {
137+
return Err(err_protocol!(
138+
"instance '{}' found but no TCP port available",
139+
instance_name
140+
));
141+
}
142+
}
143+
}
144+
}
145+
146+
Err(err_protocol!(
147+
"instance '{}' not found in SSRP response",
148+
instance_name
149+
))
150+
}
151+
152+
fn parse_instance_info<'a>(data: &'a str) -> InstanceInfo<'a> {
153+
let mut info = InstanceInfo {
154+
server_name: None,
155+
instance_name: None,
156+
is_clustered: None,
157+
version: None,
158+
tcp_port: None,
159+
};
160+
161+
let mut tokens = data.split(';');
162+
while let Some(key) = tokens.next() {
163+
let value = tokens.next();
164+
165+
match key {
166+
"ServerName" => info.server_name = value,
167+
"InstanceName" => info.instance_name = value,
168+
"IsClustered" => {
169+
info.is_clustered = value.and_then(|v| match v {
170+
"Yes" => Some(true),
171+
"No" => Some(false),
172+
_ => None,
173+
});
174+
}
175+
"Version" => info.version = value,
176+
"tcp" => {
177+
info.tcp_port = value.and_then(|v| v.parse::<u16>().ok());
178+
}
179+
_ => {
180+
if !key.is_empty() {
181+
log::debug!("ignoring unknown SSRP key: '{}'", key);
182+
}
183+
}
184+
}
185+
}
186+
187+
info
188+
}
189+
190+
#[cfg(test)]
191+
mod tests {
192+
use super::*;
193+
194+
#[test]
195+
fn test_find_instance_tcp_port_single_instance() {
196+
let data = "ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
197+
let port = find_instance_tcp_port(data, "SQLEXPRESS").unwrap();
198+
assert_eq!(port, 1433);
199+
}
200+
201+
#[test]
202+
fn test_find_instance_tcp_port_multiple_instances() {
203+
let data = "ServerName;SRV1;InstanceName;INST1;IsClustered;No;Version;15.0.2000.5;tcp;1433;;ServerName;SRV1;InstanceName;INST2;IsClustered;No;Version;16.0.1000.6;tcp;1434;np;\\\\SRV1\\pipe\\MSSQL$INST2\\sql\\query;;";
204+
let port = find_instance_tcp_port(data, "INST2").unwrap();
205+
assert_eq!(port, 1434);
206+
}
207+
208+
#[test]
209+
fn test_find_instance_tcp_port_case_insensitive() {
210+
let data = "ServerName;MYSERVER;InstanceName;SQLExpress;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
211+
let port = find_instance_tcp_port(data, "sqlexpress").unwrap();
212+
assert_eq!(port, 1433);
213+
}
214+
215+
#[test]
216+
fn test_find_instance_tcp_port_instance_not_found() {
217+
let data = "ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;tcp;1433;;";
218+
let result = find_instance_tcp_port(data, "NOTFOUND");
219+
assert!(result.is_err());
220+
}
221+
222+
#[test]
223+
fn test_find_instance_tcp_port_no_tcp_port() {
224+
let data =
225+
"ServerName;MYSERVER;InstanceName;SQLEXPRESS;IsClustered;No;Version;15.0.2000.5;;";
226+
let result = find_instance_tcp_port(data, "SQLEXPRESS");
227+
assert!(result.is_err());
228+
}
229+
}

0 commit comments

Comments
 (0)