1use crate::error::{Error, Result};
34use crate::idempotency::{Begin, IdempotencyStore, StoredResponse};
35use crate::rate_limit::{Decision, RateLimitStore};
36use crate::session::{SessionRecord, SessionStore};
37use async_trait::async_trait;
38use bytes::Bytes;
39use std::collections::VecDeque;
40use std::sync::Mutex;
41use std::time::{Duration, SystemTime, UNIX_EPOCH};
42use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
43use tokio::net::TcpStream;
44
45const MAX_BULK: usize = 64 * 1024 * 1024;
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum Value {
50 Nil,
51 Int(i64),
52 Status(String),
53 Bulk(Vec<u8>),
54 Array(Vec<Value>),
55 Error(String),
57}
58
59impl Value {
60 pub fn into_bytes(self) -> Option<Vec<u8>> {
62 match self {
63 Value::Bulk(b) => Some(b),
64 _ => None,
65 }
66 }
67
68 pub fn as_int(&self) -> Option<i64> {
69 match self {
70 Value::Int(n) => Some(*n),
71 _ => None,
72 }
73 }
74
75 pub fn is_ok(&self) -> bool {
76 matches!(self, Value::Status(s) if s == "OK")
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct Cmd {
83 args: Vec<Vec<u8>>,
84}
85
86impl Cmd {
87 pub fn new(name: &str) -> Self {
88 Self {
89 args: vec![name.as_bytes().to_vec()],
90 }
91 }
92
93 pub fn arg(mut self, arg: impl AsRef<[u8]>) -> Self {
94 self.args.push(arg.as_ref().to_vec());
95 self
96 }
97
98 pub fn int(self, n: i64) -> Self {
99 self.arg(n.to_string())
100 }
101
102 pub fn encode(&self) -> Vec<u8> {
104 let mut out = format!("*{}\r\n", self.args.len()).into_bytes();
105 for a in &self.args {
106 out.extend_from_slice(format!("${}\r\n", a.len()).as_bytes());
107 out.extend_from_slice(a);
108 out.extend_from_slice(b"\r\n");
109 }
110 out
111 }
112}
113
114struct Conn {
115 io: BufReader<TcpStream>,
116}
117
118impl Conn {
119 async fn read_line(&mut self) -> Result<Vec<u8>> {
120 let mut line = Vec::new();
121 let n = self.io.read_until(b'\n', &mut line).await.map_err(io_err)?;
122 if n == 0 || !line.ends_with(b"\r\n") {
123 return Err(Error::internal("Redis closed the connection"));
124 }
125 line.truncate(line.len() - 2);
126 Ok(line)
127 }
128
129 fn read_value<'a>(
130 &'a mut self,
131 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
132 Box::pin(async move {
133 let line = self.read_line().await?;
134 let (kind, rest) = line
135 .split_first()
136 .ok_or_else(|| Error::internal("Empty Redis reply"))?;
137 let text = || String::from_utf8_lossy(rest).into_owned();
138 let number = || -> Result<i64> {
139 text()
140 .parse()
141 .map_err(|_| Error::internal("Malformed Redis reply"))
142 };
143 match kind {
144 b'+' => Ok(Value::Status(text())),
145 b'-' => Ok(Value::Error(text())),
146 b':' => Ok(Value::Int(number()?)),
147 b'$' => {
148 let len = number()?;
149 if len < 0 {
150 return Ok(Value::Nil);
151 }
152 let len = len as usize;
153 if len > MAX_BULK {
154 return Err(Error::internal("Redis reply too large"));
155 }
156 let mut buf = vec![0u8; len + 2];
157 self.io.read_exact(&mut buf).await.map_err(io_err)?;
158 buf.truncate(len);
159 Ok(Value::Bulk(buf))
160 }
161 b'*' => {
162 let len = number()?;
163 if len < 0 {
164 return Ok(Value::Nil);
165 }
166 let mut items = Vec::with_capacity((len as usize).min(1024));
167 for _ in 0..len {
168 items.push(self.read_value().await?);
169 }
170 Ok(Value::Array(items))
171 }
172 _ => Err(Error::internal("Unknown Redis reply type")),
173 }
174 })
175 }
176}
177
178fn io_err(e: std::io::Error) -> Error {
179 Error::internal(format!("Redis I/O: {e}"))
180}
181
182pub struct Redis {
184 addr: String,
185 username: Option<String>,
186 password: Option<String>,
187 db: u32,
188 timeout: Duration,
189 max_idle: usize,
190 idle: Mutex<VecDeque<Conn>>,
191}
192
193impl std::fmt::Debug for Redis {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("Redis")
196 .field("addr", &self.addr)
197 .field("db", &self.db)
198 .finish_non_exhaustive()
199 }
200}
201
202impl Redis {
203 pub fn from_url(url: &str) -> Result<Self> {
205 let bad =
206 || Error::internal("Redis URL must look like redis://[user:password@]host[:port][/db]");
207 let rest = url.strip_prefix("redis://").ok_or_else(bad)?;
208 let (authority, db) = match rest.split_once('/') {
209 Some((a, d)) if !d.is_empty() => (a, d.parse::<u32>().map_err(|_| bad())?),
210 Some((a, _)) => (a, 0),
211 None => (rest, 0),
212 };
213 let (credentials, host) = match authority.rsplit_once('@') {
214 Some((c, h)) => (Some(c), h),
215 None => (None, authority),
216 };
217 if host.is_empty() {
218 return Err(bad());
219 }
220 let (username, password) = match credentials {
221 None => (None, None),
222 Some(c) => match c.split_once(':') {
223 Some((u, p)) => ((!u.is_empty()).then(|| decode(u)), Some(decode(p))),
224 None => (None, Some(decode(c))),
225 },
226 };
227 let addr = if host.contains(':') && !host.starts_with('[') || host.contains("]:") {
228 host.to_string()
229 } else {
230 format!("{host}:6379")
231 };
232 Ok(Self {
233 addr,
234 username,
235 password,
236 db,
237 timeout: Duration::from_secs(5),
238 max_idle: 16,
239 idle: Mutex::new(VecDeque::new()),
240 })
241 }
242
243 pub fn timeout(mut self, timeout: Duration) -> Self {
245 self.timeout = timeout;
246 self
247 }
248
249 pub fn max_idle(mut self, max: usize) -> Self {
251 self.max_idle = max;
252 self
253 }
254
255 async fn connect(&self) -> Result<Conn> {
256 let stream = TcpStream::connect(&self.addr).await.map_err(io_err)?;
257 let _ = stream.set_nodelay(true);
258 let mut conn = Conn {
259 io: BufReader::new(stream),
260 };
261 if let Some(password) = &self.password {
262 let mut auth = Cmd::new("AUTH");
263 if let Some(user) = &self.username {
264 auth = auth.arg(user);
265 }
266 expect_ok(exchange(&mut conn, &[auth.arg(password)]).await?)?;
267 }
268 if self.db != 0 {
269 expect_ok(exchange(&mut conn, &[Cmd::new("SELECT").int(i64::from(self.db))]).await?)?;
270 }
271 Ok(conn)
272 }
273
274 fn take_idle(&self) -> Option<Conn> {
275 self.idle
276 .lock()
277 .unwrap_or_else(|e| e.into_inner())
278 .pop_front()
279 }
280
281 fn put_idle(&self, conn: Conn) {
282 let mut idle = self.idle.lock().unwrap_or_else(|e| e.into_inner());
283 if idle.len() < self.max_idle {
284 idle.push_back(conn);
285 }
286 }
287
288 pub async fn pipeline(&self, cmds: &[Cmd]) -> Result<Vec<Value>> {
291 let attempt = |mut conn: Conn| async move {
292 let replies = exchange(&mut conn, cmds).await?;
293 Ok::<_, Error>((conn, replies))
294 };
295 tokio::time::timeout(self.timeout, async {
296 if let Some(conn) = self.take_idle() {
299 if let Ok((conn, replies)) = attempt(conn).await {
300 self.put_idle(conn);
301 return Ok(replies);
302 }
303 }
304 let (conn, replies) = attempt(self.connect().await?).await?;
305 self.put_idle(conn);
306 Ok(replies)
307 })
308 .await
309 .map_err(|_| Error::internal("Redis timed out"))?
310 }
311
312 pub async fn run(&self, cmd: Cmd) -> Result<Value> {
314 match self.pipeline(std::slice::from_ref(&cmd)).await?.pop() {
315 Some(Value::Error(e)) => Err(Error::internal(format!("Redis: {e}"))),
316 Some(v) => Ok(v),
317 None => Err(Error::internal("Redis sent no reply")),
318 }
319 }
320}
321
322async fn exchange(conn: &mut Conn, cmds: &[Cmd]) -> Result<Vec<Value>> {
323 let mut out = Vec::new();
324 for c in cmds {
325 out.extend_from_slice(&c.encode());
326 }
327 conn.io.write_all(&out).await.map_err(io_err)?;
328 let mut replies = Vec::with_capacity(cmds.len());
329 for _ in cmds {
330 replies.push(conn.read_value().await?);
331 }
332 Ok(replies)
333}
334
335fn expect_ok(mut replies: Vec<Value>) -> Result<()> {
336 match replies.pop() {
337 Some(v) if v.is_ok() => Ok(()),
338 Some(Value::Error(e)) => Err(Error::internal(format!("Redis: {e}"))),
339 _ => Err(Error::internal("Unexpected Redis reply")),
340 }
341}
342
343fn decode(s: &str) -> String {
344 urlencoding::decode(s)
345 .map(|c| c.into_owned())
346 .unwrap_or_else(|_| s.to_string())
347}
348
349fn millis(d: Duration) -> i64 {
350 i64::try_from(d.as_millis()).unwrap_or(i64::MAX).max(1)
351}
352
353pub struct RedisSessionStore {
357 redis: Redis,
358 prefix: String,
359 ttl: Duration,
360}
361
362impl RedisSessionStore {
363 pub fn new(redis: Redis) -> Self {
365 Self {
366 redis,
367 prefix: "ullun:session:".into(),
368 ttl: Duration::from_secs(24 * 3600),
369 }
370 }
371
372 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
374 self.prefix = prefix.into();
375 self
376 }
377
378 pub fn ttl(mut self, ttl: Duration) -> Self {
381 self.ttl = ttl;
382 self
383 }
384
385 fn key(&self, id: &str) -> String {
386 format!("{}{}", self.prefix, id)
387 }
388}
389
390#[async_trait]
391impl SessionStore for RedisSessionStore {
392 async fn load(&self, id: &str) -> Option<SessionRecord> {
393 let raw = self
394 .redis
395 .run(Cmd::new("GET").arg(self.key(id)))
396 .await
397 .ok()?
398 .into_bytes()?;
399 let doc: serde_json::Value = serde_json::from_slice(&raw).ok()?;
400 Some(SessionRecord {
401 data: doc.get("d")?.as_object()?.clone(),
402 created_at: doc.get("c")?.as_u64()?,
403 last_seen: doc.get("l")?.as_u64()?,
404 })
405 }
406
407 async fn save(&self, id: &str, record: &SessionRecord) {
408 let doc = serde_json::json!({
409 "d": record.data,
410 "c": record.created_at,
411 "l": record.last_seen,
412 });
413 let cmd = Cmd::new("SET")
414 .arg(self.key(id))
415 .arg(doc.to_string())
416 .arg("PX")
417 .int(millis(self.ttl));
418 if let Err(e) = self.redis.run(cmd).await {
419 eprintln!("Saving session to Redis failed: {}", e.message());
420 }
421 }
422
423 async fn delete(&self, id: &str) {
424 let _ = self.redis.run(Cmd::new("DEL").arg(self.key(id))).await;
425 }
426}
427
428pub struct RedisRateLimitStore {
433 redis: Redis,
434 prefix: String,
435}
436
437impl RedisRateLimitStore {
438 pub fn new(redis: Redis) -> Self {
439 Self {
440 redis,
441 prefix: "ullun:rl:".into(),
442 }
443 }
444
445 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
446 self.prefix = prefix.into();
447 self
448 }
449
450 async fn fixed(&self, key: &str, limit: u32, window: Duration) -> Result<Decision> {
451 let k = format!("{}{}", self.prefix, key);
452 let window_ms = millis(window);
453 let replies = self
455 .redis
456 .pipeline(&[
457 Cmd::new("SET")
458 .arg(&k)
459 .arg("0")
460 .arg("NX")
461 .arg("PX")
462 .int(window_ms),
463 Cmd::new("INCR").arg(&k),
464 Cmd::new("PTTL").arg(&k),
465 ])
466 .await?;
467 let count = replies.get(1).and_then(Value::as_int).unwrap_or(1).max(1) as u64;
468 let mut ttl = replies.get(2).and_then(Value::as_int).unwrap_or(window_ms);
469 if ttl < 0 {
470 let _ = self
472 .redis
473 .run(Cmd::new("PEXPIRE").arg(&k).int(window_ms))
474 .await;
475 ttl = window_ms;
476 }
477 Ok(Decision {
478 allowed: count <= u64::from(limit),
479 limit,
480 remaining: u64::from(limit).saturating_sub(count) as u32,
481 reset_secs: ((ttl as u64).div_ceil(1000)).max(1),
482 })
483 }
484
485 async fn sliding(&self, key: &str, limit: u32, window: Duration) -> Result<Decision> {
486 let window_ms = millis(window) as u64;
487 let now_ms = SystemTime::now()
488 .duration_since(UNIX_EPOCH)
489 .map(|d| d.as_millis() as u64)
490 .unwrap_or(0);
491 let (index, elapsed) = (now_ms / window_ms, now_ms % window_ms);
492 let cur = format!("{}{}:{}", self.prefix, key, index);
493 let prev = format!("{}{}:{}", self.prefix, key, index.wrapping_sub(1));
494 let replies = self
495 .redis
496 .pipeline(&[
497 Cmd::new("INCR").arg(&cur),
498 Cmd::new("PEXPIRE").arg(&cur).int((window_ms * 2) as i64),
499 Cmd::new("GET").arg(&prev),
500 ])
501 .await?;
502 let count = replies.first().and_then(Value::as_int).unwrap_or(1).max(1) as f64;
503 let previous = match replies.get(2) {
504 Some(Value::Bulk(b)) => String::from_utf8_lossy(b).parse::<f64>().unwrap_or(0.0),
505 _ => 0.0,
506 };
507 let weight = 1.0 - elapsed as f64 / window_ms as f64;
508 let estimate = previous * weight + count;
509 let limit_f = f64::from(limit);
510 let reset_secs = ((window_ms - elapsed).div_ceil(1000)).max(1);
511 if estimate <= limit_f {
512 return Ok(Decision {
513 allowed: true,
514 limit,
515 remaining: (limit_f - estimate).floor().max(0.0) as u32,
516 reset_secs,
517 });
518 }
519 let _ = self.redis.run(Cmd::new("DECR").arg(&cur)).await;
521 Ok(Decision {
522 allowed: false,
523 limit,
524 remaining: 0,
525 reset_secs,
526 })
527 }
528}
529
530#[async_trait]
531impl RateLimitStore for RedisRateLimitStore {
532 async fn hit(&self, key: &str, limit: u32, window: Duration, sliding: bool) -> Decision {
533 let result = if sliding {
534 self.sliding(key, limit, window).await
535 } else {
536 self.fixed(key, limit, window).await
537 };
538 result.unwrap_or_else(|e| {
539 eprintln!("Rate limit store unavailable, allowing: {}", e.message());
540 Decision {
541 allowed: true,
542 limit,
543 remaining: limit,
544 reset_secs: window.as_secs().max(1),
545 }
546 })
547 }
548}
549
550pub struct RedisIdempotencyStore {
557 redis: Redis,
558 prefix: String,
559}
560
561const RUNNING: u8 = 0;
562const DONE: u8 = 1;
563
564fn encode_entry(fingerprint: u64, response: Option<&StoredResponse>) -> Vec<u8> {
565 let mut out = fingerprint.to_be_bytes().to_vec();
566 let Some(res) = response else {
567 out.push(RUNNING);
568 return out;
569 };
570 out.push(DONE);
571 out.extend_from_slice(&res.status.to_be_bytes());
572 out.extend_from_slice(&(res.headers.len() as u32).to_be_bytes());
573 for (name, value) in &res.headers {
574 out.extend_from_slice(&(name.len() as u32).to_be_bytes());
575 out.extend_from_slice(name.as_bytes());
576 out.extend_from_slice(&(value.len() as u32).to_be_bytes());
577 out.extend_from_slice(value);
578 }
579 out.extend_from_slice(&res.body);
580 out
581}
582
583fn decode_entry(raw: &[u8]) -> Option<(u64, Option<StoredResponse>)> {
584 fn take<'a>(buf: &mut &'a [u8], n: usize) -> Option<&'a [u8]> {
585 (buf.len() >= n).then(|| {
586 let (head, rest) = buf.split_at(n);
587 *buf = rest;
588 head
589 })
590 }
591 fn u32_of(buf: &mut &[u8]) -> Option<usize> {
592 Some(u32::from_be_bytes(take(buf, 4)?.try_into().ok()?) as usize)
593 }
594 let mut buf = raw;
595 let fingerprint = u64::from_be_bytes(take(&mut buf, 8)?.try_into().ok()?);
596 match take(&mut buf, 1)?[0] {
597 RUNNING => Some((fingerprint, None)),
598 DONE => {
599 let status = u16::from_be_bytes(take(&mut buf, 2)?.try_into().ok()?);
600 let count = u32_of(&mut buf)?;
601 let mut headers = Vec::new();
602 for _ in 0..count {
603 let n = u32_of(&mut buf)?;
604 let name = String::from_utf8(take(&mut buf, n)?.to_vec()).ok()?;
605 let n = u32_of(&mut buf)?;
606 headers.push((name, take(&mut buf, n)?.to_vec()));
607 }
608 Some((
609 fingerprint,
610 Some(StoredResponse {
611 status,
612 headers,
613 body: Bytes::copy_from_slice(buf),
614 }),
615 ))
616 }
617 _ => None,
618 }
619}
620
621impl RedisIdempotencyStore {
622 pub fn new(redis: Redis) -> Self {
623 Self {
624 redis,
625 prefix: "ullun:idem:".into(),
626 }
627 }
628
629 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
630 self.prefix = prefix.into();
631 self
632 }
633
634 fn key(&self, key: &str) -> String {
635 format!("{}{}", self.prefix, key)
636 }
637
638 async fn try_begin(&self, key: &str, fingerprint: u64, lock_ttl: Duration) -> Result<Begin> {
639 let k = self.key(key);
640 let claim = encode_entry(fingerprint, None);
641 for _ in 0..3 {
643 let set = Cmd::new("SET")
644 .arg(&k)
645 .arg(&claim)
646 .arg("NX")
647 .arg("PX")
648 .int(millis(lock_ttl));
649 if self.redis.run(set).await?.is_ok() {
650 return Ok(Begin::Started);
651 }
652 let Some(raw) = self.redis.run(Cmd::new("GET").arg(&k)).await?.into_bytes() else {
653 continue;
654 };
655 return Ok(match decode_entry(&raw) {
656 Some((fp, _)) if fp != fingerprint => Begin::Mismatch,
657 Some((_, None)) => Begin::InFlight,
658 Some((_, Some(res))) => Begin::Replay(res),
659 None => Begin::Unavailable,
661 });
662 }
663 Ok(Begin::Unavailable)
664 }
665}
666
667#[async_trait]
668impl IdempotencyStore for RedisIdempotencyStore {
669 async fn begin(&self, key: &str, fingerprint: u64, lock_ttl: Duration) -> Begin {
670 self.try_begin(key, fingerprint, lock_ttl)
671 .await
672 .unwrap_or_else(|e| {
673 eprintln!("Idempotency store unavailable: {}", e.message());
674 Begin::Unavailable
675 })
676 }
677
678 async fn complete(&self, key: &str, response: StoredResponse, ttl: Duration) {
679 let k = self.key(key);
680 let Ok(Some(raw)) = self
682 .redis
683 .run(Cmd::new("GET").arg(&k))
684 .await
685 .map(Value::into_bytes)
686 else {
687 return;
688 };
689 let Some((fingerprint, _)) = decode_entry(&raw) else {
690 return;
691 };
692 let cmd = Cmd::new("SET")
693 .arg(&k)
694 .arg(encode_entry(fingerprint, Some(&response)))
695 .arg("XX")
696 .arg("PX")
697 .int(millis(ttl));
698 if let Err(e) = self.redis.run(cmd).await {
699 eprintln!("Saving idempotent response failed: {}", e.message());
700 }
701 }
702
703 async fn abandon(&self, key: &str) {
704 let k = self.key(key);
705 if let Ok(Value::Bulk(raw)) = self.redis.run(Cmd::new("GET").arg(&k)).await {
707 if matches!(decode_entry(&raw), Some((_, None))) {
708 let _ = self.redis.run(Cmd::new("DEL").arg(&k)).await;
709 }
710 }
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn commands_are_encoded_as_resp_arrays() {
720 let cmd = Cmd::new("SET").arg("k").arg(b"v\r\n").int(5);
721 assert_eq!(
722 cmd.encode(),
723 b"*4\r\n$3\r\nSET\r\n$1\r\nk\r\n$3\r\nv\r\n\r\n$1\r\n5\r\n".to_vec()
724 );
725 }
726
727 #[test]
728 fn urls_are_parsed() {
729 let r = Redis::from_url("redis://localhost").unwrap();
730 assert_eq!((r.addr.as_str(), r.db), ("localhost:6379", 0));
731 let r = Redis::from_url("redis://h:7000/3").unwrap();
732 assert_eq!((r.addr.as_str(), r.db), ("h:7000", 3));
733 let r = Redis::from_url("redis://:p%40ss@h/").unwrap();
734 assert_eq!(r.password.as_deref(), Some("p@ss"));
735 assert_eq!(r.username, None);
736 let r = Redis::from_url("redis://ann:secret@h:1/2").unwrap();
737 assert_eq!(r.username.as_deref(), Some("ann"));
738 assert_eq!(r.password.as_deref(), Some("secret"));
739 for bad in [
740 "",
741 "localhost",
742 "rediss://h",
743 "redis://",
744 "redis://h/x",
745 "http://h",
746 ] {
747 assert!(Redis::from_url(bad).is_err(), "{bad}");
748 }
749 }
750
751 #[test]
752 fn idempotency_entries_roundtrip() {
753 let running = encode_entry(42, None);
754 assert_eq!(
755 decode_entry(&running).map(|(f, r)| (f, r.is_none())),
756 Some((42, true))
757 );
758 let res = StoredResponse {
759 status: 201,
760 headers: vec![
761 ("content-type".into(), b"application/json".to_vec()),
762 ("x".into(), vec![0, 255]),
763 ],
764 body: Bytes::from_static(b"{\"id\":1}\r\n\0"),
765 };
766 let (fp, got) = decode_entry(&encode_entry(7, Some(&res))).unwrap();
767 let got = got.unwrap();
768 assert_eq!(
769 (fp, got.status, &got.headers, &got.body),
770 (7, 201, &res.headers, &res.body)
771 );
772 let full = encode_entry(7, Some(&res));
774 for cut in 0..full.len().min(40) {
775 let _ = decode_entry(&full[..cut]);
776 }
777 assert!(decode_entry(&full[..12]).is_none());
778 assert!(decode_entry(b"short").is_none());
779 let mut unknown = encode_entry(1, None);
780 *unknown.last_mut().unwrap() = 9;
781 assert!(decode_entry(&unknown).is_none());
782 }
783}