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(&params.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) = &params.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}