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
37 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
38 Ok(user) => user,
39 Err(e) => return ApiError::from(e).into_response(),
40 };
41
42 if let Err(e) = validate_did(&input.service_did) {
43 return ApiError::InvalidRequest(format!("Invalid serviceDid: {}", e)).into_response();
44 }
45
46 if input.token.is_empty() || input.token.len() > 4096 {
47 return ApiError::InvalidRequest("Invalid push token".to_string()).into_response();
48 }
49
50 if !VALID_PLATFORMS.contains(&input.platform.as_str()) {
51 return ApiError::InvalidRequest(format!(
52 "Invalid platform. Must be one of: {}",
53 VALID_PLATFORMS.join(", ")
54 ))
55 .into_response();
56 }
57
58 if input.app_id.is_empty() || input.app_id.len() > 256 {
59 return ApiError::InvalidRequest("Invalid appId".to_string()).into_response();
60 }
61
62 let appview_url = match std::env::var("APPVIEW_URL") {
63 Ok(url) => url,
64 Err(_) => {
65 return ApiError::UpstreamUnavailable("No upstream AppView configured".to_string())
66 .into_response();
67 }
68 };
69
70 if let Err(e) = is_ssrf_safe(&appview_url) {
71 error!("SSRF check failed for appview URL: {}", e);
72 return ApiError::UpstreamUnavailable(format!("Invalid upstream URL: {}", e))
73 .into_response();
74 }
75
76 let key_row = match sqlx::query!(
77 "SELECT key_bytes, encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1",
78 auth_user.did
79 )
80 .fetch_optional(&state.db)
81 .await
82 {
83 Ok(Some(row)) => row,
84 Ok(None) => {
85 error!(did = %auth_user.did, "No signing key found for user");
86 return ApiError::InternalError.into_response();
87 }
88 Err(e) => {
89 error!(error = ?e, "Database error fetching signing key");
90 return ApiError::DatabaseError.into_response();
91 }
92 };
93
94 let decrypted_key =
95 match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
96 Ok(k) => k,
97 Err(e) => {
98 error!(error = ?e, "Failed to decrypt signing key");
99 return ApiError::InternalError.into_response();
100 }
101 };
102
103 let service_token = match crate::auth::create_service_token(
104 &auth_user.did,
105 &input.service_did,
106 "app.bsky.notification.registerPush",
107 &decrypted_key,
108 ) {
109 Ok(t) => t,
110 Err(e) => {
111 error!(error = ?e, "Failed to create service token");
112 return ApiError::InternalError.into_response();
113 }
114 };
115
116 let target_url = format!("{}/xrpc/app.bsky.notification.registerPush", appview_url);
117 info!(
118 target = %target_url,
119 service_did = %input.service_did,
120 platform = %input.platform,
121 "Proxying registerPush request"
122 );
123
124 let client = proxy_client();
125 let request_body = json!({
126 "serviceDid": input.service_did,
127 "token": input.token,
128 "platform": input.platform,
129 "appId": input.app_id
130 });
131
132 match client
133 .post(&target_url)
134 .header("Authorization", format!("Bearer {}", service_token))
135 .header("Content-Type", "application/json")
136 .json(&request_body)
137 .send()
138 .await
139 {
140 Ok(resp) => {
141 let status =
142 StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
143 if status.is_success() {
144 StatusCode::OK.into_response()
145 } else {
146 let body = resp.bytes().await.unwrap_or_default();
147 error!(
148 status = %status,
149 "registerPush upstream error"
150 );
151 ApiError::from_upstream_response(status.as_u16(), &body).into_response()
152 }
153 }
154 Err(e) => {
155 error!(error = ?e, "Error proxying registerPush");
156 if e.is_timeout() {
157 ApiError::UpstreamTimeout.into_response()
158 } else if e.is_connect() {
159 ApiError::UpstreamUnavailable("Failed to connect to upstream".to_string())
160 .into_response()
161 } else {
162 ApiError::UpstreamFailure.into_response()
163 }
164 }
165 }
166}