this repo has no description
1use crate::api::proxy_client::{is_ssrf_safe, proxy_client, validate_did};
2use crate::api::ApiError;
3use crate::state::AppState;
4use axum::{
5 extract::State,
6 http::{HeaderMap, StatusCode},
7 response::{IntoResponse, Response},
8 Json,
9};
10use serde::Deserialize;
11use serde_json::json;
12use tracing::{error, info};
13
14#[derive(Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct RegisterPushInput {
17 pub service_did: String,
18 pub token: String,
19 pub platform: String,
20 pub app_id: String,
21}
22
23const VALID_PLATFORMS: &[&str] = &["ios", "android", "web"];
24
25pub async fn register_push(
26 State(state): State<AppState>,
27 headers: HeaderMap,
28 Json(input): Json<RegisterPushInput>,
29) -> Response {
30 let token = match crate::auth::extract_bearer_token_from_header(
31 headers.get("Authorization").and_then(|h| h.to_str().ok()),
32 ) {
33 Some(t) => t,
34 None => return ApiError::AuthenticationRequired.into_response(),
35 };
36 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
37 Ok(user) => user,
38 Err(e) => return ApiError::from(e).into_response(),
39 };
40 if let Err(e) = validate_did(&input.service_did) {
41 return ApiError::InvalidRequest(format!("Invalid serviceDid: {}", e)).into_response();
42 }
43 if input.token.is_empty() || input.token.len() > 4096 {
44 return ApiError::InvalidRequest("Invalid push token".to_string()).into_response();
45 }
46 if !VALID_PLATFORMS.contains(&input.platform.as_str()) {
47 return ApiError::InvalidRequest(format!(
48 "Invalid platform. Must be one of: {}",
49 VALID_PLATFORMS.join(", ")
50 ))
51 .into_response();
52 }
53 if input.app_id.is_empty() || input.app_id.len() > 256 {
54 return ApiError::InvalidRequest("Invalid appId".to_string()).into_response();
55 }
56 let appview_url = match std::env::var("APPVIEW_URL") {
57 Ok(url) => url,
58 Err(_) => {
59 return ApiError::UpstreamUnavailable("No upstream AppView configured".to_string())
60 .into_response();
61 }
62 };
63 if let Err(e) = is_ssrf_safe(&appview_url) {
64 error!("SSRF check failed for appview URL: {}", e);
65 return ApiError::UpstreamUnavailable(format!("Invalid upstream URL: {}", e))
66 .into_response();
67 }
68 let key_row = match sqlx::query!(
69 "SELECT key_bytes, encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1",
70 auth_user.did
71 )
72 .fetch_optional(&state.db)
73 .await
74 {
75 Ok(Some(row)) => row,
76 Ok(None) => {
77 error!(did = %auth_user.did, "No signing key found for user");
78 return ApiError::InternalError.into_response();
79 }
80 Err(e) => {
81 error!(error = ?e, "Database error fetching signing key");
82 return ApiError::DatabaseError.into_response();
83 }
84 };
85 let decrypted_key =
86 match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
87 Ok(k) => k,
88 Err(e) => {
89 error!(error = ?e, "Failed to decrypt signing key");
90 return ApiError::InternalError.into_response();
91 }
92 };
93 let service_token = match crate::auth::create_service_token(
94 &auth_user.did,
95 &input.service_did,
96 "app.bsky.notification.registerPush",
97 &decrypted_key,
98 ) {
99 Ok(t) => t,
100 Err(e) => {
101 error!(error = ?e, "Failed to create service token");
102 return ApiError::InternalError.into_response();
103 }
104 };
105 let target_url = format!("{}/xrpc/app.bsky.notification.registerPush", appview_url);
106 info!(
107 target = %target_url,
108 service_did = %input.service_did,
109 platform = %input.platform,
110 "Proxying registerPush request"
111 );
112 let client = proxy_client();
113 let request_body = json!({
114 "serviceDid": input.service_did,
115 "token": input.token,
116 "platform": input.platform,
117 "appId": input.app_id
118 });
119 match client
120 .post(&target_url)
121 .header("Authorization", format!("Bearer {}", service_token))
122 .header("Content-Type", "application/json")
123 .json(&request_body)
124 .send()
125 .await
126 {
127 Ok(resp) => {
128 let status =
129 StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
130 if status.is_success() {
131 StatusCode::OK.into_response()
132 } else {
133 let body = resp.bytes().await.unwrap_or_default();
134 error!(
135 status = %status,
136 "registerPush upstream error"
137 );
138 ApiError::from_upstream_response(status.as_u16(), &body).into_response()
139 }
140 }
141 Err(e) => {
142 error!(error = ?e, "Error proxying registerPush");
143 if e.is_timeout() {
144 ApiError::UpstreamTimeout.into_response()
145 } else if e.is_connect() {
146 ApiError::UpstreamUnavailable("Failed to connect to upstream".to_string())
147 .into_response()
148 } else {
149 ApiError::UpstreamFailure.into_response()
150 }
151 }
152 }
153}