Skip to content

Commit 3d85e9b

Browse files
authored
Merge pull request #46 from icgood/callback
Simplify callback with asyncio.start_server
2 parents 79e37db + 48a25b2 commit 3d85e9b

8 files changed

Lines changed: 72 additions & 29 deletions

File tree

.github/workflows/docker-build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
with:
2828
images: |
2929
ghcr.io/${{ github.repository }}
30+
labels: |
31+
org.opencontainers.image.source=https://github.com/icgood/proxy-protocol/tree/main/docker
3032
tags: |
3133
type=edge,branch=main
3234
type=pep440,pattern={{major}}

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,13 @@ from proxyprotocol.sock import SocketInfo
4343
async def run(host: str, port: int) -> None:
4444
pp_detect = ProxyProtocolDetect()
4545
pp_reader = ProxyProtocolReader(pp_detect)
46-
callback = partial(on_connection, pp_reader)
46+
callback = reader.get_callback(on_connection)
4747
server = await asyncio.start_server(callback, host, port)
4848
async with server:
4949
await server.serve_forever()
5050

51-
async def on_connection(pp_reader: ProxyProtocolReader,
52-
reader: StreamReader, writer: StreamWriter) -> None:
53-
result = await pp_reader.read(reader)
54-
info = SocketInfo.get(writer, result)
51+
async def on_connection(reader: StreamReader, writer: StreamWriter,
52+
info: SocketInfo) -> None:
5553
print(info.family, info.peername)
5654
# ... continue using connection
5755
```

proxyprotocol/checksum.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,19 @@ class Checksum(Protocol):
2121
"""Provides typing compatible with the `crc32c.crc32c
2222
<https://github.com/ICRAR/crc32c#usage>`_ function, if it is installed.
2323
24-
Args:
25-
val: The bytestring to checksum.
26-
crc: The checksum of previous portions of data.
24+
.. automethod:: __call__
2725
2826
"""
2927

3028
__slots__: Sequence[str] = []
3129

3230
@abstractmethod
3331
def __call__(self, val: bytes, crc: int = ...) -> int:
32+
"""The checksum function.
33+
34+
Args:
35+
val: The bytestring to checksum.
36+
crc: The checksum of previous portions of data.
37+
38+
"""
3439
...

proxyprotocol/reader.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11

22
from __future__ import annotations
33

4-
from typing_extensions import Final
4+
import asyncio
5+
from asyncio import StreamReader, StreamWriter
6+
from functools import partial
7+
from typing import Any, Awaitable, Callable, Coroutine, Union
8+
from typing_extensions import Final, TypeAlias
59

610
from . import ProxyProtocol, ProxyProtocolIncompleteError, \
711
ProxyProtocolWantRead
812
from .result import ProxyResult, ProxyResultUnknown
13+
from .sock import SocketInfo
914
from .typing import StreamReaderProtocol
1015

1116
__all__ = ['ProxyProtocolReader']
1217

18+
_Callback: TypeAlias = Callable[
19+
[StreamReader, StreamWriter], Awaitable[None]]
20+
_WrappedCallback: TypeAlias = Callable[
21+
[StreamReader, StreamWriter, SocketInfo], Coroutine[Any, Any, None]]
22+
1323

1424
class ProxyProtocolReader:
1525
"""Read a PROXY protocol header from a stream.
@@ -47,7 +57,35 @@ async def read(self, reader: StreamReaderProtocol) -> ProxyResult:
4757
return self.pp.unpack(view)
4858
except ProxyProtocolIncompleteError as exc:
4959
want_read = exc.want_read
50-
try:
51-
data += await self._handle_want(reader, want_read)
52-
except (EOFError, ConnectionResetError) as exc:
53-
return ProxyResultUnknown(exc)
60+
data += await self._handle_want(reader, want_read)
61+
62+
def get_callback(self, callback: _WrappedCallback,
63+
timeout: Union[None, int, float] = 3) -> _Callback:
64+
"""Get a callback object for use as the *client_connected_cb* argument
65+
to :func:`asyncio.start_server`.
66+
67+
The returned callback will first read the PROXY protocol header before
68+
starting the provided *callback* as a :class:`~asyncio.Task`. The
69+
*callback* argument is similar to *client_connected_cb* but with an
70+
additional positional argument -- the
71+
:class:`~proxyprotocol.sock.SocketInfo` read from the header.
72+
73+
Args:
74+
callback: Async function with arguments ``(reader, writer,
75+
sock_info)`` called after successfully reading the header.
76+
timeout: A timeout in seconds to allow for reading the header.
77+
78+
"""
79+
return partial(self._read_then_call, callback, timeout)
80+
81+
async def _read_then_call(self, callback: _WrappedCallback,
82+
timeout: Union[None, int, float],
83+
reader: StreamReader, writer: StreamWriter) \
84+
-> None:
85+
try:
86+
result = await asyncio.wait_for(self.read(reader), timeout)
87+
except Exception as exc:
88+
writer.close()
89+
result = ProxyResultUnknown(exc)
90+
sock_info = SocketInfo.get(writer, result)
91+
asyncio.create_task(callback(reader, writer, sock_info))

proxyprotocol/server/echo.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from contextlib import closing
1212
from functools import partial
1313

14-
from .. import ProxyProtocol
1514
from ..reader import ProxyProtocolReader
1615
from ..sock import SocketInfo
1716
from . import Address
@@ -39,7 +38,8 @@ def main() -> int:
3938

4039
async def run(address: Address) -> int:
4140
loop = asyncio.get_event_loop()
42-
callback = partial(run_conn, address.pp)
41+
reader = ProxyProtocolReader(address.pp)
42+
callback = reader.get_callback(run_conn)
4343
server = await asyncio.start_server(
4444
callback, address.host, address.port or 0, ssl=address.ssl)
4545
async with server:
@@ -53,12 +53,9 @@ async def run(address: Address) -> int:
5353
return 0
5454

5555

56-
async def run_conn(pp: ProxyProtocol, reader: StreamReader,
57-
writer: StreamWriter) -> None:
58-
pp_reader = ProxyProtocolReader(pp)
56+
async def run_conn(reader: StreamReader, writer: StreamWriter,
57+
sock_info: SocketInfo) -> None:
5958
with closing(writer):
60-
result = await pp_reader.read(reader)
61-
sock_info = SocketInfo.get(writer, result)
6259
_log.info('[%s] Connection received: %s',
6360
sock_info.unique_id.hex(), sock_info)
6461
if sock_info.dnsbl is not None:

proxyprotocol/sock.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from abc import abstractmethod, ABCMeta
66
from ipaddress import ip_address, IPv4Address, IPv6Address
77
from socket import AddressFamily, SocketKind
8-
from typing import Union, Optional
8+
from typing import Dict, Optional, Union
99

10-
from .result import ProxyResult
10+
from .result import is_unknown, ProxyResult
1111
from .typing import SockAddr, Cipher, PeerCert, TransportProtocol
1212

1313
__all__ = ['SocketInfo', 'SocketInfoProxy', 'SocketInfoLocal']
@@ -324,8 +324,13 @@ def dnsbl(self) -> Optional[str]:
324324
return self._result.tlv.ext.dnsbl
325325

326326
def __repr__(self) -> str:
327-
return f'<SocketInfoProxy peername={self.peername_str!r} ' \
328-
f'sockname={self.sockname_str!r}>'
327+
data: Dict[str, object] = {'peername': self.peername_str,
328+
'sockname': self.sockname_str}
329+
if is_unknown(self._result):
330+
data['exc'] = self._result.exception
331+
data_str = ''.join(f' {k}={v!r}' for k, v in data.items()
332+
if v is not None)
333+
return f'<SocketInfoProxy{data_str}>'
329334

330335

331336
class SocketInfoLocal(SocketInfo):

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ build-backend = 'hatchling.build'
2525

2626
[project]
2727
name = 'proxy-protocol'
28-
version = '0.9.1'
28+
version = '0.10.0'
2929
authors = [
3030
{ name = 'Ian Good', email = 'ian@icgood.net' },
3131
]
@@ -35,8 +35,6 @@ readme = { file = 'README.md', content-type = 'text/markdown' }
3535
requires-python = '~=3.8'
3636
classifiers = [
3737
'Development Status :: 3 - Alpha',
38-
'Topic :: Communications :: Email :: Post-Office',
39-
'Topic :: Communications :: Email :: Post-Office :: IMAP',
4038
'Intended Audience :: Developers',
4139
'Intended Audience :: Information Technology',
4240
'License :: OSI Approved :: MIT License',

test/test_sock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def test_str_ipv6(self) -> None:
236236
"sockname='[::1]:10'>", repr(info))
237237

238238
def test_str_unknown(self) -> None:
239-
result = ProxyResultUnknown()
239+
result = ProxyResultUnknown(RuntimeError())
240240
info = SocketInfo.get(self.transport, result)
241-
self.assertEqual('<SocketInfoProxy peername=None sockname=None>',
241+
self.assertEqual('<SocketInfoProxy exc=RuntimeError()>',
242242
repr(info))

0 commit comments

Comments
 (0)