1use crate::auth::Credential;
5use crate::capabilities::{Capabilities, Feature};
6use crate::config::Profile;
7use crate::error::{Error, ErrorKind, Result};
8use reqwest::{Client, Method, Response, StatusCode};
9use serde_json::Value;
10use std::collections::BTreeMap;
11use std::io::Write;
12use std::time::Duration;
13use tokio::sync::OnceCell;
14
15const API_VERSION: &str = "2023-10-31";
17const MAX_ATTEMPTS: u32 = 3;
18
19const CAPTURED_HEADERS: [&str; 3] = [
28 "x-found-handling-cluster",
29 "x-found-handling-instance",
30 "x-elastic-product",
31];
32
33fn parse_response_json(text: &str) -> Result<Value> {
36 validate_json_integer_ranges(text)?;
37 serde_json::from_str(text)
38 .map_err(|e| Error::new(ErrorKind::Http, format!("parsing response JSON: {e}")))
39}
40
41fn validate_json_integer_ranges(text: &str) -> Result<()> {
46 let bytes = text.as_bytes();
47 let mut index = 0;
48
49 while index < bytes.len() {
50 if bytes[index] == b'"' {
51 index += 1;
52 while index < bytes.len() {
53 match bytes[index] {
54 b'\\' => index += 2,
55 b'"' => {
56 index += 1;
57 break;
58 }
59 _ => index += 1,
60 }
61 }
62 continue;
63 }
64
65 if bytes[index] != b'-' && !bytes[index].is_ascii_digit() {
66 index += 1;
67 continue;
68 }
69
70 let start = index;
71 if bytes[index] == b'-' {
72 index += 1;
73 }
74 if index == bytes.len() || !bytes[index].is_ascii_digit() {
75 index = start + 1;
76 continue;
77 }
78
79 if bytes[index] == b'0' {
80 index += 1;
81 } else {
82 while index < bytes.len() && bytes[index].is_ascii_digit() {
83 index += 1;
84 }
85 }
86
87 let mut is_integer = true;
88 if bytes.get(index) == Some(&b'.') {
89 is_integer = false;
90 index += 1;
91 while index < bytes.len() && bytes[index].is_ascii_digit() {
92 index += 1;
93 }
94 }
95 if matches!(bytes.get(index), Some(b'e' | b'E')) {
96 is_integer = false;
97 index += 1;
98 if matches!(bytes.get(index), Some(b'+' | b'-')) {
99 index += 1;
100 }
101 while index < bytes.len() && bytes[index].is_ascii_digit() {
102 index += 1;
103 }
104 }
105
106 if is_integer {
107 let number = &text[start..index];
108 let in_range = if bytes[start] == b'-' {
109 number.parse::<i64>().is_ok()
110 } else {
111 number.parse::<u64>().is_ok()
112 };
113 if !in_range {
114 return Err(Error::new(
115 ErrorKind::Http,
116 format!(
117 "parsing response JSON: integer {number} is outside supported integer range"
118 ),
119 ));
120 }
121 }
122 }
123
124 Ok(())
125}
126
127#[derive(Debug, Clone)]
132pub struct Responded {
133 pub body: Value,
134 pub headers: BTreeMap<String, String>,
135}
136
137impl Responded {
138 pub fn header(&self, name: &str) -> Option<&str> {
143 self.headers.get(&name.to_ascii_lowercase()).map(|s| &**s)
144 }
145}
146
147pub fn urlencode(s: &str) -> String {
152 let mut out = String::with_capacity(s.len());
153 for b in s.bytes() {
154 match b {
155 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
156 out.push(b as char)
157 }
158 _ => out.push_str(&format!("%{b:02X}")),
159 }
160 }
161 out
162}
163
164pub struct Transport {
165 client: Client,
166 one_shot_client: Client,
167 base: String,
168 kibana_url: String,
172 es_base: String,
175 has_es_url: bool,
180 space: String,
181 auth_header: String,
182 debug: bool,
183 capabilities: OnceCell<Capabilities>,
184}
185
186impl Transport {
187 fn client_builder(profile: &Profile) -> reqwest::ClientBuilder {
188 Client::builder()
189 .timeout(Duration::from_secs(profile.timeout_secs))
190 .danger_accept_invalid_certs(!profile.verify)
191 }
192
193 pub fn new(profile: &Profile) -> Result<Transport> {
194 Self::with_debug(profile, false)
195 }
196
197 pub fn with_debug(profile: &Profile, debug: bool) -> Result<Transport> {
201 let mut profile = profile.clone();
204 profile.strip_userinfo();
205 let credential = Credential::from_profile(&profile)?;
206 let client = Self::client_builder(&profile)
207 .build()
208 .map_err(|e| Error::new(ErrorKind::Connection, format!("building HTTP client: {e}")))?;
209 let one_shot_client = Self::client_builder(&profile)
210 .redirect(reqwest::redirect::Policy::none())
211 .pool_max_idle_per_host(0)
215 .retry(reqwest::retry::never())
216 .build()
217 .map_err(|e| {
218 Error::new(
219 ErrorKind::Connection,
220 format!("building one-shot HTTP client: {e}"),
221 )
222 })?;
223
224 let base = profile.kibana_url.trim_end_matches('/').to_string();
225 let kibana_url = profile.kibana_url.clone();
226 let has_es_url = profile.es_url.is_some();
227 let es_base = profile
228 .es_url
229 .as_deref()
230 .unwrap_or(&profile.kibana_url)
231 .trim_end_matches('/')
232 .to_string();
233
234 Ok(Transport {
235 client,
236 one_shot_client,
237 base,
238 kibana_url,
239 es_base,
240 has_es_url,
241 space: profile.space.clone(),
242 auth_header: credential.header_value(),
243 debug,
244 capabilities: OnceCell::new(),
245 })
246 }
247
248 fn debug_log(&self, method: &Method, url: &str, status: u16, attempt: u32) {
254 if !self.debug {
255 return;
256 }
257 if attempt > 1 {
258 eprintln!(
259 "[debug] {} {url} -> {status} (attempt {attempt})",
260 method.as_str()
261 );
262 } else {
263 eprintln!("[debug] {} {url} -> {status}", method.as_str());
264 }
265 }
266
267 fn debug_request(&self, method: &Method, url: &str, attempt: u32) {
269 if !self.debug {
270 return;
271 }
272 if attempt > 1 {
273 eprintln!("[debug] -> {} {url} (attempt {attempt})", method.as_str());
274 } else {
275 eprintln!("[debug] -> {} {url}", method.as_str());
276 }
277 }
278
279 fn debug_failure(&self, method: &Method, url: &str, what: &str) {
281 if !self.debug {
282 return;
283 }
284 let _ = writeln!(
285 std::io::stderr(),
286 "[debug] {} {url} -> {what}",
287 method.as_str()
288 );
289 }
290
291 pub fn space_path(space: &str, path: &str) -> String {
295 if space.is_empty() || space == "default" {
296 path.to_string()
297 } else {
298 format!("/s/{space}{path}")
299 }
300 }
301
302 pub fn kibana_url(&self) -> &str {
304 &self.kibana_url
305 }
306
307 pub fn space(&self) -> &str {
310 &self.space
311 }
312
313 pub fn has_es_url(&self) -> bool {
318 self.has_es_url
319 }
320
321 pub async fn capabilities(&self) -> Result<&Capabilities> {
323 self.capabilities
324 .get_or_try_init(|| Capabilities::probe(self, self.kibana_url()))
325 .await
326 }
327
328 pub async fn require_feature(&self, feature: Feature) -> Result<()> {
330 self.capabilities().await?.require_feature(feature)
331 }
332
333 fn url(&self, path: &str) -> String {
334 format!("{}{}", self.base, Self::space_path(&self.space, path))
335 }
336
337 async fn response_text(
340 &self,
341 method: &Method,
342 url: &str,
343 response: Response,
344 ) -> Result<String> {
345 match response.text().await {
346 Ok(text) => Ok(text),
347 Err(e) if e.is_timeout() => {
348 self.debug_failure(method, url, "timeout");
349 Err(Error::new(
350 ErrorKind::Timeout,
351 format!("request timed out while reading response body: {e}"),
352 ))
353 }
354 Err(e) => {
355 self.debug_failure(method, url, "connection error");
356 Err(Error::new(
357 ErrorKind::Connection,
358 format!("request failed while reading response body: {e}"),
359 ))
360 }
361 }
362 }
363
364 async fn send_retrying<F>(
365 &self,
366 method: Method,
367 url: &str,
368 attempt_limit: u32,
369 mut build: F,
370 ) -> Result<Response>
371 where
372 F: FnMut() -> Result<reqwest::RequestBuilder>,
373 {
374 let mut attempt = 0;
375
376 loop {
377 attempt += 1;
378 let req = build()?;
379
380 self.debug_request(&method, url, attempt);
381 let result = req.send().await;
382
383 let response = match result {
384 Ok(r) => r,
385 Err(e) if e.is_timeout() => {
386 self.debug_failure(&method, url, "timeout");
387 return Err(Error::new(
388 ErrorKind::Timeout,
389 format!("request timed out: {e}"),
390 ));
391 }
392 Err(e) => {
393 self.debug_failure(&method, url, "connection error");
394 return Err(Error::new(
395 ErrorKind::Connection,
396 format!("request failed: {e}"),
397 ));
398 }
399 };
400
401 let status = response.status();
402 self.debug_log(&method, url, status.as_u16(), attempt);
403 if status.is_success() {
404 return Ok(response);
405 }
406
407 let transient = status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error();
410 if transient && attempt < attempt_limit {
411 let backoff = Duration::from_millis(200 * 2u64.pow(attempt - 1));
412 tokio::time::sleep(backoff).await;
413 continue;
414 }
415
416 let code = status.as_u16();
417 let text = self.response_text(&method, url, response).await?;
418 return Err(Error::from_response_body(code, &text));
419 }
420 }
421
422 async fn send(&self, method: Method, path: &str, body: Option<&Value>) -> Result<Response> {
423 self.send_with_attempt_limit(method, path, body, MAX_ATTEMPTS)
424 .await
425 }
426
427 async fn send_with_attempt_limit(
428 &self,
429 method: Method,
430 path: &str,
431 body: Option<&Value>,
432 attempt_limit: u32,
433 ) -> Result<Response> {
434 self.send_with_client_attempt_limit(&self.client, method, path, body, attempt_limit)
435 .await
436 }
437
438 async fn send_with_client_attempt_limit(
439 &self,
440 client: &Client,
441 method: Method,
442 path: &str,
443 body: Option<&Value>,
444 attempt_limit: u32,
445 ) -> Result<Response> {
446 let url = self.url(path);
447 let request_method = method.clone();
448 self.send_retrying(method, &url, attempt_limit, || {
449 let mut req = client
450 .request(request_method.clone(), &url)
451 .header("Authorization", &self.auth_header)
452 .header("elastic-api-version", API_VERSION);
453
454 if request_method != Method::GET {
456 req = req.header("kbn-xsrf", "true");
457 }
458 if let Some(b) = body {
459 req = req.json(b);
460 }
461
462 Ok(req)
463 })
464 .await
465 }
466
467 async fn send_json(&self, method: Method, path: &str, body: Option<&Value>) -> Result<Value> {
468 self.send_json_with_attempt_limit(method, path, body, MAX_ATTEMPTS)
469 .await
470 }
471
472 async fn send_json_with_attempt_limit(
473 &self,
474 method: Method,
475 path: &str,
476 body: Option<&Value>,
477 attempt_limit: u32,
478 ) -> Result<Value> {
479 let url = self.url(path);
480 let response = self
481 .send_with_attempt_limit(method.clone(), path, body, attempt_limit)
482 .await?;
483 let text = self.response_text(&method, &url, response).await?;
484 if text.trim().is_empty() {
485 return Ok(Value::Null);
486 }
487 parse_response_json(&text)
488 }
489
490 async fn send_json_once(
491 &self,
492 method: Method,
493 path: &str,
494 body: Option<&Value>,
495 ) -> Result<Value> {
496 let url = self.url(path);
497 let response = self
498 .send_with_client_attempt_limit(&self.one_shot_client, method.clone(), path, body, 1)
499 .await?;
500 let text = self.response_text(&method, &url, response).await?;
501 if text.trim().is_empty() {
502 return Ok(Value::Null);
503 }
504 parse_response_json(&text)
505 }
506
507 pub async fn get(&self, path: &str) -> Result<Value> {
508 self.send_json(Method::GET, path, None).await
509 }
510
511 pub async fn get_internal(&self, path: &str) -> Result<Value> {
517 let method = Method::GET;
518 let url = self.url(path);
519 let response = self
520 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
521 Ok(self
522 .client
523 .request(Method::GET, &url)
524 .header("Authorization", &self.auth_header)
525 .header("x-elastic-internal-origin", "Kibana"))
526 })
527 .await?;
528
529 let text = self.response_text(&method, &url, response).await?;
534 if text.trim().is_empty() {
535 return Ok(Value::Null);
536 }
537 parse_response_json(&text)
538 }
539
540 pub async fn post_internal(&self, path: &str, body: &Value) -> Result<Value> {
550 let method = Method::POST;
551 let url = self.url(path);
552 let response = self
553 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
554 Ok(self
555 .client
556 .request(Method::POST, &url)
557 .header("Authorization", &self.auth_header)
558 .header("kbn-xsrf", "true")
559 .header("x-elastic-internal-origin", "Kibana")
560 .json(body))
561 })
562 .await?;
563
564 let text = self.response_text(&method, &url, response).await?;
565 if text.trim().is_empty() {
566 return Ok(Value::Null);
567 }
568 parse_response_json(&text)
569 }
570
571 pub async fn get_with_headers(&self, path: &str) -> Result<Responded> {
576 let method = Method::GET;
577 let url = self.url(path);
578 let response = self.send(method.clone(), path, None).await?;
579
580 let mut headers = BTreeMap::new();
581 for name in CAPTURED_HEADERS {
582 if let Some(value) = response.headers().get(name)
583 && let Ok(text) = value.to_str()
584 {
585 headers.insert(name.to_string(), text.to_string());
586 }
587 }
588
589 let text = self.response_text(&method, &url, response).await?;
590 let body = if text.trim().is_empty() {
591 Value::Null
592 } else {
593 parse_response_json(&text)?
594 };
595
596 Ok(Responded { body, headers })
597 }
598
599 pub async fn post(&self, path: &str, body: Option<&Value>) -> Result<Value> {
600 self.send_json(Method::POST, path, body).await
601 }
602
603 pub async fn post_once(&self, path: &str, body: Option<&Value>) -> Result<Value> {
610 self.send_json_once(Method::POST, path, body).await
611 }
612
613 pub async fn put(&self, path: &str, body: &Value) -> Result<Value> {
614 self.send_json(Method::PUT, path, Some(body)).await
615 }
616
617 pub async fn put_once(&self, path: &str, body: &Value) -> Result<Value> {
624 self.send_json_once(Method::PUT, path, Some(body)).await
625 }
626
627 pub async fn patch(&self, path: &str, body: &Value) -> Result<Value> {
628 self.send_json(Method::PATCH, path, Some(body)).await
629 }
630
631 pub async fn delete(&self, path: &str) -> Result<Value> {
632 self.send_json(Method::DELETE, path, None).await
633 }
634
635 pub async fn delete_once(&self, path: &str) -> Result<Value> {
642 self.send_json_once(Method::DELETE, path, None).await
643 }
644
645 pub async fn get_absolute_es(&self, path: &str) -> Result<Value> {
649 self.send_absolute_es(Method::GET, path, None).await
650 }
651
652 pub async fn post_absolute_es(&self, path: &str, body: &Value) -> Result<Value> {
655 self.send_absolute_es(Method::POST, path, Some(body)).await
656 }
657
658 pub async fn delete_absolute_es(&self, path: &str) -> Result<Value> {
662 self.send_absolute_es(Method::DELETE, path, None).await
663 }
664
665 pub async fn delete_absolute_es_json(&self, path: &str, body: &Value) -> Result<Value> {
668 self.send_absolute_es(Method::DELETE, path, Some(body))
669 .await
670 }
671
672 async fn send_absolute_es(
673 &self,
674 method: Method,
675 path: &str,
676 body: Option<&Value>,
677 ) -> Result<Value> {
678 let url = format!("{}{}", self.es_base, path);
679 let request_method = method.clone();
680 let response = self
681 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
682 let mut req = self
683 .client
684 .request(request_method.clone(), &url)
685 .header("Authorization", &self.auth_header);
686 if let Some(b) = body {
687 req = req.json(b);
688 }
689 Ok(req)
690 })
691 .await?;
692
693 let text = self.response_text(&method, &url, response).await?;
694 if text.trim().is_empty() {
695 return Ok(Value::Null);
696 }
697 parse_response_json(&text)
698 }
699
700 pub async fn post_text(&self, path: &str, body: Option<&Value>) -> Result<String> {
702 let method = Method::POST;
703 let url = self.url(path);
704 let response = self.send(method.clone(), path, body).await?;
705 self.response_text(&method, &url, response).await
706 }
707
708 pub async fn post_multipart_ndjson(&self, path: &str, ndjson: &str) -> Result<Value> {
710 self.post_multipart_ndjson_named(path, "rules.ndjson", ndjson)
711 .await
712 }
713
714 pub async fn post_multipart_ndjson_named(
716 &self,
717 path: &str,
718 filename: &str,
719 ndjson: &str,
720 ) -> Result<Value> {
721 let method = Method::POST;
722 let url = self.url(path);
723 let response = self
724 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
725 let part = reqwest::multipart::Part::text(ndjson.to_string())
728 .file_name(filename.to_string())
729 .mime_str("application/octet-stream")
730 .map_err(|e| Error::new(ErrorKind::Error, format!("building upload: {e}")))?;
731 let form = reqwest::multipart::Form::new().part("file", part);
732
733 Ok(self
734 .client
735 .post(&url)
736 .header("Authorization", &self.auth_header)
737 .header("elastic-api-version", API_VERSION)
738 .header("kbn-xsrf", "true")
739 .multipart(form))
740 })
741 .await?;
742
743 let text = self.response_text(&method, &url, response).await?;
744 parse_response_json(&text)
745 }
746}