use std::future::{Ready, ready};
use actix_web::dev::Payload;
use actix_web::error::ErrorBadRequest;
use actix_web::http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, HeaderValue};
use actix_web::web::Json;
use actix_web::{Error as ActixError, FromRequest, HttpRequest, HttpResponse, Responder};
use tracing::trace;
use crate::http::CORS_ALLOW_ORIGIN;
use crate::query::{RequestParams, RequestParamsError};
use crate::{Rel, WebFingerRequest, WebFingerResponse};
const CORS_ALLOW_ORIGIN_HEADER: HeaderValue = HeaderValue::from_static(CORS_ALLOW_ORIGIN);
const JRD_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/jrd+json");
impl Responder for WebFingerResponse {
type Body = <Json<WebFingerResponse> as Responder>::Body;
fn respond_to(self, request: &HttpRequest) -> HttpResponse<Self::Body> {
let mut response = Json(self).respond_to(request);
response
.headers_mut()
.insert(ACCESS_CONTROL_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN_HEADER);
response
.headers_mut()
.insert(CONTENT_TYPE, JRD_CONTENT_TYPE);
response
}
}
impl FromRequest for WebFingerRequest {
type Error = ActixError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
trace!(?req, "extracting WebFingerRequest from request");
ready(extract_request(req))
}
}
fn extract_request(req: &HttpRequest) -> Result<WebFingerRequest, ActixError> {
let host = req
.uri()
.host()
.or_else(|| req.headers().get("host").and_then(|h| h.to_str().ok()))
.map(|h| h.to_string())
.ok_or(ErrorBadRequest("missing host"))?;
let query: RequestParams = req.query_string().parse()?;
let rels = query
.rel
.into_iter()
.map(Rel::try_new)
.collect::<Result<Vec<_>, _>>()
.map_err(ErrorBadRequest)?;
Ok(WebFingerRequest {
host,
resource: query.resource,
rels,
})
}
impl From<RequestParamsError> for ActixError {
fn from(error: RequestParamsError) -> Self {
ErrorBadRequest(error)
}
}
#[cfg(test)]
mod tests {
use actix_web::body::to_bytes;
use actix_web::http::StatusCode;
use actix_web::{App, HttpResponse, test, web};
use super::*;
use crate::WELL_KNOWN_PATH;
type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
async fn webfinger(request: WebFingerRequest) -> HttpResponse {
HttpResponse::Ok().body(request.resource.to_string())
}
async fn webfinger_rels(request: WebFingerRequest) -> HttpResponse {
let rels = request
.rels
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>();
HttpResponse::Ok().json(rels)
}
async fn webfinger_response() -> WebFingerResponse {
WebFingerResponse::new("acct:carol@example.com")
}
#[actix_web::test]
async fn successful_response_sets_cors_header() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
let app = test::init_service(app).await;
let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
assert_eq!(
response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN),
Some(&CORS_ALLOW_ORIGIN_HEADER),
);
Ok(())
}
#[actix_web::test]
async fn webfinger_response_uses_jrd_content_type() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
let app = test::init_service(app).await;
let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
assert_eq!(
response.headers().get(CONTENT_TYPE),
Some(&JRD_CONTENT_TYPE),
);
Ok(())
}
#[actix_web::test]
async fn valid_percent_encoded_resource() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"acct:bad@example.org");
Ok(())
}
#[actix_web::test]
async fn wrong_path_is_not_routed() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let request = test::TestRequest::get()
.uri("/webfinger?resource=acct%3Abad%40example.org")
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
Ok(())
}
#[actix_web::test]
async fn wrong_method_is_not_routed() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
let request = test::TestRequest::post()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
Ok(())
}
#[actix_web::test]
async fn request_with_invalid_resource() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=http%3A%2F%2F%5B%3A%3A1");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"invalid resource: invalid authority");
Ok(())
}
#[actix_web::test]
async fn relative_resource_is_bad_request() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=/relative");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(
body.as_ref(),
b"invalid resource: resource must be an absolute URI",
);
Ok(())
}
#[actix_web::test]
async fn valid_request_with_repeated_rel_params() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=profile&rel=avatar");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), br#"["profile","avatar"]"#);
Ok(())
}
#[actix_web::test]
async fn rel_params_are_percent_decoded() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let rel = "http%3A%2F%2Fwebfinger.example%2Frel%2Fprofile-page";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel={rel}");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(
body.as_ref(),
br#"["http://webfinger.example/rel/profile-page"]"#,
);
Ok(())
}
#[actix_web::test]
async fn invalid_rel_is_bad_request() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=author%20avatar");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"invalid relation type: author avatar");
Ok(())
}
#[actix_web::test]
async fn invalid_percent_encoded_rel_is_bad_request() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%FF");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
Ok(())
}
#[actix_web::test]
async fn malformed_percent_escape_is_bad_request() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%GG");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
Ok(())
}
#[actix_web::test]
async fn resource_parameter_order_does_not_matter() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
let app = test::init_service(app).await;
let resource = "acct%3Acarol%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?rel=profile&resource={resource}");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), br#"["profile"]"#);
Ok(())
}
#[actix_web::test]
async fn encoded_delimiters_stay_inside_resource() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let resource = "https%3A%2F%2Fexample.org%2Fprofile%3Fa%3D1%26b%3D2";
let uri = format!("{WELL_KNOWN_PATH}?resource={resource}");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"https://example.org/profile?a=1&b=2");
Ok(())
}
#[actix_web::test]
async fn plus_is_not_decoded_as_space() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Acarol+tag%40example.org");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::OK, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"acct:carol+tag@example.org");
Ok(())
}
#[actix_web::test]
async fn request_with_multiple_resources() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let carol = "acct%3Acarol%40example.org";
let alice = "acct%3Aalice%40example.org";
let uri = format!("{WELL_KNOWN_PATH}?resource={carol}&resource={alice}");
let request = test::TestRequest::get()
.uri(&uri)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"multiple resource parameters");
Ok(())
}
#[actix_web::test]
async fn request_with_missing_resource() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let request = test::TestRequest::get()
.uri(WELL_KNOWN_PATH)
.insert_header(("host", "example.org"))
.to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"missing resource parameter");
Ok(())
}
#[actix_web::test]
async fn request_with_no_host() -> Result {
let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
let app = test::init_service(app).await;
let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
let request = test::TestRequest::get().uri(&uri).to_request();
let response = test::call_service(&app, request).await;
assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
let body = to_bytes(response.into_body()).await?;
assert_eq!(body.as_ref(), b"missing host");
Ok(())
}
}