Skip to main content

nntp_proxy/protocol/
responses.rs

1//! NNTP response message constants and construction helpers
2//!
3//! This module provides pre-defined NNTP response messages and helpers
4//! for constructing responses according to RFC 3977.
5
6/// Line ending: "\r\n"
7pub const CRLF: &[u8] = b"\r\n";
8
9/// Minimum response length (3-digit code + CRLF)
10pub const MIN_RESPONSE_LENGTH: usize = 5;
11
12// Authentication responses (RFC 4643)
13
14/// Authentication required response (381)
15pub const AUTH_REQUIRED: &[u8] = b"381 Password required\r\n";
16
17/// Authentication accepted response (281)
18pub const AUTH_ACCEPTED: &[u8] = b"281 Authentication accepted\r\n";
19
20/// Authentication failed response (481)
21pub const AUTH_FAILED: &[u8] = b"481 Authentication failed\r\n";
22
23/// Authentication required for this command (480)
24pub const AUTH_REQUIRED_FOR_COMMAND: &[u8] = b"480 Authentication required\r\n";
25
26/// Authentication commands issued out of sequence (482)
27///
28/// Per [RFC 4643 §2.3.2](https://datatracker.ietf.org/doc/html/rfc4643#section-2.3.2),
29/// AUTHINFO PASS without a prior AUTHINFO USER must return 482.
30pub const AUTH_OUT_OF_SEQUENCE: &[u8] = b"482 Authentication commands issued out of sequence\r\n";
31
32/// Already authenticated (502)
33///
34/// Per [RFC 4643 §2.2](https://datatracker.ietf.org/doc/html/rfc4643#section-2.2),
35/// any AUTHINFO command after successful authentication must return 502.
36pub const AUTH_ALREADY_AUTHENTICATED: &[u8] = b"502 Already authenticated\r\n";
37
38/// Unrecognized AUTHINFO subcommand (501)
39///
40/// Per [RFC 4643 §2.3.1](https://datatracker.ietf.org/doc/html/rfc4643#section-2.3.1),
41/// AUTHINFO with a subcommand the server does not support must return 501.
42pub const AUTH_UNKNOWN_SUBCOMMAND: &[u8] = b"501 Syntax error in command\r\n";
43
44// Standard responses
45
46/// Proxy greeting for per-command routing mode (201 = posting not permitted)
47pub const PROXY_GREETING_PCR: &[u8] = b"201 NNTP Proxy Ready (Per-Command Routing)\r\n";
48
49/// Connection closing response (205)
50pub const CONNECTION_CLOSING: &[u8] = b"205 Connection closing\r\n";
51
52/// Goodbye message (205) - common in tests
53pub const GOODBYE: &[u8] = b"205 Goodbye\r\n";
54
55// Error responses
56
57/// Posting not permitted (440)
58///
59/// Per [RFC 3977 §6.3.1](https://datatracker.ietf.org/doc/html/rfc3977#section-6.3.1),
60/// servers that do not permit posting MUST return 440.
61pub const POSTING_NOT_PERMITTED: &[u8] = b"440 Posting not permitted\r\n";
62
63/// Backend error response (503)
64pub const BACKEND_ERROR: &[u8] = b"503 Backend error\r\n";
65
66/// Backend server unavailable response (400)
67pub const BACKEND_UNAVAILABLE: &[u8] = b"400 Backend server unavailable\r\n";
68
69/// Command line exceeded the RFC 3977 512-byte limit.
70pub const COMMAND_TOO_LONG: &[u8] = b"501 Command too long\r\n";
71
72/// Command line failed request-line syntax validation.
73pub const COMMAND_SYNTAX_ERROR_RESPONSE: &[u8] = b"501 Syntax error in command\r\n";
74
75/// No such article response (430)
76pub const NO_SUCH_ARTICLE: &[u8] = b"430 No such article\r\n";
77
78// Capabilities responses (RFC 3977 §5.2 + RFC 4643 §3.2)
79
80// Response construction helpers
81
82/// Construct a greeting response (200)
83///
84/// # Examples
85/// ```
86/// use nntp_proxy::protocol::greeting;
87///
88/// let msg = greeting("news.example.com ready");
89/// assert_eq!(msg, "200 news.example.com ready\r\n");
90/// ```
91#[inline]
92#[must_use]
93pub fn greeting(message: &str) -> String {
94    format!("200 {message}\r\n")
95}
96
97/// Construct a read-only greeting response (201)
98#[inline]
99#[must_use]
100pub fn greeting_readonly(message: &str) -> String {
101    format!("201 {message}\r\n")
102}
103
104/// Construct a generic OK response (200)
105#[inline]
106#[must_use]
107pub fn ok_response(message: &str) -> String {
108    format!("200 {message}\r\n")
109}
110
111/// Construct a generic error response with custom code and message
112///
113/// # Examples
114/// ```
115/// use nntp_proxy::protocol::error_response;
116///
117/// let msg = error_response(430, "No such article");
118/// assert_eq!(msg, "430 No such article\r\n");
119/// ```
120#[inline]
121#[must_use]
122pub fn error_response(code: u16, message: &str) -> String {
123    format!("{code} {message}\r\n")
124}
125
126/// Construct a response with custom status code and message
127#[inline]
128#[must_use]
129pub fn response(code: u16, message: &str) -> String {
130    format!("{code} {message}\r\n")
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_constants() {
139        assert_eq!(CRLF, b"\r\n");
140    }
141
142    #[test]
143    fn test_greeting() {
144        assert_eq!(greeting("Ready"), "200 Ready\r\n");
145        assert_eq!(
146            greeting("news.example.com ready"),
147            "200 news.example.com ready\r\n"
148        );
149        assert_eq!(greeting_readonly("Read only"), "201 Read only\r\n");
150    }
151
152    #[test]
153    fn test_ok_response() {
154        assert_eq!(ok_response("OK"), "200 OK\r\n");
155        assert_eq!(ok_response("Command accepted"), "200 Command accepted\r\n");
156    }
157
158    #[test]
159    fn test_error_response() {
160        assert_eq!(
161            error_response(430, "No such article"),
162            "430 No such article\r\n"
163        );
164        assert_eq!(
165            error_response(500, "Command not recognized"),
166            "500 Command not recognized\r\n"
167        );
168    }
169
170    #[test]
171    fn test_response() {
172        assert_eq!(
173            response(215, "Newsgroups follow"),
174            "215 Newsgroups follow\r\n"
175        );
176        assert_eq!(
177            response(381, "Password required"),
178            "381 Password required\r\n"
179        );
180    }
181
182    #[test]
183    fn test_auth_constants() {
184        assert_eq!(AUTH_REQUIRED, b"381 Password required\r\n");
185        assert_eq!(AUTH_ACCEPTED, b"281 Authentication accepted\r\n");
186        assert_eq!(AUTH_FAILED, b"481 Authentication failed\r\n");
187        assert_eq!(
188            AUTH_REQUIRED_FOR_COMMAND,
189            b"480 Authentication required\r\n"
190        );
191    }
192
193    #[test]
194    fn test_standard_responses() {
195        assert!(PROXY_GREETING_PCR.starts_with(b"201"));
196        assert!(CONNECTION_CLOSING.starts_with(b"205"));
197        assert!(GOODBYE.starts_with(b"205"));
198    }
199
200    #[test]
201    fn test_error_constants() {
202        assert!(POSTING_NOT_PERMITTED.starts_with(b"440"));
203        assert!(BACKEND_ERROR.starts_with(b"503"));
204        assert!(BACKEND_UNAVAILABLE.starts_with(b"400"));
205    }
206
207    #[test]
208    fn test_all_responses_end_with_crlf() {
209        assert!(AUTH_REQUIRED.ends_with(CRLF));
210        assert!(AUTH_ACCEPTED.ends_with(CRLF));
211        assert!(AUTH_FAILED.ends_with(CRLF));
212        assert!(AUTH_REQUIRED_FOR_COMMAND.ends_with(CRLF));
213        assert!(PROXY_GREETING_PCR.ends_with(CRLF));
214        assert!(CONNECTION_CLOSING.ends_with(CRLF));
215        assert!(GOODBYE.ends_with(CRLF));
216        assert!(POSTING_NOT_PERMITTED.ends_with(CRLF));
217        assert!(BACKEND_ERROR.ends_with(CRLF));
218        assert!(BACKEND_UNAVAILABLE.ends_with(CRLF));
219    }
220}