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