Skip to content

Commit 199f8af

Browse files
committed
fix: double-serialization of session state
Use `serde_json::Map<String, serde_json::Value>` instead of `HashMap<String, String>`. Has the added benefit of fully type-safe json storage in sessions. see actix#366
1 parent e3c3ffd commit 199f8af

File tree

4 files changed

+29
-25
lines changed

4 files changed

+29
-25
lines changed

actix-session/src/middleware.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, fmt, future::Future, pin::Pin, rc::Rc};
1+
use std::{fmt, future::Future, pin::Pin, rc::Rc};
22

33
use actix_utils::future::{ready, Ready};
44
use actix_web::{
@@ -15,7 +15,7 @@ use crate::{
1515
self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
1616
TtlExtensionPolicy,
1717
},
18-
storage::{LoadError, SessionKey, SessionStore},
18+
storage::{LoadError, SessionKey, SessionState, SessionStore},
1919
Session, SessionStatus,
2020
};
2121

@@ -358,7 +358,7 @@ fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Op
358358
async fn load_session_state<Store: SessionStore>(
359359
session_key: Option<SessionKey>,
360360
storage_backend: &Store,
361-
) -> Result<(Option<SessionKey>, HashMap<String, String>), actix_web::Error> {
361+
) -> Result<(Option<SessionKey>, SessionState), actix_web::Error> {
362362
if let Some(session_key) = session_key {
363363
match storage_backend.load(&session_key).await {
364364
Ok(state) => {
@@ -376,7 +376,7 @@ async fn load_session_state<Store: SessionStore>(
376376
empty session."
377377
);
378378

379-
Ok((None, HashMap::new()))
379+
Ok((None, SessionState::new()))
380380
}
381381
}
382382

@@ -388,14 +388,14 @@ async fn load_session_state<Store: SessionStore>(
388388
"Invalid session state, creating a new empty session."
389389
);
390390

391-
Ok((Some(session_key), HashMap::new()))
391+
Ok((Some(session_key), SessionState::new()))
392392
}
393393

394394
LoadError::Other(err) => Err(e500(err)),
395395
},
396396
}
397397
} else {
398-
Ok((None, HashMap::new()))
398+
Ok((None, SessionState::new()))
399399
}
400400
}
401401

actix-session/src/session.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::{
22
cell::{Ref, RefCell},
3-
collections::HashMap,
43
error::Error as StdError,
54
mem,
65
rc::Rc,
@@ -16,6 +15,9 @@ use actix_web::{
1615
use anyhow::Context;
1716
use derive_more::derive::{Display, From};
1817
use serde::{de::DeserializeOwned, Serialize};
18+
use serde_json::Value;
19+
20+
use crate::storage::SessionState;
1921

2022
/// The primary interface to access and modify session state.
2123
///
@@ -70,7 +72,7 @@ pub enum SessionStatus {
7072

7173
#[derive(Default)]
7274
struct SessionInner {
73-
state: HashMap<String, String>,
75+
state: SessionState,
7476
status: SessionStatus,
7577
}
7678

@@ -79,9 +81,9 @@ impl Session {
7981
///
8082
/// It returns an error if it fails to deserialize as `T` the JSON value associated with `key`.
8183
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, SessionGetError> {
82-
if let Some(val_str) = self.0.borrow().state.get(key) {
84+
if let Some(value) = self.0.borrow().state.get(key) {
8385
Ok(Some(
84-
serde_json::from_str(val_str)
86+
serde_json::from_value(value.to_owned())
8587
.with_context(|| {
8688
format!(
8789
"Failed to deserialize the JSON-encoded session data attached to key \
@@ -100,7 +102,7 @@ impl Session {
100102
/// Get all raw key-value data from the session.
101103
///
102104
/// Note that values are JSON encoded.
103-
pub fn entries(&self) -> Ref<'_, HashMap<String, String>> {
105+
pub fn entries(&self) -> Ref<'_, SessionState> {
104106
Ref::map(self.0.borrow(), |inner| &inner.state)
105107
}
106108

@@ -139,7 +141,7 @@ impl Session {
139141
})
140142
.map_err(SessionInsertError)?;
141143

142-
inner.state.insert(key, val);
144+
inner.state.insert(key, Value::String(val));
143145
}
144146

145147
Ok(())
@@ -148,7 +150,7 @@ impl Session {
148150
/// Remove value from the session.
149151
///
150152
/// If present, the JSON encoded value is returned.
151-
pub fn remove(&self, key: &str) -> Option<String> {
153+
pub fn remove(&self, key: &str) -> Option<Value> {
152154
let mut inner = self.0.borrow_mut();
153155

154156
if inner.status != SessionStatus::Purged {
@@ -165,9 +167,9 @@ impl Session {
165167
///
166168
/// Returns `None` if key was not present in session. Returns `T` if deserialization succeeds,
167169
/// otherwise returns un-deserialized JSON string.
168-
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
170+
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, Value>> {
169171
self.remove(key)
170-
.map(|val_str| match serde_json::from_str(&val_str) {
172+
.map(|val| match serde_json::from_value(val.clone()) {
171173
Ok(val) => Ok(val),
172174
Err(_err) => {
173175
tracing::debug!(
@@ -176,7 +178,7 @@ impl Session {
176178
std::any::type_name::<T>()
177179
);
178180

179-
Err(val_str)
181+
Err(val)
180182
}
181183
})
182184
}
@@ -216,11 +218,13 @@ impl Session {
216218
#[allow(clippy::needless_pass_by_ref_mut)]
217219
pub(crate) fn set_session(
218220
req: &mut ServiceRequest,
219-
data: impl IntoIterator<Item = (String, String)>,
221+
data: impl IntoIterator<Item = (String, impl Into<Value>)>,
220222
) {
221223
let session = Session::get_session(&mut req.extensions_mut());
222224
let mut inner = session.0.borrow_mut();
223-
inner.state.extend(data);
225+
inner
226+
.state
227+
.extend(data.into_iter().map(|(k, v)| (k, v.into())));
224228
}
225229

226230
/// Returns session status and iterator of key-value pairs of changes.
@@ -229,9 +233,7 @@ impl Session {
229233
/// typemap, leaving behind a new empty map. It should only be used when the session is being
230234
/// finalised (i.e. in `SessionMiddleware`).
231235
#[allow(clippy::needless_pass_by_ref_mut)]
232-
pub(crate) fn get_changes<B>(
233-
res: &mut ServiceResponse<B>,
234-
) -> (SessionStatus, HashMap<String, String>) {
236+
pub(crate) fn get_changes<B>(res: &mut ServiceResponse<B>) -> (SessionStatus, SessionState) {
235237
if let Some(s_impl) = res
236238
.request()
237239
.extensions()
@@ -240,7 +242,7 @@ impl Session {
240242
let state = mem::take(&mut s_impl.borrow_mut().state);
241243
(s_impl.borrow().status.clone(), state)
242244
} else {
243-
(SessionStatus::Unchanged, HashMap::new())
245+
(SessionStatus::Unchanged, SessionState::new())
244246
}
245247
}
246248

actix-session/src/storage/interface.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
use std::{collections::HashMap, future::Future};
1+
use std::future::Future;
22

33
use actix_web::cookie::time::Duration;
44
use derive_more::derive::Display;
5+
use serde_json::{Map, Value};
56

67
use super::SessionKey;
78

8-
pub(crate) type SessionState = HashMap<String, String>;
9+
/// Convenience type for the map structure backing session state.
10+
pub type SessionState = Map<String, Value>;
911

1012
/// The interface to retrieve and save the current session data from/to the chosen storage backend.
1113
///

actix-session/src/storage/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub use self::cookie::CookieSessionStore;
1313
#[cfg(feature = "redis-session")]
1414
pub use self::redis_rs::{RedisSessionStore, RedisSessionStoreBuilder};
1515
pub use self::{
16-
interface::{LoadError, SaveError, SessionStore, UpdateError},
16+
interface::{LoadError, SaveError, SessionState, SessionStore, UpdateError},
1717
session_key::SessionKey,
1818
utils::generate_session_key,
1919
};

0 commit comments

Comments
 (0)