1use crate::expiring::Expiring;
2use anyhow::{anyhow, Context};
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4use std::{
5 future::Future,
6 sync::Arc,
7 time::{Duration, Instant},
8};
9use surf::{http::Method, RequestBuilder, Url};
10
11#[derive(Debug, Deserialize, Serialize)]
12pub struct Release {
13 pub tag_name: String,
14 pub name: String,
15 pub body: String,
16 pub draft: bool,
17 pub assets: Vec<Asset>,
18}
19
20#[derive(Debug, Deserialize, Serialize)]
21pub struct Asset {
22 pub name: String,
23 pub url: String,
24}
25
26pub struct AppClient {
27 id: usize,
28 private_key: String,
29 jwt_bearer_header: Expiring<String>,
30}
31
32#[derive(Deserialize)]
33struct Installation {
34 #[allow(unused)]
35 id: usize,
36}
37
38impl AppClient {
39 #[cfg(test)]
40 pub fn test() -> Arc<Self> {
41 Arc::new(Self {
42 id: Default::default(),
43 private_key: Default::default(),
44 jwt_bearer_header: Default::default(),
45 })
46 }
47
48 pub fn new(id: usize, private_key: String) -> Arc<Self> {
49 Arc::new(Self {
50 id,
51 private_key,
52 jwt_bearer_header: Default::default(),
53 })
54 }
55
56 pub async fn repo(self: &Arc<Self>, nwo: String) -> tide::Result<RepoClient> {
57 let installation: Installation = self
58 .request(
59 Method::Get,
60 &format!("/repos/{}/installation", &nwo),
61 |refresh| self.bearer_header(refresh),
62 )
63 .await?;
64
65 Ok(RepoClient {
66 app: self.clone(),
67 nwo,
68 installation_id: installation.id,
69 installation_token_header: Default::default(),
70 })
71 }
72
73 pub fn user(self: &Arc<Self>, access_token: String) -> UserClient {
74 UserClient {
75 app: self.clone(),
76 access_token,
77 }
78 }
79
80 async fn request<T, F, G>(
81 &self,
82 method: Method,
83 path: &str,
84 get_auth_header: F,
85 ) -> tide::Result<T>
86 where
87 T: DeserializeOwned,
88 F: Fn(bool) -> G,
89 G: Future<Output = tide::Result<String>>,
90 {
91 let mut retried = false;
92
93 loop {
94 let response = RequestBuilder::new(
95 method,
96 Url::parse(&format!("https://api.github.com{}", path))?,
97 )
98 .header("Accept", "application/vnd.github.v3+json")
99 .header("Authorization", get_auth_header(retried).await?)
100 .recv_json()
101 .await;
102
103 if let Err(error) = response.as_ref() {
104 if error.status() == 401 && !retried {
105 retried = true;
106 continue;
107 }
108 }
109
110 return response;
111 }
112 }
113
114 async fn bearer_header(&self, refresh: bool) -> tide::Result<String> {
115 if refresh {
116 self.jwt_bearer_header.clear().await;
117 }
118
119 self.jwt_bearer_header
120 .get_or_refresh(|| async {
121 use jwt_simple::{algorithms::RS256KeyPair, prelude::*};
122 use std::time;
123
124 let key_pair = RS256KeyPair::from_pem(&self.private_key)
125 .with_context(|| format!("invalid private key {:?}", self.private_key))?;
126 let mut claims = Claims::create(Duration::from_mins(10));
127 claims.issued_at = Some(Clock::now_since_epoch() - Duration::from_mins(1));
128 claims.issuer = Some(self.id.to_string());
129 let token = key_pair.sign(claims).context("failed to sign claims")?;
130 let expires_at = time::Instant::now() + time::Duration::from_secs(9 * 60);
131
132 Ok((format!("Bearer {}", token), expires_at))
133 })
134 .await
135 }
136
137 async fn installation_token_header(
138 &self,
139 header: &Expiring<String>,
140 installation_id: usize,
141 refresh: bool,
142 ) -> tide::Result<String> {
143 if refresh {
144 header.clear().await;
145 }
146
147 header
148 .get_or_refresh(|| async {
149 #[derive(Debug, Deserialize)]
150 struct AccessToken {
151 token: String,
152 }
153
154 let access_token: AccessToken = self
155 .request(
156 Method::Post,
157 &format!("/app/installations/{}/access_tokens", installation_id),
158 |refresh| self.bearer_header(refresh),
159 )
160 .await?;
161
162 let header = format!("Token {}", access_token.token);
163 let expires_at = Instant::now() + Duration::from_secs(60 * 30);
164
165 Ok((header, expires_at))
166 })
167 .await
168 }
169}
170
171pub struct RepoClient {
172 app: Arc<AppClient>,
173 nwo: String,
174 installation_id: usize,
175 installation_token_header: Expiring<String>,
176}
177
178impl RepoClient {
179 #[cfg(test)]
180 pub fn test(app_client: &Arc<AppClient>) -> Self {
181 Self {
182 app: app_client.clone(),
183 nwo: String::new(),
184 installation_id: 0,
185 installation_token_header: Default::default(),
186 }
187 }
188
189 pub async fn releases(&self) -> tide::Result<Vec<Release>> {
190 self.get(&format!("/repos/{}/releases?per_page=100", self.nwo))
191 .await
192 }
193
194 pub async fn release_asset(&self, tag: &str, name: &str) -> tide::Result<surf::Body> {
195 let release: Release = self
196 .get(&format!("/repos/{}/releases/tags/{}", self.nwo, tag))
197 .await?;
198
199 let asset = release
200 .assets
201 .iter()
202 .find(|asset| asset.name == name)
203 .ok_or_else(|| anyhow!("no asset found with name {}", name))?;
204
205 let request = surf::get(&asset.url)
206 .header("Accept", "application/octet-stream'")
207 .header(
208 "Authorization",
209 self.installation_token_header(false).await?,
210 );
211
212 let client = surf::client();
213 let mut response = client.send(request).await?;
214
215 // Avoid using `surf::middleware::Redirect` because that type forwards
216 // the original request headers to the redirect URI. In this case, the
217 // redirect will be to S3, which forbids us from supplying an
218 // `Authorization` header.
219 if response.status().is_redirection() {
220 if let Some(url) = response.header("location") {
221 let request = surf::get(url.as_str()).header("Accept", "application/octet-stream");
222 response = client.send(request).await?;
223 }
224 }
225
226 if !response.status().is_success() {
227 Err(anyhow!("failed to fetch release asset {} {}", tag, name))?;
228 }
229
230 Ok(response.take_body())
231 }
232
233 async fn get<T: DeserializeOwned>(&self, path: &str) -> tide::Result<T> {
234 self.request::<T>(Method::Get, path).await
235 }
236
237 async fn request<T: DeserializeOwned>(&self, method: Method, path: &str) -> tide::Result<T> {
238 Ok(self
239 .app
240 .request(method, path, |refresh| {
241 self.installation_token_header(refresh)
242 })
243 .await?)
244 }
245
246 async fn installation_token_header(&self, refresh: bool) -> tide::Result<String> {
247 self.app
248 .installation_token_header(
249 &self.installation_token_header,
250 self.installation_id,
251 refresh,
252 )
253 .await
254 }
255}
256
257pub struct UserClient {
258 app: Arc<AppClient>,
259 access_token: String,
260}
261
262#[derive(Clone, Debug, Deserialize, Serialize)]
263pub struct User {
264 pub login: String,
265 pub avatar_url: String,
266}
267
268impl UserClient {
269 pub async fn details(&self) -> tide::Result<User> {
270 Ok(self
271 .app
272 .request(Method::Get, "/user", |_| async {
273 Ok(self.access_token_header())
274 })
275 .await?)
276 }
277
278 fn access_token_header(&self) -> String {
279 format!("Token {}", self.access_token)
280 }
281}