Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Add API for SessionStore #159

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added src/hip/_async/__init__.py
Empty file.
107 changes: 107 additions & 0 deletions src/hip/_async/session_store.pyi
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(
Copy link
Contributor Author

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.

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!")
23 changes: 23 additions & 0 deletions src/hip/models.pyi
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: ...
87 changes: 87 additions & 0 deletions src/hip/structures.pyi
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."""