api docs++ (#4421)

This commit is contained in:
Maximilian Hils 2021-02-03 00:14:54 +01:00 committed by GitHub
parent a793a6256a
commit fccc153fdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 29 deletions

View File

@ -16,6 +16,7 @@ if TYPE_CHECKING:
class ConnectionState(Flag): class ConnectionState(Flag):
"""The current state of the underlying socket."""
CLOSED = 0 CLOSED = 0
CAN_READ = 1 CAN_READ = 1
CAN_WRITE = 2 CAN_WRITE = 2
@ -30,56 +31,78 @@ Address = Tuple[str, int]
class Connection(serializable.Serializable, metaclass=ABCMeta): class Connection(serializable.Serializable, metaclass=ABCMeta):
""" """
Connections exposed to the layers only contain metadata, no socket objects. Base class for client and server connections.
The connection object only exposes metadata about the connection, but not the underlying socket object.
This is intentional, all I/O should be handled by mitmproxy.proxy.server exclusively.
""" """
# all connections have a unique id. While # all connections have a unique id. While
# f.client_conn == f2.client_conn already holds true for live flows (where we have object identity), # f.client_conn == f2.client_conn already holds true for live flows (where we have object identity),
# we also want these semantics for recorded flows. # we also want these semantics for recorded flows.
id: str id: str
"""A unique UUID to identify the connection."""
state: ConnectionState state: ConnectionState
"""The current connection state."""
peername: Optional[Address] peername: Optional[Address]
"""The remote's `(ip, port)` tuple for this connection."""
sockname: Optional[Address] sockname: Optional[Address]
"""Our local `(ip, port)` tuple for this connection."""
error: Optional[str] = None error: Optional[str] = None
"""A string describing the connection error."""
tls: bool = False tls: bool = False
"""
`True` if TLS should be established, `False` otherwise.
Note that this property only describes if a connection should eventually be protected using TLS.
To check if TLS has already been established, use `Connection.tls_established`.
"""
certificate_list: Sequence[certs.Cert] = () certificate_list: Sequence[certs.Cert] = ()
""" """
The TLS certificate list as sent by the peer. The TLS certificate list as sent by the peer.
The first certificate is the end-entity certificate. The first certificate is the end-entity certificate.
[RFC 8446] Prior to TLS 1.3, "certificate_list" ordering required each > [RFC 8446] Prior to TLS 1.3, "certificate_list" ordering required each
certificate to certify the one immediately preceding it; however, > certificate to certify the one immediately preceding it; however,
some implementations allowed some flexibility. Servers sometimes > some implementations allowed some flexibility. Servers sometimes
send both a current and deprecated intermediate for transitional > send both a current and deprecated intermediate for transitional
purposes, and others are simply configured incorrectly, but these > purposes, and others are simply configured incorrectly, but these
cases can nonetheless be validated properly. For maximum > cases can nonetheless be validated properly. For maximum
compatibility, all implementations SHOULD be prepared to handle > compatibility, all implementations SHOULD be prepared to handle
potentially extraneous certificates and arbitrary orderings from any > potentially extraneous certificates and arbitrary orderings from any
TLS version, with the exception of the end-entity certificate which > TLS version, with the exception of the end-entity certificate which
MUST be first. > MUST be first.
""" """
alpn: Optional[bytes] = None alpn: Optional[bytes] = None
"""The application-layer protocol as negotiated using
[ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation)."""
alpn_offers: Sequence[bytes] = () alpn_offers: Sequence[bytes] = ()
"""The ALPN offers as sent in the ClientHello."""
# we may want to add SSL_CIPHER_description here, but that's currently not exposed by cryptography # we may want to add SSL_CIPHER_description here, but that's currently not exposed by cryptography
cipher: Optional[str] = None cipher: Optional[str] = None
"""The active cipher name as returned by OpenSSL's SSL_CIPHER_get_name""" """The active cipher name as returned by OpenSSL's `SSL_CIPHER_get_name`."""
cipher_list: Sequence[str] = () cipher_list: Sequence[str] = ()
"""Ciphers accepted by the proxy server on this connection.""" """Ciphers accepted by the proxy server on this connection."""
tls_version: Optional[str] = None tls_version: Optional[str] = None
"""The active TLS version."""
sni: Union[str, Literal[True], None] sni: Union[str, Literal[True], None]
"""
The [Server Name Indication (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) sent in the ClientHello.
For server connections, this value may also be set to `True`, which means "use `Server.address`".
"""
timestamp_end: Optional[float] = None timestamp_end: Optional[float] = None
"""Connection end timestamp""" """*Timestamp:* Connection has been closed."""
timestamp_tls_setup: Optional[float] = None timestamp_tls_setup: Optional[float] = None
"""TLS session established.""" """*Timestamp:* TLS handshake has been completed successfully."""
@property @property
def connected(self): def connected(self) -> bool:
"""`True` if Connection.state is ConnectionState.OPEN, `False` otherwise. Read-only."""
return self.state is ConnectionState.OPEN return self.state is ConnectionState.OPEN
@property @property
def tls_established(self) -> bool: def tls_established(self) -> bool:
"""`True` if TLS has been established, `False` otherwise. Read-only."""
return self.timestamp_tls_setup is not None return self.timestamp_tls_setup is not None
def __eq__(self, other): def __eq__(self, other):
@ -102,26 +125,35 @@ class Connection(serializable.Serializable, metaclass=ABCMeta):
@property @property
def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover
warnings.warn("Server.alpn_proto_negotiated is deprecated, use Server.alpn instead.", DeprecationWarning) """*Deprecated:* An outdated alias for Connection.alpn."""
warnings.warn("Connection.alpn_proto_negotiated is deprecated, use Connection.alpn instead.",
DeprecationWarning)
return self.alpn return self.alpn
class Client(Connection): class Client(Connection):
state: ConnectionState = ConnectionState.OPEN """A connection between a client and mitmproxy."""
peername: Address peername: Address
"""The client's address."""
sockname: Address sockname: Address
"""The local address we received this connection on."""
timestamp_start: float
"""TCP SYN received"""
mitmcert: Optional[certs.Cert] = None mitmcert: Optional[certs.Cert] = None
"""
The certificate used by mitmproxy to establish TLS with the client.
"""
sni: Union[str, None] = None sni: Union[str, None] = None
"""The Server Name Indication sent by the client."""
timestamp_start: float
"""*Timestamp:* TCP SYN received"""
def __init__(self, peername, sockname, timestamp_start): def __init__(self, peername, sockname, timestamp_start):
self.id = str(uuid.uuid4()) self.id = str(uuid.uuid4())
self.peername = peername self.peername = peername
self.sockname = sockname self.sockname = sockname
self.timestamp_start = timestamp_start self.timestamp_start = timestamp_start
self.state = ConnectionState.OPEN
def __str__(self): def __str__(self):
if self.alpn: if self.alpn:
@ -190,6 +222,7 @@ class Client(Connection):
@property @property
def address(self): # pragma: no cover def address(self): # pragma: no cover
"""*Deprecated:* An outdated alias for Client.peername."""
warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2) warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2)
return self.peername return self.peername
@ -200,11 +233,13 @@ class Client(Connection):
@property @property
def cipher_name(self) -> Optional[str]: # pragma: no cover def cipher_name(self) -> Optional[str]: # pragma: no cover
"""*Deprecated:* An outdated alias for Connection.cipher."""
warnings.warn("Client.cipher_name is deprecated, use Client.cipher instead.", DeprecationWarning, stacklevel=2) warnings.warn("Client.cipher_name is deprecated, use Client.cipher instead.", DeprecationWarning, stacklevel=2)
return self.cipher return self.cipher
@property @property
def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover
"""*Deprecated:* An outdated alias for Connection.certificate_list[0]."""
warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning, warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning,
stacklevel=2) stacklevel=2)
if self.certificate_list: if self.certificate_list:
@ -222,23 +257,27 @@ class Client(Connection):
class Server(Connection): class Server(Connection):
state: ConnectionState = ConnectionState.CLOSED """A connection between mitmproxy and an upstream server."""
peername: Optional[Address] = None peername: Optional[Address] = None
"""The server's resolved `(ip, port)` tuple. Will be set during connection establishment."""
sockname: Optional[Address] = None sockname: Optional[Address] = None
address: Optional[Address] address: Optional[Address]
"""The server's `(host, port)` address tuple. The host can either be a domain or a plain IP address."""
timestamp_start: Optional[float] = None timestamp_start: Optional[float] = None
"""TCP SYN sent""" """*Timestamp:* TCP SYN sent."""
timestamp_tcp_setup: Optional[float] = None timestamp_tcp_setup: Optional[float] = None
"""TCP ACK received""" """*Timestamp:* TCP ACK received."""
sni: Union[str, Literal[True], None] = True sni: Union[str, Literal[True], None] = True
"""True: client SNI, False: no SNI, bytes: custom value"""
via: Optional[server_spec.ServerSpec] = None via: Optional[server_spec.ServerSpec] = None
"""An optional proxy server specification via which the connection should be established."""
def __init__(self, address: Optional[Address]): def __init__(self, address: Optional[Address]):
self.id = str(uuid.uuid4()) self.id = str(uuid.uuid4())
self.address = address self.address = address
self.state = ConnectionState.CLOSED
def __str__(self): def __str__(self):
if self.alpn: if self.alpn:
@ -308,11 +347,13 @@ class Server(Connection):
@property @property
def ip_address(self) -> Optional[Address]: # pragma: no cover def ip_address(self) -> Optional[Address]: # pragma: no cover
"""*Deprecated:* An outdated alias for `Server.peername`."""
warnings.warn("Server.ip_address is deprecated, use Server.peername instead.", DeprecationWarning, stacklevel=2) warnings.warn("Server.ip_address is deprecated, use Server.peername instead.", DeprecationWarning, stacklevel=2)
return self.peername return self.peername
@property @property
def cert(self) -> Optional[certs.Cert]: # pragma: no cover def cert(self) -> Optional[certs.Cert]: # pragma: no cover
"""*Deprecated:* An outdated alias for `Connection.certificate_list[0]`."""
warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning, warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning,
stacklevel=2) stacklevel=2)
if self.certificate_list: if self.certificate_list:
@ -332,7 +373,7 @@ class Server(Connection):
class Context: class Context:
""" """
Layers get a context object that has all contextual information they require. The context object provided to each `mitmproxy.proxy.layer.Layer` by its parent layer.
""" """
client: Client client: Client

View File

@ -76,7 +76,7 @@ class CommandCompleted(Event):
issubclass(command_cls, commands.Command) and command_cls is not commands.Command issubclass(command_cls, commands.Command) and command_cls is not commands.Command
) )
if not valid_command_subclass: if not valid_command_subclass:
raise RuntimeError(f"{command_cls} needs a properly annotated command attribute.") warnings.warn(f"{command_cls} needs a properly annotated command attribute.", RuntimeWarning)
if command_cls in command_reply_subclasses: if command_cls in command_reply_subclasses:
other = command_reply_subclasses[command_cls] other = command_reply_subclasses[command_cls]
warnings.warn(f"Two conflicting subclasses for {command_cls}: {cls} and {other}", RuntimeWarning) warnings.warn(f"Two conflicting subclasses for {command_cls}: {cls} and {other}", RuntimeWarning)

View File

@ -24,7 +24,7 @@ def test_command_completed():
class FooCommand(commands.Command): class FooCommand(commands.Command):
pass pass
with pytest.raises(RuntimeError, match="properly annotated"): with pytest.warns(RuntimeWarning, match="properly annotated"):
class FooCompleted(events.CommandCompleted): class FooCompleted(events.CommandCompleted):
pass pass

View File

@ -29,7 +29,8 @@ class TestLayer:
assert ( assert (
tutils.Playbook(tlayer, hooks=True, logs=True) tutils.Playbook(tlayer, hooks=True, logs=True)
<< commands.Log(" >> Start({})", "debug") << commands.Log(" >> Start({})", "debug")
<< commands.Log(" << OpenConnection({'connection': Server({'id': '…rverid', 'address': None})})", << commands.Log(" << OpenConnection({'connection': Server({'id': '…rverid', 'address': None, "
"'state': <ConnectionState.CLOSED: 0>})})",
"debug") "debug")
<< commands.OpenConnection(tctx.server) << commands.OpenConnection(tctx.server)
>> events.DataReceived(tctx.client, b"foo") >> events.DataReceived(tctx.client, b"foo")