Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/sessions.py: 47%

116 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-22 06:29 +0000

1from __future__ import annotations 

2 

3import hashlib 

4import typing as t 

5from collections.abc import MutableMapping 

6from datetime import datetime 

7from datetime import timezone 

8 

9from itsdangerous import BadSignature 

10from itsdangerous import URLSafeTimedSerializer 

11from werkzeug.datastructures import CallbackDict 

12 

13from .json.tag import TaggedJSONSerializer 

14 

15if t.TYPE_CHECKING: # pragma: no cover 

16 import typing_extensions as te 

17 

18 from .app import Flask 

19 from .wrappers import Request 

20 from .wrappers import Response 

21 

22 

23# TODO generic when Python > 3.8 

24class SessionMixin(MutableMapping): # type: ignore[type-arg] 

25 """Expands a basic dictionary with session attributes.""" 

26 

27 @property 

28 def permanent(self) -> bool: 

29 """This reflects the ``'_permanent'`` key in the dict.""" 

30 return self.get("_permanent", False) 

31 

32 @permanent.setter 

33 def permanent(self, value: bool) -> None: 

34 self["_permanent"] = bool(value) 

35 

36 #: Some implementations can detect whether a session is newly 

37 #: created, but that is not guaranteed. Use with caution. The mixin 

38 # default is hard-coded ``False``. 

39 new = False 

40 

41 #: Some implementations can detect changes to the session and set 

42 #: this when that happens. The mixin default is hard coded to 

43 #: ``True``. 

44 modified = True 

45 

46 #: Some implementations can detect when session data is read or 

47 #: written and set this when that happens. The mixin default is hard 

48 #: coded to ``True``. 

49 accessed = True 

50 

51 

52# TODO generic when Python > 3.8 

53class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg] 

54 """Base class for sessions based on signed cookies. 

55 

56 This session backend will set the :attr:`modified` and 

57 :attr:`accessed` attributes. It cannot reliably track whether a 

58 session is new (vs. empty), so :attr:`new` remains hard coded to 

59 ``False``. 

60 """ 

61 

62 #: When data is changed, this is set to ``True``. Only the session 

63 #: dictionary itself is tracked; if the session contains mutable 

64 #: data (for example a nested dict) then this must be set to 

65 #: ``True`` manually when modifying that data. The session cookie 

66 #: will only be written to the response if this is ``True``. 

67 modified = False 

68 

69 #: When data is read or written, this is set to ``True``. Used by 

70 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` 

71 #: header, which allows caching proxies to cache different pages for 

72 #: different users. 

73 accessed = False 

74 

75 def __init__(self, initial: t.Any = None) -> None: 

76 def on_update(self: te.Self) -> None: 

77 self.modified = True 

78 self.accessed = True 

79 

80 super().__init__(initial, on_update) 

81 

82 def __getitem__(self, key: str) -> t.Any: 

83 self.accessed = True 

84 return super().__getitem__(key) 

85 

86 def get(self, key: str, default: t.Any = None) -> t.Any: 

87 self.accessed = True 

88 return super().get(key, default) 

89 

90 def setdefault(self, key: str, default: t.Any = None) -> t.Any: 

91 self.accessed = True 

92 return super().setdefault(key, default) 

93 

94 

95class NullSession(SecureCookieSession): 

96 """Class used to generate nicer error messages if sessions are not 

97 available. Will still allow read-only access to the empty session 

98 but fail on setting. 

99 """ 

100 

101 def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: 

102 raise RuntimeError( 

103 "The session is unavailable because no secret " 

104 "key was set. Set the secret_key on the " 

105 "application to something unique and secret." 

106 ) 

107 

108 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 

109 del _fail 

110 

111 

112class SessionInterface: 

113 """The basic interface you have to implement in order to replace the 

114 default session interface which uses werkzeug's securecookie 

115 implementation. The only methods you have to implement are 

116 :meth:`open_session` and :meth:`save_session`, the others have 

117 useful defaults which you don't need to change. 

118 

119 The session object returned by the :meth:`open_session` method has to 

120 provide a dictionary like interface plus the properties and methods 

121 from the :class:`SessionMixin`. We recommend just subclassing a dict 

122 and adding that mixin:: 

123 

124 class Session(dict, SessionMixin): 

125 pass 

126 

127 If :meth:`open_session` returns ``None`` Flask will call into 

128 :meth:`make_null_session` to create a session that acts as replacement 

129 if the session support cannot work because some requirement is not 

130 fulfilled. The default :class:`NullSession` class that is created 

131 will complain that the secret key was not set. 

132 

133 To replace the session interface on an application all you have to do 

134 is to assign :attr:`flask.Flask.session_interface`:: 

135 

136 app = Flask(__name__) 

137 app.session_interface = MySessionInterface() 

138 

139 Multiple requests with the same session may be sent and handled 

140 concurrently. When implementing a new session interface, consider 

141 whether reads or writes to the backing store must be synchronized. 

142 There is no guarantee on the order in which the session for each 

143 request is opened or saved, it will occur in the order that requests 

144 begin and end processing. 

145 

146 .. versionadded:: 0.8 

147 """ 

148 

149 #: :meth:`make_null_session` will look here for the class that should 

150 #: be created when a null session is requested. Likewise the 

151 #: :meth:`is_null_session` method will perform a typecheck against 

152 #: this type. 

153 null_session_class = NullSession 

154 

155 #: A flag that indicates if the session interface is pickle based. 

156 #: This can be used by Flask extensions to make a decision in regards 

157 #: to how to deal with the session object. 

158 #: 

159 #: .. versionadded:: 0.10 

160 pickle_based = False 

161 

162 def make_null_session(self, app: Flask) -> NullSession: 

163 """Creates a null session which acts as a replacement object if the 

164 real session support could not be loaded due to a configuration 

165 error. This mainly aids the user experience because the job of the 

166 null session is to still support lookup without complaining but 

167 modifications are answered with a helpful error message of what 

168 failed. 

169 

170 This creates an instance of :attr:`null_session_class` by default. 

171 """ 

172 return self.null_session_class() 

173 

174 def is_null_session(self, obj: object) -> bool: 

175 """Checks if a given object is a null session. Null sessions are 

176 not asked to be saved. 

177 

178 This checks if the object is an instance of :attr:`null_session_class` 

179 by default. 

180 """ 

181 return isinstance(obj, self.null_session_class) 

182 

183 def get_cookie_name(self, app: Flask) -> str: 

184 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" 

185 return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] 

186 

187 def get_cookie_domain(self, app: Flask) -> str | None: 

188 """The value of the ``Domain`` parameter on the session cookie. If not set, 

189 browsers will only send the cookie to the exact domain it was set from. 

190 Otherwise, they will send it to any subdomain of the given value as well. 

191 

192 Uses the :data:`SESSION_COOKIE_DOMAIN` config. 

193 

194 .. versionchanged:: 2.3 

195 Not set by default, does not fall back to ``SERVER_NAME``. 

196 """ 

197 return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] 

198 

199 def get_cookie_path(self, app: Flask) -> str: 

200 """Returns the path for which the cookie should be valid. The 

201 default implementation uses the value from the ``SESSION_COOKIE_PATH`` 

202 config var if it's set, and falls back to ``APPLICATION_ROOT`` or 

203 uses ``/`` if it's ``None``. 

204 """ 

205 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] 

206 

207 def get_cookie_httponly(self, app: Flask) -> bool: 

208 """Returns True if the session cookie should be httponly. This 

209 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` 

210 config var. 

211 """ 

212 return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] 

213 

214 def get_cookie_secure(self, app: Flask) -> bool: 

215 """Returns True if the cookie should be secure. This currently 

216 just returns the value of the ``SESSION_COOKIE_SECURE`` setting. 

217 """ 

218 return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] 

219 

220 def get_cookie_samesite(self, app: Flask) -> str | None: 

221 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the 

222 ``SameSite`` attribute. This currently just returns the value of 

223 the :data:`SESSION_COOKIE_SAMESITE` setting. 

224 """ 

225 return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] 

226 

227 def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: 

228 """A helper method that returns an expiration date for the session 

229 or ``None`` if the session is linked to the browser session. The 

230 default implementation returns now + the permanent session 

231 lifetime configured on the application. 

232 """ 

233 if session.permanent: 

234 return datetime.now(timezone.utc) + app.permanent_session_lifetime 

235 return None 

236 

237 def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: 

238 """Used by session backends to determine if a ``Set-Cookie`` header 

239 should be set for this session cookie for this response. If the session 

240 has been modified, the cookie is set. If the session is permanent and 

241 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is 

242 always set. 

243 

244 This check is usually skipped if the session was deleted. 

245 

246 .. versionadded:: 0.11 

247 """ 

248 

249 return session.modified or ( 

250 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] 

251 ) 

252 

253 def open_session(self, app: Flask, request: Request) -> SessionMixin | None: 

254 """This is called at the beginning of each request, after 

255 pushing the request context, before matching the URL. 

256 

257 This must return an object which implements a dictionary-like 

258 interface as well as the :class:`SessionMixin` interface. 

259 

260 This will return ``None`` to indicate that loading failed in 

261 some way that is not immediately an error. The request 

262 context will fall back to using :meth:`make_null_session` 

263 in this case. 

264 """ 

265 raise NotImplementedError() 

266 

267 def save_session( 

268 self, app: Flask, session: SessionMixin, response: Response 

269 ) -> None: 

270 """This is called at the end of each request, after generating 

271 a response, before removing the request context. It is skipped 

272 if :meth:`is_null_session` returns ``True``. 

273 """ 

274 raise NotImplementedError() 

275 

276 

277session_json_serializer = TaggedJSONSerializer() 

278 

279 

280class SecureCookieSessionInterface(SessionInterface): 

281 """The default session interface that stores sessions in signed cookies 

282 through the :mod:`itsdangerous` module. 

283 """ 

284 

285 #: the salt that should be applied on top of the secret key for the 

286 #: signing of cookie based sessions. 

287 salt = "cookie-session" 

288 #: the hash function to use for the signature. The default is sha1 

289 digest_method = staticmethod(hashlib.sha1) 

290 #: the name of the itsdangerous supported key derivation. The default 

291 #: is hmac. 

292 key_derivation = "hmac" 

293 #: A python serializer for the payload. The default is a compact 

294 #: JSON derived serializer with support for some extra Python types 

295 #: such as datetime objects or tuples. 

296 serializer = session_json_serializer 

297 session_class = SecureCookieSession 

298 

299 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: 

300 if not app.secret_key: 

301 return None 

302 signer_kwargs = dict( 

303 key_derivation=self.key_derivation, digest_method=self.digest_method 

304 ) 

305 return URLSafeTimedSerializer( 

306 app.secret_key, 

307 salt=self.salt, 

308 serializer=self.serializer, 

309 signer_kwargs=signer_kwargs, 

310 ) 

311 

312 def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: 

313 s = self.get_signing_serializer(app) 

314 if s is None: 

315 return None 

316 val = request.cookies.get(self.get_cookie_name(app)) 

317 if not val: 

318 return self.session_class() 

319 max_age = int(app.permanent_session_lifetime.total_seconds()) 

320 try: 

321 data = s.loads(val, max_age=max_age) 

322 return self.session_class(data) 

323 except BadSignature: 

324 return self.session_class() 

325 

326 def save_session( 

327 self, app: Flask, session: SessionMixin, response: Response 

328 ) -> None: 

329 name = self.get_cookie_name(app) 

330 domain = self.get_cookie_domain(app) 

331 path = self.get_cookie_path(app) 

332 secure = self.get_cookie_secure(app) 

333 samesite = self.get_cookie_samesite(app) 

334 httponly = self.get_cookie_httponly(app) 

335 

336 # Add a "Vary: Cookie" header if the session was accessed at all. 

337 if session.accessed: 

338 response.vary.add("Cookie") 

339 

340 # If the session is modified to be empty, remove the cookie. 

341 # If the session is empty, return without setting the cookie. 

342 if not session: 

343 if session.modified: 

344 response.delete_cookie( 

345 name, 

346 domain=domain, 

347 path=path, 

348 secure=secure, 

349 samesite=samesite, 

350 httponly=httponly, 

351 ) 

352 response.vary.add("Cookie") 

353 

354 return 

355 

356 if not self.should_set_cookie(app, session): 

357 return 

358 

359 expires = self.get_expiration_time(app, session) 

360 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore 

361 response.set_cookie( 

362 name, 

363 val, # type: ignore 

364 expires=expires, 

365 httponly=httponly, 

366 domain=domain, 

367 path=path, 

368 secure=secure, 

369 samesite=samesite, 

370 ) 

371 response.vary.add("Cookie")