Skip to main content

oracledb_protocol/thin/
connect.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5/// Whether the connect data travels inline in the CONNECT packet. Longer
6/// descriptors must be sent in a separate DATA packet right after the CONNECT
7/// packet (reference messages/connect.pyx `ConnectMessage.send`); the caller
8/// owns that follow-up send.
9pub fn connect_data_fits_inline(connect_data: &str) -> bool {
10    connect_data.len() <= TNS_MAX_CONNECT_DATA
11}
12
13pub fn build_connect_packet_payload(connect_data: &str, sdu: u16) -> Result<Vec<u8>> {
14    let connect_bytes = connect_data.as_bytes();
15    let connect_len =
16        u16::try_from(connect_bytes.len()).map_err(|_| ProtocolError::PacketTooLarge {
17            length: connect_bytes.len(),
18        })?;
19
20    let mut writer = TtcWriter::new();
21    writer.write_u16be(TNS_VERSION_DESIRED);
22    writer.write_u16be(TNS_VERSION_MIN);
23    writer.write_u16be(TNS_GSO_DONT_CARE);
24    writer.write_u16be(sdu);
25    writer.write_u16be(sdu);
26    writer.write_u16be(TNS_PROTOCOL_CHARACTERISTICS);
27    writer.write_u16be(0);
28    writer.write_u16be(1);
29    writer.write_u16be(connect_len);
30    writer.write_u16be(74);
31    writer.write_u32be(0);
32    let nsi_flags = TNS_NSI_SUPPORT_SECURITY_RENEG | TNS_NSI_DISABLE_NA;
33    writer.write_u8(nsi_flags);
34    writer.write_u8(nsi_flags);
35    writer.write_u64be(0);
36    writer.write_u64be(0);
37    writer.write_u64be(0);
38    writer.write_u32be(u32::from(sdu));
39    writer.write_u32be(u32::from(sdu));
40    writer.write_u32be(0);
41    writer.write_u32be(0);
42    // Connect data above TNS_MAX_CONNECT_DATA is carried in a separate DATA
43    // packet; the header still advertises the full length either way.
44    if connect_data_fits_inline(connect_data) {
45        writer.write_raw(connect_bytes);
46    }
47    Ok(writer.into_bytes())
48}
49
50pub fn parse_accept_payload(payload: &[u8]) -> Result<AcceptInfo> {
51    let mut reader = TtcReader::new(payload);
52    let protocol_version = reader.read_u16be()?;
53    // Refuse below-floor servers BEFORE touching the rest of the payload
54    // (reference messages/connect.pyx: `if protocol_version <
55    // TNS_VERSION_MIN_ACCEPTED: ERR_SERVER_VERSION_NOT_SUPPORTED`). Pre-12.1
56    // servers use an older, shorter ACCEPT layout — Oracle 11g (version 314)
57    // sends 24 payload bytes, so parsing on would die with a misleading
58    // "truncated TTC payload" instead of naming the real problem.
59    if protocol_version < TNS_VERSION_MIN_ACCEPTED {
60        return Err(ProtocolError::UnsupportedVersion {
61            version: protocol_version,
62            minimum: TNS_VERSION_MIN_ACCEPTED,
63        });
64    }
65    let protocol_options = reader.read_u16be()?;
66    reader.skip(10)?;
67    let flags1 = reader.read_u8()?;
68    if has_u8_flag(flags1, TNS_NSI_NA_REQUIRED) {
69        return Err(ProtocolError::UnsupportedFeature(
70            "Native Network Encryption and Data Integrity",
71        ));
72    }
73    reader.skip(9)?;
74    let sdu = reader.read_u32be()?;
75    let mut flags2 = 0;
76    if protocol_version >= 318 {
77        reader.skip(5)?;
78        flags2 = reader.read_u32be()?;
79    }
80
81    Ok(AcceptInfo {
82        protocol_version,
83        protocol_options,
84        sdu,
85        supports_fast_auth: has_u32_flag(flags2, TNS_ACCEPT_FLAG_FAST_AUTH),
86        supports_oob_check: has_u32_flag(flags2, TNS_ACCEPT_FLAG_CHECK_OOB),
87        // Reference: Capabilities.supports_oob = protocol_options &
88        // TNS_GSO_CAN_RECV_ATTENTION (capabilities.pyx:121).
89        supports_oob: protocol_options & TNS_GSO_CAN_RECV_ATTENTION != 0,
90        supports_end_of_response: protocol_version >= 319
91            && has_u32_flag(flags2, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE),
92    })
93}
94
95pub fn build_fast_auth_phase_one_payload(
96    user: &str,
97    program: &str,
98    machine: &str,
99    osuser: &str,
100    terminal: &str,
101    pid: u32,
102) -> Result<Vec<u8>> {
103    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
104        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
105    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
106    Ok(out)
107}
108
109// Byte layout of FAST_AUTH_PREFIX_HEX (mirrors reference
110// messages/fast_auth.pyx `FastAuthMessage._write_message`):
111//   [0..4)    fast-auth envelope: msg type 34, version 1, char-conv flags
112//   [4..23)   embedded protocol-negotiation message (msg type 1, version 6,
113//             terminator, driver name string + NUL)
114//   [23..29)  envelope glue: unused server charset/ncharset placeholders +
115//             the pinned ttc field version byte
116//   [29..)    embedded data-types message (msg type 2, UTF8 charsets,
117//             encoding flags, compile/runtime caps, static type table)
118// The classic (non-fast-auth) handshake sends the same two embedded messages
119// as standalone round trips, so pre-23ai servers negotiate byte-identically
120// to the reference implementation.
121const FAST_AUTH_PROTOCOL_MSG_START: usize = 4;
122const FAST_AUTH_PROTOCOL_MSG_END: usize = 23;
123const FAST_AUTH_DATA_TYPES_MSG_START: usize = 29;
124
125fn fast_auth_prefix_slice(start: usize, end: Option<usize>) -> Result<Vec<u8>> {
126    let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
127        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
128    let slice = match end {
129        Some(end) => prefix.get(start..end),
130        None => prefix.get(start..),
131    };
132    slice
133        .map(<[u8]>::to_vec)
134        .ok_or(ProtocolError::TtcDecode("fast-auth prefix too short"))
135}
136
137/// Standalone TTC protocol-negotiation message (msg type 1) for the classic
138/// pre-23ai handshake. Byte-identical to the copy embedded in the fast-auth
139/// bundle (reference messages/protocol.pyx `ProtocolMessage._write_message`).
140pub fn build_protocol_negotiation_payload() -> Result<Vec<u8>> {
141    let payload = fast_auth_prefix_slice(
142        FAST_AUTH_PROTOCOL_MSG_START,
143        Some(FAST_AUTH_PROTOCOL_MSG_END),
144    )?;
145    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_PROTOCOL));
146    Ok(payload)
147}
148
149/// Standalone TTC data-types message (msg type 2) for the classic pre-23ai
150/// handshake. Byte-identical to the copy embedded in the fast-auth bundle
151/// (reference messages/data_types.pyx `DataTypesMessage._write_message`).
152pub fn build_data_types_payload() -> Result<Vec<u8>> {
153    let payload = fast_auth_prefix_slice(FAST_AUTH_DATA_TYPES_MSG_START, None)?;
154    debug_assert_eq!(payload.first(), Some(&TNS_MSG_TYPE_DATA_TYPES));
155    Ok(payload)
156}
157
158/// Standalone auth phase-one function message for the classic pre-23ai
159/// handshake — the same message [`build_fast_auth_phase_one_payload`] appends
160/// after the fast-auth bundle, sent on its own round trip instead.
161pub fn build_auth_phase_one_payload(
162    user: &str,
163    program: &str,
164    machine: &str,
165    osuser: &str,
166    terminal: &str,
167    pid: u32,
168) -> Result<Vec<u8>> {
169    let mut out = Vec::new();
170    append_auth_phase_one(&mut out, user, program, machine, osuser, terminal, pid)?;
171    Ok(out)
172}
173
174/// Fast-auth bundle for **token authentication**: the same static
175/// protocol/data-types prefix as [`build_fast_auth_phase_one_payload`], but with
176/// a phase-two `AUTH_TOKEN` message appended (no verifier round-trip). The caller
177/// sends this once and reads a single auth response.
178pub fn build_fast_auth_token_payload(
179    user: &str,
180    token: &str,
181    driver_name: &str,
182    version_num: u32,
183    connect_string: &str,
184    edition: Option<&str>,
185) -> Result<Vec<u8>> {
186    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
187        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
188    append_auth_phase_two_token(
189        &mut out,
190        user,
191        token,
192        driver_name,
193        version_num,
194        connect_string,
195        edition,
196    )?;
197    Ok(out)
198}
199
200pub fn build_function_payload(function_code: u8, ttc_field_version: u8) -> Vec<u8> {
201    build_function_payload_with_seq(function_code, 1, ttc_field_version)
202}
203
204pub fn build_function_payload_with_seq(
205    function_code: u8,
206    seq_num: u8,
207    ttc_field_version: u8,
208) -> Vec<u8> {
209    build_function_payload_with_seq_and_token(function_code, seq_num, 0, ttc_field_version)
210}
211
212/// Bare function message with an explicit pipeline token (messages/base.pyx
213/// `_write_function_code` writes `ub8 token_num` for field version >= 23.1
214/// ext 1; non-pipelined messages carry 0). On a pre-23.1-ext-1 connection the
215/// token field does not exist on the wire at all; pipelining (nonzero tokens)
216/// only happens on 23ai-negotiated connections, so no token is ever dropped.
217pub fn build_function_payload_with_seq_and_token(
218    function_code: u8,
219    seq_num: u8,
220    token_num: u64,
221    ttc_field_version: u8,
222) -> Vec<u8> {
223    let mut writer = TtcWriter::new();
224    writer.write_function_code_with_seq(function_code, seq_num);
225    if version_gates::writes_pipeline_token(ttc_field_version) {
226        writer.write_ub8(token_num);
227    } else {
228        debug_assert_eq!(
229            token_num, 0,
230            "pipeline tokens require a 23ai-negotiated connection"
231        );
232    }
233    writer.into_bytes()
234}
235
236pub(crate) fn skip_protocol_message(
237    reader: &mut TtcReader<'_>,
238) -> Result<Option<ClientCapabilities>> {
239    let _server_version = reader.read_u8()?;
240    reader.skip(1)?;
241    loop {
242        if reader.read_u8()? == 0 {
243            break;
244        }
245    }
246    let charset_id = reader.read_u16le()?;
247    let _server_flags = reader.read_u8()?;
248    let num_elem = reader.read_u16le()?;
249    reader.skip(usize::from(num_elem) * 5)?;
250    let fdo_len = reader.read_u16be()?;
251    reader.skip(usize::from(fdo_len))?;
252    let compile_caps = reader.read_bytes()?;
253    let runtime_caps = reader.read_bytes()?;
254    let Some(compile_caps) = compile_caps else {
255        return Ok(None);
256    };
257    let server_ttc_field_version = compile_caps
258        .get(TNS_CCAP_FIELD_VERSION)
259        .copied()
260        .unwrap_or_else(|| ClientCapabilities::default().ttc_field_version);
261    // The effective field version is the LOWER of what the server reports and
262    // what this client supports (reference capabilities.pyx
263    // `_adjust_for_server_compile_caps`: "if server < client: client =
264    // server"). Taking the max would over-claim 23ai-era field formats against
265    // pre-23ai servers.
266    let ttc_field_version =
267        server_ttc_field_version.min(ClientCapabilities::default().ttc_field_version);
268    let max_string_size = if runtime_caps
269        .as_deref()
270        .and_then(|caps| caps.get(TNS_RCAP_TTC))
271        .is_some_and(|flags| flags & TNS_RCAP_TTC_32K != 0)
272    {
273        32_767
274    } else {
275        4_000
276    };
277    Ok(Some(ClientCapabilities {
278        ttc_field_version,
279        max_string_size,
280        charset_id,
281    }))
282}
283
284pub(crate) fn skip_data_types_response(reader: &mut TtcReader<'_>) -> Result<()> {
285    loop {
286        let data_type = reader.read_u16be()?;
287        if data_type == 0 {
288            break;
289        }
290        let conv_data_type = reader.read_u16be()?;
291        if conv_data_type != 0 {
292            reader.skip(4)?;
293        }
294    }
295    Ok(())
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn classic_handshake_messages_slice_the_fast_auth_prefix() {
304        let protocol = build_protocol_negotiation_payload().expect("protocol payload");
305        assert_eq!(protocol[0], TNS_MSG_TYPE_PROTOCOL);
306        assert_eq!(protocol[1], 6, "protocol version byte (8.1 and higher)");
307        assert_eq!(protocol[2], 0, "array terminator");
308        assert!(
309            protocol.ends_with(b"python-oracledb\0"),
310            "driver name string with NUL terminator"
311        );
312
313        let data_types = build_data_types_payload().expect("data types payload");
314        assert_eq!(data_types[0], TNS_MSG_TYPE_DATA_TYPES);
315        // UTF8 charset (873) little-endian for charset and ncharset.
316        assert_eq!(&data_types[1..5], &[0x69, 0x03, 0x69, 0x03]);
317
318        // The two standalone messages are exact slices of the fast-auth bundle,
319        // so classic and fast-auth handshakes can never drift apart.
320        let full = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
321        assert_eq!(full[0], TNS_MSG_TYPE_FAST_AUTH);
322        assert_eq!(
323            &full[FAST_AUTH_PROTOCOL_MSG_START..FAST_AUTH_PROTOCOL_MSG_END],
324            &protocol[..]
325        );
326        assert_eq!(&full[FAST_AUTH_DATA_TYPES_MSG_START..], &data_types[..]);
327    }
328
329    // ---- ACCEPT protocol-version gate boundary tests ----------------------
330    //
331    // parse_accept_payload mirrors three reference gates keyed on the ACCEPT's
332    // protocol_version / protocol_options (references connect.pyx:65/75/111,
333    // capabilities.pyx:126, protocol.pyx:262). The live matrix crosses these
334    // (all live servers are >= 318, 23ai advertises end-of-response), but this
335    // offline test pins each boundary exactly.
336
337    /// A full (>= 318 layout) ACCEPT payload with a caller-controlled
338    /// protocol_version field, so the same trailing flags2 bytes can be parsed
339    /// on either side of the 318/319 gates.
340    fn accept_bytes(version: u16, options: u16, flags2: u32) -> Vec<u8> {
341        let mut w = TtcWriter::new();
342        w.write_u16be(version); // protocol version
343        w.write_u16be(options); // protocol options
344        w.write_raw(&[0u8; 10]); // skip(10)
345        w.write_u8(0); // flags1 (no NA_REQUIRED)
346        w.write_raw(&[0u8; 9]); // skip(9)
347        w.write_u32be(8192); // sdu
348        w.write_raw(&[0u8; 5]); // skip(5) before flags2
349        w.write_u32be(flags2); // flags2 (only read when version >= 318)
350        w.into_bytes()
351    }
352
353    #[test]
354    fn accept_parsing_gates_capabilities_on_protocol_version() {
355        // protocol.pyx:262 — supports_oob is a plain flag on protocol_options,
356        // independent of protocol_version.
357        assert!(
358            !parse_accept_payload(&accept_bytes(319, 0, 0))
359                .unwrap()
360                .supports_oob,
361            "no CAN_RECV_ATTENTION bit => supports_oob false"
362        );
363        assert!(
364            parse_accept_payload(&accept_bytes(319, TNS_GSO_CAN_RECV_ATTENTION, 0))
365                .unwrap()
366                .supports_oob,
367            "CAN_RECV_ATTENTION bit => supports_oob true"
368        );
369
370        // connect.pyx:75 (MIN_OOB_CHECK, >= 318) — flags2 (and everything it
371        // carries) is only read at/above 318. Same trailing bytes, version off
372        // by one, must flip the derived capability.
373        let flags2 = TNS_ACCEPT_FLAG_FAST_AUTH | TNS_ACCEPT_FLAG_CHECK_OOB;
374        assert!(
375            !parse_accept_payload(&accept_bytes(317, 0, flags2))
376                .unwrap()
377                .supports_fast_auth,
378            "below 318 flags2 is not read"
379        );
380        let at_318 = parse_accept_payload(&accept_bytes(318, 0, flags2)).unwrap();
381        assert!(at_318.supports_fast_auth, "at 318 flags2 is read");
382        assert!(
383            at_318.supports_oob_check,
384            "at 318 the CHECK_OOB flag is read"
385        );
386
387        // capabilities.pyx:126 (MIN_END_OF_RESPONSE) — end-of-response requires
388        // BOTH protocol_version >= 319 AND the flag. Prove the version gate and
389        // the flag gate independently.
390        assert!(
391            !parse_accept_payload(&accept_bytes(318, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
392                .unwrap()
393                .supports_end_of_response,
394            "318 < 319: no end-of-response even with the flag"
395        );
396        assert!(
397            parse_accept_payload(&accept_bytes(319, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
398                .unwrap()
399                .supports_end_of_response,
400            "319 + flag: end-of-response negotiated"
401        );
402        assert!(
403            !parse_accept_payload(&accept_bytes(319, 0, 0))
404                .unwrap()
405                .supports_end_of_response,
406            "319 without the flag: no end-of-response"
407        );
408    }
409}