1use std::collections::HashMap;
32use std::sync::Arc;
33use std::time::{Duration, SystemTime, UNIX_EPOCH};
34
35use async_trait::async_trait;
36use base64::{engine::general_purpose::STANDARD, Engine};
37use bytes::Bytes;
38use serde_json::Value;
39
40use crate::batch::{BatchConfig, BatchFlusher, BatchSink, FlushError};
41use crate::context::Context;
42use crate::outbound::{OutboundClient, OutboundRequest};
43use crate::plugins::resources::PluginResources;
44use crate::plugins::util::log_entry::{build_entry, parse_log_format, LogFormat};
45use crate::plugins::{Plugin, PluginOutput, PluginResult};
46
47const API_VERSION: &str = "0.6.0";
48const SIGNATURE_METHOD: &str = "hmac-sha1";
49
50pub struct SlsLoggerPlugin {
52 sink: BatchSink,
53 log_format: Option<LogFormat>,
54 include_req_body: bool,
55 include_resp_body: bool,
56}
57
58struct SlsFlusher {
60 client: Arc<OutboundClient>,
61 endpoint_host: String,
63 port: u16,
64 logstore: String,
65 access_key_id: String,
66 access_key_secret: String,
67 timeout: Duration,
68}
69
70impl SlsLoggerPlugin {
71 pub fn from_config(
98 config: &HashMap<String, Value>,
99 resources: &Arc<PluginResources>,
100 ) -> Result<Self, String> {
101 let host = required_string(config, "host")?;
102 let project = required_string(config, "project")?;
103 let logstore = required_string(config, "logstore")?;
104 let access_key_id = required_string(config, "access_key_id")?;
105 let access_key_secret = required_string(config, "access_key_secret")?;
106 let port = config
107 .get("port")
108 .and_then(|v| v.as_u64())
109 .ok_or_else(|| "sls-logger requires 'port'".to_string())? as u16;
110
111 let timeout = Duration::from_millis(
112 config
113 .get("timeout")
114 .and_then(|v| v.as_u64())
115 .unwrap_or(5000),
116 );
117
118 let include_req_body = config
119 .get("include_req_body")
120 .and_then(|v| v.as_bool())
121 .unwrap_or(false);
122 let include_resp_body = config
123 .get("include_resp_body")
124 .and_then(|v| v.as_bool())
125 .unwrap_or(false);
126 let log_format = parse_log_format(config)?;
127
128 let batch_cfg = BatchConfig::from_config(config)?;
129 let flusher = Arc::new(SlsFlusher {
130 client: resources.outbound.clone(),
131 endpoint_host: format!("{project}.{host}"),
132 port,
133 logstore,
134 access_key_id,
135 access_key_secret,
136 timeout,
137 });
138 let sink = BatchSink::spawn("sls-logger", batch_cfg, flusher);
139
140 Ok(Self {
141 sink,
142 log_format,
143 include_req_body,
144 include_resp_body,
145 })
146 }
147}
148
149fn required_string(config: &HashMap<String, Value>, key: &str) -> Result<String, String> {
150 config
151 .get(key)
152 .and_then(|v| v.as_str())
153 .filter(|s| !s.is_empty())
154 .map(|s| s.to_string())
155 .ok_or_else(|| format!("sls-logger requires '{key}'"))
156}
157
158fn build_logs_payload(entries: &[Value], now_unix: u64) -> Value {
162 let logs: Vec<Value> = entries
163 .iter()
164 .map(|entry| {
165 let mut obj = serde_json::Map::new();
166 obj.insert("__time__".to_string(), Value::from(now_unix));
167 if let Some(map) = entry.as_object() {
168 for (k, v) in map {
169 obj.insert(k.clone(), Value::String(stringify(v)));
170 }
171 } else {
172 obj.insert("log".to_string(), Value::String(stringify(entry)));
173 }
174 Value::Object(obj)
175 })
176 .collect();
177 serde_json::json!({
178 "__topic__": "",
179 "__source__": "",
180 "__logs__": logs,
181 })
182}
183
184fn stringify(v: &Value) -> String {
186 match v {
187 Value::String(s) => s.clone(),
188 other => other.to_string(),
189 }
190}
191
192fn sign_request(
200 access_key_id: &str,
201 access_key_secret: &str,
202 logstore: &str,
203 body: &[u8],
204 date: &str,
205) -> (Vec<(String, String)>, String) {
206 let content_md5 = md5_hex_upper(body);
207 let content_type = "application/json";
208 let body_size = body.len().to_string();
209
210 let canonical_headers = format!(
212 "x-log-apiversion:{API_VERSION}\nx-log-bodyrawsize:{body_size}\nx-log-signaturemethod:{SIGNATURE_METHOD}"
213 );
214 let canonical_resource = format!("/logstores/{logstore}/shards/lb");
215
216 let sign_string = format!(
217 "POST\n{content_md5}\n{content_type}\n{date}\n{canonical_headers}\n{canonical_resource}"
218 );
219 let signature = STANDARD.encode(hmac_sha1(
220 access_key_secret.as_bytes(),
221 sign_string.as_bytes(),
222 ));
223
224 let headers = vec![
225 ("Content-Type".to_string(), content_type.to_string()),
226 ("Content-MD5".to_string(), content_md5),
227 ("Date".to_string(), date.to_string()),
228 ("x-log-apiversion".to_string(), API_VERSION.to_string()),
229 (
230 "x-log-signaturemethod".to_string(),
231 SIGNATURE_METHOD.to_string(),
232 ),
233 ("x-log-bodyrawsize".to_string(), body_size),
234 (
235 "Authorization".to_string(),
236 format!("LOG {access_key_id}:{signature}"),
237 ),
238 ];
239 (headers, signature)
240}
241
242fn hmac_sha1(key: &[u8], msg: &[u8]) -> Vec<u8> {
244 let k = ring::hmac::Key::new(ring::hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key);
245 ring::hmac::sign(&k, msg).as_ref().to_vec()
246}
247
248fn md5_hex_upper(data: &[u8]) -> String {
250 let digest = md5(data);
251 let mut s = String::with_capacity(32);
252 for b in digest {
253 s.push_str(&format!("{b:02X}"));
254 }
255 s
256}
257
258fn md5(input: &[u8]) -> [u8; 16] {
262 const S: [u32; 64] = [
263 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
264 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
265 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
266 ];
267 const K: [u32; 64] = [
268 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613,
269 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193,
270 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d,
271 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
272 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122,
273 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa,
274 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244,
275 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
276 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb,
277 0xeb86d391,
278 ];
279
280 let mut a0: u32 = 0x67452301;
281 let mut b0: u32 = 0xefcdab89;
282 let mut c0: u32 = 0x98badcfe;
283 let mut d0: u32 = 0x10325476;
284
285 let mut msg = input.to_vec();
286 let bit_len = (input.len() as u64).wrapping_mul(8);
287 msg.push(0x80);
288 while msg.len() % 64 != 56 {
289 msg.push(0);
290 }
291 msg.extend_from_slice(&bit_len.to_le_bytes());
292
293 let (chunks, _remainder) = msg.as_chunks::<64>();
296 for chunk in chunks {
297 let mut m = [0u32; 16];
298 for (i, word) in m.iter_mut().enumerate() {
299 *word = u32::from_le_bytes([
300 chunk[i * 4],
301 chunk[i * 4 + 1],
302 chunk[i * 4 + 2],
303 chunk[i * 4 + 3],
304 ]);
305 }
306
307 let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
308 for i in 0..64 {
309 let (f, g) = match i {
310 0..=15 => ((b & c) | (!b & d), i),
311 16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
312 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
313 _ => (c ^ (b | !d), (7 * i) % 16),
314 };
315 let f = f.wrapping_add(a).wrapping_add(K[i]).wrapping_add(m[g]);
316 a = d;
317 d = c;
318 c = b;
319 b = b.wrapping_add(f.rotate_left(S[i]));
320 }
321 a0 = a0.wrapping_add(a);
322 b0 = b0.wrapping_add(b);
323 c0 = c0.wrapping_add(c);
324 d0 = d0.wrapping_add(d);
325 }
326
327 let mut out = [0u8; 16];
328 out[0..4].copy_from_slice(&a0.to_le_bytes());
329 out[4..8].copy_from_slice(&b0.to_le_bytes());
330 out[8..12].copy_from_slice(&c0.to_le_bytes());
331 out[12..16].copy_from_slice(&d0.to_le_bytes());
332 out
333}
334
335fn format_http_date(unix_secs: u64) -> String {
338 const DAYS: [&str; 7] = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"];
339 const MONTHS: [&str; 12] = [
340 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
341 ];
342 let days = unix_secs / 86400;
343 let secs_of_day = unix_secs % 86400;
344 let (hour, minute, second) = (
345 secs_of_day / 3600,
346 (secs_of_day % 3600) / 60,
347 secs_of_day % 60,
348 );
349 let weekday = DAYS[(days % 7) as usize];
351
352 let mut year = 1970u64;
354 let mut days_left = days;
355 loop {
356 let leap =
357 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
358 let year_days = if leap { 366 } else { 365 };
359 if days_left < year_days {
360 break;
361 }
362 days_left -= year_days;
363 year += 1;
364 }
365 let leap = (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400);
366 let month_lengths = [
367 31,
368 if leap { 29 } else { 28 },
369 31,
370 30,
371 31,
372 30,
373 31,
374 31,
375 30,
376 31,
377 30,
378 31,
379 ];
380 let mut month = 0usize;
381 while days_left >= month_lengths[month] {
382 days_left -= month_lengths[month];
383 month += 1;
384 }
385 let day = days_left + 1;
386
387 format!(
388 "{weekday}, {day:02} {mon} {year:04} {hour:02}:{minute:02}:{second:02} GMT",
389 mon = MONTHS[month],
390 )
391}
392
393#[async_trait]
394impl BatchFlusher for SlsFlusher {
395 async fn flush(&self, entries: &[Value]) -> Result<(), FlushError> {
396 let now = SystemTime::now()
397 .duration_since(UNIX_EPOCH)
398 .map(|d| d.as_secs())
399 .unwrap_or(0);
400 let payload = build_logs_payload(entries, now);
401 let body = serde_json::to_vec(&payload).map_err(|e| FlushError {
402 message: format!("sls payload encode failed: {e}"),
403 first_fail: None,
404 })?;
405
406 let date = format_http_date(now);
407 let (mut headers, _sig) = sign_request(
408 &self.access_key_id,
409 &self.access_key_secret,
410 &self.logstore,
411 &body,
412 &date,
413 );
414 headers.push(("Host".to_string(), self.endpoint_host.clone()));
415
416 let url = format!(
417 "https://{}:{}/logstores/{}/shards/lb",
418 self.endpoint_host, self.port, self.logstore
419 );
420 let req = OutboundRequest {
421 method: http::Method::POST,
422 url,
423 headers,
424 body: Bytes::from(body),
425 timeout: self.timeout,
426 ssl_verify: true,
427 tls: None,
428 };
429
430 match self.client.request(req).await {
431 Ok(resp) if resp.status == 200 => Ok(()),
432 Ok(resp) => Err(FlushError {
433 message: format!(
434 "sls returned status {}: {}",
435 resp.status,
436 String::from_utf8_lossy(&resp.body)
437 ),
438 first_fail: None,
439 }),
440 Err(e) => Err(FlushError {
441 message: format!("sls callout failed: {e}"),
442 first_fail: None,
443 }),
444 }
445 }
446}
447
448#[async_trait]
449impl Plugin for SlsLoggerPlugin {
450 fn plugin_type(&self) -> &str {
451 "sls-logger"
452 }
453
454 fn reads_response_body(&self) -> bool {
455 crate::plugins::util::log_entry::reads_response_body(
456 self.log_format.as_ref(),
457 self.include_resp_body,
458 )
459 }
460
461 async fn execute(&self, ctx: Context) -> PluginResult {
462 let entry = build_entry(
463 &ctx,
464 self.log_format.as_ref(),
465 self.include_req_body,
466 self.include_resp_body,
467 );
468 self.sink.push(entry);
469 Ok(PluginOutput::success(ctx))
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use serde_json::json;
477
478 fn cfg(v: Value) -> HashMap<String, Value> {
479 serde_json::from_value(v).unwrap()
480 }
481
482 fn full_cfg() -> Value {
483 json!({
484 "host": "cn-hangzhou.log.aliyuncs.com",
485 "port": 443,
486 "project": "proj",
487 "logstore": "store",
488 "access_key_id": "id",
489 "access_key_secret": "secret"
490 })
491 }
492
493 #[tokio::test]
494 async fn requires_all_fields() {
495 assert!(SlsLoggerPlugin::from_config(
496 &cfg(json!({ "host": "h", "port": 443, "project": "p", "logstore": "l", "access_key_id": "i" })),
497 &PluginResources::empty()
498 )
499 .is_err());
500 assert!(SlsLoggerPlugin::from_config(&cfg(full_cfg()), &PluginResources::empty()).is_ok());
501 }
502
503 #[test]
504 fn md5_known_vectors() {
505 assert_eq!(md5_hex_upper(b""), "D41D8CD98F00B204E9800998ECF8427E");
506 assert_eq!(md5_hex_upper(b"abc"), "900150983CD24FB0D6963F7D28E17F72");
507 assert_eq!(
508 md5_hex_upper(b"The quick brown fox jumps over the lazy dog"),
509 "9E107D9D372BB6826BD81D3542A419D6"
510 );
511 }
512
513 #[test]
514 fn hmac_sha1_rfc2202_vector() {
515 let key = [0x0bu8; 20];
517 let tag = hmac_sha1(&key, b"Hi There");
518 let hex: String = tag.iter().map(|b| format!("{b:02x}")).collect();
519 assert_eq!(hex, "b617318655057264e28bc0b6fb378c8ef146be00");
520 }
521
522 #[test]
523 fn http_date_formatting() {
524 assert_eq!(format_http_date(0), "Thu, 01 Jan 1970 00:00:00 GMT");
526 assert_eq!(
528 format_http_date(1234567890),
529 "Fri, 13 Feb 2009 23:31:30 GMT"
530 );
531 }
532
533 #[test]
534 fn sign_request_is_stable() {
535 let body = br#"{"__topic__":"","__source__":"","__logs__":[]}"#;
536 let (headers, sig) = sign_request(
537 "AKID",
538 "AKSECRET",
539 "store",
540 body,
541 "Mon, 03 Jan 2022 04:05:06 GMT",
542 );
543 let (_, sig2) = sign_request(
545 "AKID",
546 "AKSECRET",
547 "store",
548 body,
549 "Mon, 03 Jan 2022 04:05:06 GMT",
550 );
551 assert_eq!(sig, sig2);
552 let auth = headers
554 .iter()
555 .find(|(k, _)| k == "Authorization")
556 .map(|(_, v)| v.clone())
557 .unwrap();
558 assert_eq!(auth, format!("LOG AKID:{sig}"));
559 assert_eq!(sig, "ftz3mY2D1oijCA9FoZlVAx47RzQ=");
561 }
562
563 #[test]
564 fn logs_payload_stringifies_values() {
565 let entries = vec![json!({ "status": 200, "path": "/x" })];
566 let payload = build_logs_payload(&entries, 1000);
567 let log = &payload["__logs__"][0];
568 assert_eq!(log["__time__"], json!(1000));
569 assert_eq!(log["status"], json!("200"));
570 assert_eq!(log["path"], json!("/x"));
571 assert_eq!(payload["__topic__"], json!(""));
572 }
573}