This repository was archived by the owner on Apr 14, 2022. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 22
Add API for SessionStore #159
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import os | ||
import ssl | ||
import pathlib | ||
import typing | ||
from ..models import URL, Origin | ||
from ..structures import AltSvc, HSTS | ||
|
||
class SessionStore: | ||
"""This is the interface for the storage of information | ||
that a session needs to remember outside the scope of | ||
a single HTTP lifecycle. Here's a list of information that | ||
is stored within a 'SessionStore' along with it's corresponding key: | ||
|
||
- Cookies ('cookies') | ||
- AltSvc ('altsvc') | ||
- Permanent Redirects ('redirect') | ||
- HSTS ('hsts') | ||
- TLS Session Tickets ('tls_session_tickets') | ||
- Cached responses ('responses') | ||
|
||
Cached response bodies have the chance of being quite large so | ||
shouldn't be cached if the session store isn't writing to disk. | ||
This means that session stores that don't write to disk can | ||
choose not to implement response caching. | ||
""" | ||
|
||
async def get_altsvc(self, origin: Origin) -> typing.List[AltSvc]: | ||
"""Gets a list of 'AltSvc' for the origin""" | ||
async def get_cookies( | ||
self, origin: Origin, | ||
) -> typing.Any: # TODO: Replace with 'Cookies' type when implemented | ||
"""Gets a collection of cookies that should be sent for all requests to the Origin""" | ||
async def get_redirects(self, url: URL) -> typing.Optional[URL]: | ||
"""Gets a permanent redirect for the given URL if one has been received""" | ||
async def get_hsts(self, origin: Origin) -> typing.Optional[HSTS]: | ||
"""Determines if the origin should only be accessed over TLS by | ||
comparing the host to a preloaded list of domains or if the | ||
'Strict-Transport-Security' header was sent on a previous response. | ||
""" | ||
async def get_tls_session_tickets( | ||
self, origin: Origin | ||
) -> typing.Optional[ssl.SSLSession]: | ||
"""Loads any 'ssl.SSLSession' instances for a given Origin in order | ||
to resume TLS for a faster handshake. Using session resumption only works | ||
for TLSv1.2 for now but that's a large chunk of TLS usage right now so still | ||
worth implementing. | ||
|
||
Somehow the SSLSession will have to be routed down to the low-level socket | ||
creation stage because SSLSocket.session must be set before calling do_handshake(). | ||
""" | ||
async def get_response( | ||
self, request: typing.Any | ||
) -> typing.Optional[ | ||
typing.Any | ||
]: # TODO: Replace with 'Response' type when implemented. | ||
"""Looks for a response in the cache for the given request. | ||
This will need to parse the 'Cache-Control' header and look at | ||
extensions to see if the Request actually wants us to look in | ||
the cache. | ||
""" | ||
async def clear(self, origin: Origin, keys: typing.Collection[str] = None) -> None: | ||
"""Clears data from the session store. | ||
If given an Origin without a key then will clear all information for that key. | ||
If given an Origin and keys then only that information at those keys will | ||
be cleared. | ||
|
||
Session stores should take care that data is actually deleted | ||
all the way down, so if data is stored on disk the files should | ||
at least be scheduled for deletion. | ||
""" | ||
|
||
class MemorySessionStore(SessionStore): | ||
"""This is a session store implementation that uses memory | ||
to hold on to information but never writes info to disk | ||
or stores it persistently. This means that once the | ||
program terminates all session store information will | ||
be lost. | ||
|
||
This is the default session store type if no other | ||
session store is specified. | ||
""" | ||
|
||
class FilesystemSessionStore(SessionStore): | ||
"""A session store that persists data stored onto the disk""" | ||
|
||
def __init__(self, path: typing.Union[str, pathlib.Path]): ... | ||
|
||
class EmptySessionStore(SessionStore): | ||
"""This is a session store that drops everything that's handed to it.""" | ||
|
||
def arg_to_session_store( | ||
session_store: typing.Union[None, str, pathlib.Path, "SessionStore"] | ||
) -> SessionStore: | ||
"""Converts a value passed to 'session_store' on a Session into | ||
a 'SessionStore' object. This allows users to specify ':memory:' | ||
or a 'pathlib.Path' object to use instead of having to construct | ||
the 'SessionStore' object manually. | ||
""" | ||
if session_store is None: | ||
return EmptySessionStore() | ||
elif session_store == ":memory:": | ||
return MemorySessionStore() | ||
elif isinstance(session_store, (str, pathlib.Path)) and os.path.isdir(session_store): | ||
return FilesystemSessionStore(session_store) | ||
elif isinstance(session_store, SessionStore): | ||
return session_store | ||
raise ValueError("not a session store!") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import typing | ||
|
||
class Origin: | ||
def __init__(self, scheme: str, host: str, port: int): | ||
self.scheme = scheme | ||
self.host = host | ||
self.port = port | ||
|
||
class URL: | ||
def __init__( | ||
self, | ||
url: str = None, | ||
scheme: str = None, | ||
username: str = None, | ||
password: str = None, | ||
host: str = None, | ||
port: int = None, | ||
path: str = None, | ||
query: typing.Union[str, typing.Mapping[str, str]] = None, | ||
fragment: str = None, | ||
): ... # TODO: Implement the URL interface | ||
@property | ||
def origin(self) -> Origin: ... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import typing | ||
|
||
class AltSvc(typing.NamedTuple): | ||
"""Holds onto information found from the 'AltSvc' HTTP header. | ||
What's important to note is that even though we're connecting | ||
to a different host and port than the request origin we need | ||
to act as though we're still talking to the originally requested | ||
origin (in the 'Host' header, checking hostname on certificate, etc). | ||
""" | ||
|
||
alpn_protocol: str | ||
host: str | ||
port: int | ||
expires_at: int | ||
@classmethod | ||
def from_header(cls, value: str) -> typing.List["AltSvc"]: | ||
"""Parses the value of the 'AltSvc' header according to RFC 7838 | ||
and returns a list of values. | ||
""" | ||
|
||
class HSTS(typing.NamedTuple): | ||
"""Holds onto information about whether a given host should be only | ||
accessed via TLS. See RFC 6797. 'preload' isn't defined in the RFC | ||
but is used to signal that the website wishes to be in the HSTS preload | ||
list. We can potentially use this as a signal that the website doesn't | ||
want to expire ever? Also the 'preload' flag is set if this 'HSTS' | ||
instance was grabbed from a static HSTS preload list. | ||
""" | ||
|
||
host: str | ||
include_subdomains: bool | ||
expires_at: typing.Optional[int] | ||
preload: bool | ||
@classmethod | ||
def from_header(cls, value: str) -> "HSTS": | ||
"""Parses the value of the 'Strict-Transport-Security' header.""" | ||
|
||
class RequestCacheControl(typing.NamedTuple): | ||
"""A parsed 'Cache-Control' header from a request. | ||
Requests support the following directives: max-age, max-stale, | ||
min-fresh, no-cache, no-store, no-transform, only-if-cached. | ||
|
||
For cache-control structures 'None' means not present, 'True' | ||
means that the directive was present but without a d=[x] value, | ||
and an integer/string means that there was a value for that | ||
directive. All directives that aren't understood within | ||
the context are added within 'other_directives' but are not | ||
used by the client library to make decisions. | ||
""" | ||
|
||
max_age: typing.Optional[int] | ||
max_stale: typing.Optional[typing.Union[typing.Literal[True], int]] | ||
min_fresh: typing.Optional[int] | ||
no_cache: typing.Optional[typing.Literal[True]] | ||
no_store: typing.Optional[typing.Literal[True]] | ||
no_transform: typing.Optional[typing.Literal[True]] | ||
only_if_cached: typing.Optional[typing.Literal[True]] | ||
|
||
other_directives: typing.Tuple[str, ...] | ||
@classmethod | ||
def from_header(cls, value: str) -> "RequestCacheControl": | ||
"""Parses the value of 'Cache-Control' from a request headers.""" | ||
|
||
class ResponseCacheControl(typing.NamedTuple): | ||
"""A parsed 'Cache-Control' header from a response. | ||
Responses support the following directives: must-revalidate, | ||
no-cache, no-store, no-transform, public, private, proxy-revalidate, | ||
max-age, s-maxage, immutable, stale-while-revalidated, stale-if-error | ||
""" | ||
|
||
must_revalidate: typing.Optional[typing.Literal[True]] | ||
no_cache: typing.Optional[typing.Literal[True]] | ||
no_store: typing.Optional[typing.Literal[True]] | ||
no_transform: typing.Optional[typing.Literal[True]] | ||
public: typing.Optional[typing.Literal[True]] | ||
private: typing.Optional[typing.Literal[True]] | ||
proxy_revalidate: typing.Optional[typing.Literal[True]] | ||
max_age: typing.Optional[typing.Literal[True]] | ||
s_maxage: typing.Optional[typing.Literal[True]] | ||
immutable: typing.Optional[typing.Literal[True]] | ||
stale_while_revalidate: typing.Optional[int] | ||
stale_if_error: typing.Optional[int] | ||
|
||
other_directives: typing.Tuple[str, ...] | ||
@classmethod | ||
def from_header(cls, value: str) -> "ResponseCacheControl": | ||
"""Parses the value of 'Cache-Control' from a response headers.""" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should change this to singular to match the interface.