1use crate::server::influxdb::codec::{self, WriteBatch};
2use anyhow::{ensure, Context, Result};
3use bytes::Bytes;
4use http_body_util::{BodyExt, Full, Limited};
5use hyper::{
6 client::conn::http1,
7 header::{
8 AUTHORIZATION, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, HOST,
9 RETRY_AFTER, TRANSFER_ENCODING,
10 },
11 Request, Uri,
12};
13use hyper_util::rt::TokioIo;
14use serde::{Deserialize, Serialize};
15use std::time::Duration;
16pub const IO_TIMEOUT: Duration = Duration::from_secs(10);
17pub const MAX_RESPONSE_BYTES: usize = 64 * 1024;
18#[derive(Clone)]
19pub struct Origin {
20 pub authority: String,
21 pub connect_addr: String,
22}
23impl Origin {
24 pub fn parse(value: &str) -> Result<Self> {
25 ensure!(
26 !value.contains(['#', '@']),
27 "HTTP origin cannot contain credentials/fragment"
28 );
29 let value = if value.starts_with("http://") {
30 value.to_string()
31 } else {
32 format!("http://{value}")
33 };
34 let uri: Uri = value.parse().context("invalid HTTP origin")?;
35 ensure!(
36 uri.scheme_str() == Some("http")
37 && uri.query().is_none()
38 && matches!(uri.path(), "" | "/"),
39 "cleartext HTTP origin only, without path/query"
40 );
41 let authority = uri.authority().context("HTTP host required")?;
42 ensure!(authority.as_str().len() <= 1024, "HTTP origin byte limit");
43 let host = authority.host();
44 let connect_addr = format!("{}:{}", host, authority.port_u16().unwrap_or(8086));
45 Ok(Self {
46 authority: authority.as_str().to_string(),
47 connect_addr,
48 })
49 }
50}
51pub(crate) fn query(value: &str) -> String {
52 let mut out = String::new();
53 for b in value.bytes() {
54 if b.is_ascii_alphanumeric() || b"-._~".contains(&b) {
55 out.push(b as char);
56 } else {
57 use std::fmt::Write;
58 let _ = write!(out, "%{b:02X}");
59 }
60 }
61 out
62}
63#[derive(Clone, Debug, Serialize, Deserialize)]
64pub struct WriteError {
65 pub code: String,
66 pub message: String,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub line: Option<usize>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub accepted_points: Option<usize>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub rejected_points: Option<usize>,
73}
74#[derive(Clone, Debug, Serialize)]
75pub struct WriteResponse {
76 pub org: String,
77 pub bucket: String,
78 pub point_count: usize,
79 pub status: u16,
80 pub error: Option<WriteError>,
81 pub retry_after_seconds: Option<u16>,
82}
83pub async fn write(
84 origin: Origin,
85 token: Option<String>,
86 batch: WriteBatch,
87 body: Vec<u8>,
88) -> Result<WriteResponse> {
89 tokio::time::timeout(IO_TIMEOUT, async move {
90 let socket = tokio::net::TcpStream::connect(&origin.connect_addr).await?;
91 let (mut sender, connection) = http1::Builder::new()
92 .max_headers(64)
93 .max_buf_size(32 * 1024)
94 .handshake(TokioIo::new(socket))
95 .await?;
96 let mut request = Request::builder()
97 .method("POST")
98 .uri(format!(
99 "/api/v2/write?org={}&bucket={}&precision={}",
100 query(&batch.org),
101 query(&batch.bucket),
102 batch.precision.as_str()
103 ))
104 .header(HOST, &origin.authority)
105 .header(CONNECTION, "close")
106 .header(CONTENT_TYPE, "text/plain; charset=utf-8")
107 .header(
108 CONTENT_ENCODING,
109 if batch.gzip { "gzip" } else { "identity" },
110 );
111 if let Some(token) = token {
112 request = request.header(AUTHORIZATION, format!("Token {token}"));
113 }
114 let request = request.body(Full::new(Bytes::from(body)))?;
115 let exchange = async {
116 let response = sender.send_request(request).await?;
117 let (parts, body) = response.into_parts();
118 ensure!(
119 parts.headers.get_all(RETRY_AFTER).iter().count() <= 1,
120 "duplicate Retry-After"
121 );
122 let retry = parts
123 .headers
124 .get(RETRY_AFTER)
125 .map(|h| {
126 h.to_str()
127 .context("invalid Retry-After")?
128 .parse::<u16>()
129 .context("only numeric Retry-After supported")
130 })
131 .transpose()?;
132 if let Some(n) = retry {
133 ensure!(
134 (1..=3600).contains(&n) && matches!(parts.status.as_u16(), 429 | 503),
135 "invalid Retry-After range/status"
136 );
137 }
138 ensure!(
139 parts.headers.get_all(CONTENT_ENCODING).iter().count() <= 1,
140 "duplicate response encoding"
141 );
142 ensure!(
143 parts
144 .headers
145 .get(CONTENT_ENCODING)
146 .map(|v| v.as_bytes() == b"identity")
147 .unwrap_or(true),
148 "compressed HTTP responses unsupported"
149 );
150 let bytes = Limited::new(body, MAX_RESPONSE_BYTES)
151 .collect()
152 .await
153 .map_err(|_| anyhow::anyhow!("invalid/oversized response body"))?
154 .to_bytes();
155 let status = parts.status.as_u16();
156 let error = if status == 204 {
157 ensure!(
158 bytes.is_empty()
159 && retry.is_none()
160 && parts
161 .headers
162 .get_all(CONTENT_LENGTH)
163 .iter()
164 .all(|h| h.as_bytes() == b"0")
165 && !parts.headers.contains_key(TRANSFER_ENCODING),
166 "204 response must be empty"
167 );
168 None
169 } else {
170 ensure!(
171 [400, 401, 403, 404, 405, 408, 413, 415, 422, 429, 500, 503].contains(&status),
172 "unsupported InfluxDB write status"
173 );
174 let ct = parts
175 .headers
176 .get(CONTENT_TYPE)
177 .and_then(|v| v.to_str().ok())
178 .context("JSON error content type required")?;
179 ensure!(
180 ct.split(';')
181 .next()
182 .is_some_and(|v| v.trim().eq_ignore_ascii_case("application/json")),
183 "JSON error content type required"
184 );
185 let error: WriteError =
186 serde_json::from_slice(&bytes).context("invalid InfluxDB JSON error")?;
187 ensure!(
188 !error.code.is_empty()
189 && error.code.len() <= 128
190 && !crate::utils::sanitize::has_controls(&error.code)
191 && error.message.len() <= 1024
192 && !crate::utils::sanitize::has_controls(&error.message),
193 "error text limit"
194 );
195 if let Some(line) = error.line {
196 ensure!((1..=codec::MAX_LINES).contains(&line), "invalid error line");
197 }
198 for n in [error.accepted_points, error.rejected_points]
199 .into_iter()
200 .flatten()
201 {
202 ensure!(n <= batch.points.len(), "invalid partial-write count");
203 }
204 if let (Some(a), Some(r)) = (error.accepted_points, error.rejected_points) {
205 ensure!(
206 a + r == batch.points.len(),
207 "inconsistent partial-write counts"
208 );
209 }
210 Some(error)
211 };
212 Ok::<_, anyhow::Error>(WriteResponse {
213 org: batch.org,
214 bucket: batch.bucket,
215 point_count: batch.points.len(),
216 status,
217 error,
218 retry_after_seconds: retry,
219 })
220 };
221 tokio::pin!(connection);
222 tokio::pin!(exchange);
223 tokio::select! {
225 result = &mut exchange => result,
226 result = &mut connection => {
227 result.context("HTTP driver failed")?;
228 exchange.await
229 }
230 }
231 })
232 .await
233 .context("InfluxDB exchange deadline exceeded")?
234}