fix2 tokens
This commit is contained in:
1 parent
49dad1a5f4
commit
e3ee6319b9
25 files changed
+1420
-223
No files matched your search
+21
-2
@@ -19,11 +19,30 @@ use axum::{
|
||||
};
|
||||
use pages::*;
|
||||
use tower::{ServiceBuilder, service_fn};
|
||||
use tower_http::{services::ServeDir, set_header::SetResponseHeaderLayer, trace::TraceLayer};
|
||||
use tower_http::{
|
||||
services::ServeDir,
|
||||
set_header::SetResponseHeaderLayer,
|
||||
trace::{MakeSpan, TraceLayer},
|
||||
};
|
||||
use tracing::Span;
|
||||
|
||||
use crate::{api, auth, state::SharedState, websocket};
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PathOnlyMakeSpan;
|
||||
|
||||
impl<B> MakeSpan<B> for PathOnlyMakeSpan {
|
||||
fn make_span(&mut self, request: &axum::http::Request<B>) -> Span {
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = %request.method(),
|
||||
path = %request.uri().path(),
|
||||
version = ?request.version(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router(
|
||||
state: SharedState,
|
||||
static_dir: &str,
|
||||
@@ -208,7 +227,7 @@ pub fn router(
|
||||
.layer(DefaultBodyLimit::max(
|
||||
upload_max_size_bytes.saturating_add(1024 * 1024),
|
||||
))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(TraceLayer::new_for_http().make_span_with(PathOnlyMakeSpan))
|
||||
.layer(middleware::from_fn(require_csrf_token))
|
||||
.layer(middleware::from_fn(apply_response_header_policy))
|
||||
.with_state(state)
|
||||
|
||||
+222
-34
@@ -8,12 +8,123 @@
|
||||
*/
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderValue, StatusCode, header},
|
||||
extract::{OriginalUri, Path, RawQuery, State},
|
||||
http::{HeaderMap, HeaderValue, StatusCode, Uri, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
use crate::{assets, db, state::SharedState};
|
||||
use crate::{assets, auth, db, state::SharedState};
|
||||
|
||||
fn decode_query_component(value: &str) -> Option<String> {
|
||||
let bytes = value.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
match bytes[index] {
|
||||
b'%' if index + 2 < bytes.len() => {
|
||||
let high = (bytes[index + 1] as char).to_digit(16)? as u8;
|
||||
let low = (bytes[index + 2] as char).to_digit(16)? as u8;
|
||||
decoded.push((high << 4) | low);
|
||||
index += 3;
|
||||
}
|
||||
b'%' => return None,
|
||||
b'+' => {
|
||||
decoded.push(b' ');
|
||||
index += 1;
|
||||
}
|
||||
byte => {
|
||||
decoded.push(byte);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
fn share_token_from_query(query: Option<&str>) -> (bool, Option<String>) {
|
||||
let Some(query) = query else {
|
||||
return (false, None);
|
||||
};
|
||||
for field in query.split('&') {
|
||||
let (name, value) = field.split_once('=').unwrap_or((field, ""));
|
||||
if decode_query_component(name).as_deref() == Some("share") {
|
||||
return (true, decode_query_component(value));
|
||||
}
|
||||
}
|
||||
(false, None)
|
||||
}
|
||||
|
||||
fn canonical_resource_url(uri: &Uri) -> String {
|
||||
let remaining_query = uri.query().map(|query| {
|
||||
query
|
||||
.split('&')
|
||||
.filter(|field| {
|
||||
let name = field.split_once('=').map_or(*field, |(name, _)| name);
|
||||
decode_query_component(name).as_deref() != Some("share")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("&")
|
||||
});
|
||||
match remaining_query.as_deref().filter(|query| !query.is_empty()) {
|
||||
Some(query) => format!("{}?{query}", uri.path()),
|
||||
None => uri.path().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn share_session_redirect(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
uri: &Uri,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
share: Option<&str>,
|
||||
) -> Response {
|
||||
let client_key = crate::security::client_key(headers);
|
||||
let cookie = match share.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(share) => match auth::create_share_session(
|
||||
state,
|
||||
kind,
|
||||
slug,
|
||||
share,
|
||||
crate::security::share_session_token(headers, kind, slug),
|
||||
&client_key,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(session)) => Some(crate::security::share_session_cookie(
|
||||
kind,
|
||||
slug,
|
||||
&session.token,
|
||||
session.max_age_seconds,
|
||||
)),
|
||||
Ok(None) => None,
|
||||
Err(error) => {
|
||||
tracing::warn!(error = %error.message, kind, slug, "failed to exchange share link for guest session");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut response = StatusCode::SEE_OTHER.into_response();
|
||||
response.headers_mut().insert(
|
||||
header::LOCATION,
|
||||
HeaderValue::from_str(&canonical_resource_url(uri))
|
||||
.expect("request URI is a valid redirect location"),
|
||||
);
|
||||
if let Some(cookie) = cookie {
|
||||
response.headers_mut().insert(header::SET_COOKIE, cookie);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-store, max-age=0"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
"referrer-policy",
|
||||
HeaderValue::from_static("no-referrer"),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
fn render_editor_page(
|
||||
state: &SharedState,
|
||||
@@ -153,19 +264,39 @@ pub(super) async fn home(State(state): State<SharedState>) -> Response {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn pad(State(state): State<SharedState>, Path(slug): Path<String>) -> Response {
|
||||
pub(super) async fn pad(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path(slug): Path<String>,
|
||||
RawQuery(query): RawQuery,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Response {
|
||||
match db::find_pad(&state.db, &slug).await {
|
||||
Ok(Some(pad)) => render_editor_page(
|
||||
&state,
|
||||
"pad",
|
||||
"pad",
|
||||
&pad.title,
|
||||
"RustPad",
|
||||
"/",
|
||||
"home-brand",
|
||||
"note",
|
||||
"<kbd>Alt+Enter</kbd><span>New line while editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>",
|
||||
),
|
||||
Ok(Some(pad)) => {
|
||||
let (has_share, share) = share_token_from_query(query.as_deref());
|
||||
if has_share {
|
||||
return share_session_redirect(
|
||||
&state,
|
||||
&headers,
|
||||
&uri,
|
||||
"pad",
|
||||
&slug,
|
||||
share.as_deref(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
render_editor_page(
|
||||
&state,
|
||||
"pad",
|
||||
"pad",
|
||||
&pad.title,
|
||||
"RustPad",
|
||||
"/",
|
||||
"home-brand",
|
||||
"note",
|
||||
"<kbd>Alt+Enter</kbd><span>New line while editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>",
|
||||
)
|
||||
}
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
@@ -214,10 +345,25 @@ pub(super) async fn public_page(
|
||||
|
||||
pub(super) async fn workspace(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path(workspace_slug): Path<String>,
|
||||
RawQuery(query): RawQuery,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Response {
|
||||
match db::find_workspace(&state.db, &workspace_slug).await {
|
||||
Ok(Some(workspace)) => {
|
||||
let (has_share, share) = share_token_from_query(query.as_deref());
|
||||
if has_share {
|
||||
return share_session_redirect(
|
||||
&state,
|
||||
&headers,
|
||||
&uri,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
share.as_deref(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let html = include_str!("../../static/workspace.html").replace(
|
||||
"__WORKSPACE_TITLE__",
|
||||
&escape_html(if workspace.is_private != 0 {
|
||||
@@ -254,7 +400,10 @@ pub(super) async fn workspace(
|
||||
|
||||
pub(super) async fn note(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
RawQuery(query): RawQuery,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Response {
|
||||
let workspace = match db::find_workspace(&state.db, &workspace_slug).await {
|
||||
Ok(Some(workspace)) => workspace,
|
||||
@@ -276,25 +425,39 @@ pub(super) async fn note(
|
||||
};
|
||||
|
||||
match db::find_note(&state.db, workspace.id, ¬e_slug).await {
|
||||
Ok(Some(note)) => render_editor_page(
|
||||
&state,
|
||||
"note",
|
||||
"note",
|
||||
if workspace.is_private != 0 {
|
||||
"Note"
|
||||
} else {
|
||||
¬e.title
|
||||
},
|
||||
if workspace.is_private != 0 {
|
||||
"Workspace"
|
||||
} else {
|
||||
&workspace.title
|
||||
},
|
||||
&format!("/w/{workspace_slug}"),
|
||||
"",
|
||||
"workspace",
|
||||
"",
|
||||
),
|
||||
Ok(Some(note)) => {
|
||||
let (has_share, share) = share_token_from_query(query.as_deref());
|
||||
if has_share {
|
||||
return share_session_redirect(
|
||||
&state,
|
||||
&headers,
|
||||
&uri,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
share.as_deref(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
render_editor_page(
|
||||
&state,
|
||||
"note",
|
||||
"note",
|
||||
if workspace.is_private != 0 {
|
||||
"Note"
|
||||
} else {
|
||||
¬e.title
|
||||
},
|
||||
if workspace.is_private != 0 {
|
||||
"Workspace"
|
||||
} else {
|
||||
&workspace.title
|
||||
},
|
||||
&format!("/w/{workspace_slug}"),
|
||||
"",
|
||||
"workspace",
|
||||
"",
|
||||
)
|
||||
}
|
||||
Ok(None) => error_response(
|
||||
StatusCode::NOT_FOUND,
|
||||
"404",
|
||||
@@ -400,3 +563,28 @@ fn escape_html(value: &str) -> String {
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn share_exchange_redirects_to_canonical_resource_path() {
|
||||
let uri: Uri = "/w/private?view=all&share=secret&page=2".parse().unwrap();
|
||||
assert_eq!(canonical_resource_url(&uri), "/w/private?view=all&page=2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoded_or_repeated_parameters_are_parsed_without_rejection() {
|
||||
let uri: Uri = "/w/private?%73hare=one&share=two".parse().unwrap();
|
||||
assert_eq!(share_token_from_query(uri.query()), (true, Some("one".into())));
|
||||
assert_eq!(canonical_resource_url(&uri), "/w/private");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_share_parameter_is_still_removed_from_the_url() {
|
||||
let uri: Uri = "/w/private?share=%ZZ&keep=no".parse().unwrap();
|
||||
assert_eq!(share_token_from_query(uri.query()), (true, None));
|
||||
assert_eq!(canonical_resource_url(&uri), "/w/private?keep=no");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user