1use std::fmt;
7use std::io::{self, Read, Write};
8use std::net::{SocketAddr, TcpStream};
9use std::time::Duration;
10
11const SAM_VERSION: &str = "3.1";
13
14const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16
17const READ_TIMEOUT: Duration = Duration::from_secs(30);
19
20const I2P_BASE64_ALPHABET: &[u8; 64] =
24 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
25
26fn i2p_base64_decode_table() -> [u8; 256] {
28 let mut table = [255u8; 256];
29 for (i, &ch) in I2P_BASE64_ALPHABET.iter().enumerate() {
30 table[ch as usize] = i as u8;
31 }
32 table[b'=' as usize] = 0;
34 table
35}
36
37pub fn i2p_base64_encode(data: &[u8]) -> String {
39 let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
40 for chunk in data.chunks(3) {
41 let b0 = chunk[0] as u32;
42 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
43 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
44 let triple = (b0 << 16) | (b1 << 8) | b2;
45
46 out.push(I2P_BASE64_ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
47 out.push(I2P_BASE64_ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
48
49 if chunk.len() > 1 {
50 out.push(I2P_BASE64_ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
51 } else {
52 out.push('=');
53 }
54
55 if chunk.len() > 2 {
56 out.push(I2P_BASE64_ALPHABET[(triple & 0x3F) as usize] as char);
57 } else {
58 out.push('=');
59 }
60 }
61 out
62}
63
64pub fn i2p_base64_decode(s: &str) -> Result<Vec<u8>, SamError> {
66 let table = i2p_base64_decode_table();
67 let bytes = s.as_bytes();
68
69 if bytes.len() % 4 != 0 {
70 return Err(SamError::InvalidResponse(format!(
71 "invalid I2P base64 length: {}",
72 bytes.len()
73 )));
74 }
75
76 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
77
78 for chunk in bytes.chunks(4) {
79 let mut vals = [0u8; 4];
80 let mut pad_count = 0;
81 for (i, &b) in chunk.iter().enumerate() {
82 if b == b'=' {
83 pad_count += 1;
84 vals[i] = 0;
85 } else {
86 let v = table[b as usize];
87 if v == 255 {
88 return Err(SamError::InvalidResponse(format!(
89 "invalid I2P base64 character: {:?}",
90 b as char
91 )));
92 }
93 vals[i] = v;
94 }
95 }
96
97 let triple = (vals[0] as u32) << 18
98 | (vals[1] as u32) << 12
99 | (vals[2] as u32) << 6
100 | (vals[3] as u32);
101
102 out.push((triple >> 16) as u8);
103 if pad_count < 2 {
104 out.push((triple >> 8) as u8);
105 }
106 if pad_count < 1 {
107 out.push(triple as u8);
108 }
109 }
110
111 Ok(out)
112}
113
114#[derive(Debug)]
118pub enum SamError {
119 Io(io::Error),
121 Protocol(String),
123 InvalidResponse(String),
125}
126
127impl fmt::Display for SamError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 SamError::Io(e) => write!(f, "SAM I/O error: {}", e),
131 SamError::Protocol(msg) => write!(f, "SAM protocol error: {}", msg),
132 SamError::InvalidResponse(msg) => write!(f, "SAM invalid response: {}", msg),
133 }
134 }
135}
136
137impl From<io::Error> for SamError {
138 fn from(e: io::Error) -> Self {
139 SamError::Io(e)
140 }
141}
142
143#[derive(Clone, Debug)]
147pub struct Destination {
148 pub data: Vec<u8>,
150}
151
152impl Destination {
153 pub fn to_i2p_base64(&self) -> String {
155 i2p_base64_encode(&self.data)
156 }
157
158 pub fn from_i2p_base64(s: &str) -> Result<Self, SamError> {
160 let data = i2p_base64_decode(s)?;
161 Ok(Destination { data })
162 }
163
164 pub fn base32_address(&self) -> String {
167 let hash = rns_crypto::sha256::sha256(&self.data);
168 let encoded = base32_encode(&hash);
169 format!("{}.b32.i2p", encoded)
170 }
171}
172
173#[derive(Clone, Debug)]
175pub struct KeyPair {
176 pub destination: Destination,
177 pub private_key: Vec<u8>,
179}
180
181const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
184
185fn base32_encode(data: &[u8]) -> String {
187 let mut out = String::with_capacity((data.len() * 8 + 4) / 5);
188 let mut buffer: u64 = 0;
189 let mut bits: u32 = 0;
190
191 for &byte in data {
192 buffer = (buffer << 8) | byte as u64;
193 bits += 8;
194 while bits >= 5 {
195 bits -= 5;
196 out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1F) as usize] as char);
197 }
198 }
199 if bits > 0 {
200 out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1F) as usize] as char);
201 }
202 out
203}
204
205fn parse_kv(token: &str) -> Option<(&str, &str)> {
209 let eq = token.find('=')?;
210 Some((&token[..eq], &token[eq + 1..]))
211}
212
213fn read_line(stream: &mut TcpStream) -> Result<String, SamError> {
220 let mut line = Vec::new();
221 let mut byte = [0u8; 1];
222 loop {
223 match stream.read_exact(&mut byte) {
224 Ok(()) => {
225 if byte[0] == b'\n' {
226 break;
227 }
228 line.push(byte[0]);
229 }
230 Err(e) => return Err(SamError::Io(e)),
231 }
232 }
233 String::from_utf8(line)
234 .map_err(|e| SamError::InvalidResponse(format!("non-UTF8 SAM response: {}", e)))
235}
236
237fn hello_connect(sam_addr: &SocketAddr) -> Result<TcpStream, SamError> {
239 let mut stream = TcpStream::connect_timeout(sam_addr, CONNECT_TIMEOUT)?;
240 stream.set_read_timeout(Some(READ_TIMEOUT))?;
241 stream.set_write_timeout(Some(READ_TIMEOUT))?;
242
243 write!(stream, "HELLO VERSION MIN={v} MAX={v}\n", v = SAM_VERSION)?;
245 stream.flush()?;
246
247 let line = read_line(&mut stream)?;
249 let resp = parse_sam_response(&line)?;
251 if resp.command != "HELLO" || resp.subcommand != "REPLY" {
252 return Err(SamError::InvalidResponse(format!(
253 "expected HELLO REPLY, got: {}",
254 line
255 )));
256 }
257 check_result(&resp)?;
258
259 Ok(stream)
260}
261
262struct SamResponse {
264 command: String,
265 subcommand: String,
266 params: Vec<(String, String)>,
267}
268
269impl SamResponse {
270 fn get(&self, key: &str) -> Option<&str> {
271 for (k, v) in &self.params {
272 if k == key {
273 return Some(v);
274 }
275 }
276 None
277 }
278}
279
280fn parse_sam_response(line: &str) -> Result<SamResponse, SamError> {
284 let mut parts = line.splitn(3, ' ');
285 let command = parts
286 .next()
287 .ok_or_else(|| SamError::InvalidResponse("empty response".into()))?
288 .to_string();
289 let subcommand = parts.next().unwrap_or("").to_string();
290 let rest = parts.next().unwrap_or("");
291
292 let mut params = Vec::new();
293 for token in rest.split_whitespace() {
296 if let Some((k, v)) = parse_kv(token) {
297 params.push((k.to_string(), v.to_string()));
298 }
299 }
300
301 Ok(SamResponse {
302 command,
303 subcommand,
304 params,
305 })
306}
307
308fn check_result(resp: &SamResponse) -> Result<(), SamError> {
310 match resp.get("RESULT") {
311 Some("OK") => Ok(()),
312 Some(result) => {
313 let message = resp.get("MESSAGE").unwrap_or("(no message)");
314 Err(SamError::Protocol(format!(
315 "RESULT={} MESSAGE={}",
316 result, message
317 )))
318 }
319 None => Ok(()), }
321}
322
323pub fn dest_generate(sam_addr: &SocketAddr) -> Result<KeyPair, SamError> {
328 let mut stream = hello_connect(sam_addr)?;
329
330 write!(stream, "DEST GENERATE SIGNATURE_TYPE=7\n")?;
331 stream.flush()?;
332
333 let line = read_line(&mut stream)?;
334 let resp = parse_sam_response(&line)?;
335
336 if resp.command != "DEST" || resp.subcommand != "REPLY" {
337 return Err(SamError::InvalidResponse(format!(
338 "expected DEST REPLY, got: {}",
339 line
340 )));
341 }
342
343 let pub_b64 = resp
344 .get("PUB")
345 .ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PUB".into()))?;
346 let priv_b64 = resp
347 .get("PRIV")
348 .ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PRIV".into()))?;
349
350 let dest_data = i2p_base64_decode(pub_b64)?;
351 let priv_data = i2p_base64_decode(priv_b64)?;
352
353 Ok(KeyPair {
354 destination: Destination { data: dest_data },
355 private_key: priv_data,
356 })
357}
358
359pub fn session_create(
362 sam_addr: &SocketAddr,
363 session_id: &str,
364 private_key_b64: &str,
365) -> Result<TcpStream, SamError> {
366 let mut stream = hello_connect(sam_addr)?;
367
368 write!(
369 stream,
370 "SESSION CREATE STYLE=STREAM ID={} DESTINATION={} SIGNATURE_TYPE=7\n",
371 session_id, private_key_b64,
372 )?;
373 stream.flush()?;
374
375 let line = read_line(&mut stream)?;
376 let resp = parse_sam_response(&line)?;
377
378 if resp.command != "SESSION" || resp.subcommand != "STATUS" {
379 return Err(SamError::InvalidResponse(format!(
380 "expected SESSION STATUS, got: {}",
381 line
382 )));
383 }
384 check_result(&resp)?;
385
386 Ok(stream)
388}
389
390pub fn stream_connect(
393 sam_addr: &SocketAddr,
394 session_id: &str,
395 destination: &str,
396) -> Result<TcpStream, SamError> {
397 let mut stream = hello_connect(sam_addr)?;
398
399 write!(
400 stream,
401 "STREAM CONNECT ID={} DESTINATION={} SILENT=false\n",
402 session_id, destination,
403 )?;
404 stream.flush()?;
405
406 let line = read_line(&mut stream)?;
407 let resp = parse_sam_response(&line)?;
408
409 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
410 return Err(SamError::InvalidResponse(format!(
411 "expected STREAM STATUS, got: {}",
412 line
413 )));
414 }
415 check_result(&resp)?;
416
417 stream.set_read_timeout(None)?;
420 stream.set_write_timeout(None)?;
421
422 Ok(stream)
423}
424
425pub fn stream_accept(
428 sam_addr: &SocketAddr,
429 session_id: &str,
430) -> Result<(TcpStream, Destination), SamError> {
431 let mut stream = hello_connect(sam_addr)?;
432
433 write!(stream, "STREAM ACCEPT ID={} SILENT=false\n", session_id,)?;
434 stream.flush()?;
435
436 let line = read_line(&mut stream)?;
437 let resp = parse_sam_response(&line)?;
438
439 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
440 return Err(SamError::InvalidResponse(format!(
441 "expected STREAM STATUS, got: {}",
442 line
443 )));
444 }
445 check_result(&resp)?;
446
447 let dest_line = read_line(&mut stream)?;
450 let remote_dest = Destination::from_i2p_base64(dest_line.trim())?;
451
452 stream.set_read_timeout(None)?;
454 stream.set_write_timeout(None)?;
455
456 Ok((stream, remote_dest))
457}
458
459pub fn naming_lookup(sam_addr: &SocketAddr, name: &str) -> Result<Destination, SamError> {
462 let mut stream = hello_connect(sam_addr)?;
463 naming_lookup_on(&mut stream, name)
464}
465
466pub fn naming_lookup_on(stream: &mut TcpStream, name: &str) -> Result<Destination, SamError> {
470 write!(stream, "NAMING LOOKUP NAME={}\n", name)?;
471 stream.flush()?;
472
473 let line = read_line(stream)?;
474 let resp = parse_sam_response(&line)?;
475
476 if resp.command != "NAMING" || resp.subcommand != "REPLY" {
477 return Err(SamError::InvalidResponse(format!(
478 "expected NAMING REPLY, got: {}",
479 line
480 )));
481 }
482 check_result(&resp)?;
483
484 let value = resp
485 .get("VALUE")
486 .ok_or_else(|| SamError::InvalidResponse("NAMING REPLY missing VALUE".into()))?;
487
488 Destination::from_i2p_base64(value)
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
498 fn base64_encode_empty() {
499 assert_eq!(i2p_base64_encode(b""), "");
500 }
501
502 #[test]
503 fn base64_roundtrip() {
504 let data: Vec<u8> = (0..=255).collect();
505 let encoded = i2p_base64_encode(&data);
506 let decoded = i2p_base64_decode(&encoded).unwrap();
507 assert_eq!(decoded, data);
508 }
509
510 #[test]
511 fn base64_known_value() {
512 let encoded = i2p_base64_encode(b"Hello");
515 assert_eq!(encoded, "SGVsbG8=");
516 let decoded = i2p_base64_decode(&encoded).unwrap();
517 assert_eq!(decoded, b"Hello");
518 }
519
520 #[test]
521 fn base64_i2p_specific_chars() {
522 let data = [0xFB, 0xEF, 0xBE];
525 let encoded = i2p_base64_encode(&data);
526 assert!(encoded.contains('-') || encoded.contains('~'));
527 let decoded = i2p_base64_decode(&encoded).unwrap();
529 assert_eq!(decoded, data);
530 }
531
532 #[test]
533 fn base64_all_alphabet_chars_roundtrip() {
534 let data: Vec<u8> = (0..48).collect();
536 let encoded = i2p_base64_encode(&data);
537 let decoded = i2p_base64_decode(&encoded).unwrap();
538 assert_eq!(decoded, data);
539 }
540
541 #[test]
542 fn base64_padding_1() {
543 let encoded = i2p_base64_encode(&[0xFF]);
545 assert_eq!(encoded.len(), 4);
546 assert!(encoded.ends_with("=="));
547 let decoded = i2p_base64_decode(&encoded).unwrap();
548 assert_eq!(decoded, vec![0xFF]);
549 }
550
551 #[test]
552 fn base64_padding_2() {
553 let encoded = i2p_base64_encode(&[0xFF, 0xFE]);
555 assert_eq!(encoded.len(), 4);
556 assert!(encoded.ends_with('='));
557 let decoded = i2p_base64_decode(&encoded).unwrap();
558 assert_eq!(decoded, vec![0xFF, 0xFE]);
559 }
560
561 #[test]
562 fn base64_no_padding() {
563 let encoded = i2p_base64_encode(&[0xFF, 0xFE, 0xFD]);
565 assert_eq!(encoded.len(), 4);
566 assert!(!encoded.contains('='));
567 let decoded = i2p_base64_decode(&encoded).unwrap();
568 assert_eq!(decoded, vec![0xFF, 0xFE, 0xFD]);
569 }
570
571 #[test]
572 fn base64_decode_invalid_char() {
573 let result = i2p_base64_decode("!!!=");
574 assert!(result.is_err());
575 }
576
577 #[test]
578 fn base64_decode_invalid_length() {
579 let result = i2p_base64_decode("ABC");
580 assert!(result.is_err());
581 }
582
583 #[test]
586 fn base32_encode_empty() {
587 assert_eq!(base32_encode(&[]), "");
588 }
589
590 #[test]
591 fn base32_encode_known() {
592 let result = base32_encode(b"Hello");
594 assert_eq!(result, "jbswy3dp");
595 }
596
597 #[test]
598 fn base32_encode_sha256() {
599 let hash = rns_crypto::sha256::sha256(b"");
601 let encoded = base32_encode(&hash);
602 assert_eq!(encoded.len(), 52);
604 assert!(encoded
606 .chars()
607 .all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c)));
608 }
609
610 #[test]
613 fn destination_base32_address() {
614 let dest = Destination {
615 data: vec![0x42; 387], };
617 let addr = dest.base32_address();
618 assert!(addr.ends_with(".b32.i2p"));
619 assert_eq!(addr.len(), 60);
621 }
622
623 #[test]
624 fn destination_roundtrip_base64() {
625 let data: Vec<u8> = (0..=255).cycle().take(387).collect();
626 let dest = Destination { data: data.clone() };
627 let b64 = dest.to_i2p_base64();
628 let dest2 = Destination::from_i2p_base64(&b64).unwrap();
629 assert_eq!(dest2.data, data);
630 }
631
632 #[test]
635 fn parse_hello_reply() {
636 let line = "HELLO REPLY RESULT=OK VERSION=3.1";
637 let resp = parse_sam_response(line).unwrap();
638 assert_eq!(resp.command, "HELLO");
639 assert_eq!(resp.subcommand, "REPLY");
640 assert_eq!(resp.get("RESULT"), Some("OK"));
641 assert_eq!(resp.get("VERSION"), Some("3.1"));
642 }
643
644 #[test]
645 fn parse_session_status_ok() {
646 let line = "SESSION STATUS RESULT=OK DESTINATION=AAAA";
647 let resp = parse_sam_response(line).unwrap();
648 assert_eq!(resp.command, "SESSION");
649 assert_eq!(resp.subcommand, "STATUS");
650 assert_eq!(resp.get("RESULT"), Some("OK"));
651 assert_eq!(resp.get("DESTINATION"), Some("AAAA"));
652 }
653
654 #[test]
655 fn parse_session_status_error() {
656 let line = "SESSION STATUS RESULT=DUPLICATED_ID";
657 let resp = parse_sam_response(line).unwrap();
658 assert_eq!(resp.get("RESULT"), Some("DUPLICATED_ID"));
659 let err = check_result(&resp);
660 assert!(err.is_err());
661 }
662
663 #[test]
664 fn parse_stream_status_error() {
665 let line = "STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE=unreachable";
666 let resp = parse_sam_response(line).unwrap();
667 assert_eq!(resp.get("RESULT"), Some("CANT_REACH_PEER"));
668 assert_eq!(resp.get("MESSAGE"), Some("unreachable"));
669 let err = check_result(&resp);
670 assert!(err.is_err());
671 if let Err(SamError::Protocol(msg)) = err {
672 assert!(msg.contains("CANT_REACH_PEER"));
673 }
674 }
675
676 #[test]
677 fn parse_naming_reply() {
678 let line = "NAMING REPLY RESULT=OK NAME=test.b32.i2p VALUE=AAAA";
679 let resp = parse_sam_response(line).unwrap();
680 assert_eq!(resp.command, "NAMING");
681 assert_eq!(resp.subcommand, "REPLY");
682 assert_eq!(resp.get("NAME"), Some("test.b32.i2p"));
683 assert_eq!(resp.get("VALUE"), Some("AAAA"));
684 }
685
686 #[test]
687 fn parse_naming_not_found() {
688 let line = "NAMING REPLY RESULT=KEY_NOT_FOUND";
689 let resp = parse_sam_response(line).unwrap();
690 let err = check_result(&resp);
691 assert!(err.is_err());
692 }
693
694 #[test]
695 fn parse_dest_reply() {
696 let line = "DEST REPLY PUB=AAAA PRIV=BBBB";
697 let resp = parse_sam_response(line).unwrap();
698 assert_eq!(resp.command, "DEST");
699 assert_eq!(resp.subcommand, "REPLY");
700 assert_eq!(resp.get("PUB"), Some("AAAA"));
701 assert_eq!(resp.get("PRIV"), Some("BBBB"));
702 }
703
704 #[test]
705 fn parse_stream_status_timeout() {
706 let line = "STREAM STATUS RESULT=TIMEOUT";
707 let resp = parse_sam_response(line).unwrap();
708 let err = check_result(&resp);
709 assert!(err.is_err());
710 if let Err(SamError::Protocol(msg)) = err {
711 assert!(msg.contains("TIMEOUT"));
712 }
713 }
714
715 #[test]
716 fn check_result_ok() {
717 let line = "TEST REPLY RESULT=OK";
718 let resp = parse_sam_response(line).unwrap();
719 assert!(check_result(&resp).is_ok());
720 }
721
722 #[test]
723 fn check_result_no_result_field() {
724 let line = "TEST REPLY FOO=BAR";
725 let resp = parse_sam_response(line).unwrap();
726 assert!(check_result(&resp).is_ok());
728 }
729
730 #[test]
731 fn sam_error_display() {
732 let io_err = SamError::Io(io::Error::new(io::ErrorKind::Other, "test"));
733 assert!(format!("{}", io_err).contains("test"));
734
735 let proto_err = SamError::Protocol("CANT_REACH_PEER".into());
736 assert!(format!("{}", proto_err).contains("CANT_REACH_PEER"));
737
738 let inv_err = SamError::InvalidResponse("bad".into());
739 assert!(format!("{}", inv_err).contains("bad"));
740 }
741}