mirror of
https://github.com/django/daphne.git
synced 2024-11-25 17:23:43 +03:00
Merge pull request #37 from mcallistersean/ticket_10
use twisted endpoint description strings to bind to ports and sockets
This commit is contained in:
commit
fd83678276
11
README.rst
11
README.rst
|
@ -37,6 +37,17 @@ To achieve this you can use the --fd flag::
|
||||||
|
|
||||||
daphne --fd 5 django_project.asgi:channel_layer
|
daphne --fd 5 django_project.asgi:channel_layer
|
||||||
|
|
||||||
|
|
||||||
|
If you want more control over the port/socket bindings you can fall back to
|
||||||
|
using `twisted's endpoint description strings
|
||||||
|
<http://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html#serverFromString>`_
|
||||||
|
by using the `--endpoint (-e)` flag, which can be used multiple times.
|
||||||
|
This line would start a SSL server on port 443, assuming that `key.pem` and `crt.pem`
|
||||||
|
exist in the current directory (requires pyopenssl to be installed)::
|
||||||
|
|
||||||
|
daphne -e ssl:443:privateKey=key.pem:certKey=crt.pem django_project.asgi:channel_layer
|
||||||
|
|
||||||
|
|
||||||
To see all available command line options run daphne with the *-h* flag.
|
To see all available command line options run daphne with the *-h* flag.
|
||||||
|
|
||||||
Root Path (SCRIPT_NAME)
|
Root Path (SCRIPT_NAME)
|
||||||
|
|
|
@ -2,12 +2,14 @@ import sys
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import importlib
|
import importlib
|
||||||
from .server import Server
|
from .server import Server, build_endpoint_description_strings
|
||||||
from .access import AccessLogGenerator
|
from .access import AccessLogGenerator
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_HOST = '127.0.0.1'
|
||||||
|
DEFAULT_PORT = 8000
|
||||||
|
|
||||||
class CommandLineInterface(object):
|
class CommandLineInterface(object):
|
||||||
"""
|
"""
|
||||||
|
@ -25,14 +27,14 @@ class CommandLineInterface(object):
|
||||||
'--port',
|
'--port',
|
||||||
type=int,
|
type=int,
|
||||||
help='Port number to listen on',
|
help='Port number to listen on',
|
||||||
default=8000,
|
default=None,
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-b',
|
'-b',
|
||||||
'--bind',
|
'--bind',
|
||||||
dest='host',
|
dest='host',
|
||||||
help='The host/address to bind to',
|
help='The host/address to bind to',
|
||||||
default="127.0.0.1",
|
default=None,
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-u',
|
'-u',
|
||||||
|
@ -48,6 +50,14 @@ class CommandLineInterface(object):
|
||||||
help='Bind to a file descriptor rather than a TCP host/port or named unix socket',
|
help='Bind to a file descriptor rather than a TCP host/port or named unix socket',
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--endpoint',
|
||||||
|
dest='socket_strings',
|
||||||
|
action='append',
|
||||||
|
help='Use raw server strings passed directly to twisted',
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-v',
|
'-v',
|
||||||
'--verbosity',
|
'--verbosity',
|
||||||
|
@ -105,6 +115,8 @@ class CommandLineInterface(object):
|
||||||
action='store_true',
|
action='store_true',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.server = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def entrypoint(cls):
|
def entrypoint(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -143,18 +155,34 @@ class CommandLineInterface(object):
|
||||||
channel_layer = importlib.import_module(module_path)
|
channel_layer = importlib.import_module(module_path)
|
||||||
for bit in object_path.split("."):
|
for bit in object_path.split("."):
|
||||||
channel_layer = getattr(channel_layer, bit)
|
channel_layer = getattr(channel_layer, bit)
|
||||||
# Run server
|
|
||||||
logger.info(
|
if not any([args.host, args.port, args.unix_socket, args.file_descriptor, args.socket_strings]):
|
||||||
"Starting server at %s, channel layer %s",
|
# no advanced binding options passed, patch in defaults
|
||||||
(args.unix_socket if args.unix_socket else "%s:%s" % (args.host, args.port)),
|
args.host = DEFAULT_HOST
|
||||||
args.channel_layer,
|
args.port = DEFAULT_PORT
|
||||||
)
|
elif args.host and not args.port:
|
||||||
Server(
|
args.port = DEFAULT_PORT
|
||||||
channel_layer=channel_layer,
|
elif args.port and not args.host:
|
||||||
|
args.host = DEFAULT_HOST
|
||||||
|
|
||||||
|
# build endpoint description strings from (optional) cli arguments
|
||||||
|
endpoints = build_endpoint_description_strings(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
unix_socket=args.unix_socket,
|
unix_socket=args.unix_socket,
|
||||||
file_descriptor=args.file_descriptor,
|
file_descriptor=args.file_descriptor
|
||||||
|
)
|
||||||
|
endpoints = sorted(
|
||||||
|
args.socket_strings + endpoints
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
'Starting server at %s, channel layer %s.' %
|
||||||
|
(', '.join(endpoints), args.channel_layer)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.server = Server(
|
||||||
|
channel_layer=channel_layer,
|
||||||
|
endpoints=endpoints,
|
||||||
http_timeout=args.http_timeout,
|
http_timeout=args.http_timeout,
|
||||||
ping_interval=args.ping_interval,
|
ping_interval=args.ping_interval,
|
||||||
ping_timeout=args.ping_timeout,
|
ping_timeout=args.ping_timeout,
|
||||||
|
@ -164,4 +192,5 @@ class CommandLineInterface(object):
|
||||||
verbosity=args.verbosity,
|
verbosity=args.verbosity,
|
||||||
proxy_forwarded_address_header='X-Forwarded-For' if args.proxy_headers else None,
|
proxy_forwarded_address_header='X-Forwarded-For' if args.proxy_headers else None,
|
||||||
proxy_forwarded_port_header='X-Forwarded-Port' if args.proxy_headers else None,
|
proxy_forwarded_port_header='X-Forwarded-Port' if args.proxy_headers else None,
|
||||||
).run()
|
)
|
||||||
|
self.server.run()
|
||||||
|
|
|
@ -3,6 +3,7 @@ import socket
|
||||||
|
|
||||||
from twisted.internet import reactor, defer
|
from twisted.internet import reactor, defer
|
||||||
from twisted.logger import globalLogBeginner, STDLibLogObserver
|
from twisted.logger import globalLogBeginner, STDLibLogObserver
|
||||||
|
from twisted.internet.endpoints import serverFromString
|
||||||
|
|
||||||
from .http_protocol import HTTPFactory
|
from .http_protocol import HTTPFactory
|
||||||
|
|
||||||
|
@ -14,8 +15,9 @@ class Server(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
channel_layer,
|
channel_layer,
|
||||||
host="127.0.0.1",
|
host=None,
|
||||||
port=8000,
|
port=None,
|
||||||
|
endpoints=[],
|
||||||
unix_socket=None,
|
unix_socket=None,
|
||||||
file_descriptor=None,
|
file_descriptor=None,
|
||||||
signal_handlers=True,
|
signal_handlers=True,
|
||||||
|
@ -31,10 +33,23 @@ class Server(object):
|
||||||
verbosity=1
|
verbosity=1
|
||||||
):
|
):
|
||||||
self.channel_layer = channel_layer
|
self.channel_layer = channel_layer
|
||||||
self.host = host
|
self.endpoints = endpoints
|
||||||
self.port = port
|
|
||||||
self.unix_socket = unix_socket
|
if any([host, port, unix_socket, file_descriptor]):
|
||||||
self.file_descriptor = file_descriptor
|
raise DeprecationWarning('''
|
||||||
|
The host/port/unix_socket/file_descriptor keyword arguments to %s are deprecated.
|
||||||
|
''' % self.__class__.__name__)
|
||||||
|
# build endpoint description strings from deprecated kwargs
|
||||||
|
self.endpoints = sorted(self.endpoints + build_endpoint_description_strings(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
unix_socket=unix_socket,
|
||||||
|
file_descriptor=file_descriptor
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(self.endpoints) == 0:
|
||||||
|
raise UserWarning("No endpoints. This server will not listen on anything.")
|
||||||
|
|
||||||
self.signal_handlers = signal_handlers
|
self.signal_handlers = signal_handlers
|
||||||
self.action_logger = action_logger
|
self.action_logger = action_logger
|
||||||
self.http_timeout = http_timeout
|
self.http_timeout = http_timeout
|
||||||
|
@ -67,15 +82,6 @@ class Server(object):
|
||||||
globalLogBeginner.beginLoggingTo([lambda _: None], redirectStandardIO=False, discardBuffer=True)
|
globalLogBeginner.beginLoggingTo([lambda _: None], redirectStandardIO=False, discardBuffer=True)
|
||||||
else:
|
else:
|
||||||
globalLogBeginner.beginLoggingTo([STDLibLogObserver(__name__)])
|
globalLogBeginner.beginLoggingTo([STDLibLogObserver(__name__)])
|
||||||
# Listen on a socket
|
|
||||||
if self.unix_socket:
|
|
||||||
reactor.listenUNIX(self.unix_socket, self.factory)
|
|
||||||
elif self.file_descriptor:
|
|
||||||
# socket returns the same socket if supplied with a fileno
|
|
||||||
sock = socket.socket(fileno=self.file_descriptor)
|
|
||||||
reactor.adoptStreamPort(self.file_descriptor, sock.family, self.factory)
|
|
||||||
else:
|
|
||||||
reactor.listenTCP(self.port, self.factory, interface=self.host)
|
|
||||||
|
|
||||||
if "twisted" in self.channel_layer.extensions and False:
|
if "twisted" in self.channel_layer.extensions and False:
|
||||||
logger.info("Using native Twisted mode on channel layer")
|
logger.info("Using native Twisted mode on channel layer")
|
||||||
|
@ -84,6 +90,12 @@ class Server(object):
|
||||||
logger.info("Using busy-loop synchronous mode on channel layer")
|
logger.info("Using busy-loop synchronous mode on channel layer")
|
||||||
reactor.callLater(0, self.backend_reader_sync)
|
reactor.callLater(0, self.backend_reader_sync)
|
||||||
reactor.callLater(2, self.timeout_checker)
|
reactor.callLater(2, self.timeout_checker)
|
||||||
|
|
||||||
|
for socket_description in self.endpoints:
|
||||||
|
logger.info("Listening on endpoint %s" % socket_description)
|
||||||
|
ep = serverFromString(reactor, socket_description)
|
||||||
|
ep.listen(self.factory)
|
||||||
|
|
||||||
reactor.run(installSignalHandlers=self.signal_handlers)
|
reactor.run(installSignalHandlers=self.signal_handlers)
|
||||||
|
|
||||||
def backend_reader_sync(self):
|
def backend_reader_sync(self):
|
||||||
|
@ -156,3 +168,35 @@ class Server(object):
|
||||||
"""
|
"""
|
||||||
self.factory.check_timeouts()
|
self.factory.check_timeouts()
|
||||||
reactor.callLater(2, self.timeout_checker)
|
reactor.callLater(2, self.timeout_checker)
|
||||||
|
|
||||||
|
|
||||||
|
def build_endpoint_description_strings(
|
||||||
|
host=None,
|
||||||
|
port=None,
|
||||||
|
unix_socket=None,
|
||||||
|
file_descriptor=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Build a list of twisted endpoint description strings that the server will listen on.
|
||||||
|
This is to streamline the generation of twisted endpoint description strings from easier
|
||||||
|
to use command line args such as host, port, unix sockets etc.
|
||||||
|
"""
|
||||||
|
socket_descriptions = []
|
||||||
|
if host and port:
|
||||||
|
socket_descriptions.append('tcp:port=%d:interface=%s' % (int(port), host))
|
||||||
|
elif any([host, port]):
|
||||||
|
raise ValueError('TCP binding requires both port and host kwargs.')
|
||||||
|
|
||||||
|
if unix_socket:
|
||||||
|
socket_descriptions.append('unix:%s' % unix_socket)
|
||||||
|
|
||||||
|
if file_descriptor:
|
||||||
|
socket_descriptions.append('fd:domain=INET:fileno=%d' % int(file_descriptor))
|
||||||
|
|
||||||
|
return socket_descriptions
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
3
daphne/tests/asgi.py
Normal file
3
daphne/tests/asgi.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
channel_layer = {}
|
182
daphne/tests/test_endpoints.py
Normal file
182
daphne/tests/test_endpoints.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
# coding: utf8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from unittest import TestCase
|
||||||
|
from six import string_types
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..server import Server, build_endpoint_description_strings
|
||||||
|
from ..cli import CommandLineInterface
|
||||||
|
|
||||||
|
# this is the callable that will be tested here
|
||||||
|
build = build_endpoint_description_strings
|
||||||
|
|
||||||
|
|
||||||
|
class TestEndpointDescriptions(TestCase):
|
||||||
|
|
||||||
|
def testBasics(self):
|
||||||
|
self.assertEqual(build(), [], msg="Empty list returned when no kwargs given")
|
||||||
|
|
||||||
|
def testTcpPortBindings(self):
|
||||||
|
self.assertEqual(
|
||||||
|
build(port=1234, host='example.com'),
|
||||||
|
['tcp:port=1234:interface=example.com']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
build(port=8000, host='127.0.0.1'),
|
||||||
|
['tcp:port=8000:interface=127.0.0.1']
|
||||||
|
)
|
||||||
|
|
||||||
|
# incomplete port/host kwargs raise errors
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError,
|
||||||
|
build, port=123
|
||||||
|
)
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError,
|
||||||
|
build, host='example.com'
|
||||||
|
)
|
||||||
|
|
||||||
|
def testUnixSocketBinding(self):
|
||||||
|
self.assertEqual(
|
||||||
|
build(unix_socket='/tmp/daphne.sock'),
|
||||||
|
['unix:/tmp/daphne.sock']
|
||||||
|
)
|
||||||
|
|
||||||
|
def testFileDescriptorBinding(self):
|
||||||
|
self.assertEqual(
|
||||||
|
build(file_descriptor=5),
|
||||||
|
['fd:domain=INET:fileno=5']
|
||||||
|
)
|
||||||
|
|
||||||
|
def testMultipleEnpoints(self):
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(
|
||||||
|
build(
|
||||||
|
file_descriptor=123,
|
||||||
|
unix_socket='/tmp/daphne.sock',
|
||||||
|
port=8080,
|
||||||
|
host='10.0.0.1'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
sorted([
|
||||||
|
'tcp:port=8080:interface=10.0.0.1',
|
||||||
|
'unix:/tmp/daphne.sock',
|
||||||
|
'fd:domain=INET:fileno=123'
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIInterface(TestCase):
|
||||||
|
|
||||||
|
# construct a string that will be accepted as the channel_layer argument
|
||||||
|
_import_channel_layer_string = 'daphne.tests.asgi:channel_layer'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
# patch out the servers run method
|
||||||
|
self._default_server_run = Server.run
|
||||||
|
Server.run = lambda x: x
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
# restore the original server run method
|
||||||
|
Server.run = self._default_server_run
|
||||||
|
|
||||||
|
def build_cli(self, cli_args=''):
|
||||||
|
# split the string and append the channel_layer positional argument
|
||||||
|
if isinstance(cli_args, string_types):
|
||||||
|
cli_args = cli_args.split()
|
||||||
|
|
||||||
|
args = cli_args + [self._import_channel_layer_string]
|
||||||
|
cli = CommandLineInterface()
|
||||||
|
cli.run(args)
|
||||||
|
return cli
|
||||||
|
|
||||||
|
def get_endpoints(self, cli_args=''):
|
||||||
|
cli = self.build_cli(cli_args=cli_args)
|
||||||
|
return cli.server.endpoints
|
||||||
|
|
||||||
|
def checkCLI(self, args='', endpoints=[], msg='Expected endpoints do not match.'):
|
||||||
|
cli = self.build_cli(cli_args=args)
|
||||||
|
generated_endpoints = sorted(cli.server.endpoints)
|
||||||
|
endpoints.sort()
|
||||||
|
self.assertEqual(
|
||||||
|
generated_endpoints,
|
||||||
|
endpoints,
|
||||||
|
msg=msg
|
||||||
|
)
|
||||||
|
|
||||||
|
def testCLIBasics(self):
|
||||||
|
self.checkCLI(
|
||||||
|
'',
|
||||||
|
['tcp:port=8000:interface=127.0.0.1']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-p 123',
|
||||||
|
['tcp:port=123:interface=127.0.0.1']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-b 10.0.0.1',
|
||||||
|
['tcp:port=8000:interface=10.0.0.1']
|
||||||
|
)
|
||||||
|
self.checkCLI(
|
||||||
|
'-p 8080 -b example.com',
|
||||||
|
['tcp:port=8080:interface=example.com']
|
||||||
|
)
|
||||||
|
|
||||||
|
def testCLIEndpointCreation(self):
|
||||||
|
self.checkCLI(
|
||||||
|
'-p 8080 -u /tmp/daphne.sock',
|
||||||
|
[
|
||||||
|
'tcp:port=8080:interface=127.0.0.1',
|
||||||
|
'unix:/tmp/daphne.sock',
|
||||||
|
],
|
||||||
|
'Default binding host patched in when only port given'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-b example.com -u /tmp/daphne.sock',
|
||||||
|
[
|
||||||
|
'tcp:port=8000:interface=example.com',
|
||||||
|
'unix:/tmp/daphne.sock',
|
||||||
|
],
|
||||||
|
'Default port patched in when missing.'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-u /tmp/daphne.sock --fd 5',
|
||||||
|
[
|
||||||
|
'fd:domain=INET:fileno=5',
|
||||||
|
'unix:/tmp/daphne.sock'
|
||||||
|
],
|
||||||
|
'File descriptor and unix socket bound, TCP ignored.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def testMixedCLIEndpointCreation(self):
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-p 8080 -e unix:/tmp/daphne.sock',
|
||||||
|
[
|
||||||
|
'tcp:port=8080:interface=127.0.0.1',
|
||||||
|
'unix:/tmp/daphne.sock'
|
||||||
|
],
|
||||||
|
'Mix host/port args with endpoint args'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.checkCLI(
|
||||||
|
'-p 8080 -e tcp:port=8080:interface=127.0.0.1',
|
||||||
|
[
|
||||||
|
'tcp:port=8080:interface=127.0.0.1',
|
||||||
|
] * 2,
|
||||||
|
'Do not try to de-duplicate endpoint description strings.'
|
||||||
|
'This would fail when running the server.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def testCustomEndpoints(self):
|
||||||
|
self.checkCLI(
|
||||||
|
'-e imap:',
|
||||||
|
['imap:']
|
||||||
|
)
|
24
daphne/twisted/plugins/fd_endpoint.py
Normal file
24
daphne/twisted/plugins/fd_endpoint.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from twisted.plugin import IPlugin
|
||||||
|
from zope.interface import implementer
|
||||||
|
from twisted.internet.interfaces import IStreamServerEndpointStringParser
|
||||||
|
from twisted.internet import endpoints
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(IPlugin, IStreamServerEndpointStringParser)
|
||||||
|
class _FDParser(object):
|
||||||
|
prefix = "fd"
|
||||||
|
|
||||||
|
def _parseServer(self, reactor, fileno, domain=socket.AF_INET):
|
||||||
|
fileno = int(fileno)
|
||||||
|
return endpoints.AdoptedStreamServerEndpoint(reactor, fileno, domain)
|
||||||
|
|
||||||
|
def parseStreamServer(self, reactor, *args, **kwargs):
|
||||||
|
# Delegate to another function with a sane signature. This function has
|
||||||
|
# an insane signature to trick zope.interface into believing the
|
||||||
|
# interface is correctly implemented.
|
||||||
|
return self._parseServer(reactor, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
parser = _FDParser()
|
4
setup.py
4
setup.py
|
@ -4,7 +4,6 @@ from setuptools import find_packages, setup
|
||||||
|
|
||||||
from daphne import __version__
|
from daphne import __version__
|
||||||
|
|
||||||
|
|
||||||
# We use the README as the long_description
|
# We use the README as the long_description
|
||||||
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
|
readme_path = os.path.join(os.path.dirname(__file__), "README.rst")
|
||||||
with open(readme_path) as fp:
|
with open(readme_path) as fp:
|
||||||
|
@ -20,7 +19,8 @@ setup(
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
license='BSD',
|
license='BSD',
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
packages=find_packages(),
|
package_dir={'twisted': 'daphne/twisted'},
|
||||||
|
packages=find_packages() + ['twisted.plugins'],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'asgiref>=0.13',
|
'asgiref>=0.13',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user