this repo has no description
1use crate::api::proxy_client::{
2 is_ssrf_safe, proxy_client, validate_at_uri, validate_limit, MAX_RESPONSE_SIZE,
3};
4use crate::api::ApiError;
5use crate::state::AppState;
6use axum::{
7 extract::{Query, State},
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11use serde::Deserialize;
12use std::collections::HashMap;
13use tracing::{error, info};
14#[derive(Deserialize)]
15pub struct GetFeedParams {
16 pub feed: String,
17 pub limit: Option<u32>,
18 pub cursor: Option<String>,
19}
20pub async fn get_feed(
21 State(state): State<AppState>,
22 headers: axum::http::HeaderMap,
23 Query(params): Query<GetFeedParams>,
24) -> Response {
25 let token = match crate::auth::extract_bearer_token_from_header(
26 headers.get("Authorization").and_then(|h| h.to_str().ok()),
27 ) {
28 Some(t) => t,
29 None => return ApiError::AuthenticationRequired.into_response(),
30 };
31 if let Err(e) = crate::auth::validate_bearer_token(&state.db, &token).await {
32 return ApiError::from(e).into_response();
33 };
34 if let Err(e) = validate_at_uri(¶ms.feed) {
35 return ApiError::InvalidRequest(format!("Invalid feed URI: {}", e)).into_response();
36 }
37 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
38 let appview_url = match std::env::var("APPVIEW_URL") {
39 Ok(url) => url,
40 Err(_) => {
41 return ApiError::UpstreamUnavailable("No upstream AppView configured".to_string())
42 .into_response();
43 }
44 };
45 if let Err(e) = is_ssrf_safe(&appview_url) {
46 error!("SSRF check failed for appview URL: {}", e);
47 return ApiError::UpstreamUnavailable(format!("Invalid upstream URL: {}", e))
48 .into_response();
49 }
50 let limit = validate_limit(params.limit, 50, 100);
51 let mut query_params = HashMap::new();
52 query_params.insert("feed".to_string(), params.feed.clone());
53 query_params.insert("limit".to_string(), limit.to_string());
54 if let Some(cursor) = ¶ms.cursor {
55 query_params.insert("cursor".to_string(), cursor.clone());
56 }
57 let target_url = format!("{}/xrpc/app.bsky.feed.getFeed", appview_url);
58 info!(target = %target_url, feed = %params.feed, "Proxying getFeed request");
59 let client = proxy_client();
60 let mut request_builder = client.get(&target_url).query(&query_params);
61 if let Some(auth) = auth_header {
62 request_builder = request_builder.header("Authorization", auth);
63 }
64 match request_builder.send().await {
65 Ok(resp) => {
66 let status =
67 StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
68 let content_length = resp.content_length().unwrap_or(0);
69 if content_length > MAX_RESPONSE_SIZE {
70 error!(
71 content_length,
72 max = MAX_RESPONSE_SIZE,
73 "getFeed response too large"
74 );
75 return ApiError::UpstreamFailure.into_response();
76 }
77 let resp_headers = resp.headers().clone();
78 let body = match resp.bytes().await {
79 Ok(b) => {
80 if b.len() as u64 > MAX_RESPONSE_SIZE {
81 error!(len = b.len(), "getFeed response body exceeded limit");
82 return ApiError::UpstreamFailure.into_response();
83 }
84 b
85 }
86 Err(e) => {
87 error!(error = ?e, "Error reading getFeed response");
88 return ApiError::UpstreamFailure.into_response();
89 }
90 };
91 let mut response_builder = axum::response::Response::builder().status(status);
92 if let Some(ct) = resp_headers.get("content-type") {
93 response_builder = response_builder.header("content-type", ct);
94 }
95 match response_builder.body(axum::body::Body::from(body)) {
96 Ok(r) => r,
97 Err(e) => {
98 error!(error = ?e, "Error building getFeed response");
99 ApiError::UpstreamFailure.into_response()
100 }
101 }
102 }
103 Err(e) => {
104 error!(error = ?e, "Error proxying getFeed");
105 if e.is_timeout() {
106 ApiError::UpstreamTimeout.into_response()
107 } else if e.is_connect() {
108 ApiError::UpstreamUnavailable("Failed to connect to upstream".to_string())
109 .into_response()
110 } else {
111 ApiError::UpstreamFailure.into_response()
112 }
113 }
114 }
115}