PKcpK:zaiohttp_remotes/__init__.py"""Control remote side information. Properly sets up host, scheme and remote properties of aiohttp.web.Request if the server is deployed behind reverse proxy. """ __version__ = '0.0.2' from .allowed_hosts import AllowedHosts from .basic_auth import BasicAuth from .cloudflare import Cloudflare from .forwarded import ForwardedRelaxed, ForwardedStrict from .secure import Secure from .x_forwarded import XForwardedRelaxed, XForwardedStrict async def setup(app, *args): for arg in args: await arg.setup(app) __all__ = ('AllowedHosts', 'BasicAuth', 'Cloudflare', 'ForwardedRelaxed', 'ForwardedStrict', 'Secure', 'XForwardedRelaxed', 'XForwardedStrict', 'setup') PKYfK^Xaiohttp_remotes/abc.pyimport abc from aiohttp import web class ABC(abc.ABC): @abc.abstractmethod async def setup(self, app): pass # pragma: no cover async def raise_error(self, request): raise web.HTTPBadRequest() PKYfKvXX aiohttp_remotes/allowed_hosts.pyfrom aiohttp import web from .abc import ABC class ANY: def __contains__(self, item): return True class AllowedHosts(ABC): def __init__(self, allowed_hosts=None, *, white_paths=()): if allowed_hosts is None: allowed_hosts = {'*'} else: allowed_hosts = set(allowed_hosts) if '*' in allowed_hosts: allowed_hosts = ANY() self._allowed_hosts = allowed_hosts self._white_paths = set(white_paths) async def setup(self, app): app.middlewares.append(self.middleware) @web.middleware async def middleware(self, request, handler): if ( request.path not in self._white_paths and request.host not in self._allowed_hosts ): await self.raise_error(request) return await handler(request) PKYfK{)6..aiohttp_remotes/basic_auth.pyimport base64 import binascii from aiohttp import hdrs, web from .abc import ABC class BasicAuth(ABC): def __init__(self, username, password, realm, *, white_paths=()): self._username = username self._password = password self._realm = realm self._white_paths = set(white_paths) async def setup(self, app): app.middlewares.append(self.middleware) @property def username(self): return self._username @property def password(self): return self._password @property def realm(self): return self._realm @property def white_paths(self): return self._white_paths async def raise_error(self, request): raise web.HTTPUnauthorized( headers={ hdrs.WWW_AUTHENTICATE: 'Basic realm={}'.format(self._realm) }, ) @web.middleware async def middleware(self, request, handler): if request.path not in self._white_paths: auth_header = request.headers.get(hdrs.AUTHORIZATION) if auth_header is None or not auth_header.startswith('Basic '): return await self.raise_error(request) try: secret = auth_header[6:].encode('utf-8') auth_decoded = base64.decodestring(secret).decode('utf-8') except (UnicodeDecodeError, UnicodeEncodeError, binascii.Error): await self.raise_error(request) credentials = auth_decoded.split(':') if len(credentials) != 2: await self.raise_error(request) username, password = credentials if username != self._username or password != self._password: await self.raise_error(request) return await handler(request) PK7fKXQQaiohttp_remotes/cloudflare.pyfrom ipaddress import ip_address, ip_network import aiohttp from aiohttp import web from .abc import ABC from .log import logger class Cloudflare(ABC): def __init__(self, client=None): self._ip_networks = set() self._client = client def _parse_mask(self, text): ret = set() for mask in text.splitlines(): try: mask = ip_network(mask) except (ValueError, TypeError): continue ret.add(mask) return ret async def setup(self, app): if self._client is not None: # pragma: no branch client = self._client else: client = aiohttp.ClientSession() # pragma: no cover try: async with client.get( 'https://www.cloudflare.com/ips-v4') as response: self._ip_networks |= self._parse_mask(await response.text()) async with client.get( 'https://www.cloudflare.com/ips-v6') as response: self._ip_networks |= self._parse_mask(await response.text()) finally: if self._client is None: # pragma: no cover await client.close() if not self._ip_networks: raise RuntimeError("No networks are available") app.middlewares.append(self.middleware) @web.middleware async def middleware(self, request, handler): remote_ip = ip_address(request.remote) for network in self._ip_networks: if remote_ip in network: request = request.clone( remote=request.headers['CF-CONNECTING-IP']) return await handler(request) msg = "Not cloudflare: %(remote_ip)s" context = {'remote_ip': remote_ip} logger.error(msg, context) await self.raise_error(request) PKYfK aiohttp_remotes/exceptions.pyfrom .log import logger class RemoteError(Exception): def log(self, request): raise NotImplementedError # pragma: no cover class TooManyHeaders(RemoteError): @property def header(self): return self.args[0] def log(self, request): msg = "Too many headers for %(header)s" context = {'header': self.header} extra = context.copy() extra['request'] = request logger.error(msg, context, extra=extra) class IncorrectIPCount(RemoteError): @property def expected(self): return self.args[0] @property def actual(self): return self.args[1] def log(self, request): msg = ("Too many X-Forwarded-For values: %(actual)s, " "expected %(expected)s") context = {'actual': self.actual, 'expected': self.expected} extra = context.copy() extra['request'] = request logger.error(msg, context, extra=extra) class IncorrectForwardedCount(RemoteError): @property def expected(self): return self.args[0] @property def actual(self): return self.args[1] def log(self, request): msg = ("Too many Forwarded values: %(actual)s, " "expected %(expected)s") context = {'actual': self.actual, 'expected': self.expected} extra = context.copy() extra['request'] = request logger.error(msg, context, extra=extra) class IncorrectProtoCount(RemoteError): @property def expected(self): return self.args[0] @property def actual(self): return self.args[1] def log(self, request): msg = ("Too many X-Forwarded-Proto values: %(actual)s, " "expected %(expected)s") context = {'actual': self.actual, 'expected': self.expected} extra = context.copy() extra['request'] = request logger.error(msg, context, extra=extra) class UntrustedIP(RemoteError): @property def ip(self): return self.args[0] @property def trusted(self): return self.args[1] def log(self, request): msg = "Untrusted IP: %(ip)s, trusted: %(expected)s" context = {'ip': self.ip, 'trusted': self.trusted} extra = context.copy() extra['request'] = request logger.error(msg, context, extra=extra) logger.error(msg, context, extra=extra) PKYfKwK:j j aiohttp_remotes/forwarded.pyfrom ipaddress import ip_address from aiohttp import web from .abc import ABC from .exceptions import IncorrectForwardedCount, RemoteError from .utils import parse_trusted_list, remote_ip class ForwardedRelaxed(ABC): def __init__(self, num=1): self._num = num async def setup(self, app): app.middlewares.append(self.middleware) @web.middleware async def middleware(self, request, handler): overrides = {} for elem in reversed(request.forwarded[-self._num:]): for_ = elem.get('for') if for_: overrides['remote'] = for_ proto = elem.get('proto') if proto is not None: overrides['scheme'] = proto host = elem.get('host') if host is not None: overrides['host'] = host request = request.clone(**overrides) return await handler(request) class ForwardedStrict(ABC): def __init__(self, trusted, *, white_paths=()): self._trusted = parse_trusted_list(trusted) self._white_paths = set(white_paths) async def setup(self, app): app.middlewares.append(self.middleware) @web.middleware async def middleware(self, request, handler): if request.path in self._white_paths: return await handler(request) try: overrides = {} forwarded = request.forwarded if len(self._trusted) != len(forwarded): raise IncorrectForwardedCount(len(self._trusted), len(forwarded)) peer_ip, _ = request.transport.get_extra_info('peername') ips = [ip_address(peer_ip)] for elem in reversed(request.forwarded): for_ = elem.get('for') if for_: ips.append(ip_address(for_)) proto = elem.get('proto') if proto is not None: overrides['scheme'] = proto host = elem.get('host') if host is not None: overrides['host'] = host overrides['remote'] = str(remote_ip(self._trusted, ips)) request = request.clone(**overrides) return await handler(request) except RemoteError as exc: exc.log(request) await self.raise_error(request) PKYfKIk88aiohttp_remotes/log.pyimport logging logger = logging.getLogger(__package__) PKYfKEWx aiohttp_remotes/secure.pyfrom aiohttp import web from yarl import URL from .abc import ABC from .log import logger @web.middleware class Secure(ABC): def __init__(self, *, redirect=True, redirect_url=None, x_frame='DENY', sts='max-age=31536000; includeSubDomains', cto='nosniff', xss='1; mode=block', white_paths=()): self._redirect = redirect if redirect_url is not None: redirect_url = URL(redirect_url) if redirect_url.scheme != 'https': raise ValueError("Redirection url {} should have " "HTTPS scheme".format(redirect_url)) if redirect_url.origin() != redirect_url: raise ValueError("Redirection url {} should have no " "path, query and fragment parts".format( redirect_url)) self._redirect_url = redirect_url self._x_frame = x_frame self._sts = sts self._cto = cto self._xss = xss self._white_paths = set(white_paths) async def setup(self, app): app.on_response_prepare.append(self.on_response_prepare) app.middlewares.append(self.middleware) async def on_response_prepare(self, request, response): x_frame = self._x_frame if x_frame is not None: response.headers.setdefault('X-Frame-Options', x_frame) sts = self._sts if sts is not None: response.headers.setdefault('Strict-Transport-Security', sts) cto = self._cto if cto is not None: response.headers.setdefault('X-Content-Type-Options', cto) xss = self._xss if xss is not None: response.headers.setdefault('X-XSS-Protection', xss) @web.middleware async def middleware(self, request, handler): whitepath = request.path in self._white_paths if not whitepath and not request.secure: if self._redirect: if self._redirect_url: url = self._redirect_url.join(request.url.relative()) else: url = request.url.with_scheme('https').with_port(None) raise web.HTTPPermanentRedirect(url) else: msg = "Not secure URL %(url)s" logger.error(msg, {'url': request.url}) await self.raise_error(request) return await handler(request) PKYfKaiohttp_remotes/utils.pyfrom collections.abc import Container, Sequence from ipaddress import (IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network) from .exceptions import IncorrectIPCount, UntrustedIP MSG = ("Trusted list should be a sequence of sets " "with either addresses or networks.") IP_CLASSES = (IPv4Address, IPv6Address, IPv4Network, IPv6Network) def parse_trusted_list(lst): if isinstance(lst, str) or not isinstance(lst, Sequence): raise TypeError(MSG) out = [] has_ellipsis = False for elem in lst: if elem is ...: has_ellipsis = True new_elem = ... else: if has_ellipsis: raise ValueError( "Ellipsis is allowed only at the end of list") if isinstance(elem, str) or not isinstance(elem, Container): raise TypeError(MSG) new_elem = [] for item in elem: if isinstance(item, IP_CLASSES): new_elem.append(item) continue try: new_elem.append(ip_address(item)) except ValueError: try: new_elem.append(ip_network(item)) except ValueError: raise ValueError( "{!r} is not IPv4 or IPv6 address or network" .format(item)) out.append(new_elem) return out def remote_ip(trusted, ips): if len(trusted) + 1 != len(ips): raise IncorrectIPCount(len(trusted) + 1, ips) for i in range(len(trusted)): ip = ips[i] tr = trusted[i] if tr is ...: return ip check_ip(tr, ip) return ips[-1] def check_ip(trusted, ip): for elem in trusted: if isinstance(elem, (IPv4Address, IPv6Address)): if elem == ip: break else: if ip in elem: break else: raise UntrustedIP(ip, trusted) PKYfKLVa77aiohttp_remotes/x_forwarded.pyfrom ipaddress import ip_address from aiohttp import hdrs, web from .abc import ABC from .exceptions import IncorrectProtoCount, RemoteError, TooManyHeaders from .utils import parse_trusted_list, remote_ip class XForwardedBase(ABC): async def setup(self, app): app.middlewares.append(self.middleware) def get_forwarded_for(self, headers): forwarded_for = headers.getall(hdrs.X_FORWARDED_FOR, []) if not forwarded_for: return [] if len(forwarded_for) > 1: raise TooManyHeaders(hdrs.X_FORWARDED_FOR) forwarded_for = forwarded_for[0].split(',') forwarded_for = [ ip_address(addr) for addr in (a.strip() for a in forwarded_for) if addr ] return forwarded_for def get_forwarded_proto(self, headers): forwarded_proto = headers.getall(hdrs.X_FORWARDED_PROTO, []) if not forwarded_proto: return [] if len(forwarded_proto) > 1: raise TooManyHeaders(hdrs.X_FORWARDED_PROTO) forwarded_proto = forwarded_proto[0].split(',') forwarded_proto = [p.strip() for p in forwarded_proto] return forwarded_proto def get_forwarded_host(self, headers): forwarded_host = headers.getall(hdrs.X_FORWARDED_HOST, []) if len(forwarded_host) > 1: raise TooManyHeaders(hdrs.X_FORWARDED_HOST) return forwarded_host[0] if forwarded_host else None class XForwardedRelaxed(XForwardedBase): def __init__(self, num=1): self._num = num @web.middleware async def middleware(self, request, handler): try: overrides = {} headers = request.headers forwarded_for = self.get_forwarded_for(headers) if forwarded_for: overrides['remote'] = str(forwarded_for[-self._num]) proto = self.get_forwarded_proto(headers) if proto: overrides['scheme'] = proto[-self._num] host = self.get_forwarded_host(headers) if host is not None: overrides['host'] = host request = request.clone(**overrides) return await handler(request) except RemoteError as exc: exc.log(request) await self.raise_error(request) class XForwardedStrict(XForwardedBase): def __init__(self, trusted, *, white_paths=()): self._trusted = parse_trusted_list(trusted) self._white_paths = set(white_paths) @web.middleware async def middleware(self, request, handler): if request.path in self._white_paths: return await handler(request) try: overrides = {} headers = request.headers forwarded_for = self.get_forwarded_for(headers) peer_ip, _ = request.transport.get_extra_info('peername') ips = [ip_address(peer_ip)] + list(reversed(forwarded_for)) ip = remote_ip(self._trusted, ips) overrides['remote'] = str(ip) proto = self.get_forwarded_proto(headers) if proto: if len(proto) > len(self._trusted): raise IncorrectProtoCount(len(self._trusted), proto) overrides['scheme'] = proto[0] host = self.get_forwarded_host(headers) if host is not None: overrides['host'] = host request = request.clone(**overrides) return await handler(request) except RemoteError as exc: exc.log(request) await self.raise_error(request) PKj|ZKLBB'aiohttp-remotes-0.0.2.dist-info/LICENSEThe MIT License Copyright (c) 2017 Ocean S. A. https://ocean.io/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H=nRR%aiohttp-remotes-0.0.2.dist-info/WHEEL1 W%0n1JBZS`ޝ/Qf:3&.>`6Kŵ+|Eh Y00?PK!HyIz(aiohttp-remotes-0.0.2.dist-info/METADATAN@"M4V)zieBwn![A,\sL\::+fRa x\T''zJ0d WpR@`YaX{6ܺYU K.kN(Cmm2̂qk613Rc(D%mеl?2h޳~ BoxNǐԵ-:^$ åJ.a"u[P} _4b5em"A+j'2ڎϔue\Cf'"lZ"{+uc򋇾^PK!HQ(&aiohttp-remotes-0.0.2.dist-info/RECORDْH~f.UE@Q`'I$YL#&"8zx(np,8~q܄VvjX$k/GEh mw͓Bb ?IhѼ~F* ) nC=ܽ\8awmRq ш^rdH}V^r;d5:}i@'XԲ5c 9UkElȊoj)/d(^4aQDaÝ,? :8}PhȞuž#+5ni̹=*rEԃ.b63]r [Ioj %"Aүƥ2G䔩Q+}CJX/JSoh "d]ت/CN,Zp)⋏PM)TfL:Pש :Je. "ikC\N38@Fx`!R'NxH0W%6Pvꧾcƛ6 q‰kHs6H/KÒ|$Jڪs? EZ5D\s lv Gd9"`Y<{uD%Sṷ/"m랬ɞ+@{gtRE1 '_ l}uuoPKcpK:zaiohttp_remotes/__init__.pyPKYfK^Xaiohttp_remotes/abc.pyPKYfKvXX .aiohttp_remotes/allowed_hosts.pyPKYfK{)6..aiohttp_remotes/basic_auth.pyPK7fKXQQ-aiohttp_remotes/cloudflare.pyPKYfK aiohttp_remotes/exceptions.pyPKYfKwK:j j  aiohttp_remotes/forwarded.pyPKYfKIk88G*aiohttp_remotes/log.pyPKYfKEWx *aiohttp_remotes/secure.pyPKYfK4aiohttp_remotes/utils.pyPKYfKLVa77=aiohttp_remotes/x_forwarded.pyPKj|ZKLBB'Kaiohttp-remotes-0.0.2.dist-info/LICENSEPK!H=nRR%Paiohttp-remotes-0.0.2.dist-info/WHEELPK!HyIz(Paiohttp-remotes-0.0.2.dist-info/METADATAPK!HQ(&9Raiohttp-remotes-0.0.2.dist-info/RECORDPKuGU