···
use atrium_api::agent::SessionManager;
3
-
ApiDescription, Body, ConfigDropshot, ConfigLogging, ConfigLoggingLevel, HttpError,
4
-
HttpResponse, HttpResponseSeeOther, Query, RequestContext, ServerBuilder, ServerContext,
5
-
endpoint, http_response_see_other,
2
+
use atrium_oauth::CallbackParams;
5
+
extract::{Query, State},
6
+
response::{Html, Redirect},
8
-
Response, StatusCode,
9
-
header::{ORIGIN, USER_AGENT},
11
-
use metrics::{counter, histogram};
12
-
use std::error::Error;
10
+
use serde::Deserialize;
15
-
use atrium_oauth::CallbackParams;
16
-
use schemars::JsonSchema;
17
-
use serde::{Deserialize, Serialize};
18
-
use tokio::time::Instant;
12
+
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use crate::{Client, authorize, client};
···
const INDEX_HTML: &str = include_str!("../static/index.html");
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
26
-
pub async fn serve(shutdown: CancellationToken) -> Result<(), Box<dyn Error + Send + Sync>> {
27
-
let config_logging = ConfigLogging::StderrTerminal {
28
-
level: ConfigLoggingLevel::Info,
20
+
pub async fn serve(shutdown: CancellationToken) {
21
+
let state = AppState {
22
+
client: Arc::new(client()),
31
-
let log = config_logging.to_logger("example-basic")?;
25
+
let app = Router::new()
26
+
.route("/", get(|| async { Html(INDEX_HTML) }))
27
+
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
28
+
.route("/auth", get(start_oauth))
29
+
.route("/authorized", get(complete_oauth))
33
-
let mut api = ApiDescription::new();
34
-
api.register(index).unwrap();
35
-
api.register(favicon).unwrap();
36
-
api.register(openapi).unwrap();
37
-
api.register(start_oauth).unwrap();
38
-
api.register(finish_oauth).unwrap();
40
-
// TODO: put spec in a once cell / lazy lock thing?
41
-
let spec = Arc::new(
44
-
env!("CARGO_PKG_VERSION")
47
-
eprintln!("failed to parse cargo package version for openapi: {e:?}")
49
-
.unwrap_or(semver::Version::new(0, 0, 1)),
51
-
.description("An atproto identity verifier that is very much not ready for real use")
52
-
.contact_name("part of @microcosm.blue")
53
-
.contact_url("https://microcosm.blue")
59
-
client: client().into(),
32
+
let listener = TcpListener::bind("0.0.0.0:9997")
34
+
.expect("listener binding to work");
62
-
let server = ServerBuilder::new(api, ctx, log)
63
-
.config(ConfigDropshot {
64
-
bind_address: "0.0.0.0:9997".parse().unwrap(),
65
-
..Default::default()
70
-
s = server.wait_for_shutdown() => {
72
-
log::info!("server shut down normally.");
74
-
_ = shutdown.cancelled() => {
75
-
log::info!("shutting down: closing server");
76
-
server.close().await?;
36
+
axum::serve(listener, app)
37
+
.with_graceful_shutdown(async move { shutdown.cancelled().await })
84
-
pub spec: Arc<serde_json::Value>,
88
-
async fn instrument_handler<T, H, R>(ctx: &RequestContext<T>, handler: H) -> Result<R, HttpError>
91
-
H: Future<Output = Result<R, HttpError>>,
94
-
let start = Instant::now();
95
-
let result = handler.await;
96
-
let latency = start.elapsed();
97
-
let status_code = match &result {
98
-
Ok(response) => response.status_code(),
99
-
Err(e) => e.status_code.as_status(),
101
-
.as_str() // just the number (.to_string()'s Display does eg `200 OK`)
103
-
let endpoint = ctx.endpoint.operation_id.clone();
104
-
let headers = ctx.request.headers();
105
-
let origin = headers
107
-
.and_then(|v| v.to_str().ok())
112
-
.and_then(|v| v.to_str().ok())
114
-
if ua.starts_with("Mozilla/5.0 ") {
122
-
counter!("server_requests_total",
123
-
"endpoint" => endpoint.clone(),
124
-
"origin" => origin,
126
-
"status_code" => status_code,
129
-
histogram!("server_handler_latency", "endpoint" => endpoint).record(latency.as_micros() as f64);
133
-
use dropshot::{HttpResponseHeaders, HttpResponseOk};
135
-
pub type OkCorsResponse<T> = Result<HttpResponseHeaders<HttpResponseOk<T>>, HttpError>;
137
-
/// Helper for constructing Ok responses: return OkCors(T).into()
138
-
/// (not happy with this yet)
139
-
pub struct OkCors<T: Serialize + JsonSchema + Send + Sync>(pub T);
141
-
impl<T> From<OkCors<T>> for OkCorsResponse<T>
143
-
T: Serialize + JsonSchema + Send + Sync,
145
-
fn from(ok: OkCors<T>) -> OkCorsResponse<T> {
146
-
let mut res = HttpResponseHeaders::new_unnamed(HttpResponseOk(ok.0));
148
-
.insert("access-control-allow-origin", "*".parse().unwrap());
153
-
// TODO: cors for HttpError
155
-
/// Serve index page as html
160
-
* not useful to have this in openapi
162
-
unpublished = true,
164
-
async fn index(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
165
-
instrument_handler(&ctx, async {
166
-
Ok(Response::builder()
167
-
.status(StatusCode::OK)
168
-
.header(http::header::CONTENT_TYPE, "text/html")
169
-
.body(INDEX_HTML.into())?)
174
-
/// Serve index page as html
177
-
path = "/favicon.ico",
179
-
* not useful to have this in openapi
181
-
unpublished = true,
183
-
async fn favicon(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
184
-
instrument_handler(&ctx, async {
185
-
Ok(Response::builder()
186
-
.status(StatusCode::OK)
187
-
.header(http::header::CONTENT_TYPE, "image/x-icon")
188
-
.body(FAVICON.to_vec().into())?)
193
-
/// Meta: get the openapi spec for this api
198
-
* not useful to have this in openapi
200
-
unpublished = true,
202
-
async fn openapi(ctx: RequestContext<Context>) -> OkCorsResponse<serde_json::Value> {
203
-
instrument_handler(&ctx, async {
204
-
let spec = (*ctx.context().spec).clone();
205
-
OkCors(spec).into()
210
-
#[derive(Debug, Deserialize, JsonSchema)]
211
-
struct BeginOauthQuery {
47
+
#[derive(Debug, Deserialize)]
48
+
struct BeginOauthParams {
219
-
ctx: RequestContext<Context>,
220
-
query: Query<BeginOauthQuery>,
221
-
) -> Result<HttpResponseSeeOther, HttpError> {
222
-
let BeginOauthQuery { handle } = query.into_inner();
224
-
instrument_handler(&ctx, async {
225
-
let Context { client, .. } = ctx.context();
227
-
let auth_url = authorize(client, &handle).await;
229
-
http_response_see_other(auth_url)
234
-
#[derive(Debug, Deserialize, JsonSchema)]
235
-
struct AuthorizedCallbackQuery {
237
-
state: Option<String>,
238
-
iss: Option<String>,
240
-
impl From<AuthorizedCallbackQuery> for CallbackParams {
241
-
fn from(q: AuthorizedCallbackQuery) -> Self {
242
-
let AuthorizedCallbackQuery { code, state, iss } = q;
243
-
Self { code, state, iss }
52
+
State(state): State<AppState>,
53
+
Query(params): Query<BeginOauthParams>,
55
+
let AppState { client } = state;
56
+
let BeginOauthParams { handle } = params;
57
+
let auth_url = authorize(&client, &handle).await;
58
+
Redirect::to(&auth_url)
248
-
path = "/authorized",
250
-
async fn finish_oauth(
251
-
ctx: RequestContext<Context>,
252
-
query: Query<AuthorizedCallbackQuery>,
253
-
) -> Result<Response<Body>, HttpError> {
254
-
instrument_handler(&ctx, async {
255
-
let Context { client, .. } = ctx.context();
256
-
let params = query.into_inner();
258
-
let Ok((oauth_session, _)) = client.callback(params.into()).await else {
259
-
panic!("failed to do client callback");
262
-
let did = oauth_session.did().await.expect("a did to be present");
264
-
Ok(Response::builder()
265
-
.status(StatusCode::OK)
266
-
.header(http::header::CONTENT_TYPE, "text/html")
267
-
.body(format!("sup: {did:?}").into())?)
61
+
async fn complete_oauth(
62
+
State(state): State<AppState>,
63
+
Query(params): Query<CallbackParams>,
65
+
let AppState { client } = state;
66
+
let Ok((oauth_session, _)) = client.callback(params).await else {
67
+
panic!("failed to do client callback");
69
+
let did = oauth_session.did().await.expect("a did to be present");
70
+
Html(format!("sup: {did:?}"))