Skip to content

Commit 7842447

Browse files
committed
improve authorization flow, http basic auth, and error handling
1 parent dac7482 commit 7842447

File tree

4 files changed

+83
-59
lines changed

4 files changed

+83
-59
lines changed

examples/official-site/functions.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
SELECT 'authentication' AS component,
2+
sqlpage.basic_auth_password() AS password,
3+
'$argon2id$v=19$m=16,t=2,p=1$TERTd0lIcUpraWFTcmRQYw$+bjtag7Xjb6p1dsuYOkngw' AS password_hash;
4+
15
select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1;
26

37
select 'text' as component, 'SQLPage built-in functions' as title;

src/render.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,6 @@ impl<W: std::io::Write> HeaderContext<W> {
143143
fn authentication(mut self, data: &JsonValue) -> anyhow::Result<PageContext<W>> {
144144
use argon2::Argon2;
145145
use password_hash::PasswordHash;
146-
let link = get_object_str(data, "link")
147-
.with_context(|| "The authentication component requires a 'link' property")?;
148146
let password_hash = get_object_str(data, "password_hash");
149147
let password = get_object_str(data, "password");
150148
if let (Some(password), Some(password_hash)) = (password, password_hash) {
@@ -159,9 +157,16 @@ impl<W: std::io::Write> HeaderContext<W> {
159157
}
160158
}
161159
// The authentication failed
162-
self.response.status(StatusCode::FOUND);
163-
self.response.insert_header((header::LOCATION, link));
164-
self.has_status = true;
160+
if let Some(link) = get_object_str(data, "link") {
161+
self.response.status(StatusCode::FOUND);
162+
self.response.insert_header((header::LOCATION, link));
163+
self.has_status = true;
164+
} else {
165+
self.response.status(StatusCode::UNAUTHORIZED);
166+
self.response
167+
.insert_header((header::WWW_AUTHENTICATE, "Basic realm=\"Auth required\""));
168+
self.has_status = true;
169+
}
165170
Ok(PageContext::Close(self))
166171
}
167172

src/webserver/database/mod.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ pub async fn stream_query_results_direct<'a>(
120120
for res in &sql_file.statements {
121121
match res {
122122
Ok(stmt)=>{
123-
let query = bind_parameters(stmt, request)?;
123+
let query = bind_parameters(stmt, request)
124+
.with_context(|| format!("Unable to bind parameters to the SQL statement: {stmt}"))?;
124125
let mut stream = query.fetch_many(&db.connection);
125126
while let Some(elem) = stream.next().await {
126127
yield elem.with_context(|| format!("Error while running SQL: {stmt}"))
@@ -147,7 +148,8 @@ fn bind_parameters<'a>(
147148
) -> anyhow::Result<Query<'a, sqlx::Any, AnyArguments<'a>>> {
148149
let mut arguments = AnyArguments::default();
149150
for param in &stmt.parameters {
150-
let argument = extract_req_param(param, request)?;
151+
let argument = extract_req_param(param, request)
152+
.with_context(|| format!("Unable to extract {param:?} from the HTTP request"))?;
151153
log::debug!("Binding value {:?} in statement {}", &argument, stmt);
152154
match argument {
153155
None => arguments.add(None::<String>),
@@ -189,17 +191,21 @@ pub struct ErrorWithStatus {
189191
}
190192
impl std::fmt::Display for ErrorWithStatus {
191193
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192-
write!(f, "HTTP error with status {}", self.status)
194+
write!(f, "{}", self.status)
193195
}
194196
}
195197
impl std::error::Error for ErrorWithStatus {}
196198

197199
fn extract_basic_auth(request: &RequestInfo) -> anyhow::Result<&Basic> {
198-
request.basic_auth.as_ref().ok_or_else(|| {
199-
anyhow::Error::new(ErrorWithStatus {
200-
status: StatusCode::UNAUTHORIZED,
200+
request
201+
.basic_auth
202+
.as_ref()
203+
.ok_or_else(|| {
204+
anyhow::Error::new(ErrorWithStatus {
205+
status: StatusCode::UNAUTHORIZED,
206+
})
201207
})
202-
})
208+
.with_context(|| "Expected the user to be authenticated with HTTP basic auth")
203209
}
204210

205211
fn extract_basic_auth_username(request: &RequestInfo) -> anyhow::Result<&str> {

src/webserver/http.rs

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
use crate::render::{HeaderContext, PageContext, RenderContext};
2-
use crate::webserver::database::{stream_query_results, DbItem};
2+
use crate::webserver::database::{stream_query_results, DbItem, ErrorWithStatus};
33
use crate::{AppState, Config, ParsedSqlFile};
44
use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest};
55
use actix_web::error::ErrorInternalServerError;
66
use actix_web::http::header::{ContentType, Header, HttpDate, IfModifiedSince, LastModified};
7+
use actix_web::http::{header, StatusCode};
78
use actix_web::web::Form;
89
use actix_web::{
910
dev::ServiceResponse, middleware, middleware::Logger, web, web::Bytes, App, FromRequest,
1011
HttpResponse, HttpServer,
1112
};
1213

13-
use actix_web::body::MessageBody;
14+
use actix_web::body::{BoxBody, MessageBody};
1415
use actix_web_httpauth::headers::authorization::{Authorization, Basic};
1516
use anyhow::Context;
1617
use chrono::{DateTime, Utc};
@@ -145,56 +146,40 @@ async fn stream_response(
145146
async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
146147
app_state: Arc<AppState>,
147148
database_entries: S,
148-
) -> actix_web::Result<ResponseWithWriter<S>> {
149+
) -> anyhow::Result<ResponseWithWriter<S>> {
149150
let (sender, receiver) = mpsc::channel(MAX_PENDING_MESSAGES);
150151
let writer = ResponseWriter::new(sender);
151152
let mut head_context = HeaderContext::new(app_state, writer);
152153
let mut stream = Box::pin(database_entries);
153154
while let Some(item) = stream.next().await {
154155
match item {
155-
DbItem::Row(data) => {
156-
match head_context.handle_row(data).await.map_err(|e| {
157-
log::error!("Error while handling header context data: {e}");
158-
ErrorInternalServerError(e)
159-
})? {
160-
PageContext::Header(h) => {
161-
head_context = h;
162-
}
163-
PageContext::Body {
164-
mut http_response,
156+
DbItem::Row(data) => match head_context.handle_row(data).await? {
157+
PageContext::Header(h) => {
158+
head_context = h;
159+
}
160+
PageContext::Body {
161+
mut http_response,
162+
renderer,
163+
} => {
164+
let body_stream = tokio_stream::wrappers::ReceiverStream::new(receiver);
165+
let http_response = http_response.streaming(body_stream);
166+
return Ok(ResponseWithWriter {
167+
http_response,
165168
renderer,
166-
} => {
167-
let body_stream = tokio_stream::wrappers::ReceiverStream::new(receiver);
168-
let http_response = http_response.streaming(body_stream);
169-
return Ok(ResponseWithWriter {
170-
http_response,
171-
renderer,
172-
database_entries_stream: stream,
173-
});
174-
}
175-
PageContext::Close(h) => {
176-
head_context = h;
177-
break;
178-
}
169+
database_entries_stream: stream,
170+
});
179171
}
180-
}
181-
DbItem::FinishedQuery => {
182-
log::debug!("finished query");
183-
}
184-
DbItem::Error(source_err) => {
185-
let err = anyhow::format_err!(
186-
"An error occurred at the top of your SQL file: {source_err:#}"
187-
);
188-
log::error!("Response building error: {err}");
189-
return Err(ErrorInternalServerError(err));
190-
}
172+
PageContext::Close(h) => {
173+
head_context = h;
174+
break;
175+
}
176+
},
177+
DbItem::FinishedQuery => log::debug!("finished query"),
178+
DbItem::Error(source_err) => return Err(source_err),
191179
}
192180
}
193181
log::debug!("No SQL statements left to execute for the body of the response");
194-
let (renderer, http_response) = head_context.close().await.map_err(|e| {
195-
log::error!("Error while closing header context: {e}");
196-
ErrorInternalServerError(e)
197-
})?;
182+
let (renderer, http_response) = head_context.close().await?;
198183
Ok(ResponseWithWriter {
199184
http_response,
200185
renderer,
@@ -237,18 +222,42 @@ async fn render_sql(
237222
.unwrap_or_else(|e| log::error!("could not send headers {e:?}"));
238223
stream_response(database_entries_stream, renderer).await;
239224
}
240-
Err(e) => {
241-
log::error!("An error occured while building response headers: {e}");
242-
let http_response = ErrorInternalServerError(e).into();
243-
resp_send
244-
.send(http_response)
245-
.unwrap_or_else(|_| log::error!("could not send headers"));
225+
Err(err) => {
226+
send_anyhow_error(&err, resp_send);
246227
}
247228
}
248229
});
249230
resp_recv.await.map_err(ErrorInternalServerError)
250231
}
251232

233+
fn send_anyhow_error(e: &anyhow::Error, resp_send: tokio::sync::oneshot::Sender<HttpResponse>) {
234+
log::error!("An error occurred before starting to send the response body: {e:#}");
235+
let mut resp = HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR).set_body(BoxBody::new(
236+
format!("Sorry, but we were not able to process your request. \n\nError:\n\n {e:?}"),
237+
));
238+
resp.headers_mut().insert(
239+
header::CONTENT_TYPE,
240+
header::HeaderValue::from_static("text/plain"),
241+
);
242+
if let Some(&ErrorWithStatus { status }) = e.downcast_ref() {
243+
*resp.status_mut() = status;
244+
if status == StatusCode::UNAUTHORIZED {
245+
resp.headers_mut().insert(
246+
header::WWW_AUTHENTICATE,
247+
header::HeaderValue::from_static(
248+
"Basic realm=\"Authentication required\", charset=\"UTF-8\"",
249+
),
250+
);
251+
resp = resp.set_body(BoxBody::new(
252+
"Sorry, but you are not authorized to access this page.",
253+
));
254+
}
255+
};
256+
resp_send
257+
.send(resp)
258+
.unwrap_or_else(|_| log::error!("could not send headers"));
259+
}
260+
252261
type ParamMap = HashMap<String, SingleOrVec>;
253262

254263
#[derive(Debug)]

0 commit comments

Comments
 (0)