Skip to content

Commit 8d69c2c

Browse files
authored
Merge pull request #18 from icgood/pp-query
Specify PROXY protocol version in query params
2 parents ecdc2ca + 33770d8 commit 8d69c2c

7 files changed

Lines changed: 59 additions & 44 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ together can demonstrate the process end-to-end: use `proxyprotocol-server`
8787
to proxy connections with a PROXY protocol header to `proxyprotocol-echo`,
8888
which then displays the original connection information.
8989

90+
The `hostname:port` arguments used by both types of servers are parsed by the
91+
[`Address`][8] class, which allows for customization of SSL/TLS and PROXY
92+
protocol versions.
93+
9094
### Echo Server
9195

9296
The `proxyprotocol-echo` server expects inbound connections to provide a PROXY
@@ -106,7 +110,7 @@ header to indicate the original connection information.
106110

107111
```bash
108112
proxyprotocol-server --help
109-
proxyprotocol-server v2 --service localhost:10000 localhost:10007
113+
proxyprotocol-server --service localhost:10000 localhost:10007
110114
```
111115

112116
## Development and Testing
@@ -157,3 +161,4 @@ hinting to the extent possible and common in the rest of the codebase.
157161
[5]: https://docs.python.org/3/library/venv.html
158162
[6]: https://www.python.org/dev/peps/pep-0484/
159163
[7]: http://mypy-lang.org/
164+
[8]: https://icgood.github.io/proxy-protocol/proxyprotocol.html#proxyprotocol.server.Address

doc/source/proxyprotocol.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@
5959

6060
.. automodule:: proxyprotocol.sock
6161
:members:
62+
63+
``proxyprotocol.server``
64+
------------------------
65+
66+
.. automodule:: proxyprotocol.server
67+
:members:

proxyprotocol/server/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from typing import Optional
55
from typing_extensions import Final
66

7+
from .. import ProxyProtocol
8+
from ..version import ProxyProtocolVersion
9+
710
__all__ = ['Address']
811

912

@@ -12,6 +15,7 @@ class Address:
1215
1316
* ``HOST``
1417
* ``HOST:PORT``
18+
* ``HOST:PORT?pp=v1``
1519
* ``ssl://HOST:PORT`` (outbound addresses only)
1620
* ``ssl://HOST:PORT?cert=/path/to/cert.pem``
1721
* ``ssl://HOST:PORT?cert=cert.pem&key=privkey.pem&verify=CERT_REQUIRED``
@@ -25,7 +29,7 @@ class Address:
2529
def __init__(self, addr: str, *, server: bool = False) -> None:
2630
super().__init__()
2731
url = urlsplit(addr)
28-
if not url.scheme and not url.netloc:
32+
if not url.scheme or not url.netloc:
2933
url = urlsplit('//' + addr)
3034
if url.query:
3135
query = parse_qs(url.query)
@@ -46,9 +50,15 @@ def port(self) -> Optional[int]:
4650
"""The port parsed from the address."""
4751
return self.url.port or None
4852

53+
@property
54+
def pp(self) -> ProxyProtocol:
55+
"""The PROXY protocol implementation."""
56+
pp_version = self.query.get('pp', [''])[-1] or 'detect'
57+
return ProxyProtocolVersion.get(pp_version)
58+
4959
@property
5060
def ssl(self) -> Optional[SSLContext]:
51-
"""The :class:`~ssl.SSLContext` to use on the address."""
61+
"""The: class:`~ssl.SSLContext` to use on the address."""
5262
if self.url.scheme == 'ssl':
5363
if self._ssl is None:
5464
if self.server:

proxyprotocol/server/echo.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import sys
99
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
1010
from asyncio import CancelledError, StreamReader, StreamWriter
11+
from contextlib import closing
1112
from functools import partial
1213

1314
from .. import ProxyProtocol
1415
from ..sock import SocketInfo
15-
from ..version import ProxyProtocolVersion
1616
from . import Address
1717

1818
__all__ = ['main']
@@ -23,9 +23,6 @@
2323
def main() -> int:
2424
parser = ArgumentParser(description=__doc__,
2525
formatter_class=ArgumentDefaultsHelpFormatter)
26-
parser.add_argument('type', default='detect', nargs='?',
27-
choices=[v.name.lower() for v in ProxyProtocolVersion],
28-
help='the PROXY protocol version')
2926
parser.add_argument('address', metavar='HOST:PORT',
3027
type=partial(Address, server=True),
3128
nargs='?', default=':10007',
@@ -36,13 +33,12 @@ def main() -> int:
3633
level=logging.INFO,
3734
format='%(asctime)-15s %(name)s %(message)s')
3835

39-
pp = ProxyProtocolVersion.get(args.type)
40-
return asyncio.run(run(pp, args.address))
36+
return asyncio.run(run(args.address))
4137

4238

43-
async def run(pp: ProxyProtocol, address: Address) -> int:
39+
async def run(address: Address) -> int:
4440
loop = asyncio.get_event_loop()
45-
callback = partial(run_conn, pp)
41+
callback = partial(run_conn, address.pp)
4642
server = await asyncio.start_server(
4743
callback, address.host, address.port or 0, ssl=address.ssl)
4844
async with server:
@@ -58,20 +54,21 @@ async def run(pp: ProxyProtocol, address: Address) -> int:
5854

5955
async def run_conn(pp: ProxyProtocol, reader: StreamReader,
6056
writer: StreamWriter) -> None:
61-
result = await pp.read(reader)
62-
sock_info = SocketInfo(writer, result)
63-
_log.info('[%s] Connection received: %s',
64-
sock_info.unique_id.hex(), sock_info)
65-
try:
66-
while True:
67-
line = await reader.readline()
68-
if not line:
69-
break
70-
writer.write(line)
71-
except IOError:
72-
pass
73-
finally:
74-
_log.info('[%s] Connection lost', sock_info.unique_id.hex())
57+
with closing(writer):
58+
result = await pp.read(reader)
59+
sock_info = SocketInfo(writer, result)
60+
_log.info('[%s] Connection received: %s',
61+
sock_info.unique_id.hex(), sock_info)
62+
try:
63+
while True:
64+
line = await reader.readline()
65+
if not line:
66+
break
67+
writer.write(line)
68+
except IOError:
69+
pass
70+
finally:
71+
_log.info('[%s] Connection lost', sock_info.unique_id.hex())
7572

7673

7774
if __name__ == '__main__':

proxyprotocol/server/main.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
from contextlib import AsyncExitStack
1515
from functools import partial
1616

17-
from .. import ProxyProtocol
18-
from ..version import ProxyProtocolVersion
1917
from . import Address
2018
from .protocol import DownstreamProtocol, UpstreamProtocol
2119

@@ -25,16 +23,14 @@
2523
def main() -> int:
2624
parser = ArgumentParser(description=__doc__,
2725
formatter_class=ArgumentDefaultsHelpFormatter)
28-
parser.add_argument('--service', nargs=2, metavar='HOST:PORT', default=[],
26+
parser.add_argument('--service', nargs=2, default=[],
27+
metavar=('SOURCE:PORT', 'DEST:PORT'),
2928
action='append', dest='services',
3029
help='source and destination of a service')
3130
parser.add_argument('--buf-len', metavar='BYTES', default=262144, type=int,
3231
help='size of the read buffer')
3332
parser.add_argument('-q', '--quiet', action='store_true',
3433
help='show only upstream connection errors')
35-
parser.add_argument('type', default='detect', nargs='?',
36-
choices=[v.name.lower() for v in ProxyProtocolVersion],
37-
help='the PROXY protocol version')
3834
args = parser.parse_args()
3935

4036
if not args.services:
@@ -44,17 +40,16 @@ def main() -> int:
4440
level=logging.ERROR if args.quiet else logging.INFO,
4541
format='%(asctime)-15s %(name)s %(message)s')
4642

47-
pp = ProxyProtocolVersion.get(args.type)
48-
return asyncio.run(run(pp, args))
43+
return asyncio.run(run(args))
4944

5045

51-
async def run(pp: ProxyProtocol, args: Namespace) -> int:
46+
async def run(args: Namespace) -> int:
5247
loop = asyncio.get_running_loop()
5348
services = [(Address(source, server=True), Address(dest))
5449
for (source, dest) in args.services]
5550
buf_len: int = args.buf_len
5651
new_server = partial(DownstreamProtocol, UpstreamProtocol,
57-
pp, loop, buf_len)
52+
loop, buf_len)
5853
servers = [
5954
await loop.create_server(partial(new_server, dest),
6055
source.host, source.port or 0,

proxyprotocol/server/protocol.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@
2626

2727
class _Base(BufferedProtocol, metaclass=ABCMeta):
2828

29-
def __init__(self, pp: ProxyProtocol, buf_len: int) -> None:
29+
def __init__(self, buf_len: int) -> None:
3030
super().__init__()
31-
self.pp: Final = pp
3231
self._paused = False
3332
self._buf: bytearray = bytearray(buf_len)
3433
self._queue: Deque[bytes] = deque()
@@ -94,16 +93,17 @@ def proxy_data(self, data: memoryview) -> None:
9493
class DownstreamProtocol(_Base):
9594

9695
def __init__(self, upstream_protocol: Type[UpstreamProtocol],
97-
pp: ProxyProtocol, loop: AbstractEventLoop,
98-
buf_len: int, upstream: Address) -> None:
99-
super().__init__(pp, buf_len)
96+
loop: AbstractEventLoop, buf_len: int,
97+
upstream: Address) -> None:
98+
super().__init__(buf_len)
10099
self.loop: Final = loop
101100
self.upstream: Final = upstream
102101
self.id: Final = uuid4().bytes
103102
self._waiting: Deque[memoryview] = deque()
104103
self._connect: Optional[Task[Any]] = None
105104
self._upstream: Optional[UpstreamProtocol] = None
106-
self._upstream_factory = partial(upstream_protocol, self)
105+
self._upstream_factory = partial(upstream_protocol, self, buf_len,
106+
upstream.pp)
107107

108108
def close(self) -> None:
109109
super().close()
@@ -158,8 +158,10 @@ def proxy_data(self, data: memoryview) -> None:
158158

159159
class UpstreamProtocol(_Base):
160160

161-
def __init__(self, downstream: DownstreamProtocol) -> None:
162-
super().__init__(downstream.pp, len(downstream._buf))
161+
def __init__(self, downstream: DownstreamProtocol, buf_len: int,
162+
pp: ProxyProtocol) -> None:
163+
super().__init__(buf_len)
164+
self.pp: Final = pp
163165
self.downstream: Final = downstream
164166

165167
def close(self) -> None:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
license = f.read()
2929

3030
setup(name='proxy-protocol',
31-
version='0.5.6',
31+
version='0.5.7',
3232
author='Ian Good',
3333
author_email='ian@icgood.net',
3434
description='PROXY protocol library with asyncio server implementation',

0 commit comments

Comments
 (0)