···11+use crate::api::proxy_client::{
22+ is_ssrf_safe, proxy_client, validate_at_uri, validate_limit, MAX_RESPONSE_SIZE,
33+};
44+use crate::api::ApiError;
55+use crate::state::AppState;
66+use axum::{
77+ extract::{Query, State},
88+ http::StatusCode,
99+ response::{IntoResponse, Response},
1010+};
1111+use serde::Deserialize;
1212+use std::collections::HashMap;
1313+use tracing::{error, info};
1414+1515+#[derive(Deserialize)]
1616+pub struct GetFeedParams {
1717+ pub feed: String,
1818+ pub limit: Option<u32>,
1919+ pub cursor: Option<String>,
2020+}
2121+2222+pub async fn get_feed(
2323+ State(state): State<AppState>,
2424+ headers: axum::http::HeaderMap,
2525+ Query(params): Query<GetFeedParams>,
2626+) -> Response {
2727+ let token = match crate::auth::extract_bearer_token_from_header(
2828+ headers.get("Authorization").and_then(|h| h.to_str().ok()),
2929+ ) {
3030+ Some(t) => t,
3131+ None => return ApiError::AuthenticationRequired.into_response(),
3232+ };
3333+3434+ if let Err(e) = crate::auth::validate_bearer_token(&state.db, &token).await {
3535+ return ApiError::from(e).into_response();
3636+ };
3737+3838+ if let Err(e) = validate_at_uri(¶ms.feed) {
3939+ return ApiError::InvalidRequest(format!("Invalid feed URI: {}", e)).into_response();
4040+ }
4141+4242+ let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
4343+4444+ let appview_url = match std::env::var("APPVIEW_URL") {
4545+ Ok(url) => url,
4646+ Err(_) => {
4747+ return ApiError::UpstreamUnavailable("No upstream AppView configured".to_string())
4848+ .into_response();
4949+ }
5050+ };
5151+5252+ if let Err(e) = is_ssrf_safe(&appview_url) {
5353+ error!("SSRF check failed for appview URL: {}", e);
5454+ return ApiError::UpstreamUnavailable(format!("Invalid upstream URL: {}", e))
5555+ .into_response();
5656+ }
5757+5858+ let limit = validate_limit(params.limit, 50, 100);
5959+ let mut query_params = HashMap::new();
6060+ query_params.insert("feed".to_string(), params.feed.clone());
6161+ query_params.insert("limit".to_string(), limit.to_string());
6262+ if let Some(cursor) = ¶ms.cursor {
6363+ query_params.insert("cursor".to_string(), cursor.clone());
6464+ }
6565+6666+ let target_url = format!("{}/xrpc/app.bsky.feed.getFeed", appview_url);
6767+ info!(target = %target_url, feed = %params.feed, "Proxying getFeed request");
6868+6969+ let client = proxy_client();
7070+ let mut request_builder = client.get(&target_url).query(&query_params);
7171+7272+ if let Some(auth) = auth_header {
7373+ request_builder = request_builder.header("Authorization", auth);
7474+ }
7575+7676+ match request_builder.send().await {
7777+ Ok(resp) => {
7878+ let status =
7979+ StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
8080+8181+ let content_length = resp.content_length().unwrap_or(0);
8282+ if content_length > MAX_RESPONSE_SIZE {
8383+ error!(
8484+ content_length,
8585+ max = MAX_RESPONSE_SIZE,
8686+ "getFeed response too large"
8787+ );
8888+ return ApiError::UpstreamFailure.into_response();
8989+ }
9090+9191+ let resp_headers = resp.headers().clone();
9292+ let body = match resp.bytes().await {
9393+ Ok(b) => {
9494+ if b.len() as u64 > MAX_RESPONSE_SIZE {
9595+ error!(len = b.len(), "getFeed response body exceeded limit");
9696+ return ApiError::UpstreamFailure.into_response();
9797+ }
9898+ b
9999+ }
100100+ Err(e) => {
101101+ error!(error = ?e, "Error reading getFeed response");
102102+ return ApiError::UpstreamFailure.into_response();
103103+ }
104104+ };
105105+106106+ let mut response_builder = axum::response::Response::builder().status(status);
107107+ if let Some(ct) = resp_headers.get("content-type") {
108108+ response_builder = response_builder.header("content-type", ct);
109109+ }
110110+111111+ match response_builder.body(axum::body::Body::from(body)) {
112112+ Ok(r) => r,
113113+ Err(e) => {
114114+ error!(error = ?e, "Error building getFeed response");
115115+ ApiError::UpstreamFailure.into_response()
116116+ }
117117+ }
118118+ }
119119+ Err(e) => {
120120+ error!(error = ?e, "Error proxying getFeed");
121121+ if e.is_timeout() {
122122+ ApiError::UpstreamTimeout.into_response()
123123+ } else if e.is_connect() {
124124+ ApiError::UpstreamUnavailable("Failed to connect to upstream".to_string())
125125+ .into_response()
126126+ } else {
127127+ ApiError::UpstreamFailure.into_response()
128128+ }
129129+ }
130130+ }
131131+}
+8
src/api/feed/mod.rs
···11+mod actor_likes;
22+mod author_feed;
33+mod custom_feed;
44+mod post_thread;
15mod timeline;
2677+pub use actor_likes::get_actor_likes;
88+pub use author_feed::get_author_feed;
99+pub use custom_feed::get_feed;
1010+pub use post_thread::get_post_thread;
311pub use timeline::get_timeline;
···44pub mod feed;
55pub mod identity;
66pub mod moderation;
77+pub mod notification;
78pub mod proxy;
99+pub mod proxy_client;
1010+pub mod read_after_write;
811pub mod repo;
912pub mod server;
1013pub mod validation;
11141215pub use error::ApiError;
1616+pub use proxy_client::{proxy_client, validate_at_uri, validate_did, validate_limit, AtUriParts};
+3
src/api/notification/mod.rs
···11+mod register_push;
22+33+pub use register_push::register_push;