nntp_proxy/auth/
backend.rs

1//! Backend server authentication
2
3use anyhow::Result;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5use tracing::debug;
6
7use crate::pool::BufferPool;
8use crate::protocol::{ResponseParser, authinfo_pass, authinfo_user};
9
10/// Handles authentication to backend NNTP servers
11pub struct BackendAuthenticator;
12
13impl BackendAuthenticator {
14    /// Authenticate to backend server - generic over stream types to support both TCP and future TLS
15    #[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        // Send AUTHINFO USER command
28        backend_stream
29            .write_all(authinfo_user(username).as_bytes())
30            .await?;
31
32        // Read response
33        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        // Should get 381 (password required) or 281 (authenticated)
38        if ResponseParser::is_auth_success(&buffer[..n]) {
39            // Already authenticated with just username
40            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        // Send AUTHINFO PASS command
47        backend_stream
48            .write_all(authinfo_pass(password).as_bytes())
49            .await?;
50
51        // Read final response
52        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        // Should get 281 (authenticated)
57        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    /// Read and forward the backend server's greeting to the client - generic over stream types
68    #[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        // Read the server greeting
81        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        // Forward greeting to client
95        client_stream.write_all(greeting).await?;
96
97        Ok(())
98    }
99
100    /// Perform authentication and forward the greeting to the client - generic over stream types
101    #[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        // Read the server greeting first and forward it
116        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        // Forward greeting to client immediately
130        client_stream.write_all(greeting).await?;
131
132        // Return buffer before calling authenticate
133
134        // Now perform authentication on backend
135        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 ResponseParser::is_auth_success
145    #[test]
146    fn test_auth_response_parsing() {
147        // Test successful auth response
148        let auth_success = b"281 Authentication accepted\r\n";
149        assert!(ResponseParser::is_auth_success(auth_success));
150
151        // Test password required response
152        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        // Test failure response
157        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 greeting detection
163    #[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    /// Test buffer pool interaction in authentication
176    #[tokio::test]
177    async fn test_buffer_pool_usage() {
178        let buffer_pool = BufferPool::new(BufferSize::new(8192).unwrap(), 2);
179
180        // Verify we can get and return buffers
181        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        // Should be able to get them again
188        let buffer3 = buffer_pool.get_buffer().await;
189        assert_eq!(buffer3.capacity(), 8192);
190    }
191
192    /// Test authentication command formatting
193    #[test]
194    fn test_authentication_command_format() {
195        // Verify the commands we construct are correct
196        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 edge cases in command formatting
207    #[test]
208    fn test_authentication_command_edge_cases() {
209        // Username with spaces (invalid but test the format)
210        let username = "user with spaces";
211        let user_command = authinfo_user(username);
212        assert!(user_command.contains("user with spaces"));
213
214        // Empty username
215        let empty_command = authinfo_user("");
216        assert_eq!(empty_command, "AUTHINFO USER \r\n");
217
218        // Password with special characters
219        let password = "p@ssw0rd!#$";
220        let pass_command = authinfo_pass(password);
221        assert_eq!(pass_command, "AUTHINFO PASS p@ssw0rd!#$\r\n");
222    }
223}