zopewsgi.py 7.23 KB
Newer Older
1 2 3 4
# Modules aliases to support both python2 and python3
from future import standard_library
standard_library.install_aliases()

5 6 7 8 9
import argparse
from io import BytesIO
import logging
import os
import posixpath
10
import signal
11
import socket
12
import sys
13 14
from tempfile import TemporaryFile
import time
15
from urllib import quote, splitport
16

17
from waitress.server import create_server
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
import ZConfig
import Zope2
from Zope2.Startup.run import make_wsgi_app

from Products.ERP5Type.patches.WSGIPublisher import publish_module


# this class licensed under the MIT license (stolen from pyramid_translogger)
class TransLogger(object):

    format = ('%(REMOTE_ADDR)s - %(REMOTE_USER)s [%(time)s] '
              '"%(REQUEST_METHOD)s %(REQUEST_URI)s %(HTTP_VERSION)s" '
              '%(status)s %(bytes)s "%(HTTP_REFERER)s" "%(HTTP_USER_AGENT)s"')

    def __init__(self, application, logger):
        self.application = application
        self.logger = logger

    def __call__(self, environ, start_response):
        start = time.localtime()
        req_uri = quote(environ.get('SCRIPT_NAME', '')
                               + environ.get('PATH_INFO', ''))
        if environ.get('QUERY_STRING'):
            req_uri += '?'+environ['QUERY_STRING']
        method = environ['REQUEST_METHOD']
        def replacement_start_response(status, headers, exc_info=None):
            # @@: Ideally we would count the bytes going by if no
            # content-length header was provided; but that does add
            # some overhead, so at least for now we'll be lazy.
            bytes = None
            for name, value in headers:
                if name.lower() == 'content-length':
                    bytes = value
            self.write_log(environ, method, req_uri, start, status, bytes)
            return start_response(status, headers)
        return self.application(environ, replacement_start_response)

    def write_log(self, environ, method, req_uri, start, status, bytes):
        if bytes is None:
            bytes = '-'
        if time.daylight:
                offset = time.altzone / 60 / 60 * -100
        else:
                offset = time.timezone / 60 / 60 * -100
        if offset >= 0:
                offset = "+%0.4d" % (offset)
        elif offset < 0:
                offset = "%0.4d" % (offset)
        d = {
            'REMOTE_ADDR': environ.get('REMOTE_ADDR') or '-',
            'REMOTE_USER': environ.get('REMOTE_USER') or '-',
            'REQUEST_METHOD': method,
            'REQUEST_URI': req_uri,
            'HTTP_VERSION': environ.get('SERVER_PROTOCOL'),
            'time': time.strftime('%d/%b/%Y:%H:%M:%S ', start) + offset,
            'status': status.split(None, 1)[0],
            'bytes': bytes,
            'HTTP_REFERER': environ.get('HTTP_REFERER', '-'),
            'HTTP_USER_AGENT': environ.get('HTTP_USER_AGENT', '-'),
            }
        message = self.format % d
        self.logger.warn(message)


82
def app_wrapper(large_file_threshold=10<<20, webdav_ports=()):
83
    try:
84
        from Products.DeadlockDebugger.dumper import dump_threads, dump_url
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    except Exception:
        dump_url = '\0'
    def app(environ, start_response):
        path_info = environ['PATH_INFO']
        if dump_url.startswith(path_info):
            query_string = environ['QUERY_STRING']
            if dump_url == (path_info + '?' + query_string if query_string
                            else path_info):
                start_response('200 OK', (('Content-type', 'text/plain'),))
                return [dump_threads()]

        original_wsgi_input = environ['wsgi.input']
        if not hasattr(original_wsgi_input, 'seek'):
            # Convert environ['wsgi.input'] to a file-like object.
            cl = environ.get('CONTENT_LENGTH')
            cl = int(cl) if cl else 0
            if cl > large_file_threshold:
                new_wsgi_input = environ['wsgi.input'] = TemporaryFile('w+b')
            else:
                new_wsgi_input = environ['wsgi.input'] = BytesIO()

            rest = cl
            chunksize = 1<<20
            try:
                while chunksize < rest:
                    new_wsgi_input.write(original_wsgi_input.read(chunksize))
                    rest -= chunksize
                if rest:
                    new_wsgi_input.write(original_wsgi_input.read(rest))
            except (socket.error, IOError):
                msg = b'Not enough data in request or socket error'
                start_response('400 Bad Request', [
                    ('Content-Type', 'text/plain'),
                    ('Content-Length', str(len(msg))),
                    ]
                )
                return [msg]
            new_wsgi_input.seek(0)

124
        if int(environ['SERVER_PORT']) in webdav_ports:
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
            # Munge the request to ensure that we call manage_FTPGet.

            # Set a flag to indicate this request came through the WebDAV source
            # port server.
            environ['WEBDAV_SOURCE_PORT'] = 1

            if environ['REQUEST_METHOD'] == 'GET':
                if os.sep != '/':
                    path_info =  path_info.replace(os.sep, '/')
                path_info = posixpath.join(path_info, 'manage_DAVget')
                path_info = posixpath.normpath(path_info)
                environ['PATH_INFO'] = path_info

        return publish_module(environ, start_response)
    return app

141
def createServer(application, logger, **kw):
142
    global server
143 144
    server = create_server(
        TransLogger(application, logger=logger),
145 146 147
        # We handle X-Forwarded-For by ourselves. See ERP5Type/patches/WSGITask.py.
        # trusted_proxy='*',
        # trusted_proxy_headers=('x-forwarded-for',),
148 149 150 151
        clear_untrusted_proxy_headers=True,
        **kw
    )
    if not hasattr(server, 'addr'):
152 153 154 155
      try:
        server.addr = kw['sockets'][0].getsockname()
      except KeyError:
        server.addr = server.adj.listen[0][3]
156 157 158
    elif not server.addr:
      server.addr = server.sockinfo[3]
    return server
159

160
def runwsgi():
161 162 163 164
    parser = argparse.ArgumentParser()
    parser.add_argument('-w', '--webdav', action='store_true')
    parser.add_argument('address', help='<ip>:<port>')
    parser.add_argument('zope_conf', help='path to zope.conf')
165
    parser.add_argument('--timerserver-interval', help='Interval for timerserver', type=float)
166 167 168 169 170 171 172 173
    args = parser.parse_args()

    startup = os.path.dirname(Zope2.Startup.__file__)
    schema = ZConfig.loadSchema(os.path.join(startup, 'zopeschema.xml'))
    conf, _ = ZConfig.loadConfig(schema, args.zope_conf)

    make_wsgi_app({}, zope_conf=args.zope_conf)

174 175 176
    from Signals.SignalHandler import SignalHandler
    SignalHandler.registerHandler(signal.SIGTERM, sys.exit)

177 178 179 180 181 182 183
    if args.timerserver_interval:
      import Products.TimerService
      Products.TimerService.timerserver.TimerServer.TimerServer(
          module='Zope2',
          interval=args.timerserver_interval,
      )

184 185 186 187 188 189
    ip, port = splitport(args.address)
    port = int(port)
    createServer(
        app_wrapper(
          large_file_threshold=conf.large_file_threshold,
          webdav_ports=[port] if args.webdav else ()),
190
        listen=args.address,
191
        logger=logging.getLogger("access"),
192
        threads=conf.zserver_threads,
193
        asyncore_use_poll=True,
194 195
        # Prevent waitress from adding its own Via and Server response headers.
        ident=None,
196
    ).run()