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 base: String,
167 kibana_url: String,
171 es_base: String,
174 has_es_url: bool,
179 space: String,
180 auth_header: String,
181 debug: bool,
182 capabilities: OnceCell<Capabilities>,
183}
184
185impl Transport {
186 pub fn new(profile: &Profile) -> Result<Transport> {
187 Self::with_debug(profile, false)
188 }
189
190 pub fn with_debug(profile: &Profile, debug: bool) -> Result<Transport> {
194 let mut profile = profile.clone();
197 profile.strip_userinfo();
198 let credential = Credential::from_profile(&profile)?;
199 let client = Client::builder()
200 .timeout(Duration::from_secs(profile.timeout_secs))
201 .danger_accept_invalid_certs(!profile.verify)
202 .build()
203 .map_err(|e| Error::new(ErrorKind::Connection, format!("building HTTP client: {e}")))?;
204
205 let base = profile.kibana_url.trim_end_matches('/').to_string();
206 let kibana_url = profile.kibana_url.clone();
207 let has_es_url = profile.es_url.is_some();
208 let es_base = profile
209 .es_url
210 .as_deref()
211 .unwrap_or(&profile.kibana_url)
212 .trim_end_matches('/')
213 .to_string();
214
215 Ok(Transport {
216 client,
217 base,
218 kibana_url,
219 es_base,
220 has_es_url,
221 space: profile.space.clone(),
222 auth_header: credential.header_value(),
223 debug,
224 capabilities: OnceCell::new(),
225 })
226 }
227
228 fn debug_log(&self, method: &Method, url: &str, status: u16, attempt: u32) {
234 if !self.debug {
235 return;
236 }
237 if attempt > 1 {
238 eprintln!(
239 "[debug] {} {url} -> {status} (attempt {attempt})",
240 method.as_str()
241 );
242 } else {
243 eprintln!("[debug] {} {url} -> {status}", method.as_str());
244 }
245 }
246
247 fn debug_request(&self, method: &Method, url: &str, attempt: u32) {
249 if !self.debug {
250 return;
251 }
252 if attempt > 1 {
253 eprintln!("[debug] -> {} {url} (attempt {attempt})", method.as_str());
254 } else {
255 eprintln!("[debug] -> {} {url}", method.as_str());
256 }
257 }
258
259 fn debug_failure(&self, method: &Method, url: &str, what: &str) {
261 if !self.debug {
262 return;
263 }
264 let _ = writeln!(
265 std::io::stderr(),
266 "[debug] {} {url} -> {what}",
267 method.as_str()
268 );
269 }
270
271 pub fn space_path(space: &str, path: &str) -> String {
275 if space.is_empty() || space == "default" {
276 path.to_string()
277 } else {
278 format!("/s/{space}{path}")
279 }
280 }
281
282 pub fn kibana_url(&self) -> &str {
284 &self.kibana_url
285 }
286
287 pub fn has_es_url(&self) -> bool {
292 self.has_es_url
293 }
294
295 pub async fn capabilities(&self) -> Result<&Capabilities> {
297 self.capabilities
298 .get_or_try_init(|| Capabilities::probe(self, self.kibana_url()))
299 .await
300 }
301
302 pub async fn require_feature(&self, feature: Feature) -> Result<()> {
304 self.capabilities().await?.require_feature(feature)
305 }
306
307 fn url(&self, path: &str) -> String {
308 format!("{}{}", self.base, Self::space_path(&self.space, path))
309 }
310
311 async fn response_text(
314 &self,
315 method: &Method,
316 url: &str,
317 response: Response,
318 ) -> Result<String> {
319 match response.text().await {
320 Ok(text) => Ok(text),
321 Err(e) if e.is_timeout() => {
322 self.debug_failure(method, url, "timeout");
323 Err(Error::new(
324 ErrorKind::Timeout,
325 format!("request timed out while reading response body: {e}"),
326 ))
327 }
328 Err(e) => {
329 self.debug_failure(method, url, "connection error");
330 Err(Error::new(
331 ErrorKind::Connection,
332 format!("request failed while reading response body: {e}"),
333 ))
334 }
335 }
336 }
337
338 async fn send_retrying<F>(
339 &self,
340 method: Method,
341 url: &str,
342 attempt_limit: u32,
343 mut build: F,
344 ) -> Result<Response>
345 where
346 F: FnMut() -> Result<reqwest::RequestBuilder>,
347 {
348 let mut attempt = 0;
349
350 loop {
351 attempt += 1;
352 let req = build()?;
353
354 self.debug_request(&method, url, attempt);
355 let result = req.send().await;
356
357 let response = match result {
358 Ok(r) => r,
359 Err(e) if e.is_timeout() => {
360 self.debug_failure(&method, url, "timeout");
361 return Err(Error::new(
362 ErrorKind::Timeout,
363 format!("request timed out: {e}"),
364 ));
365 }
366 Err(e) => {
367 self.debug_failure(&method, url, "connection error");
368 return Err(Error::new(
369 ErrorKind::Connection,
370 format!("request failed: {e}"),
371 ));
372 }
373 };
374
375 let status = response.status();
376 self.debug_log(&method, url, status.as_u16(), attempt);
377 if status.is_success() {
378 return Ok(response);
379 }
380
381 let transient = status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error();
384 if transient && attempt < attempt_limit {
385 let backoff = Duration::from_millis(200 * 2u64.pow(attempt - 1));
386 tokio::time::sleep(backoff).await;
387 continue;
388 }
389
390 let code = status.as_u16();
391 let text = self.response_text(&method, url, response).await?;
392 return Err(Error::from_response_body(code, &text));
393 }
394 }
395
396 async fn send(&self, method: Method, path: &str, body: Option<&Value>) -> Result<Response> {
397 self.send_with_attempt_limit(method, path, body, MAX_ATTEMPTS)
398 .await
399 }
400
401 async fn send_with_attempt_limit(
402 &self,
403 method: Method,
404 path: &str,
405 body: Option<&Value>,
406 attempt_limit: u32,
407 ) -> Result<Response> {
408 let url = self.url(path);
409 let request_method = method.clone();
410 self.send_retrying(method, &url, attempt_limit, || {
411 let mut req = self
412 .client
413 .request(request_method.clone(), &url)
414 .header("Authorization", &self.auth_header)
415 .header("elastic-api-version", API_VERSION);
416
417 if request_method != Method::GET {
419 req = req.header("kbn-xsrf", "true");
420 }
421 if let Some(b) = body {
422 req = req.json(b);
423 }
424
425 Ok(req)
426 })
427 .await
428 }
429
430 async fn send_json(&self, method: Method, path: &str, body: Option<&Value>) -> Result<Value> {
431 self.send_json_with_attempt_limit(method, path, body, MAX_ATTEMPTS)
432 .await
433 }
434
435 async fn send_json_with_attempt_limit(
436 &self,
437 method: Method,
438 path: &str,
439 body: Option<&Value>,
440 attempt_limit: u32,
441 ) -> Result<Value> {
442 let url = self.url(path);
443 let response = self
444 .send_with_attempt_limit(method.clone(), path, body, attempt_limit)
445 .await?;
446 let text = self.response_text(&method, &url, response).await?;
447 if text.trim().is_empty() {
448 return Ok(Value::Null);
449 }
450 parse_response_json(&text)
451 }
452
453 pub async fn get(&self, path: &str) -> Result<Value> {
454 self.send_json(Method::GET, path, None).await
455 }
456
457 pub async fn get_internal(&self, path: &str) -> Result<Value> {
463 let method = Method::GET;
464 let url = self.url(path);
465 let response = self
466 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
467 Ok(self
468 .client
469 .request(Method::GET, &url)
470 .header("Authorization", &self.auth_header)
471 .header("x-elastic-internal-origin", "Kibana"))
472 })
473 .await?;
474
475 let text = self.response_text(&method, &url, response).await?;
480 if text.trim().is_empty() {
481 return Ok(Value::Null);
482 }
483 parse_response_json(&text)
484 }
485
486 pub async fn post_internal(&self, path: &str, body: &Value) -> Result<Value> {
496 let method = Method::POST;
497 let url = self.url(path);
498 let response = self
499 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
500 Ok(self
501 .client
502 .request(Method::POST, &url)
503 .header("Authorization", &self.auth_header)
504 .header("kbn-xsrf", "true")
505 .header("x-elastic-internal-origin", "Kibana")
506 .json(body))
507 })
508 .await?;
509
510 let text = self.response_text(&method, &url, response).await?;
511 if text.trim().is_empty() {
512 return Ok(Value::Null);
513 }
514 parse_response_json(&text)
515 }
516
517 pub async fn get_with_headers(&self, path: &str) -> Result<Responded> {
522 let method = Method::GET;
523 let url = self.url(path);
524 let response = self.send(method.clone(), path, None).await?;
525
526 let mut headers = BTreeMap::new();
527 for name in CAPTURED_HEADERS {
528 if let Some(value) = response.headers().get(name)
529 && let Ok(text) = value.to_str()
530 {
531 headers.insert(name.to_string(), text.to_string());
532 }
533 }
534
535 let text = self.response_text(&method, &url, response).await?;
536 let body = if text.trim().is_empty() {
537 Value::Null
538 } else {
539 parse_response_json(&text)?
540 };
541
542 Ok(Responded { body, headers })
543 }
544
545 pub async fn post(&self, path: &str, body: Option<&Value>) -> Result<Value> {
546 self.send_json(Method::POST, path, body).await
547 }
548
549 pub async fn put(&self, path: &str, body: &Value) -> Result<Value> {
550 self.send_json(Method::PUT, path, Some(body)).await
551 }
552
553 pub async fn put_once(&self, path: &str, body: &Value) -> Result<Value> {
559 self.send_json_with_attempt_limit(Method::PUT, path, Some(body), 1)
560 .await
561 }
562
563 pub async fn patch(&self, path: &str, body: &Value) -> Result<Value> {
564 self.send_json(Method::PATCH, path, Some(body)).await
565 }
566
567 pub async fn delete(&self, path: &str) -> Result<Value> {
568 self.send_json(Method::DELETE, path, None).await
569 }
570
571 pub async fn get_absolute_es(&self, path: &str) -> Result<Value> {
575 self.send_absolute_es(Method::GET, path, None).await
576 }
577
578 pub async fn post_absolute_es(&self, path: &str, body: &Value) -> Result<Value> {
581 self.send_absolute_es(Method::POST, path, Some(body)).await
582 }
583
584 pub async fn delete_absolute_es(&self, path: &str) -> Result<Value> {
588 self.send_absolute_es(Method::DELETE, path, None).await
589 }
590
591 pub async fn delete_absolute_es_json(&self, path: &str, body: &Value) -> Result<Value> {
594 self.send_absolute_es(Method::DELETE, path, Some(body))
595 .await
596 }
597
598 async fn send_absolute_es(
599 &self,
600 method: Method,
601 path: &str,
602 body: Option<&Value>,
603 ) -> Result<Value> {
604 let url = format!("{}{}", self.es_base, path);
605 let request_method = method.clone();
606 let response = self
607 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
608 let mut req = self
609 .client
610 .request(request_method.clone(), &url)
611 .header("Authorization", &self.auth_header);
612 if let Some(b) = body {
613 req = req.json(b);
614 }
615 Ok(req)
616 })
617 .await?;
618
619 let text = self.response_text(&method, &url, response).await?;
620 if text.trim().is_empty() {
621 return Ok(Value::Null);
622 }
623 parse_response_json(&text)
624 }
625
626 pub async fn post_text(&self, path: &str, body: Option<&Value>) -> Result<String> {
628 let method = Method::POST;
629 let url = self.url(path);
630 let response = self.send(method.clone(), path, body).await?;
631 self.response_text(&method, &url, response).await
632 }
633
634 pub async fn post_multipart_ndjson(&self, path: &str, ndjson: &str) -> Result<Value> {
636 self.post_multipart_ndjson_named(path, "rules.ndjson", ndjson)
637 .await
638 }
639
640 pub async fn post_multipart_ndjson_named(
642 &self,
643 path: &str,
644 filename: &str,
645 ndjson: &str,
646 ) -> Result<Value> {
647 let method = Method::POST;
648 let url = self.url(path);
649 let response = self
650 .send_retrying(method.clone(), &url, MAX_ATTEMPTS, || {
651 let part = reqwest::multipart::Part::text(ndjson.to_string())
654 .file_name(filename.to_string())
655 .mime_str("application/octet-stream")
656 .map_err(|e| Error::new(ErrorKind::Error, format!("building upload: {e}")))?;
657 let form = reqwest::multipart::Form::new().part("file", part);
658
659 Ok(self
660 .client
661 .post(&url)
662 .header("Authorization", &self.auth_header)
663 .header("elastic-api-version", API_VERSION)
664 .header("kbn-xsrf", "true")
665 .multipart(form))
666 })
667 .await?;
668
669 let text = self.response_text(&method, &url, response).await?;
670 parse_response_json(&text)
671 }
672}