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

136 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:03 +0000

1import hashlib 

2import typing as t 

3import warnings 

4from collections.abc import MutableMapping 

5from datetime import datetime 

6 

7from itsdangerous import BadSignature 

8from itsdangerous import URLSafeTimedSerializer 

9from werkzeug.datastructures import CallbackDict 

10 

11from .helpers import is_ip 

12from .json.tag import TaggedJSONSerializer 

13 

14if t.TYPE_CHECKING: 

15 import typing_extensions as te 

16 from .app import Flask 

17 from .wrappers import Request, Response 

18 

19 

20class SessionMixin(MutableMapping): 

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

22 

23 @property 

24 def permanent(self) -> bool: 

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

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

27 

28 @permanent.setter 

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

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

31 

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

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

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

35 new = False 

36 

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

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

39 #: ``True``. 

40 modified = True 

41 

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

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

44 #: coded to ``True``. 

45 accessed = True 

46 

47 

48class SecureCookieSession(CallbackDict, SessionMixin): 

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

50 

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

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

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

54 ``False``. 

55 """ 

56 

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

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

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

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

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

62 modified = False 

63 

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

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

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

67 #: different users. 

68 accessed = False 

69 

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

71 def on_update(self) -> None: 

72 self.modified = True 

73 self.accessed = True 

74 

75 super().__init__(initial, on_update) 

76 

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

78 self.accessed = True 

79 return super().__getitem__(key) 

80 

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

82 self.accessed = True 

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

84 

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

86 self.accessed = True 

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

88 

89 

90class NullSession(SecureCookieSession): 

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

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

93 but fail on setting. 

94 """ 

95 

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

97 raise RuntimeError( 

98 "The session is unavailable because no secret " 

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

100 "application to something unique and secret." 

101 ) 

102 

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

104 del _fail 

105 

106 

107class SessionInterface: 

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

109 default session interface which uses werkzeug's securecookie 

110 implementation. The only methods you have to implement are 

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

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

113 

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

115 provide a dictionary like interface plus the properties and methods 

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

117 and adding that mixin:: 

118 

119 class Session(dict, SessionMixin): 

120 pass 

121 

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

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

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

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

126 will complain that the secret key was not set. 

127 

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

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

130 

131 app = Flask(__name__) 

132 app.session_interface = MySessionInterface() 

133 

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

135 concurrently. When implementing a new session interface, consider 

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

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

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

139 begin and end processing. 

140 

141 .. versionadded:: 0.8 

142 """ 

143 

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

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

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

147 #: this type. 

148 null_session_class = NullSession 

149 

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

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

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

153 #: 

154 #: .. versionadded:: 0.10 

155 pickle_based = False 

156 

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

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

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

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

161 null session is to still support lookup without complaining but 

162 modifications are answered with a helpful error message of what 

163 failed. 

164 

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

166 """ 

167 return self.null_session_class() 

168 

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

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

171 not asked to be saved. 

172 

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

174 by default. 

175 """ 

176 return isinstance(obj, self.null_session_class) 

177 

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

179 """Returns the name of the session cookie. 

180 

181 Uses ``app.session_cookie_name`` which is set to ``SESSION_COOKIE_NAME`` 

182 """ 

183 return app.session_cookie_name 

184 

185 def get_cookie_domain(self, app: "Flask") -> t.Optional[str]: 

186 """Returns the domain that should be set for the session cookie. 

187 

188 Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise 

189 falls back to detecting the domain based on ``SERVER_NAME``. 

190 

191 Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is 

192 updated to avoid re-running the logic. 

193 """ 

194 

195 rv = app.config["SESSION_COOKIE_DOMAIN"] 

196 

197 # set explicitly, or cached from SERVER_NAME detection 

198 # if False, return None 

199 if rv is not None: 

200 return rv if rv else None 

201 

202 rv = app.config["SERVER_NAME"] 

203 

204 # server name not set, cache False to return none next time 

205 if not rv: 

206 app.config["SESSION_COOKIE_DOMAIN"] = False 

207 return None 

208 

209 # chop off the port which is usually not supported by browsers 

210 # remove any leading '.' since we'll add that later 

211 rv = rv.rsplit(":", 1)[0].lstrip(".") 

212 

213 if "." not in rv: 

214 # Chrome doesn't allow names without a '.'. This should only 

215 # come up with localhost. Hack around this by not setting 

216 # the name, and show a warning. 

217 warnings.warn( 

218 f"{rv!r} is not a valid cookie domain, it must contain" 

219 " a '.'. Add an entry to your hosts file, for example" 

220 f" '{rv}.localdomain', and use that instead." 

221 ) 

222 app.config["SESSION_COOKIE_DOMAIN"] = False 

223 return None 

224 

225 ip = is_ip(rv) 

226 

227 if ip: 

228 warnings.warn( 

229 "The session cookie domain is an IP address. This may not work" 

230 " as intended in some browsers. Add an entry to your hosts" 

231 ' file, for example "localhost.localdomain", and use that' 

232 " instead." 

233 ) 

234 

235 # if this is not an ip and app is mounted at the root, allow subdomain 

236 # matching by adding a '.' prefix 

237 if self.get_cookie_path(app) == "/" and not ip: 

238 rv = f".{rv}" 

239 

240 app.config["SESSION_COOKIE_DOMAIN"] = rv 

241 return rv 

242 

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

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

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

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

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

248 """ 

249 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] 

250 

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

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

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

254 config var. 

255 """ 

256 return app.config["SESSION_COOKIE_HTTPONLY"] 

257 

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

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

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

261 """ 

262 return app.config["SESSION_COOKIE_SECURE"] 

263 

264 def get_cookie_samesite(self, app: "Flask") -> str: 

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

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

267 the :data:`SESSION_COOKIE_SAMESITE` setting. 

268 """ 

269 return app.config["SESSION_COOKIE_SAMESITE"] 

270 

271 def get_expiration_time( 

272 self, app: "Flask", session: SessionMixin 

273 ) -> t.Optional[datetime]: 

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

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

276 default implementation returns now + the permanent session 

277 lifetime configured on the application. 

278 """ 

279 if session.permanent: 

280 return datetime.utcnow() + app.permanent_session_lifetime 

281 return None 

282 

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

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

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

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

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

288 always set. 

289 

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

291 

292 .. versionadded:: 0.11 

293 """ 

294 

295 return session.modified or ( 

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

297 ) 

298 

299 def open_session( 

300 self, app: "Flask", request: "Request" 

301 ) -> t.Optional[SessionMixin]: 

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

303 pushing the request context, before matching the URL. 

304 

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

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

307 

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

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

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

311 in this case. 

312 """ 

313 raise NotImplementedError() 

314 

315 def save_session( 

316 self, app: "Flask", session: SessionMixin, response: "Response" 

317 ) -> None: 

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

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

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

321 """ 

322 raise NotImplementedError() 

323 

324 

325session_json_serializer = TaggedJSONSerializer() 

326 

327 

328class SecureCookieSessionInterface(SessionInterface): 

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

330 through the :mod:`itsdangerous` module. 

331 """ 

332 

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

334 #: signing of cookie based sessions. 

335 salt = "cookie-session" 

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

337 digest_method = staticmethod(hashlib.sha1) 

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

339 #: is hmac. 

340 key_derivation = "hmac" 

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

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

343 #: such as datetime objects or tuples. 

344 serializer = session_json_serializer 

345 session_class = SecureCookieSession 

346 

347 def get_signing_serializer( 

348 self, app: "Flask" 

349 ) -> t.Optional[URLSafeTimedSerializer]: 

350 if not app.secret_key: 

351 return None 

352 signer_kwargs = dict( 

353 key_derivation=self.key_derivation, digest_method=self.digest_method 

354 ) 

355 return URLSafeTimedSerializer( 

356 app.secret_key, 

357 salt=self.salt, 

358 serializer=self.serializer, 

359 signer_kwargs=signer_kwargs, 

360 ) 

361 

362 def open_session( 

363 self, app: "Flask", request: "Request" 

364 ) -> t.Optional[SecureCookieSession]: 

365 s = self.get_signing_serializer(app) 

366 if s is None: 

367 return None 

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

369 if not val: 

370 return self.session_class() 

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

372 try: 

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

374 return self.session_class(data) 

375 except BadSignature: 

376 return self.session_class() 

377 

378 def save_session( 

379 self, app: "Flask", session: SessionMixin, response: "Response" 

380 ) -> None: 

381 name = self.get_cookie_name(app) 

382 domain = self.get_cookie_domain(app) 

383 path = self.get_cookie_path(app) 

384 secure = self.get_cookie_secure(app) 

385 samesite = self.get_cookie_samesite(app) 

386 httponly = self.get_cookie_httponly(app) 

387 

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

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

390 if not session: 

391 if session.modified: 

392 response.delete_cookie( 

393 name, 

394 domain=domain, 

395 path=path, 

396 secure=secure, 

397 samesite=samesite, 

398 httponly=httponly, 

399 ) 

400 

401 return 

402 

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

404 if session.accessed: 

405 response.vary.add("Cookie") 

406 

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

408 return 

409 

410 expires = self.get_expiration_time(app, session) 

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

412 response.set_cookie( 

413 name, 

414 val, # type: ignore 

415 expires=expires, 

416 httponly=httponly, 

417 domain=domain, 

418 path=path, 

419 secure=secure, 

420 samesite=samesite, 

421 )