Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable unix domain socket binding #1019

Merged
merged 2 commits into from May 29, 2018
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -59,13 +59,17 @@ Unreleased
:meth:`Map.bind() <routing.Map.bind>`. (`#740`_, `#768`_, `#1316`_)
- Triggering a reload while using a tool such as PDB no longer hides
input. (`#1318`_)
- The dev server can bind to a Unix socket by passing a hostname like
``unix://app.socket``. (`#209`_, `#1019`_)

.. _`#209`: https://github.com/pallets/werkzeug/pull/209
.. _`#609`: https://github.com/pallets/werkzeug/pull/609
.. _`#693`: https://github.com/pallets/werkzeug/pull/693
.. _`#718`: https://github.com/pallets/werkzeug/pull/718
.. _`#724`: https://github.com/pallets/werkzeug/pull/724
.. _`#740`: https://github.com/pallets/werkzeug/issues/740
.. _`#768`: https://github.com/pallets/werkzeug/pull/768
.. _`#1019`: https://github.com/pallets/werkzeug/issues/1019
.. _`#1023`: https://github.com/pallets/werkzeug/issues/1023
.. _`#1231`: https://github.com/pallets/werkzeug/issues/1231
.. _`#1233`: https://github.com/pallets/werkzeug/pull/1233
@@ -76,7 +76,7 @@ Colored Logging
---------------
Werkzeug is able to color the output of request logs when ran from a terminal, just install the `termcolor
<https://pypi.python.org/pypi/termcolor>`_ package. Windows users need to install `colorama
<https://pypi.python.org/pypi/colorama>`_ in addition to termcolor for this to work.
<https://pypi.python.org/pypi/colorama>`_ in addition to termcolor for this to work.

Virtual Hosts
-------------
@@ -225,3 +225,14 @@ discouraged because modern browsers do a bad job at supporting them for
security reasons.

This feature requires the pyOpenSSL library to be installed.


Unix Sockets
------------

The dev server can bind to a Unix socket instead of a TCP socket.
:func:`run_simple` will bind to a Unix socket if the ``hostname``
parameter starts with ``'unix://'``. ::

from werkzeug.serving import run_simple
run_simple('unix://example.sock', 0, app)
@@ -6,22 +6,21 @@
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import with_statement, print_function
from __future__ import print_function, with_statement

from itertools import count
import os
import signal
import sys
import textwrap
import time

import requests
import pytest

from werkzeug import serving
from werkzeug.utils import cached_property
from werkzeug._compat import to_bytes
from itertools import count

from werkzeug.urls import url_quote
from werkzeug.utils import cached_property

try:
__import__('pytest_xprocess')
@@ -61,8 +60,7 @@ def _dev_server():
sys.path.insert(0, sys.argv[1])
import testsuite_app
app = _get_pid_middleware(testsuite_app.app)
serving.run_simple(hostname='localhost', application=app,
**testsuite_app.kwargs)
serving.run_simple(application=app, **testsuite_app.kwargs)


class _ServerInfo(object):
@@ -83,11 +81,16 @@ def logfile(self):
return self.xprocess.getinfo('dev_server').logpath.open()

def request_pid(self):
for i in range(20):
if self.url.startswith('http+unix://'):
from requests_unixsocket import get as rget
else:
from requests import get as rget

for i in range(10):
time.sleep(0.1 * i)
try:
self.last_pid = int(requests.get(self.url + '/_getpid',
verify=False).text)
response = rget(self.url + '/_getpid', verify=False)
self.last_pid = int(response.text)
return self.last_pid
except Exception as e: # urllib also raises socketerrors
print(self.url)
@@ -125,41 +128,42 @@ def run_dev_server(application):
appfile = app_pkg.join('__init__.py')
port = next(port_generator)
appfile.write('\n\n'.join((
'kwargs = dict(port=%d)' % port,
"kwargs = {{'hostname': 'localhost', 'port': {port:d}}}".format(
port=port),
textwrap.dedent(application)
)))

monkeypatch.delitem(sys.modules, 'testsuite_app', raising=False)
monkeypatch.syspath_prepend(str(tmpdir))
import testsuite_app
hostname = testsuite_app.kwargs['hostname']
port = testsuite_app.kwargs['port']
addr = '{}:{}'.format(hostname, port)

if testsuite_app.kwargs.get('ssl_context', None):
url_base = 'https://localhost:{0}'.format(port)
if hostname.startswith('unix://'):
addr = hostname.split('unix://', 1)[1]
requests_url = 'http+unix://' + url_quote(addr, safe='')
elif testsuite_app.kwargs.get('ssl_context', None):
requests_url = 'https://localhost:{0}'.format(port)
else:
url_base = 'http://localhost:{0}'.format(port)
requests_url = 'http://localhost:{0}'.format(port)

info = _ServerInfo(
xprocess,
'localhost:{0}'.format(port),
url_base,
port
)
info = _ServerInfo(xprocess, addr, requests_url, port)

def preparefunc(cwd):
args = [sys.executable, __file__, str(tmpdir)]
return lambda: 'pid=%s' % info.request_pid(), args

xprocess.ensure('dev_server', preparefunc, restart=True)

@request.addfinalizer
def teardown():
# Killing the process group that runs the server, not just the
# parent process attached. xprocess is confused about Werkzeug's
# reloader and won't help here.
pid = info.request_pid()
if pid:
os.killpg(os.getpgid(pid), signal.SIGTERM)
request.addfinalizer(teardown)

return info

@@ -9,12 +9,12 @@
:license: BSD, see LICENSE for more details.
"""
import os
import socket
import ssl
import subprocess
import sys
import textwrap
import time
import subprocess


try:
import OpenSSL
@@ -450,7 +450,6 @@ def app(environ, start_response):
from httplib import HTTPConnection
else:
from http.client import HTTPConnection

conn = HTTPConnection('127.0.0.1', server.port)
conn.connect()
conn.putrequest('GET', '/')
@@ -470,3 +469,23 @@ def app(environ, start_response):
assert res.read() == b'a ,b,c ,d'

conn.close()


def can_test_unix_socket():
if not hasattr(socket, 'AF_UNIX'):
return False
try:
import requests_unixsocket # noqa: F401
except ImportError:
return False
return True


@pytest.mark.skipif(not can_test_unix_socket(), reason='Only works on UNIX')
def test_unix_socket(tmpdir, dev_server):
socket_f = str(tmpdir.join('socket'))
dev_server('''
app = None
kwargs['hostname'] = {socket!r}
'''.format(socket='unix://' + socket_f))
assert os.path.exists(socket_f)
@@ -16,6 +16,7 @@ deps =
pytest-xprocess
coverage
requests
requests_unixsocket
pyopenssl
greenlet
redis
@@ -166,6 +166,12 @@ def shutdown_server():
self.server.shutdown_signal = True

url_scheme = self.server.ssl_context is None and 'http' or 'https'
if not self.client_address:
self.client_address = '<local>'
if isinstance(self.client_address, str):
self.client_address = (self.client_address, 0)
else:
pass
path_info = url_unquote(request_url.path)

environ = {
@@ -344,6 +350,10 @@ def version_string(self):
def address_string(self):
if getattr(self, 'environ', None):
return self.environ['REMOTE_ADDR']
elif not self.client_address:
return '<local>'
elif isinstance(self.client_address, str):
return self.client_address
else:
return self.client_address[0]

@@ -556,8 +566,9 @@ def is_ssl_error(error=None):
return isinstance(error, exc_types)


def select_ip_version(host, port):
"""Returns AF_INET4 or AF_INET6 depending on where to connect to."""
def select_address_family(host, port):
"""Return ``AF_INET4``, ``AF_INET6``, or ``AF_UNIX`` depending on
the host and port."""
# disabled due to problems with current ipv6 implementations
# and various operating systems. Probably this code also is
# not supposed to work, but I can't come up with any other
@@ -570,19 +581,23 @@ def select_ip_version(host, port):
# return info[0][0]
# except socket.gaierror:
# pass
if ':' in host and hasattr(socket, 'AF_INET6'):
if host.startswith('unix://'):
return socket.AF_UNIX
elif ':' in host and hasattr(socket, 'AF_INET6'):
return socket.AF_INET6
return socket.AF_INET


def get_sockaddr(host, port, family):
"""Returns a fully qualified socket address, that can properly used by
socket.bind"""
"""Return a fully qualified socket address that can be passed to
:func:`socket.bind`."""
if family == socket.AF_UNIX:
return host.split('://', 1)[1]
try:
res = socket.getaddrinfo(host, port, family,
socket.SOCK_STREAM, socket.SOL_TCP)
res = socket.getaddrinfo(
host, port, family, socket.SOCK_STREAM, socket.SOL_TCP)
except socket.gaierror:
return (host, port)
return host, port
return res[0][4]


@@ -593,19 +608,31 @@ class BaseWSGIServer(HTTPServer, object):
multiprocess = False
request_queue_size = LISTEN_QUEUE

def __init__(self, host, port, app, handler=None,
passthrough_errors=False, ssl_context=None, fd=None):
def __init__(
self, host, port, app, handler=None, passthrough_errors=False,
ssl_context=None, fd=None
):
if handler is None:
handler = WSGIRequestHandler

self.address_family = select_ip_version(host, port)
self.address_family = select_address_family(host, port)

if fd is not None:
real_sock = socket.fromfd(fd, self.address_family,
socket.SOCK_STREAM)
real_sock = socket.fromfd(
fd, self.address_family, socket.SOCK_STREAM)
port = 0
HTTPServer.__init__(self, get_sockaddr(host, int(port),
self.address_family), handler)

server_address = get_sockaddr(host, int(port), self.address_family)

# remove socket file if it already exists
if (
self.address_family == socket.AF_UNIX
and os.path.exists(server_address)
):
os.unlink(server_address)

HTTPServer.__init__(self, server_address, handler)

self.app = app
self.passthrough_errors = passthrough_errors
self.shutdown_signal = False
@@ -738,7 +765,13 @@ def run_simple(hostname, port, application, use_reloader=False,
through the `reloader_type` parameter. See :ref:`reloader`
for more information.
:param hostname: The host for the application. eg: ``'localhost'``
.. versionchanged:: 0.15
Bind to a Unix socket by passing a path that starts with
``unix://`` as the ``hostname``.
:param hostname: The host to bind to, for example ``'localhost'``.
If the value is a path that starts with ``unix://`` it will bind
to a Unix socket instead of a TCP socket..
:param port: The port for the server. eg: ``8080``
:param application: the WSGI application to execute
:param use_reloader: should the server automatically restart the python
@@ -786,13 +819,16 @@ def run_simple(hostname, port, application, use_reloader=False,

def log_startup(sock):
display_hostname = hostname not in ('', '*') and hostname or 'localhost'
if ':' in display_hostname:
display_hostname = '[%s]' % display_hostname
quit_msg = '(Press CTRL+C to quit)'
port = sock.getsockname()[1]
_log('info', ' * Running on %s://%s:%d/ %s',
ssl_context is None and 'http' or 'https',
display_hostname, port, quit_msg)
if sock.family is socket.AF_UNIX:
_log('info', ' * Running on %s %s', display_hostname, quit_msg)
else:
if ':' in display_hostname:
display_hostname = '[%s]' % display_hostname
port = sock.getsockname()[1]
_log('info', ' * Running on %s://%s:%d/ %s',
ssl_context is None and 'http' or 'https',
display_hostname, port, quit_msg)

def inner():
try:
@@ -820,10 +856,11 @@ def inner():
# Create and destroy a socket so that any exceptions are
# raised before we spawn a separate Python interpreter and
# lose this ability.
address_family = select_ip_version(hostname, port)
address_family = select_address_family(hostname, port)
server_address = get_sockaddr(hostname, port, address_family)
s = socket.socket(address_family, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(get_sockaddr(hostname, port, address_family))
s.bind(server_address)
if hasattr(s, 'set_inheritable'):
s.set_inheritable(True)

@@ -835,6 +872,9 @@ def inner():
log_startup(s)
else:
s.close()
if address_family is socket.AF_UNIX:
_log('info', "Unlinking %s" % server_address)
os.unlink(server_address)

# Do not use relative imports, otherwise "python -m werkzeug.serving"
# breaks.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.