%PDF- %PDF-
| Direktori : /data/old/usr/lib/python3.4/site-packages/dohproxy/ |
| Current File : //data/old/usr/lib/python3.4/site-packages/dohproxy/proxy.py |
#!/usr/bin/env python3
#
# Copyright (c) 2018-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
#
import asyncio
import collections
import dns.message
import dns.rcode
import io
import ssl
from dohproxy import constants, utils
from dohproxy.protocol import (
DNSClientProtocol,
DOHDNSException,
DOHParamsException,
)
from typing import List, Tuple
from h2.config import H2Configuration
from h2.connection import H2Connection
from h2.events import (
ConnectionTerminated, DataReceived, RequestReceived, StreamEnded
)
from h2.errors import ErrorCodes
from h2.exceptions import ProtocolError
RequestData = collections.namedtuple('RequestData', ['headers', 'data'])
def parse_args():
parser = utils.proxy_parser_base(port=443, secure=True)
return parser.parse_args()
class H2Protocol(asyncio.Protocol):
def __init__(self, upstream_resolver=None, upstream_port=None,
uri=None, logger=None, debug=False):
config = H2Configuration(client_side=False, header_encoding='utf-8')
self.conn = H2Connection(config=config)
self.logger = logger
if logger is None:
self.logger = utils.configure_logger('doh-proxy', 'DEBUG')
self.transport = None
self.debug = debug
self.stream_data = {}
self.upstream_resolver = upstream_resolver
self.upstream_port = upstream_port
self.uri = constants.DOH_URI if uri is None else uri
assert upstream_resolver is not None, \
'An upstream resolver must be provided'
assert upstream_port is not None, \
'An upstream resolver port must be provided'
def connection_made(self, transport: asyncio.Transport): # type: ignore
self.transport = transport
self.conn.initiate_connection()
self.transport.write(self.conn.data_to_send())
def data_received(self, data: bytes):
try:
events = self.conn.receive_data(data)
except ProtocolError as e:
self.transport.write(self.conn.data_to_send())
self.transport.close()
else:
self.transport.write(self.conn.data_to_send())
for event in events:
if isinstance(event, RequestReceived):
self.request_received(event.headers, event.stream_id)
elif isinstance(event, DataReceived):
self.receive_data(event.data, event.stream_id)
elif isinstance(event, StreamEnded):
self.stream_complete(event.stream_id)
elif isinstance(event, ConnectionTerminated):
self.transport.close()
self.transport.write(self.conn.data_to_send())
def request_received(self, headers: List[Tuple[str, str]], stream_id: int):
_headers = collections.OrderedDict(headers)
method = _headers[':method']
# We only support GET and POST.
if method not in ('GET', 'POST'):
self.return_405(stream_id)
return
# Store off the request data.
request_data = RequestData(_headers, io.BytesIO())
self.stream_data[stream_id] = request_data
def stream_complete(self, stream_id: int):
"""
When a stream is complete, we can send our response.
"""
try:
request_data = self.stream_data[stream_id]
except KeyError:
# Just return, we probably 405'd this already
return
headers = request_data.headers
method = request_data.headers[':method']
# Handle the actual query
path, params = utils.extract_path_params(headers[':path'])
if path != self.uri:
self.return_404(stream_id)
return
if method == 'GET':
try:
ct, body = utils.extract_ct_body(params)
except DOHParamsException as e:
self.return_400(stream_id, body=e.body())
return
else:
body = request_data.data.getvalue()
ct = headers.get('content-type')
if ct != constants.DOH_MEDIA_TYPE:
self.return_415(stream_id)
return
# Do actual DNS Query
try:
dnsq = utils.dns_query_from_body(body, self.debug)
except DOHDNSException as e:
self.return_400(stream_id, body=e.body())
return
self.logger.info(
'[HTTPS] Received: ID {} Question {} Peer {}'.format(
dnsq.id,
dnsq.question[0],
self.transport.get_extra_info('peername'),
)
)
asyncio.ensure_future(self.resolve(dnsq, stream_id))
def on_answer(self, stream_id, dnsr=None, dnsq=None):
headers = {
'Content-Type': constants.DOH_MEDIA_TYPE,
}
if dnsr is None:
dnsr = dns.message.make_response(dnsq)
dnsr.set_rcode(dns.rcode.SERVFAIL)
elif len(dnsr.answer):
ttl = min(r.ttl for r in dnsr.answer)
headers['cache-control'] = 'max-age={}'.format(ttl)
self.logger.info(
'[HTTPS] Send: ID {} Question {} Peer {}'.format(
dnsr.id,
dnsr.question[0],
self.transport.get_extra_info('peername')
)
)
body = dnsr.to_wire()
response_headers = (
(':status', '200'),
('content-type', constants.DOH_MEDIA_TYPE),
('content-length', str(len(body))),
('server', 'asyncio-h2'),
)
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, body, end_stream=True)
self.transport.write(self.conn.data_to_send())
async def resolve(self, dnsq, stream_id):
qid = dnsq.id
loop = asyncio.get_event_loop()
queue = asyncio.Queue(maxsize=1)
await loop.create_datagram_endpoint(
lambda: DNSClientProtocol(dnsq, queue),
remote_addr=(self.upstream_resolver, self.upstream_port))
self.logger.debug("Waiting for DNS response")
try:
dnsr = await asyncio.wait_for(queue.get(), 10)
dnsr.id = qid
queue.task_done()
self.on_answer(stream_id, dnsr=dnsr)
except asyncio.TimeoutError:
self.logger.debug("Request timed out")
self.on_answer(stream_id, dnsq=dnsq)
def return_XXX(self, stream_id: int, status: int, body: bytes = b''):
"""
Wrapper to return a status code and some optional content.
"""
response_headers = (
(':status', str(status)),
('content-length', str(len(body))),
('server', 'asyncio-h2'),
)
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, body, end_stream=True)
def return_400(self, stream_id: int, body: bytes = b''):
"""
We don't support the given PATH, so we want to return a 403 response.
"""
self.return_XXX(stream_id, 400, body)
def return_403(self, stream_id: int, body: bytes = b''):
"""
We don't support the given PATH, so we want to return a 403 response.
"""
self.return_XXX(stream_id, 403, body)
def return_404(self, stream_id: int):
"""
We don't support the given PATH, so we want to return a 403 response.
"""
self.return_XXX(stream_id, 404, body=b'Wrong path')
def return_405(self, stream_id: int):
"""
We don't support the given method, so we want to return a 405 response.
"""
self.return_XXX(stream_id, 405)
def return_415(self, stream_id: int):
"""
We don't support the given media, so we want to return a 415 response.
"""
self.return_XXX(stream_id, 415, body=b'Unsupported content type')
def receive_data(self, data: bytes, stream_id: int):
"""
We've received some data on a stream. If that stream is one we're
expecting data on, save it off. Otherwise, reset the stream.
"""
try:
stream_data = self.stream_data[stream_id]
except KeyError:
self.conn.reset_stream(
stream_id, error_code=ErrorCodes.PROTOCOL_ERROR
)
else:
stream_data.data.write(data)
def ssl_context(options):
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(options.certfile, keyfile=options.keyfile)
ctx.set_alpn_protocols(["h2"])
ctx.options |= (
ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION
)
ctx.set_ciphers("ECDHE+AESGCM")
return ctx
def main():
args = parse_args()
logger = utils.configure_logger('doh-proxy', args.level)
ssl_ctx = ssl_context(args)
loop = asyncio.get_event_loop()
coro = loop.create_server(
lambda: H2Protocol(
upstream_resolver=args.upstream_resolver,
upstream_port=args.upstream_port,
uri=args.uri,
logger=logger,
debug=args.debug),
host=args.listen_address,
port=args.port,
ssl=ssl_ctx)
server = loop.run_until_complete(coro)
# Serve requests until Ctrl+C is pressed
logger.info('Serving on {}'.format(server))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
if __name__ == '__main__':
main()