nntp_proxy/auth/
backend.rs1use anyhow::Result;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5use tracing::debug;
6
7use crate::pool::BufferPool;
8use crate::protocol::{ResponseParser, authinfo_pass, authinfo_user};
9
10pub struct BackendAuthenticator;
12
13impl BackendAuthenticator {
14 #[allow(dead_code)]
16 async fn authenticate<S>(
17 backend_stream: &mut S,
18 username: &str,
19 password: &str,
20 buffer_pool: &BufferPool,
21 ) -> Result<()>
22 where
23 S: AsyncReadExt + AsyncWriteExt + Unpin,
24 {
25 let mut buffer = buffer_pool.get_buffer().await;
26
27 backend_stream
29 .write_all(authinfo_user(username).as_bytes())
30 .await?;
31
32 let n = buffer.read_from(backend_stream).await?;
34 let response = String::from_utf8_lossy(&buffer[..n]);
35 debug!("AUTHINFO USER response: {}", response.trim());
36
37 if ResponseParser::is_auth_success(&buffer[..n]) {
39 return Ok(());
41 } else if !ResponseParser::is_auth_required(&buffer[..n]) {
42 let error = format!("Unexpected response to AUTHINFO USER: {}", response.trim());
43 return Err(anyhow::anyhow!(error));
44 }
45
46 backend_stream
48 .write_all(authinfo_pass(password).as_bytes())
49 .await?;
50
51 let n = buffer.read_from(backend_stream).await?;
53 let response = String::from_utf8_lossy(&buffer[..n]);
54 debug!("AUTHINFO PASS response: {}", response.trim());
55
56 if ResponseParser::is_auth_success(&buffer[..n]) {
58 Ok(())
59 } else {
60 Err(anyhow::anyhow!(
61 "Authentication failed: {}",
62 response.trim()
63 ))
64 }
65 }
66
67 #[allow(dead_code)]
69 pub async fn forward_greeting<B, C>(
70 backend_stream: &mut B,
71 client_stream: &mut C,
72 buffer_pool: &BufferPool,
73 ) -> Result<()>
74 where
75 B: AsyncReadExt + AsyncWriteExt + Unpin,
76 C: AsyncReadExt + AsyncWriteExt + Unpin,
77 {
78 let mut buffer = buffer_pool.get_buffer().await;
79
80 let n = buffer.read_from(backend_stream).await?;
82 let greeting = &buffer[..n];
83 let greeting_str = String::from_utf8_lossy(greeting);
84 debug!("Backend greeting: {}", greeting_str.trim());
85
86 if !ResponseParser::is_greeting(greeting) {
87 let error = format!(
88 "Server returned non-success greeting: {}",
89 greeting_str.trim()
90 );
91 return Err(anyhow::anyhow!(error));
92 }
93
94 client_stream.write_all(greeting).await?;
96
97 Ok(())
98 }
99
100 #[allow(dead_code)]
102 pub async fn authenticate_and_forward_greeting<B, C>(
103 backend_stream: &mut B,
104 client_stream: &mut C,
105 username: &str,
106 password: &str,
107 buffer_pool: &BufferPool,
108 ) -> Result<()>
109 where
110 B: AsyncReadExt + AsyncWriteExt + Unpin,
111 C: AsyncReadExt + AsyncWriteExt + Unpin,
112 {
113 let mut buffer = buffer_pool.get_buffer().await;
114
115 let n = buffer.read_from(backend_stream).await?;
117 let greeting = &buffer[..n];
118 let greeting_str = String::from_utf8_lossy(greeting);
119 debug!("Backend greeting: {}", greeting_str.trim());
120
121 if !ResponseParser::is_greeting(greeting) {
122 let error = format!(
123 "Server returned non-success greeting: {}",
124 greeting_str.trim()
125 );
126 return Err(anyhow::anyhow!(error));
127 }
128
129 client_stream.write_all(greeting).await?;
131
132 Self::authenticate(backend_stream, username, password, buffer_pool).await
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::types::BufferSize;
143
144 #[test]
146 fn test_auth_response_parsing() {
147 let auth_success = b"281 Authentication accepted\r\n";
149 assert!(ResponseParser::is_auth_success(auth_success));
150
151 let password_required = b"381 Password required\r\n";
153 assert!(ResponseParser::is_auth_required(password_required));
154 assert!(!ResponseParser::is_auth_success(password_required));
155
156 let auth_failed = b"481 Authentication failed\r\n";
158 assert!(!ResponseParser::is_auth_success(auth_failed));
159 assert!(!ResponseParser::is_auth_required(auth_failed));
160 }
161
162 #[test]
164 fn test_greeting_parsing() {
165 let greeting = b"200 Welcome to the NNTP server\r\n";
166 assert!(ResponseParser::is_greeting(greeting));
167
168 let greeting_auth_required = b"201 Welcome, authentication required\r\n";
169 assert!(ResponseParser::is_greeting(greeting_auth_required));
170
171 let not_greeting = b"400 Service temporarily unavailable\r\n";
172 assert!(!ResponseParser::is_greeting(not_greeting));
173 }
174
175 #[tokio::test]
177 async fn test_buffer_pool_usage() {
178 let buffer_pool = BufferPool::new(BufferSize::new(8192).unwrap(), 2);
179
180 let buffer1 = buffer_pool.get_buffer().await;
182 let buffer2 = buffer_pool.get_buffer().await;
183
184 assert_eq!(buffer1.capacity(), 8192);
185 assert_eq!(buffer2.capacity(), 8192);
186
187 let buffer3 = buffer_pool.get_buffer().await;
189 assert_eq!(buffer3.capacity(), 8192);
190 }
191
192 #[test]
194 fn test_authentication_command_format() {
195 let username = "testuser";
197 let password = "testpass";
198
199 let user_command = authinfo_user(username);
200 assert_eq!(user_command, "AUTHINFO USER testuser\r\n");
201
202 let pass_command = authinfo_pass(password);
203 assert_eq!(pass_command, "AUTHINFO PASS testpass\r\n");
204 }
205
206 #[test]
208 fn test_authentication_command_edge_cases() {
209 let username = "user with spaces";
211 let user_command = authinfo_user(username);
212 assert!(user_command.contains("user with spaces"));
213
214 let empty_command = authinfo_user("");
216 assert_eq!(empty_command, "AUTHINFO USER \r\n");
217
218 let password = "p@ssw0rd!#$";
220 let pass_command = authinfo_pass(password);
221 assert_eq!(pass_command, "AUTHINFO PASS p@ssw0rd!#$\r\n");
222 }
223}