this repo has no description
1use anyhow::{anyhow, Context, Result};
2use axum::{extract::State, response::IntoResponse, Json};
3use axum_extra::extract::Query;
4use base64::{engine::general_purpose, Engine as _};
5use chrono::Utc;
6use http::{HeaderMap, StatusCode};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::errors::SupercellError;
11use crate::storage::{verification_method_get, StoragePool};
12
13use crate::crypto::{validate, JwtClaims, JwtHeader};
14
15use super::context::WebContext;
16
17#[derive(Deserialize, Default)]
18pub struct FeedParams {
19 pub feed: Option<String>,
20 pub limit: Option<u16>,
21 pub cursor: Option<String>,
22}
23
24#[derive(Serialize)]
25pub struct FeedItemView {
26 pub post: String,
27}
28
29#[derive(Serialize)]
30pub struct FeedItemsView {
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub cursor: Option<String>,
33 pub feed: Vec<FeedItemView>,
34}
35
36pub async fn handle_get_feed_skeleton(
37 State(web_context): State<WebContext>,
38 Query(feed_params): Query<FeedParams>,
39 headers: HeaderMap,
40) -> Result<impl IntoResponse, SupercellError> {
41 if feed_params.feed.is_none() {
42 return Err(anyhow!("feed parameter is required").into());
43 }
44 let feed_uri = feed_params.feed.unwrap();
45
46 let feed_control = web_context.feeds.get(&feed_uri);
47 if feed_control.is_none() {
48 return Ok((
49 StatusCode::BAD_REQUEST,
50 Json(json!({
51 "error": "UnknownFeed",
52 "message": "unknown feed",
53 })),
54 )
55 .into_response());
56 }
57
58 let feed_control = feed_control.unwrap();
59
60 if !feed_control.allowed.is_empty() {
61 let authorization = headers.get("Authorization").and_then(|value| {
62 value
63 .to_str()
64 .map(|inner_value| inner_value.to_string())
65 .ok()
66 });
67
68 let did = did_from_jwt(&web_context.pool, &web_context.external_base, authorization).await;
69
70 if let Err(err) = did {
71 tracing::info!(error = ?err, "failed to validate JWT");
72 return Ok(Json(FeedItemsView {
73 cursor: None,
74 feed: feed_control
75 .deny
76 .as_ref()
77 .map(|value| {
78 vec![FeedItemView {
79 post: value.clone(),
80 }]
81 })
82 .unwrap_or(vec![]),
83 })
84 .into_response());
85 }
86
87 let did = did.unwrap();
88
89 if !feed_control.allowed.contains(&did) {
90 return Ok(Json(FeedItemsView {
91 cursor: None,
92 feed: feed_control
93 .deny
94 .as_ref()
95 .map(|value| {
96 vec![FeedItemView {
97 post: value.clone(),
98 }]
99 })
100 .unwrap_or(vec![]),
101 })
102 .into_response());
103 }
104 }
105
106 let parsed_cursor = parse_cursor(feed_params.cursor)
107 .map(|value| value.clamp(0, 10000))
108 .unwrap_or(0) as usize;
109
110 let posts = web_context.cache.get_posts(&feed_uri, parsed_cursor).await;
111
112 if posts.is_none() {
113 return Ok((
114 StatusCode::BAD_REQUEST,
115 Json(json!({
116 "error": "UnknownFeed",
117 "message": "unknown feed",
118 })),
119 )
120 .into_response());
121 }
122 let posts = posts.unwrap();
123
124 let cursor = if posts.is_empty() {
125 Some(parsed_cursor.to_string())
126 } else {
127 Some((parsed_cursor + 1).to_string())
128 };
129
130 let feed_item_views = posts
131 .iter()
132 .map(|feed_item| FeedItemView {
133 post: feed_item.clone(),
134 })
135 .collect::<Vec<_>>();
136
137 Ok(Json(FeedItemsView {
138 cursor,
139 feed: feed_item_views,
140 })
141 .into_response())
142}
143
144pub fn split_token(token: &str) -> Result<[&str; 3]> {
145 let mut components = token.split('.');
146 let header = components.next().ok_or(anyhow!("missing header"))?;
147 let claims = components.next().ok_or(anyhow!("missing claims"))?;
148 let signature = components.next().ok_or(anyhow!("missing signature"))?;
149
150 if components.next().is_some() {
151 return Err(anyhow!("invalid token"));
152 }
153
154 Ok([header, claims, signature])
155}
156
157async fn did_from_jwt(
158 pool: &StoragePool,
159 external_base: &str,
160 authorization: Option<String>,
161) -> Result<String> {
162 let jwt = authorization
163 .and_then(|value| {
164 value
165 .strip_prefix("Bearer ")
166 .map(|inner_value| inner_value.to_string())
167 })
168 .ok_or(anyhow!("missing authorization"))?;
169 let [header_part, claims_part, signature_part] = split_token(&jwt)?;
170
171 let header: JwtHeader = {
172 let content = general_purpose::URL_SAFE_NO_PAD
173 .decode(header_part)
174 .context("unable to base64 decode content")?;
175 serde_json::from_slice(&content).context("unable to deserialize object")?
176 };
177 let claims: JwtClaims = {
178 let content = general_purpose::URL_SAFE_NO_PAD
179 .decode(claims_part)
180 .context("unable to base64 decode content")?;
181 serde_json::from_slice(&content).context("unable to deserialize object")?
182 };
183
184 let now = Utc::now();
185 let now = now.timestamp() as i32;
186
187 if header.alg != "ES256K" {
188 return Err(anyhow!("unsupported algorithm"));
189 }
190 if claims.lxm != "app.bsky.feed.getFeedSkeleton" {
191 return Err(anyhow!("invalid resource"));
192 }
193 if claims.aud != format!("did:web:{}", external_base) {
194 return Err(anyhow!("invalid audience"));
195 }
196 if claims.exp < now {
197 return Err(anyhow!("token expired"));
198 }
199 if claims.iat > now {
200 return Err(anyhow!("token issued in the future"));
201 }
202
203 let multibase = verification_method_get(pool, &claims.iss).await?;
204 if multibase.is_none() {
205 return Err(anyhow!("verification method not found"));
206 }
207 let multibase = multibase.unwrap();
208
209 let signature = general_purpose::URL_SAFE_NO_PAD
210 .decode(signature_part)
211 .context("invalid signature")?;
212 let signature: &[u8] = &signature;
213
214 let content = format!("{}.{}", header_part, claims_part);
215
216 validate(&multibase, signature, &content)?;
217
218 Ok(claims.iss)
219}
220
221fn parse_cursor(value: Option<String>) -> Option<i64> {
222 let value = value.as_ref()?;
223
224 let parts = value.split(",").collect::<Vec<&str>>();
225
226 parts.first().and_then(|value| value.parse::<i64>().ok())
227}