1use std::{
8 borrow::Cow,
9 collections::HashMap,
10 fmt::{Debug, Display},
11};
12
13use dbn::{Compression, SType, Schema};
14use hex::ToHex;
15use sha2::{Digest, Sha256};
16use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
17use tracing::{debug, error, instrument};
18
19use crate::{ApiKey, Error, USER_AGENT};
20
21use super::{SlowReaderBehavior, Subscription};
22
23pub fn determine_gateway(dataset: &str) -> String {
27 const DEFAULT_PORT: u16 = 13_000;
28
29 let dataset_subdomain: String = dataset.replace('.', "-").to_ascii_lowercase();
30 format!("{dataset_subdomain}.lsg.databento.com:{DEFAULT_PORT}")
31}
32
33pub struct Protocol<W> {
35 sender: W,
36}
37
38impl<W> Protocol<W>
39where
40 W: AsyncWriteExt + Unpin,
41{
42 pub fn new(sender: W) -> Self {
45 Self { sender }
46 }
47
48 #[instrument(skip(self, recver, key, options))]
60 pub async fn authenticate<R>(
61 &mut self,
62 recver: &mut R,
63 key: &ApiKey,
64 dataset: &str,
65 options: SessionOptions<'_>,
66 ) -> crate::Result<String>
67 where
68 R: AsyncBufReadExt + Unpin,
69 {
70 let mut greeting = String::new();
71 recver.read_line(&mut greeting).await?;
73 greeting.pop(); debug!(greeting);
76 let mut response = String::new();
77 recver.read_line(&mut response).await?;
79 response.pop(); let challenge = Challenge::parse(&response).inspect_err(|_| {
83 error!(?response, "No CRAM challenge in response from gateway");
84 })?;
85 debug!(%challenge, "Received CRAM challenge");
86
87 let auth_req = AuthRequest::new(key, dataset, &challenge, options);
89 debug!(?auth_req, "Sending CRAM reply");
90 self.sender.write_all(auth_req.as_bytes()).await?;
91
92 response.clear();
93 recver.read_line(&mut response).await?;
94 if response.is_empty() {
95 error!("Received empty auth response");
96 } else {
97 debug!(
98 auth_resp = &response[..response.len() - 1],
99 "Received auth response"
100 );
101 }
102 response.pop(); let auth_resp = AuthResponse::parse(&response)?;
105 Ok(auth_resp
106 .session_id()
107 .map(ToOwned::to_owned)
108 .unwrap_or_default())
109 }
110
111 pub async fn subscribe(&mut self, sub: &Subscription) -> crate::Result<()> {
122 let Subscription {
123 schema,
124 stype_in,
125 start,
126 use_snapshot,
127 ..
128 } = ⊂
129
130 if *use_snapshot && start.is_some() {
131 return Err(Error::BadArgument {
132 param_name: "use_snapshot",
133 desc: "cannot request snapshot with start time".to_owned(),
134 });
135 }
136 let start_nanos = sub.start.as_ref().map(|start| start.unix_timestamp_nanos());
137
138 let symbol_chunks = sub.symbols.to_chunked_api_string();
139 let last_chunk_idx = symbol_chunks.len() - 1;
140 for (i, sym_str) in symbol_chunks.into_iter().enumerate() {
141 let sub_req = SubRequest::new(
142 *schema,
143 *stype_in,
144 start_nanos,
145 *use_snapshot,
146 sub.id,
147 &sym_str,
148 i == last_chunk_idx,
149 );
150 debug!(?sub_req, "Sending subscription request");
151 self.sender.write_all(sub_req.as_bytes()).await?;
152 }
153 Ok(())
154 }
155
156 pub async fn start_session(&mut self) -> crate::Result<()> {
168 Ok(self.sender.write_all(StartRequest.as_bytes()).await?)
169 }
170
171 pub async fn shutdown(&mut self) -> crate::Result<()> {
176 Ok(self.sender.shutdown().await?)
177 }
178
179 pub fn into_inner(self) -> W {
181 self.sender
182 }
183}
184
185#[derive(Debug, Clone)]
190pub struct Challenge<'a>(&'a str);
191
192impl<'a> Challenge<'a> {
193 pub fn parse(response: &'a str) -> crate::Result<Self> {
199 if let Some(challenge) = response.strip_prefix("cram=") {
200 Ok(Self(challenge))
201 } else {
202 Err(Error::internal(
203 "no CRAM challenge in response from gateway",
204 ))
205 }
206 }
207}
208
209impl Display for Challenge<'_> {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 write!(f, "{}", self.0)
212 }
213}
214
215#[derive(Clone, Debug)]
217pub struct SessionOptions<'a> {
218 pub compression: Compression,
220 pub send_ts_out: bool,
222 pub heartbeat_interval_s: Option<i64>,
224 pub user_agent_ext: Option<&'a str>,
226 pub slow_reader_behavior: Option<SlowReaderBehavior>,
228}
229
230impl Default for SessionOptions<'_> {
231 fn default() -> Self {
232 Self {
233 compression: Compression::None,
234 send_ts_out: false,
235 heartbeat_interval_s: None,
236 user_agent_ext: None,
237 slow_reader_behavior: None,
238 }
239 }
240}
241
242fn parse_kv_pairs(s: &str) -> impl Iterator<Item = (&str, &str)> {
244 s.split('|').filter_map(|kvp| kvp.split_once('='))
245}
246
247pub trait RawApiMsg {
249 fn as_str(&self) -> &str;
251
252 fn as_bytes(&self) -> &[u8] {
254 self.as_str().as_bytes()
255 }
256}
257
258#[derive(Clone)]
263pub struct AuthRequest(String);
264
265impl AuthRequest {
266 pub fn new(
268 key: &ApiKey,
269 dataset: &str,
270 challenge: &Challenge,
271 options: SessionOptions,
272 ) -> Self {
273 let challenge_key = format!("{challenge}|{}", key.0);
274 let mut hasher = Sha256::new();
275 hasher.update(challenge_key.as_bytes());
276 let hashed = hasher.finalize();
277 let bucket_id = key.bucket_id();
278 let encoded_response = hashed.encode_hex::<String>();
279 let send_ts_out = options.send_ts_out as u8;
280 let user_agent: Cow<'_, str> = match options.user_agent_ext {
281 Some(ext) => Cow::Owned(format!("{} {ext}", *USER_AGENT)),
282 None => Cow::Borrowed(&USER_AGENT),
283 };
284 let mut req = format!(
285 "auth={encoded_response}-{bucket_id}|dataset={dataset}|encoding=dbn|compression={compression}|ts_out={send_ts_out}|client={user_agent}",
286 compression = options.compression,
287 );
288 if let Some(heartbeat_interval_s) = options.heartbeat_interval_s {
289 req = format!("{req}|heartbeat_interval_s={heartbeat_interval_s}");
290 }
291 if let Some(slow_reader_behavior) = options.slow_reader_behavior {
292 req = format!("{req}|slow_reader_behavior={slow_reader_behavior}");
293 }
294 req.push('\n');
295 Self(req)
296 }
297}
298
299impl RawApiMsg for AuthRequest {
300 fn as_str(&self) -> &str {
301 self.0.as_str()
302 }
303}
304
305impl Debug for AuthRequest {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 write!(f, "{}", &self.0[..self.0.len() - 1])
309 }
310}
311
312pub struct AuthResponse<'a>(HashMap<&'a str, &'a str>);
317
318impl<'a> AuthResponse<'a> {
319 pub fn parse(response: &'a str) -> crate::Result<Self> {
325 let auth_keys: HashMap<&'a str, &'a str> = parse_kv_pairs(response).collect();
326 if auth_keys.get("success").map(|v| *v != "1").unwrap_or(true) {
328 return Err(Error::Auth(
329 auth_keys
330 .get("error")
331 .map(|msg| (*msg).to_owned())
332 .unwrap_or_else(|| response.to_owned()),
333 ));
334 }
335 Ok(Self(auth_keys))
336 }
337
338 pub fn session_id(&self) -> Option<&str> {
340 self.0.get("session_id").copied()
341 }
342
343 pub fn get_ref(&self) -> &HashMap<&'a str, &'a str> {
345 &self.0
346 }
347}
348
349#[derive(Clone)]
354pub struct SubRequest(String);
355
356impl SubRequest {
357 pub fn new(
361 schema: Schema,
362 stype_in: SType,
363 start_nanos: Option<i128>,
364 use_snapshot: bool,
365 id: Option<u32>,
366 symbols: &str,
367 is_last: bool,
368 ) -> Self {
369 let use_snapshot = use_snapshot as u8;
370 let is_last = is_last as u8;
371 let mut args = format!(
372 "schema={schema}|stype_in={stype_in}|symbols={symbols}|snapshot={use_snapshot}|is_last={is_last}"
373 );
374
375 if let Some(start) = start_nanos {
376 args = format!("{args}|start={start}");
377 }
378 if let Some(id) = id {
379 args = format!("{args}|id={id}");
380 }
381 args.push('\n');
382 Self(args)
383 }
384}
385
386impl RawApiMsg for SubRequest {
387 fn as_str(&self) -> &str {
388 self.0.as_str()
389 }
390}
391
392impl Debug for SubRequest {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 write!(f, "{}", &self.0[..self.0.len() - 1])
396 }
397}
398
399#[derive(Debug, Clone, Copy)]
404pub struct StartRequest;
405
406impl RawApiMsg for StartRequest {
407 fn as_str(&self) -> &str {
408 "start_session\n"
409 }
410}