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    build_fast_auth_token_payload_with_pop(
187        user,
188        token,
189        driver_name,
190        version_num,
191        connect_string,
192        edition,
193        None,
194    )
195}
196
197/// [`build_fast_auth_token_payload`] with an optional OCI IAM database-token
198/// proof-of-possession (`AUTH_HEADER`/`AUTH_SIGNATURE`). `None` is identical to
199/// [`build_fast_auth_token_payload`].
200pub fn build_fast_auth_token_payload_with_pop(
201    user: &str,
202    token: &str,
203    driver_name: &str,
204    version_num: u32,
205    connect_string: &str,
206    edition: Option<&str>,
207    pop: Option<TokenPop<'_>>,
208) -> Result<Vec<u8>> {
209    let mut out = Vec::from_hex(FAST_AUTH_PREFIX_HEX)
210        .map_err(|_| ProtocolError::TtcDecode("invalid static fast-auth prefix"))?;
211    append_auth_phase_two_token_with_pop(
212        &mut out,
213        user,
214        token,
215        driver_name,
216        version_num,
217        connect_string,
218        edition,
219        pop,
220    )?;
221    Ok(out)
222}
223
224/// Builds the standalone classic-auth phase-two token message.
225///
226/// Unlike [`build_fast_auth_token_payload`], this does not prepend the
227/// fast-auth envelope. Pre-23ai servers receive protocol negotiation and data
228/// types as separate round trips, then this self-contained `AUTH_TOKEN`
229/// phase-two message.
230pub fn build_auth_phase_two_token_payload(
231    user: &str,
232    token: &str,
233    driver_name: &str,
234    version_num: u32,
235    connect_string: &str,
236    edition: Option<&str>,
237) -> Result<Vec<u8>> {
238    build_auth_phase_two_token_payload_with_pop(
239        user,
240        token,
241        driver_name,
242        version_num,
243        connect_string,
244        edition,
245        None,
246    )
247}
248
249/// [`build_auth_phase_two_token_payload`] with an optional OCI IAM database-token
250/// proof-of-possession (`AUTH_HEADER`/`AUTH_SIGNATURE`). `None` is identical to
251/// [`build_auth_phase_two_token_payload`].
252pub fn build_auth_phase_two_token_payload_with_pop(
253    user: &str,
254    token: &str,
255    driver_name: &str,
256    version_num: u32,
257    connect_string: &str,
258    edition: Option<&str>,
259    pop: Option<TokenPop<'_>>,
260) -> Result<Vec<u8>> {
261    let mut out = Vec::new();
262    append_auth_phase_two_token_with_pop(
263        &mut out,
264        user,
265        token,
266        driver_name,
267        version_num,
268        connect_string,
269        edition,
270        pop,
271    )?;
272    Ok(out)
273}
274
275pub fn build_function_payload(function_code: u8, ttc_field_version: u8) -> Vec<u8> {
276    build_function_payload_with_seq(function_code, 1, ttc_field_version)
277}
278
279pub fn build_function_payload_with_seq(
280    function_code: u8,
281    seq_num: u8,
282    ttc_field_version: u8,
283) -> Vec<u8> {
284    build_function_payload_with_seq_and_token(function_code, seq_num, 0, ttc_field_version)
285}
286
287/// Bare function message with an explicit pipeline token (messages/base.pyx
288/// `_write_function_code` writes `ub8 token_num` for field version >= 23.1
289/// ext 1; non-pipelined messages carry 0). On a pre-23.1-ext-1 connection the
290/// token field does not exist on the wire at all; pipelining (nonzero tokens)
291/// only happens on 23ai-negotiated connections, so no token is ever dropped.
292pub fn build_function_payload_with_seq_and_token(
293    function_code: u8,
294    seq_num: u8,
295    token_num: u64,
296    ttc_field_version: u8,
297) -> Vec<u8> {
298    let mut writer = TtcWriter::new();
299    writer.write_function_code_with_seq(function_code, seq_num);
300    if version_gates::writes_pipeline_token(ttc_field_version) {
301        writer.write_ub8(token_num);
302    } else {
303        debug_assert_eq!(
304            token_num, 0,
305            "pipeline tokens require a 23ai-negotiated connection"
306        );
307    }
308    writer.into_bytes()
309}
310
311pub(crate) fn skip_protocol_message(
312    reader: &mut TtcReader<'_>,
313) -> Result<Option<ClientCapabilities>> {
314    let _server_version = reader.read_u8()?;
315    reader.skip(1)?;
316    loop {
317        if reader.read_u8()? == 0 {
318            break;
319        }
320    }
321    let charset_id = reader.read_u16le()?;
322    let _server_flags = reader.read_u8()?;
323    let num_elem = reader.read_u16le()?;
324    reader.skip(usize::from(num_elem) * 5)?;
325    let fdo_len = reader.read_u16be()?;
326    reader.skip(usize::from(fdo_len))?;
327    let compile_caps = reader.read_bytes()?;
328    let runtime_caps = reader.read_bytes()?;
329    let Some(compile_caps) = compile_caps else {
330        return Ok(None);
331    };
332    let server_ttc_field_version = compile_caps
333        .get(TNS_CCAP_FIELD_VERSION)
334        .copied()
335        .unwrap_or_else(|| ClientCapabilities::default().ttc_field_version);
336    // The effective field version is the LOWER of what the server reports and
337    // what this client supports (reference capabilities.pyx
338    // `_adjust_for_server_compile_caps`: "if server < client: client =
339    // server"). Taking the max would over-claim 23ai-era field formats against
340    // pre-23ai servers.
341    let ttc_field_version =
342        server_ttc_field_version.min(ClientCapabilities::default().ttc_field_version);
343    let max_string_size = if runtime_caps
344        .as_deref()
345        .and_then(|caps| caps.get(TNS_RCAP_TTC))
346        .is_some_and(|flags| flags & TNS_RCAP_TTC_32K != 0)
347    {
348        32_767
349    } else {
350        4_000
351    };
352    Ok(Some(ClientCapabilities {
353        ttc_field_version,
354        max_string_size,
355        charset_id,
356    }))
357}
358
359pub(crate) fn skip_data_types_response(reader: &mut TtcReader<'_>) -> Result<()> {
360    loop {
361        let data_type = reader.read_u16be()?;
362        if data_type == 0 {
363            break;
364        }
365        let conv_data_type = reader.read_u16be()?;
366        if conv_data_type != 0 {
367            reader.skip(4)?;
368        }
369    }
370    Ok(())
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn classic_handshake_messages_slice_the_fast_auth_prefix() {
379        let protocol = build_protocol_negotiation_payload().expect("protocol payload");
380        assert_eq!(protocol[0], TNS_MSG_TYPE_PROTOCOL);
381        assert_eq!(protocol[1], 6, "protocol version byte (8.1 and higher)");
382        assert_eq!(protocol[2], 0, "array terminator");
383        assert!(
384            protocol.ends_with(b"python-oracledb\0"),
385            "driver name string with NUL terminator"
386        );
387
388        let data_types = build_data_types_payload().expect("data types payload");
389        assert_eq!(data_types[0], TNS_MSG_TYPE_DATA_TYPES);
390        // UTF8 charset (873) little-endian for charset and ncharset.
391        assert_eq!(&data_types[1..5], &[0x69, 0x03, 0x69, 0x03]);
392
393        // The two standalone messages are exact slices of the fast-auth bundle,
394        // so classic and fast-auth handshakes can never drift apart.
395        let full = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
396        assert_eq!(full[0], TNS_MSG_TYPE_FAST_AUTH);
397        assert_eq!(
398            &full[FAST_AUTH_PROTOCOL_MSG_START..FAST_AUTH_PROTOCOL_MSG_END],
399            &protocol[..]
400        );
401        assert_eq!(&full[FAST_AUTH_DATA_TYPES_MSG_START..], &data_types[..]);
402    }
403
404    #[test]
405    fn classic_token_payload_is_the_fast_auth_phase_two_suffix() {
406        let classic = build_auth_phase_two_token_payload(
407            "scott",
408            "token-secret",
409            "rust-oracledb",
410            4_000_000_000,
411            "db.example.com/service",
412            Some("MY_EDITION"),
413        )
414        .expect("classic token payload");
415        let fast = build_fast_auth_token_payload(
416            "scott",
417            "token-secret",
418            "rust-oracledb",
419            4_000_000_000,
420            "db.example.com/service",
421            Some("MY_EDITION"),
422        )
423        .expect("fast token payload");
424        let prefix = Vec::from_hex(FAST_AUTH_PREFIX_HEX).expect("prefix decodes");
425
426        assert_eq!(&fast[..prefix.len()], prefix.as_slice());
427        assert_eq!(&fast[prefix.len()..], classic.as_slice());
428        assert_eq!(classic[0], TNS_MSG_TYPE_FUNCTION);
429        assert_eq!(classic[1], TNS_FUNC_AUTH_PHASE_TWO);
430        assert_eq!(
431            classic[2], 1,
432            "standalone phase two is the first TTC function"
433        );
434    }
435
436    // ---- ACCEPT protocol-version gate boundary tests ----------------------
437    //
438    // parse_accept_payload mirrors three reference gates keyed on the ACCEPT's
439    // protocol_version / protocol_options (references connect.pyx:65/75/111,
440    // capabilities.pyx:126, protocol.pyx:262). The live matrix crosses these
441    // (all live servers are >= 318, 23ai advertises end-of-response), but this
442    // offline test pins each boundary exactly.
443
444    /// A full (>= 318 layout) ACCEPT payload with a caller-controlled
445    /// protocol_version field, so the same trailing flags2 bytes can be parsed
446    /// on either side of the 318/319 gates.
447    fn accept_bytes(version: u16, options: u16, flags2: u32) -> Vec<u8> {
448        let mut w = TtcWriter::new();
449        w.write_u16be(version); // protocol version
450        w.write_u16be(options); // protocol options
451        w.write_raw(&[0u8; 10]); // skip(10)
452        w.write_u8(0); // flags1 (no NA_REQUIRED)
453        w.write_raw(&[0u8; 9]); // skip(9)
454        w.write_u32be(8192); // sdu
455        w.write_raw(&[0u8; 5]); // skip(5) before flags2
456        w.write_u32be(flags2); // flags2 (only read when version >= 318)
457        w.into_bytes()
458    }
459
460    #[test]
461    fn accept_parsing_gates_capabilities_on_protocol_version() {
462        // protocol.pyx:262 — supports_oob is a plain flag on protocol_options,
463        // independent of protocol_version.
464        assert!(
465            !parse_accept_payload(&accept_bytes(319, 0, 0))
466                .unwrap()
467                .supports_oob,
468            "no CAN_RECV_ATTENTION bit => supports_oob false"
469        );
470        assert!(
471            parse_accept_payload(&accept_bytes(319, TNS_GSO_CAN_RECV_ATTENTION, 0))
472                .unwrap()
473                .supports_oob,
474            "CAN_RECV_ATTENTION bit => supports_oob true"
475        );
476
477        // connect.pyx:75 (MIN_OOB_CHECK, >= 318) — flags2 (and everything it
478        // carries) is only read at/above 318. Same trailing bytes, version off
479        // by one, must flip the derived capability.
480        let flags2 = TNS_ACCEPT_FLAG_FAST_AUTH | TNS_ACCEPT_FLAG_CHECK_OOB;
481        assert!(
482            !parse_accept_payload(&accept_bytes(317, 0, flags2))
483                .unwrap()
484                .supports_fast_auth,
485            "below 318 flags2 is not read"
486        );
487        let at_318 = parse_accept_payload(&accept_bytes(318, 0, flags2)).unwrap();
488        assert!(at_318.supports_fast_auth, "at 318 flags2 is read");
489        assert!(
490            at_318.supports_oob_check,
491            "at 318 the CHECK_OOB flag is read"
492        );
493
494        // capabilities.pyx:126 (MIN_END_OF_RESPONSE) — end-of-response requires
495        // BOTH protocol_version >= 319 AND the flag. Prove the version gate and
496        // the flag gate independently.
497        assert!(
498            !parse_accept_payload(&accept_bytes(318, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
499                .unwrap()
500                .supports_end_of_response,
501            "318 < 319: no end-of-response even with the flag"
502        );
503        assert!(
504            parse_accept_payload(&accept_bytes(319, 0, TNS_ACCEPT_FLAG_HAS_END_OF_RESPONSE))
505                .unwrap()
506                .supports_end_of_response,
507            "319 + flag: end-of-response negotiated"
508        );
509        assert!(
510            !parse_accept_payload(&accept_bytes(319, 0, 0))
511                .unwrap()
512                .supports_end_of_response,
513            "319 without the flag: no end-of-response"
514        );
515    }
516}