Skip to content

Commit ceab789

Browse files
committed
fix(http): [#184] bencoded error responses for announce request
HTTP tracker error responser must be bencoded. Fixed in the new Axum implementation.
1 parent d0c8eb0 commit ceab789

File tree

6 files changed

+163
-50
lines changed

6 files changed

+163
-50
lines changed

src/http/axum_implementation/query.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl FromStr for Param {
4545
fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
4646
let pair = raw_param.split('=').collect::<Vec<&str>>();
4747

48-
if pair.len() > 2 {
48+
if pair.len() != 2 {
4949
return Err(ParseQueryError::InvalidParam {
5050
location: Location::caller(),
5151
raw_param: raw_param.to_owned(),

src/http/axum_implementation/requests/announce.rs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ use std::str::FromStr;
44
use axum::async_trait;
55
use axum::extract::FromRequestParts;
66
use axum::http::request::Parts;
7-
use axum::http::StatusCode;
7+
use axum::response::{IntoResponse, Response};
88
use thiserror::Error;
99

10-
use crate::http::axum_implementation::query::Query;
10+
use crate::http::axum_implementation::query::{ParseQueryError, Query};
11+
use crate::http::axum_implementation::responses;
1112
use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
1213
use crate::protocol::info_hash::{ConversionError, InfoHash};
1314
use crate::tracker::peer::{self, IdConversionError};
@@ -23,17 +24,17 @@ pub struct Announce {
2324

2425
#[derive(Error, Debug)]
2526
pub enum ParseAnnounceQueryError {
26-
#[error("missing infohash {location}")]
27+
#[error("missing info_hash param: {location}")]
2728
MissingInfoHash { location: &'static Location<'static> },
28-
#[error("invalid infohash {location}")]
29+
#[error("invalid info_hash param: {location}")]
2930
InvalidInfoHash { location: &'static Location<'static> },
30-
#[error("missing peer id {location}")]
31+
#[error("missing peer_id param: {location}")]
3132
MissingPeerId { location: &'static Location<'static> },
32-
#[error("invalid peer id {location}")]
33+
#[error("invalid peer_id param: {location}")]
3334
InvalidPeerId { location: &'static Location<'static> },
34-
#[error("missing port {location}")]
35+
#[error("missing port param: {location}")]
3536
MissingPort { location: &'static Location<'static> },
36-
#[error("invalid port {location}")]
37+
#[error("invalid port param: {location}")]
3738
InvalidPort { location: &'static Location<'static> },
3839
}
3940

@@ -49,12 +50,31 @@ impl From<IdConversionError> for ParseAnnounceQueryError {
4950
impl From<ConversionError> for ParseAnnounceQueryError {
5051
#[track_caller]
5152
fn from(_err: ConversionError) -> Self {
52-
Self::InvalidPeerId {
53+
Self::InvalidInfoHash {
5354
location: Location::caller(),
5455
}
5556
}
5657
}
5758

59+
impl From<ParseQueryError> for responses::error::Error {
60+
fn from(err: ParseQueryError) -> Self {
61+
responses::error::Error {
62+
// code-review: should we expose error location in public HTTP tracker API?
63+
// Error message example: "Cannot parse query params: invalid param a=b=c in src/http/axum_implementation/query.rs:50:27"
64+
failure_reason: format!("Cannot parse query params: {err}"),
65+
}
66+
}
67+
}
68+
69+
impl From<ParseAnnounceQueryError> for responses::error::Error {
70+
fn from(err: ParseAnnounceQueryError) -> Self {
71+
responses::error::Error {
72+
// code-review: should we expose error location in public HTTP tracker API?
73+
failure_reason: format!("Cannot parse query params for announce request: {err}"),
74+
}
75+
}
76+
}
77+
5878
impl TryFrom<Query> for Announce {
5979
type Error = ParseAnnounceQueryError;
6080

@@ -107,27 +127,28 @@ impl<S> FromRequestParts<S> for ExtractAnnounceRequest
107127
where
108128
S: Send + Sync,
109129
{
110-
type Rejection = (StatusCode, &'static str);
130+
type Rejection = Response;
111131

112132
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
113-
// todo: error responses body should be bencoded
114-
115133
let raw_query = parts.uri.query();
116134

117135
if raw_query.is_none() {
118-
return Err((StatusCode::BAD_REQUEST, "missing query params"));
136+
return Err(responses::error::Error {
137+
failure_reason: "missing query params for announce request".to_string(),
138+
}
139+
.into_response());
119140
}
120141

121142
let query = raw_query.unwrap().parse::<Query>();
122143

123-
if query.is_err() {
124-
return Err((StatusCode::BAD_REQUEST, "can't parse query params"));
144+
if let Err(error) = query {
145+
return Err(responses::error::Error::from(error).into_response());
125146
}
126147

127148
let announce_request = Announce::try_from(query.unwrap());
128149

129-
if announce_request.is_err() {
130-
return Err((StatusCode::BAD_REQUEST, "can't parse query params for announce request"));
150+
if let Err(error) = announce_request {
151+
return Err(responses::error::Error::from(error).into_response());
131152
}
132153

133154
Ok(ExtractAnnounceRequest(announce_request.unwrap()))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use axum::http::StatusCode;
2+
use axum::response::{IntoResponse, Response};
3+
use serde::{self, Serialize};
4+
5+
#[derive(Serialize)]
6+
pub struct Error {
7+
#[serde(rename = "failure reason")]
8+
pub failure_reason: String,
9+
}
10+
11+
impl Error {
12+
/// # Panics
13+
///
14+
/// It would panic if the `Error` struct contained an inappropriate type.
15+
#[must_use]
16+
pub fn write(&self) -> String {
17+
serde_bencode::to_string(&self).unwrap()
18+
}
19+
}
20+
21+
impl IntoResponse for Error {
22+
fn into_response(self) -> Response {
23+
(StatusCode::OK, self.write()).into_response()
24+
}
25+
}
26+
27+
#[cfg(test)]
28+
mod tests {
29+
30+
use super::Error;
31+
32+
#[test]
33+
fn http_tracker_errors_can_be_bencoded() {
34+
let err = Error {
35+
failure_reason: "error message".to_owned(),
36+
};
37+
38+
assert_eq!(err.write(), "d14:failure reason13:error messagee"); // cspell:disable-line
39+
}
40+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod error;
12
pub mod ok;

tests/http/asserts.rs

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use super::responses::announce::{Announce, Compact, DeserializedCompact};
66
use super::responses::scrape;
77
use crate::http::responses::error::Error;
88

9-
pub fn assert_error_bencoded(response_text: &String, expected_failure_reason: &str, location: &'static Location<'static>) {
9+
pub fn assert_bencoded_error(response_text: &String, expected_failure_reason: &str, location: &'static Location<'static>) {
1010
let error_failure_reason = serde_bencode::from_str::<Error>(response_text)
1111
.unwrap_or_else(|_| panic!(
1212
"response body should be a valid bencoded string for the '{expected_failure_reason}' error, got \"{response_text}\""
@@ -18,7 +18,7 @@ pub fn assert_error_bencoded(response_text: &String, expected_failure_reason: &s
1818
error_failure_reason.contains(expected_failure_reason),
1919
r#":
2020
response: `"{error_failure_reason}"`
21-
dose not contain: `"{expected_failure_reason}"`, {location}"#
21+
does not contain: `"{expected_failure_reason}"`, {location}"#
2222
);
2323
}
2424

@@ -83,13 +83,13 @@ pub async fn assert_is_announce_response(response: Response) {
8383
pub async fn assert_internal_server_error_response(response: Response) {
8484
assert_eq!(response.status(), 200);
8585

86-
assert_error_bencoded(&response.text().await.unwrap(), "internal server", Location::caller());
86+
assert_bencoded_error(&response.text().await.unwrap(), "internal server", Location::caller());
8787
}
8888

8989
pub async fn assert_invalid_info_hash_error_response(response: Response) {
9090
assert_eq!(response.status(), 200);
9191

92-
assert_error_bencoded(
92+
assert_bencoded_error(
9393
&response.text().await.unwrap(),
9494
"no valid infohashes found",
9595
Location::caller(),
@@ -99,7 +99,7 @@ pub async fn assert_invalid_info_hash_error_response(response: Response) {
9999
pub async fn assert_invalid_peer_id_error_response(response: Response) {
100100
assert_eq!(response.status(), 200);
101101

102-
assert_error_bencoded(
102+
assert_bencoded_error(
103103
&response.text().await.unwrap(),
104104
"peer_id is either missing or invalid",
105105
Location::caller(),
@@ -109,13 +109,13 @@ pub async fn assert_invalid_peer_id_error_response(response: Response) {
109109
pub async fn assert_torrent_not_in_whitelist_error_response(response: Response) {
110110
assert_eq!(response.status(), 200);
111111

112-
assert_error_bencoded(&response.text().await.unwrap(), "is not whitelisted", Location::caller());
112+
assert_bencoded_error(&response.text().await.unwrap(), "is not whitelisted", Location::caller());
113113
}
114114

115115
pub async fn assert_peer_not_authenticated_error_response(response: Response) {
116116
assert_eq!(response.status(), 200);
117117

118-
assert_error_bencoded(
118+
assert_bencoded_error(
119119
&response.text().await.unwrap(),
120120
"The peer is not authenticated",
121121
Location::caller(),
@@ -125,13 +125,13 @@ pub async fn assert_peer_not_authenticated_error_response(response: Response) {
125125
pub async fn assert_invalid_authentication_key_error_response(response: Response) {
126126
assert_eq!(response.status(), 200);
127127

128-
assert_error_bencoded(&response.text().await.unwrap(), "is not valid", Location::caller());
128+
assert_bencoded_error(&response.text().await.unwrap(), "is not valid", Location::caller());
129129
}
130130

131131
pub async fn assert_could_not_find_remote_address_on_xff_header_error_response(response: Response) {
132132
assert_eq!(response.status(), 200);
133133

134-
assert_error_bencoded(
134+
assert_bencoded_error(
135135
&response.text().await.unwrap(),
136136
"could not find remote address: must have a x-forwarded-for when using a reverse proxy",
137137
Location::caller(),
@@ -141,9 +141,51 @@ pub async fn assert_could_not_find_remote_address_on_xff_header_error_response(r
141141
pub async fn assert_invalid_remote_address_on_xff_header_error_response(response: Response) {
142142
assert_eq!(response.status(), 200);
143143

144-
assert_error_bencoded(
144+
assert_bencoded_error(
145145
&response.text().await.unwrap(),
146146
"could not find remote address: on remote proxy and unable to parse the last x-forwarded-ip",
147147
Location::caller(),
148148
);
149149
}
150+
151+
// Specific errors for announce requests
152+
153+
pub async fn assert_missing_query_params_for_announce_request_error_response(response: Response) {
154+
assert_eq!(response.status(), 200);
155+
156+
assert_bencoded_error(
157+
&response.text().await.unwrap(),
158+
"missing query params for announce request",
159+
Location::caller(),
160+
);
161+
}
162+
163+
pub async fn assert_cannot_parse_query_param_error_response(response: Response, failure: &str) {
164+
assert_eq!(response.status(), 200);
165+
166+
assert_bencoded_error(
167+
&response.text().await.unwrap(),
168+
&format!("Cannot parse query params: {failure}"),
169+
Location::caller(),
170+
);
171+
}
172+
173+
pub async fn assert_cannot_parse_query_params_error_response(response: Response) {
174+
assert_eq!(response.status(), 200);
175+
176+
assert_bencoded_error(
177+
&response.text().await.unwrap(),
178+
"Cannot parse query params",
179+
Location::caller(),
180+
);
181+
}
182+
183+
pub async fn assert_bad_announce_request_error_response(response: Response, failure: &str) {
184+
assert_eq!(response.status(), 200);
185+
186+
assert_bencoded_error(
187+
&response.text().await.unwrap(),
188+
&format!("Cannot parse query params for announce request: {failure}"),
189+
Location::caller(),
190+
);
191+
}

0 commit comments

Comments
 (0)