'
28 | __version__ = '$Revision$'
29 |
30 | import sys
31 | import logging
32 | import socket
33 | import select
34 | import errno
35 | import io as StringIO
36 | import signal
37 | import datetime
38 | import os
39 | import warnings
40 | import traceback
41 |
42 | # Threads are required. If you want a non-threaded (forking) version, look at
43 | # SWAP .
44 | import _thread
45 | import threading
46 |
47 | __all__ = ['BaseSCGIServer']
48 |
49 | from flup.server import NoDefault
50 |
51 | # The main classes use this name for logging.
52 | LoggerName = 'scgi-wsgi'
53 |
54 | # Set up module-level logger.
55 | console = logging.StreamHandler()
56 | console.setLevel(logging.DEBUG)
57 | console.setFormatter(logging.Formatter('%(asctime)s : %(message)s',
58 | '%Y-%m-%d %H:%M:%S'))
59 | logging.getLogger(LoggerName).addHandler(console)
60 | del console
61 |
62 | class ProtocolError(Exception):
63 | """
64 | Exception raised when the server does something unexpected or
65 | sends garbled data. Usually leads to a Connection closing.
66 | """
67 | pass
68 |
69 | def recvall(sock, length):
70 | """
71 | Attempts to receive length bytes from a socket, blocking if necessary.
72 | (Socket may be blocking or non-blocking.)
73 | """
74 | dataList = []
75 | recvLen = 0
76 | while length:
77 | try:
78 | data = sock.recv(length)
79 | except socket.error as e:
80 | if e.args[0] == errno.EAGAIN:
81 | select.select([sock], [], [])
82 | continue
83 | else:
84 | raise
85 | if not data: # EOF
86 | break
87 | dataList.append(data)
88 | dataLen = len(data)
89 | recvLen += dataLen
90 | length -= dataLen
91 | return b''.join(dataList), recvLen
92 |
93 | def readNetstring(sock):
94 | """
95 | Attempt to read a netstring from a socket.
96 | """
97 | # First attempt to read the length.
98 | size = b''
99 | while True:
100 | try:
101 | c = sock.recv(1)
102 | except socket.error as e:
103 | if e.args[0] == errno.EAGAIN:
104 | select.select([sock], [], [])
105 | continue
106 | else:
107 | raise
108 | if c == b':':
109 | break
110 | if not c:
111 | raise EOFError
112 | size += c
113 |
114 | # Try to decode the length.
115 | try:
116 | size = int(size)
117 | if size < 0:
118 | raise ValueError
119 | except ValueError:
120 | raise ProtocolError('invalid netstring length')
121 |
122 | # Now read the string.
123 | s, length = recvall(sock, size)
124 |
125 | if length < size:
126 | raise EOFError
127 |
128 | # Lastly, the trailer.
129 | trailer, length = recvall(sock, 1)
130 |
131 | if length < 1:
132 | raise EOFError
133 |
134 | if trailer != b',':
135 | raise ProtocolError('invalid netstring trailer')
136 |
137 | return s
138 |
139 | class StdoutWrapper(object):
140 | """
141 | Wrapper for sys.stdout so we know if data has actually been written.
142 | """
143 | def __init__(self, stdout):
144 | self._file = stdout
145 | self.dataWritten = False
146 |
147 | def write(self, data):
148 | if data:
149 | self.dataWritten = True
150 | self._file.write(data)
151 |
152 | def writelines(self, lines):
153 | for line in lines:
154 | self.write(line)
155 |
156 | def __getattr__(self, name):
157 | return getattr(self._file, name)
158 |
159 | class Request(object):
160 | """
161 | Encapsulates data related to a single request.
162 |
163 | Public attributes:
164 | environ - Environment variables from web server.
165 | stdin - File-like object representing the request body.
166 | stdout - File-like object for writing the response.
167 | """
168 | def __init__(self, conn, environ, input, output):
169 | self._conn = conn
170 | self.environ = environ
171 | self.stdin = input
172 | self.stdout = StdoutWrapper(output)
173 |
174 | self.logger = logging.getLogger(LoggerName)
175 |
176 | def run(self):
177 | self.logger.info('%s %s%s',
178 | self.environ['REQUEST_METHOD'],
179 | self.environ.get('SCRIPT_NAME', ''),
180 | self.environ.get('PATH_INFO', ''))
181 |
182 | start = datetime.datetime.now()
183 |
184 | try:
185 | self._conn.server.handler(self)
186 | except:
187 | self.logger.exception('Exception caught from handler')
188 | if not self.stdout.dataWritten:
189 | self._conn.server.error(self)
190 |
191 | end = datetime.datetime.now()
192 |
193 | handlerTime = end - start
194 | self.logger.debug('%s %s%s done (%.3f secs)',
195 | self.environ['REQUEST_METHOD'],
196 | self.environ.get('SCRIPT_NAME', ''),
197 | self.environ.get('PATH_INFO', ''),
198 | handlerTime.seconds +
199 | handlerTime.microseconds / 1000000.0)
200 |
201 | class TimeoutException(Exception):
202 | pass
203 |
204 | class Connection(object):
205 | """
206 | Represents a single client (web server) connection. A single request
207 | is handled, after which the socket is closed.
208 | """
209 | def __init__(self, sock, addr, server, timeout):
210 | self._sock = sock
211 | self._addr = addr
212 | self.server = server
213 | self._timeout = timeout
214 |
215 | self.logger = logging.getLogger(LoggerName)
216 |
217 | def timeout_handler(self, signum, frame):
218 | self.logger.error('Timeout Exceeded')
219 | self.logger.error("\n".join(traceback.format_stack(frame)))
220 |
221 | raise TimeoutException
222 |
223 | def run(self):
224 | if len(self._addr) == 2:
225 | self.logger.debug('Connection starting up (%s:%d)',
226 | self._addr[0], self._addr[1])
227 |
228 | try:
229 | self.processInput()
230 | except (EOFError, KeyboardInterrupt):
231 | pass
232 | except ProtocolError as e:
233 | self.logger.error("Protocol error '%s'", str(e))
234 | except:
235 | self.logger.exception('Exception caught in Connection')
236 |
237 | if len(self._addr) == 2:
238 | self.logger.debug('Connection shutting down (%s:%d)',
239 | self._addr[0], self._addr[1])
240 |
241 | # All done!
242 | self._sock.close()
243 |
244 | def processInput(self):
245 | # Read headers
246 | headers = readNetstring(self._sock)
247 | headers = headers.split(b'\x00')[:-1]
248 | if len(headers) % 2 != 0:
249 | raise ProtocolError('invalid headers')
250 | environ = {}
251 | for i in range(len(headers) // 2):
252 | environ[headers[2*i].decode('latin-1')] = headers[2*i+1].decode('latin-1')
253 |
254 | clen = environ.get('CONTENT_LENGTH')
255 | if clen is None:
256 | raise ProtocolError('missing CONTENT_LENGTH')
257 | try:
258 | clen = int(clen)
259 | if clen < 0:
260 | raise ValueError
261 | except ValueError:
262 | raise ProtocolError('invalid CONTENT_LENGTH')
263 |
264 | self._sock.setblocking(1)
265 | if clen:
266 | input = self._sock.makefile('rb')
267 | else:
268 | # Empty input.
269 | input = StringIO.StringIO()
270 |
271 | # stdout
272 | output = self._sock.makefile('wb')
273 |
274 | # Allocate Request
275 | req = Request(self, environ, input, output)
276 |
277 | # If there is a timeout
278 | if self._timeout:
279 | old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler)
280 | signal.alarm(self._timeout)
281 |
282 | # Run it.
283 | req.run()
284 |
285 | output.close()
286 | input.close()
287 |
288 | # Restore old handler if timeout was given
289 | if self._timeout:
290 | signal.alarm(0)
291 | signal.signal(signal.SIGALRM, old_alarm)
292 |
293 |
294 | class BaseSCGIServer(object):
295 | # What Request class to use.
296 | requestClass = Request
297 |
298 | def __init__(self, application, scriptName=NoDefault, environ=None,
299 | multithreaded=True, multiprocess=False,
300 | bindAddress=('localhost', 4000), umask=None,
301 | allowedServers=NoDefault,
302 | loggingLevel=logging.INFO, debug=False):
303 | """
304 | scriptName is the initial portion of the URL path that "belongs"
305 | to your application. It is used to determine PATH_INFO (which doesn't
306 | seem to be passed in). An empty scriptName means your application
307 | is mounted at the root of your virtual host.
308 |
309 | environ, which must be a dictionary, can contain any additional
310 | environment variables you want to pass to your application.
311 |
312 | Set multithreaded to False if your application is not thread-safe.
313 |
314 | Set multiprocess to True to explicitly set wsgi.multiprocess to
315 | True. (Only makes sense with threaded servers.)
316 |
317 | bindAddress is the address to bind to, which must be a string or
318 | a tuple of length 2. If a tuple, the first element must be a string,
319 | which is the host name or IPv4 address of a local interface. The
320 | 2nd element of the tuple is the port number. If a string, it will
321 | be interpreted as a filename and a UNIX socket will be opened.
322 |
323 | If binding to a UNIX socket, umask may be set to specify what
324 | the umask is to be changed to before the socket is created in the
325 | filesystem. After the socket is created, the previous umask is
326 | restored.
327 |
328 | allowedServers must be None or a list of strings representing the
329 | IPv4 addresses of servers allowed to connect. None means accept
330 | connections from anywhere. By default, it is a list containing
331 | the single item '127.0.0.1'.
332 |
333 | loggingLevel sets the logging level of the module-level logger.
334 | """
335 | if environ is None:
336 | environ = {}
337 |
338 | self.application = application
339 | self.scriptName = scriptName
340 | self.environ = environ
341 | self.multithreaded = multithreaded
342 | self.multiprocess = multiprocess
343 | self.debug = debug
344 | self._bindAddress = bindAddress
345 | self._umask = umask
346 | if allowedServers is NoDefault:
347 | allowedServers = ['127.0.0.1']
348 | self._allowedServers = allowedServers
349 |
350 | # Used to force single-threadedness.
351 | self._appLock = _thread.allocate_lock()
352 |
353 | self.logger = logging.getLogger(LoggerName)
354 | self.logger.setLevel(loggingLevel)
355 |
356 | def _setupSocket(self):
357 | """Creates and binds the socket for communication with the server."""
358 | oldUmask = None
359 | if type(self._bindAddress) is str:
360 | # Unix socket
361 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
362 | try:
363 | os.unlink(self._bindAddress)
364 | except OSError:
365 | pass
366 | if self._umask is not None:
367 | oldUmask = os.umask(self._umask)
368 | else:
369 | # INET socket
370 | assert type(self._bindAddress) is tuple
371 | family = socket.AF_INET
372 | if len(self._bindAddress) > 2:
373 | family = socket.AF_INET6
374 | sock = socket.socket(family, socket.SOCK_STREAM)
375 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
376 |
377 | sock.bind(self._bindAddress)
378 | sock.listen(socket.SOMAXCONN)
379 |
380 | if oldUmask is not None:
381 | os.umask(oldUmask)
382 |
383 | return sock
384 |
385 | def _cleanupSocket(self, sock):
386 | """Closes the main socket."""
387 | sock.close()
388 |
389 | def _isClientAllowed(self, addr):
390 | ret = self._allowedServers is None or \
391 | len(addr) != 2 or \
392 | (len(addr) == 2 and addr[0] in self._allowedServers)
393 | if not ret:
394 | self.logger.warning('Server connection from %s disallowed',
395 | addr[0])
396 | return ret
397 |
398 | def handler(self, request):
399 | """
400 | WSGI handler. Sets up WSGI environment, calls the application,
401 | and sends the application's response.
402 | """
403 | environ = request.environ
404 | environ.update(self.environ)
405 |
406 | environ['wsgi.version'] = (1,0)
407 | environ['wsgi.input'] = request.stdin
408 | environ['wsgi.errors'] = sys.stderr
409 | environ['wsgi.multithread'] = self.multithreaded
410 | environ['wsgi.multiprocess'] = self.multiprocess
411 | environ['wsgi.run_once'] = False
412 |
413 | if environ.get('HTTPS', 'off') in ('on', '1'):
414 | environ['wsgi.url_scheme'] = 'https'
415 | else:
416 | environ['wsgi.url_scheme'] = 'http'
417 |
418 | self._sanitizeEnv(environ)
419 |
420 | headers_set = []
421 | headers_sent = []
422 | result = None
423 |
424 | def write(data):
425 | if type(data) is str:
426 | data = data.encode('latin-1')
427 |
428 | assert type(data) is bytes, 'write() argument must be bytes'
429 | assert headers_set, 'write() before start_response()'
430 |
431 | if not headers_sent:
432 | status, responseHeaders = headers_sent[:] = headers_set
433 | found = False
434 | for header,value in responseHeaders:
435 | if header.lower() == b'content-length':
436 | found = True
437 | break
438 | if not found and result is not None:
439 | try:
440 | if len(result) == 1:
441 | responseHeaders.append((b'Content-Length',
442 | str(len(data)).encode('latin-1')))
443 | except:
444 | pass
445 | s = b'Status: ' + status + b'\r\n'
446 | for header,value in responseHeaders:
447 | s += header + b': ' + value + b'\r\n'
448 | s += b'\r\n'
449 | request.stdout.write(s)
450 |
451 | request.stdout.write(data)
452 | request.stdout.flush()
453 |
454 | def start_response(status, response_headers, exc_info=None):
455 | if exc_info:
456 | try:
457 | if headers_sent:
458 | # Re-raise if too late
459 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
460 | finally:
461 | exc_info = None # avoid dangling circular ref
462 | else:
463 | assert not headers_set, 'Headers already set!'
464 |
465 | if type(status) is str:
466 | status = status.encode('latin-1')
467 |
468 | assert type(status) is bytes, 'Status must be a bytes'
469 | assert len(status) >= 4, 'Status must be at least 4 characters'
470 | assert int(status[:3]), 'Status must begin with 3-digit code'
471 | assert status[3] == 0x20, 'Status must have a space after code'
472 | assert type(response_headers) is list, 'Headers must be a list'
473 | new_response_headers = []
474 | for name,val in response_headers:
475 | if type(name) is str:
476 | name = name.encode('latin-1')
477 | if type(val) is str:
478 | val = val.encode('latin-1')
479 |
480 | assert type(name) is bytes, 'Header name "%s" must be bytes' % name
481 | assert type(val) is bytes, 'Value of header "%s" must be bytes' % name
482 |
483 | new_response_headers.append((name, val))
484 |
485 | headers_set[:] = [status, new_response_headers]
486 | return write
487 |
488 | if not self.multithreaded:
489 | self._appLock.acquire()
490 | try:
491 | try:
492 | result = self.application(environ, start_response)
493 | try:
494 | for data in result:
495 | if data:
496 | write(data)
497 | if not headers_sent:
498 | write(b'') # in case body was empty
499 | finally:
500 | if hasattr(result, 'close'):
501 | result.close()
502 | except socket.error as e:
503 | if e.args[0] != errno.EPIPE:
504 | raise # Don't let EPIPE propagate beyond server
505 | finally:
506 | if not self.multithreaded:
507 | self._appLock.release()
508 |
509 | def _sanitizeEnv(self, environ):
510 | """Fill-in/deduce missing values in environ."""
511 | reqUri = None
512 | if 'REQUEST_URI' in environ:
513 | reqUri = environ['REQUEST_URI'].split('?', 1)
514 |
515 | # Ensure QUERY_STRING exists
516 | if 'QUERY_STRING' not in environ or not environ['QUERY_STRING']:
517 | if reqUri is not None and len(reqUri) > 1:
518 | environ['QUERY_STRING'] = reqUri[1]
519 | else:
520 | environ['QUERY_STRING'] = ''
521 |
522 | # Check WSGI_SCRIPT_NAME
523 | scriptName = environ.get('WSGI_SCRIPT_NAME')
524 | if scriptName is None:
525 | scriptName = self.scriptName
526 | else:
527 | warnings.warn('WSGI_SCRIPT_NAME environment variable for scgi '
528 | 'servers is deprecated',
529 | DeprecationWarning)
530 | if scriptName.lower() == 'none':
531 | scriptName = None
532 |
533 | if scriptName is None:
534 | # Do nothing (most likely coming from cgi2scgi)
535 | return
536 |
537 | if scriptName is NoDefault:
538 | # Pull SCRIPT_NAME/PATH_INFO from environment, with empty defaults
539 | if 'SCRIPT_NAME' not in environ:
540 | environ['SCRIPT_NAME'] = ''
541 | if 'PATH_INFO' not in environ or not environ['PATH_INFO']:
542 | if reqUri is not None:
543 | scriptName = environ['SCRIPT_NAME']
544 | if not reqUri[0].startswith(scriptName):
545 | self.logger.warning('SCRIPT_NAME does not match request URI')
546 | environ['PATH_INFO'] = reqUri[0][len(scriptName):]
547 | else:
548 | environ['PATH_INFO'] = ''
549 | else:
550 | # Configured scriptName
551 | warnings.warn('Configured SCRIPT_NAME is deprecated\n'
552 | 'Do not use WSGI_SCRIPT_NAME or the scriptName\n'
553 | 'keyword parameter -- they will be going away',
554 | DeprecationWarning)
555 |
556 | value = environ['SCRIPT_NAME']
557 | value += environ.get('PATH_INFO', '')
558 | if not value.startswith(scriptName):
559 | self.logger.warning('scriptName does not match request URI')
560 |
561 | environ['PATH_INFO'] = value[len(scriptName):]
562 | environ['SCRIPT_NAME'] = scriptName
563 |
564 | def error(self, request):
565 | """
566 | Override to provide custom error handling. Ideally, however,
567 | all errors should be caught at the application level.
568 | """
569 | if self.debug:
570 | import cgitb
571 | request.stdout.write(b'Status: 500 Internal Server Error\r\n' +
572 | b'Content-Type: text/html\r\n\r\n' +
573 | cgitb.html(sys.exc_info()).encode('latin-1'))
574 | else:
575 | errorpage = b"""
576 |
577 | Unhandled Exception
578 |
579 | Unhandled Exception
580 | An unhandled exception was thrown by the application.
581 |
582 | """
583 | request.stdout.write(b'Status: 500 Internal Server Error\r\n' +
584 | b'Content-Type: text/html\r\n\r\n' +
585 | errorpage)
586 |
--------------------------------------------------------------------------------
/flup/server/ajp_base.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2005, 2006 Allan Saddi
2 | # All rights reserved.
3 | #
4 | # Redistribution and use in source and binary forms, with or without
5 | # modification, are permitted provided that the following conditions
6 | # are met:
7 | # 1. Redistributions of source code must retain the above copyright
8 | # notice, this list of conditions and the following disclaimer.
9 | # 2. Redistributions in binary form must reproduce the above copyright
10 | # notice, this list of conditions and the following disclaimer in the
11 | # documentation and/or other materials provided with the distribution.
12 | #
13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23 | # SUCH DAMAGE.
24 | #
25 | # $Id$
26 |
27 | __author__ = 'Allan Saddi '
28 | __version__ = '$Revision$'
29 |
30 | import sys
31 | import socket
32 | import select
33 | import struct
34 | import signal
35 | import logging
36 | import errno
37 | import datetime
38 | import time
39 | import traceback
40 |
41 | # Unfortunately, for now, threads are required.
42 | import _thread
43 | import threading
44 |
45 | from flup.server import NoDefault
46 |
47 | __all__ = ['BaseAJPServer']
48 |
49 | # Packet header prefixes.
50 | SERVER_PREFIX = b'\x12\x34'
51 | CONTAINER_PREFIX = b'AB'
52 |
53 | # Server packet types.
54 | PKTTYPE_FWD_REQ = 0x02
55 | PKTTYPE_SHUTDOWN = 0x07
56 | PKTTYPE_PING = 0x08
57 | PKTTYPE_CPING = 0x0a
58 |
59 | # Container packet types.
60 | PKTTYPE_SEND_BODY = b'\x03'
61 | PKTTYPE_SEND_HEADERS = b'\x04'
62 | PKTTYPE_END_RESPONSE = b'\x05'
63 | PKTTYPE_GET_BODY = b'\x06'
64 | PKTTYPE_CPONG = b'\x09'
65 |
66 | # Code tables for methods/headers/attributes.
67 | methodTable = [
68 | None,
69 | b'OPTIONS',
70 | b'GET',
71 | b'HEAD',
72 | b'POST',
73 | b'PUT',
74 | b'DELETE',
75 | b'TRACE',
76 | b'PROPFIND',
77 | b'PROPPATCH',
78 | b'MKCOL',
79 | b'COPY',
80 | b'MOVE',
81 | b'LOCK',
82 | b'UNLOCK',
83 | b'ACL',
84 | b'REPORT',
85 | b'VERSION-CONTROL',
86 | b'CHECKIN',
87 | b'CHECKOUT',
88 | b'UNCHECKOUT',
89 | b'SEARCH',
90 | b'MKWORKSPACE',
91 | b'UPDATE',
92 | b'LABEL',
93 | b'MERGE',
94 | b'BASELINE_CONTROL',
95 | b'MKACTIVITY'
96 | ]
97 |
98 | requestHeaderTable = [
99 | None,
100 | b'Accept',
101 | b'Accept-Charset',
102 | b'Accept-Encoding',
103 | b'Accept-Language',
104 | b'Authorization',
105 | b'Connection',
106 | b'Content-Type',
107 | b'Content-Length',
108 | b'Cookie',
109 | b'Cookie2',
110 | b'Host',
111 | b'Pragma',
112 | b'Referer',
113 | b'User-Agent'
114 | ]
115 |
116 | attributeTable = [
117 | None,
118 | b'CONTEXT',
119 | b'SERVLET_PATH',
120 | b'REMOTE_USER',
121 | b'AUTH_TYPE',
122 | b'QUERY_STRING',
123 | b'JVM_ROUTE',
124 | b'SSL_CERT',
125 | b'SSL_CIPHER',
126 | b'SSL_SESSION',
127 | None, # name follows
128 | b'SSL_KEY_SIZE'
129 | ]
130 |
131 | responseHeaderTable = [
132 | None,
133 | b'content-type',
134 | b'content-language',
135 | b'content-length',
136 | b'date',
137 | b'last-modified',
138 | b'location',
139 | b'set-cookie',
140 | b'set-cookie2',
141 | b'servlet-engine',
142 | b'status',
143 | b'www-authenticate'
144 | ]
145 |
146 | # The main classes use this name for logging.
147 | LoggerName = 'ajp-wsgi'
148 |
149 | # Set up module-level logger.
150 | console = logging.StreamHandler()
151 | console.setLevel(logging.DEBUG)
152 | console.setFormatter(logging.Formatter('%(asctime)s : %(message)s',
153 | '%Y-%m-%d %H:%M:%S'))
154 | logging.getLogger(LoggerName).addHandler(console)
155 | del console
156 |
157 | class ProtocolError(Exception):
158 | """
159 | Exception raised when the server does something unexpected or
160 | sends garbled data. Usually leads to a Connection closing.
161 | """
162 | pass
163 |
164 | def decodeString(data, pos=0):
165 | """Decode a string."""
166 | try:
167 | length = struct.unpack('>H', data[pos:pos+2])[0]
168 | pos += 2
169 | if length == 0xffff: # This was undocumented!
170 | return b'', pos
171 | s = data[pos:pos+length]
172 | return s, pos+length+1 # Don't forget NUL
173 | except Exception as e:
174 | raise ProtocolError('decodeString: '+str(e))
175 |
176 | def decodeRequestHeader(data, pos=0):
177 | """Decode a request header/value pair."""
178 | try:
179 | if data[pos] == 0xa0:
180 | # Use table
181 | i = data[pos+1]
182 | name = requestHeaderTable[i]
183 | if name is None:
184 | raise ValueError('bad request header code')
185 | pos += 2
186 | else:
187 | name, pos = decodeString(data, pos)
188 | value, pos = decodeString(data, pos)
189 | return name, value, pos
190 | except Exception as e:
191 | raise ProtocolError('decodeRequestHeader: '+str(e))
192 |
193 | def decodeAttribute(data, pos=0):
194 | """Decode a request attribute."""
195 | try:
196 | i = data[pos]
197 | pos += 1
198 | if i == 0xff:
199 | # end
200 | return None, None, pos
201 | elif i == 0x0a:
202 | # name follows
203 | name, pos = decodeString(data, pos)
204 | elif i == 0x0b:
205 | # Special handling of SSL_KEY_SIZE.
206 | name = attributeTable[i]
207 | # Value is an int, not a string.
208 | value = struct.unpack('>H', data[pos:pos+2])[0]
209 | return name, str(value), pos+2
210 | else:
211 | name = attributeTable[i]
212 | if name is None:
213 | raise ValueError('bad attribute code')
214 | value, pos = decodeString(data, pos)
215 | return name, value, pos
216 | except Exception as e:
217 | raise ProtocolError('decodeAttribute: '+str(e))
218 |
219 | def encodeString(s):
220 | """Encode a string."""
221 | return struct.pack('>H', len(s)) + s + b'\x00'
222 |
223 | def encodeResponseHeader(name, value):
224 | """Encode a response header/value pair."""
225 | lname = name.lower()
226 | if lname in responseHeaderTable:
227 | # Use table
228 | i = responseHeaderTable.index(lname)
229 | out = b'\xa0' + bytes([i])
230 | else:
231 | out = encodeString(name)
232 | out += encodeString(value)
233 | return out
234 |
235 | class Packet(object):
236 | """An AJP message packet."""
237 | def __init__(self):
238 | self.data = b''
239 | # Don't set this on write, it will be calculated automatically.
240 | self.length = 0
241 |
242 | def _recvall(sock, length):
243 | """
244 | Attempts to receive length bytes from a socket, blocking if necessary.
245 | (Socket may be blocking or non-blocking.)
246 | """
247 | dataList = []
248 | recvLen = 0
249 | while length:
250 | try:
251 | data = sock.recv(length)
252 | except socket.error as e:
253 | if e.args[0] == errno.EAGAIN:
254 | select.select([sock], [], [])
255 | continue
256 | else:
257 | raise
258 | if not data: # EOF
259 | break
260 | dataList.append(data)
261 | dataLen = len(data)
262 | recvLen += dataLen
263 | length -= dataLen
264 | return b''.join(dataList), recvLen
265 | _recvall = staticmethod(_recvall)
266 |
267 | def read(self, sock):
268 | """Attempt to read a packet from the server."""
269 | try:
270 | header, length = self._recvall(sock, 4)
271 | except socket.error:
272 | # Treat any sort of socket errors as EOF (close Connection).
273 | raise EOFError
274 |
275 | if length < 4:
276 | raise EOFError
277 |
278 | if header[:2] != SERVER_PREFIX:
279 | raise ProtocolError('invalid header')
280 |
281 | self.length = struct.unpack('>H', header[2:4])[0]
282 | if self.length:
283 | try:
284 | self.data, length = self._recvall(sock, self.length)
285 | except socket.error:
286 | raise EOFError
287 |
288 | if length < self.length:
289 | raise EOFError
290 |
291 | def _sendall(sock, data):
292 | """
293 | Writes data to a socket and does not return until all the data is sent.
294 | """
295 | length = len(data)
296 | while length:
297 | try:
298 | sent = sock.send(data)
299 | except socket.error as e:
300 | if e.args[0] == errno.EAGAIN:
301 | select.select([], [sock], [])
302 | continue
303 | else:
304 | raise
305 | data = data[sent:]
306 | length -= sent
307 | _sendall = staticmethod(_sendall)
308 |
309 | def write(self, sock):
310 | """Send a packet to the server."""
311 | self.length = len(self.data)
312 | self._sendall(sock, CONTAINER_PREFIX + struct.pack('>H', self.length))
313 | if self.length:
314 | self._sendall(sock, self.data)
315 |
316 | class InputStream(object):
317 | """
318 | File-like object that represents the request body (if any). Supports
319 | the bare mininum methods required by the WSGI spec. Thanks to
320 | StringIO for ideas.
321 | """
322 | def __init__(self, conn):
323 | self._conn = conn
324 |
325 | # See WSGIServer.
326 | self._shrinkThreshold = conn.server.inputStreamShrinkThreshold
327 |
328 | self._buf = b''
329 | self._bufList = []
330 | self._pos = 0 # Current read position.
331 | self._avail = 0 # Number of bytes currently available.
332 | self._length = 0 # Set to Content-Length in request.
333 |
334 | self.logger = logging.getLogger(LoggerName)
335 |
336 | def bytesAvailForAdd(self):
337 | return self._length - self._avail
338 |
339 | def _shrinkBuffer(self):
340 | """Gets rid of already read data (since we can't rewind)."""
341 | if self._pos >= self._shrinkThreshold:
342 | self._buf = self._buf[self._pos:]
343 | self._avail -= self._pos
344 | self._length -= self._pos
345 | self._pos = 0
346 |
347 | assert self._avail >= 0 and self._length >= 0
348 |
349 | def _waitForData(self):
350 | toAdd = min(self.bytesAvailForAdd(), 0xffff)
351 | assert toAdd > 0
352 | pkt = Packet()
353 | pkt.data = PKTTYPE_GET_BODY + \
354 | struct.pack('>H', toAdd)
355 | self._conn.writePacket(pkt)
356 | self._conn.processInput()
357 |
358 | def read(self, n=-1):
359 | if self._pos == self._length:
360 | return b''
361 | while True:
362 | if n < 0 or (self._avail - self._pos) < n:
363 | # Not enough data available.
364 | if not self.bytesAvailForAdd():
365 | # And there's no more coming.
366 | newPos = self._avail
367 | break
368 | else:
369 | # Ask for more data and wait.
370 | self._waitForData()
371 | continue
372 | else:
373 | newPos = self._pos + n
374 | break
375 | # Merge buffer list, if necessary.
376 | if self._bufList:
377 | self._buf += b''.join(self._bufList)
378 | self._bufList = []
379 | r = self._buf[self._pos:newPos]
380 | self._pos = newPos
381 | self._shrinkBuffer()
382 | return r
383 |
384 | def readline(self, length=None):
385 | if self._pos == self._length:
386 | return b''
387 | while True:
388 | # Unfortunately, we need to merge the buffer list early.
389 | if self._bufList:
390 | self._buf += b''.join(self._bufList)
391 | self._bufList = []
392 | # Find newline.
393 | i = self._buf.find(b'\n', self._pos)
394 | if i < 0:
395 | # Not found?
396 | if not self.bytesAvailForAdd():
397 | # No more data coming.
398 | newPos = self._avail
399 | break
400 | else:
401 | if length is not None and len(self._buf) >= length + self._pos:
402 | newPos = self._pos + length
403 | break
404 | # Wait for more to come.
405 | self._waitForData()
406 | continue
407 | else:
408 | newPos = i + 1
409 | break
410 | r = self._buf[self._pos:newPos]
411 | self._pos = newPos
412 | self._shrinkBuffer()
413 | return r
414 |
415 | def readlines(self, sizehint=0):
416 | total = 0
417 | lines = []
418 | line = self.readline()
419 | while line:
420 | lines.append(line)
421 | total += len(line)
422 | if 0 < sizehint <= total:
423 | break
424 | line = self.readline()
425 | return lines
426 |
427 | def __iter__(self):
428 | return self
429 |
430 | def __next__(self):
431 | r = self.readline()
432 | if not r:
433 | raise StopIteration
434 | return r
435 |
436 | def setDataLength(self, length):
437 | """
438 | Once Content-Length is known, Request calls this method to set it.
439 | """
440 | self._length = length
441 |
442 | def addData(self, data):
443 | """
444 | Adds data from the server to this InputStream. Note that we never ask
445 | the server for data beyond the Content-Length, so the server should
446 | never send us an EOF (empty string argument).
447 | """
448 | assert type(data) is bytes
449 | if not data:
450 | raise ProtocolError('short data')
451 | self._bufList.append(data)
452 | length = len(data)
453 | self._avail += length
454 | if self._avail > self._length:
455 | raise ProtocolError('too much data')
456 |
457 | class Request(object):
458 | """
459 | A Request object. A more fitting name would probably be Transaction, but
460 | it's named Request to mirror my FastCGI driver. :) This object
461 | encapsulates all the data about the HTTP request and allows the handler
462 | to send a response.
463 |
464 | The only attributes/methods that the handler should concern itself
465 | with are: environ, input, startResponse(), and write().
466 | """
467 | # Do not ever change the following value.
468 | _maxWrite = 8192 - 4 - 3 - 1 # 8k - pkt header - send body header - NUL
469 |
470 | def __init__(self, conn):
471 | self._conn = conn
472 |
473 | self.environ = {}
474 | self.input = InputStream(conn)
475 |
476 | self._headersSent = False
477 |
478 | self.logger = logging.getLogger(LoggerName)
479 |
480 | def run(self):
481 | self.logger.info('%s %s',
482 | self.environ['REQUEST_METHOD'],
483 | self.environ['REQUEST_URI'])
484 |
485 | start = datetime.datetime.now()
486 |
487 | try:
488 | self._conn.server.handler(self)
489 | except:
490 | self.logger.exception('Exception caught from handler')
491 | if not self._headersSent:
492 | self._conn.server.error(self)
493 |
494 | end = datetime.datetime.now()
495 |
496 | # Notify server of end of response (reuse flag is set to true).
497 | pkt = Packet()
498 | pkt.data = PKTTYPE_END_RESPONSE + b'\x01'
499 | self._conn.writePacket(pkt)
500 |
501 | handlerTime = end - start
502 | self.logger.debug('%s %s done (%.3f secs)',
503 | self.environ['REQUEST_METHOD'],
504 | self.environ['REQUEST_URI'],
505 | handlerTime.seconds +
506 | handlerTime.microseconds / 1000000.0)
507 |
508 | # The following methods are called from the Connection to set up this
509 | # Request.
510 |
511 | def setMethod(self, value):
512 | self.environ['REQUEST_METHOD'] = value.decode('latin-1')
513 |
514 | def setProtocol(self, value):
515 | self.environ['SERVER_PROTOCOL'] = value.decode('latin-1')
516 |
517 | def setRequestURI(self, value):
518 | self.environ['REQUEST_URI'] = value.decode('latin-1')
519 |
520 | def setRemoteAddr(self, value):
521 | self.environ['REMOTE_ADDR'] = value.decode('latin-1')
522 |
523 | def setRemoteHost(self, value):
524 | self.environ['REMOTE_HOST'] = value.decode('latin-1')
525 |
526 | def setServerName(self, value):
527 | self.environ['SERVER_NAME'] = value.decode('latin-1')
528 |
529 | def setServerPort(self, value):
530 | self.environ['SERVER_PORT'] = str(value)
531 |
532 | def setIsSSL(self, value):
533 | if value:
534 | self.environ['HTTPS'] = 'on'
535 |
536 | def addHeader(self, name, value):
537 | name = name.replace(b'-', b'_').upper()
538 | if name in (b'CONTENT_TYPE', b'CONTENT_LENGTH'):
539 | self.environ[name.decode('latin-1')] = value.decode('latin-1')
540 | if name == b'CONTENT_LENGTH':
541 | length = int(value)
542 | self.input.setDataLength(length)
543 | else:
544 | self.environ[(b'HTTP_'+name).decode('latin-1')] = value.decode('latin-1')
545 |
546 | def addAttribute(self, name, value):
547 | self.environ[name.decode('latin-1')] = value.decode('latin-1')
548 |
549 | # The only two methods that should be called from the handler.
550 |
551 | def startResponse(self, statusCode, statusMsg, headers):
552 | """
553 | Begin the HTTP response. This must only be called once and it
554 | must be called before any calls to write().
555 |
556 | statusCode is the integer status code (e.g. 200). statusMsg
557 | is the associated reason message (e.g.'OK'). headers is a list
558 | of 2-tuples - header name/value pairs. (Both header name and value
559 | must be strings.)
560 | """
561 | assert not self._headersSent, 'Headers already sent!'
562 |
563 | pkt = Packet()
564 | pkt.data = PKTTYPE_SEND_HEADERS + \
565 | struct.pack('>H', statusCode) + \
566 | encodeString(statusMsg) + \
567 | struct.pack('>H', len(headers)) + \
568 | b''.join([encodeResponseHeader(name, value)
569 | for name,value in headers])
570 |
571 | self._conn.writePacket(pkt)
572 |
573 | self._headersSent = True
574 |
575 | def write(self, data):
576 | """
577 | Write data (which comprises the response body). Note that due to
578 | restrictions on AJP packet size, we limit our writes to 8185 bytes
579 | each packet.
580 | """
581 | assert self._headersSent, 'Headers must be sent first!'
582 |
583 | bytesLeft = len(data)
584 | while bytesLeft:
585 | toWrite = min(bytesLeft, self._maxWrite)
586 |
587 | pkt = Packet()
588 | pkt.data = PKTTYPE_SEND_BODY + \
589 | struct.pack('>H', toWrite) + \
590 | data[:toWrite] + b'\x00' # Undocumented
591 | self._conn.writePacket(pkt)
592 |
593 | data = data[toWrite:]
594 | bytesLeft -= toWrite
595 |
596 | class TimeoutException(Exception):
597 | pass
598 |
599 | class Connection(object):
600 | """
601 | A single Connection with the server. Requests are not multiplexed over the
602 | same connection, so at any given time, the Connection is either
603 | waiting for a request, or processing a single request.
604 | """
605 | def __init__(self, sock, addr, server, timeout):
606 | self.server = server
607 | self._sock = sock
608 | self._addr = addr
609 | self._timeout = timeout
610 |
611 | self._request = None
612 |
613 | self.logger = logging.getLogger(LoggerName)
614 |
615 | def timeout_handler(self, signum, frame):
616 | self.logger.error('Timeout Exceeded')
617 | self.logger.error("\n".join(traceback.format_stack(frame)))
618 |
619 | raise TimeoutException
620 |
621 | def run(self):
622 | self.logger.debug('Connection starting up (%s:%d)',
623 | self._addr[0], self._addr[1])
624 |
625 | # Main loop. Errors will cause the loop to be exited and
626 | # the socket to be closed.
627 | while True:
628 | try:
629 | self.processInput()
630 | except ProtocolError as e:
631 | self.logger.error("Protocol error '%s'", str(e))
632 | break
633 | except (EOFError, KeyboardInterrupt):
634 | break
635 | except:
636 | self.logger.exception('Exception caught in Connection')
637 | break
638 |
639 | self.logger.debug('Connection shutting down (%s:%d)',
640 | self._addr[0], self._addr[1])
641 |
642 | self._sock.close()
643 |
644 | def processInput(self):
645 | """Wait for and process a single packet."""
646 | pkt = Packet()
647 | select.select([self._sock], [], [])
648 | pkt.read(self._sock)
649 |
650 | # Body chunks have no packet type code.
651 | if self._request is not None:
652 | self._processBody(pkt)
653 | return
654 |
655 | if not pkt.length:
656 | raise ProtocolError('unexpected empty packet')
657 |
658 | pkttype = pkt.data[0]
659 | if pkttype == PKTTYPE_FWD_REQ:
660 | self._forwardRequest(pkt)
661 | elif pkttype == PKTTYPE_SHUTDOWN:
662 | self._shutdown(pkt)
663 | elif pkttype == PKTTYPE_PING:
664 | self._ping(pkt)
665 | elif pkttype == PKTTYPE_CPING:
666 | self._cping(pkt)
667 | else:
668 | raise ProtocolError('unknown packet type')
669 |
670 | def _forwardRequest(self, pkt):
671 | """
672 | Creates a Request object, fills it in from the packet, then runs it.
673 | """
674 | assert self._request is None
675 |
676 | req = self.server.requestClass(self)
677 | i = pkt.data[1]
678 | method = methodTable[i]
679 | if method is None:
680 | raise ValueError('bad method field')
681 | req.setMethod(method)
682 | value, pos = decodeString(pkt.data, 2)
683 | req.setProtocol(value)
684 | value, pos = decodeString(pkt.data, pos)
685 | req.setRequestURI(value)
686 | value, pos = decodeString(pkt.data, pos)
687 | req.setRemoteAddr(value)
688 | value, pos = decodeString(pkt.data, pos)
689 | req.setRemoteHost(value)
690 | value, pos = decodeString(pkt.data, pos)
691 | req.setServerName(value)
692 | value = struct.unpack('>H', pkt.data[pos:pos+2])[0]
693 | req.setServerPort(value)
694 | i = pkt.data[pos+2]
695 | req.setIsSSL(i != 0)
696 |
697 | # Request headers.
698 | numHeaders = struct.unpack('>H', pkt.data[pos+3:pos+5])[0]
699 | pos += 5
700 | for i in range(numHeaders):
701 | name, value, pos = decodeRequestHeader(pkt.data, pos)
702 | req.addHeader(name, value)
703 |
704 | # Attributes.
705 | while True:
706 | name, value, pos = decodeAttribute(pkt.data, pos)
707 | if name is None:
708 | break
709 | req.addAttribute(name, value)
710 |
711 | self._request = req
712 |
713 | # Read first body chunk, if needed.
714 | if req.input.bytesAvailForAdd():
715 | self.processInput()
716 |
717 | # If there is a timeout
718 | if self._timeout:
719 | old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler)
720 | signal.alarm(self._timeout)
721 |
722 | # Run Request.
723 | req.run()
724 |
725 | self._request = None
726 |
727 | # Restore old handler if timeout was given
728 | if self._timeout:
729 | signal.alarm(0)
730 | signal.signal(signal.SIGALRM, old_alarm)
731 |
732 | def _shutdown(self, pkt):
733 | """Not sure what to do with this yet."""
734 | self.logger.info('Received shutdown request from server')
735 |
736 | def _ping(self, pkt):
737 | """I have no idea what this packet means."""
738 | self.logger.debug('Received ping')
739 |
740 | def _cping(self, pkt):
741 | """Respond to a PING (CPING) packet."""
742 | self.logger.debug('Received PING, sending PONG')
743 | pkt = Packet()
744 | pkt.data = PKTTYPE_CPONG
745 | self.writePacket(pkt)
746 |
747 | def _processBody(self, pkt):
748 | """
749 | Handles a body chunk from the server by appending it to the
750 | InputStream.
751 | """
752 | if pkt.length:
753 | length = struct.unpack('>H', pkt.data[:2])[0]
754 | self._request.input.addData(pkt.data[2:2+length])
755 | else:
756 | # Shouldn't really ever get here.
757 | self._request.input.addData(b'')
758 |
759 | def writePacket(self, pkt):
760 | """Sends a Packet to the server."""
761 | pkt.write(self._sock)
762 |
763 | class BaseAJPServer(object):
764 | # What Request class to use.
765 | requestClass = Request
766 |
767 | # Limits the size of the InputStream's string buffer to this size + 8k.
768 | # Since the InputStream is not seekable, we throw away already-read
769 | # data once this certain amount has been read. (The 8k is there because
770 | # it is the maximum size of new data added per chunk.)
771 | inputStreamShrinkThreshold = 102400 - 8192
772 |
773 | def __init__(self, application, scriptName='', environ=None,
774 | multithreaded=True, multiprocess=False,
775 | bindAddress=('localhost', 8009), allowedServers=NoDefault,
776 | loggingLevel=logging.INFO, debug=False):
777 | """
778 | scriptName is the initial portion of the URL path that "belongs"
779 | to your application. It is used to determine PATH_INFO (which doesn't
780 | seem to be passed in). An empty scriptName means your application
781 | is mounted at the root of your virtual host.
782 |
783 | environ, which must be a dictionary, can contain any additional
784 | environment variables you want to pass to your application.
785 |
786 | Set multithreaded to False if your application is not thread-safe.
787 |
788 | Set multiprocess to True to explicitly set wsgi.multiprocess to
789 | True. (Only makes sense with threaded servers.)
790 |
791 | bindAddress is the address to bind to, which must be a tuple of
792 | length 2. The first element is a string, which is the host name
793 | or IPv4 address of a local interface. The 2nd element is the port
794 | number.
795 |
796 | allowedServers must be None or a list of strings representing the
797 | IPv4 addresses of servers allowed to connect. None means accept
798 | connections from anywhere. By default, it is a list containing
799 | the single item '127.0.0.1'.
800 |
801 | loggingLevel sets the logging level of the module-level logger.
802 | """
803 | if environ is None:
804 | environ = {}
805 |
806 | self.application = application
807 | self.scriptName = scriptName
808 | self.environ = environ
809 | self.multithreaded = multithreaded
810 | self.multiprocess = multiprocess
811 | self.debug = debug
812 | self._bindAddress = bindAddress
813 | if allowedServers is NoDefault:
814 | allowedServers = ['127.0.0.1']
815 | self._allowedServers = allowedServers
816 |
817 | # Used to force single-threadedness.
818 | self._appLock = _thread.allocate_lock()
819 |
820 | self.logger = logging.getLogger(LoggerName)
821 | self.logger.setLevel(loggingLevel)
822 |
823 | def _setupSocket(self):
824 | """Creates and binds the socket for communication with the server."""
825 | sock = socket.socket()
826 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
827 | sock.bind(self._bindAddress)
828 | sock.listen(socket.SOMAXCONN)
829 | return sock
830 |
831 | def _cleanupSocket(self, sock):
832 | """Closes the main socket."""
833 | sock.close()
834 |
835 | def _isClientAllowed(self, addr):
836 | ret = self._allowedServers is None or addr[0] in self._allowedServers
837 | if not ret:
838 | self.logger.warning('Server connection from %s disallowed',
839 | addr[0])
840 | return ret
841 |
842 | def handler(self, request):
843 | """
844 | WSGI handler. Sets up WSGI environment, calls the application,
845 | and sends the application's response.
846 | """
847 | environ = request.environ
848 | environ.update(self.environ)
849 |
850 | environ['wsgi.version'] = (1,0)
851 | environ['wsgi.input'] = request.input
852 | environ['wsgi.errors'] = sys.stderr
853 | environ['wsgi.multithread'] = self.multithreaded
854 | environ['wsgi.multiprocess'] = self.multiprocess
855 | environ['wsgi.run_once'] = False
856 |
857 | if environ.get('HTTPS', 'off') in ('on', '1'):
858 | environ['wsgi.url_scheme'] = 'https'
859 | else:
860 | environ['wsgi.url_scheme'] = 'http'
861 |
862 | self._sanitizeEnv(environ)
863 |
864 | headers_set = []
865 | headers_sent = []
866 | result = None
867 |
868 | def write(data):
869 | if type(data) is str:
870 | data = data.encode('latin-1')
871 |
872 | assert type(data) is bytes, 'write() argument must be bytes'
873 | assert headers_set, 'write() before start_response()'
874 |
875 | if not headers_sent:
876 | status, responseHeaders = headers_sent[:] = headers_set
877 | statusCode = int(status[:3])
878 | statusMsg = status[4:]
879 | found = False
880 | for header,value in responseHeaders:
881 | if header.lower() == b'content-length':
882 | found = True
883 | break
884 | if not found and result is not None:
885 | try:
886 | if len(result) == 1:
887 | responseHeaders.append((b'Content-Length',
888 | str(len(data)).encode('latin-1')))
889 | except:
890 | pass
891 | request.startResponse(statusCode, statusMsg, responseHeaders)
892 |
893 | request.write(data)
894 |
895 | def start_response(status, response_headers, exc_info=None):
896 | if exc_info:
897 | try:
898 | if headers_sent:
899 | # Re-raise if too late
900 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
901 | finally:
902 | exc_info = None # avoid dangling circular ref
903 | else:
904 | assert not headers_set, 'Headers already set!'
905 |
906 | if type(status) is str:
907 | status = status.encode('latin-1')
908 |
909 | assert type(status) is bytes, 'Status must be bytes'
910 | assert len(status) >= 4, 'Status must be at least 4 bytes'
911 | assert int(status[:3]), 'Status must begin with 3-digit code'
912 | assert status[3] == 0x20, 'Status must have a space after code'
913 | assert type(response_headers) is list, 'Headers must be a list'
914 | new_response_headers = []
915 | for name,val in response_headers:
916 | if type(name) is str:
917 | name = name.encode('latin-1')
918 | if type(val) is str:
919 | val = val.encode('latin-1')
920 |
921 | assert type(name) is bytes, 'Header name "%s" must be bytes' % name
922 | assert type(val) is bytes, 'Value of header "%s" must be bytes' % name
923 |
924 | new_response_headers.append((name, val))
925 |
926 | headers_set[:] = [status, new_response_headers]
927 | return write
928 |
929 | if not self.multithreaded:
930 | self._appLock.acquire()
931 | try:
932 | try:
933 | result = self.application(environ, start_response)
934 | try:
935 | for data in result:
936 | if data:
937 | write(data)
938 | if not headers_sent:
939 | write(b'') # in case body was empty
940 | finally:
941 | if hasattr(result, 'close'):
942 | result.close()
943 | except socket.error as e:
944 | if e.args[0] != errno.EPIPE:
945 | raise # Don't let EPIPE propagate beyond server
946 | finally:
947 | if not self.multithreaded:
948 | self._appLock.release()
949 |
950 | def _sanitizeEnv(self, environ):
951 | """Fill-in/deduce missing values in environ."""
952 | # Namely SCRIPT_NAME/PATH_INFO
953 | value = environ['REQUEST_URI']
954 | scriptName = environ.get('WSGI_SCRIPT_NAME', self.scriptName)
955 | if not value.startswith(scriptName):
956 | self.logger.warning('scriptName does not match request URI')
957 |
958 | environ['PATH_INFO'] = value[len(scriptName):]
959 | environ['SCRIPT_NAME'] = scriptName
960 |
961 | reqUri = None
962 | if 'REQUEST_URI' in environ:
963 | reqUri = environ['REQUEST_URI'].split('?', 1)
964 |
965 | if 'QUERY_STRING' not in environ or not environ['QUERY_STRING']:
966 | if reqUri is not None and len(reqUri) > 1:
967 | environ['QUERY_STRING'] = reqUri[1]
968 | else:
969 | environ['QUERY_STRING'] = ''
970 |
971 | def error(self, request):
972 | """
973 | Override to provide custom error handling. Ideally, however,
974 | all errors should be caught at the application level.
975 | """
976 | if self.debug:
977 | request.startResponse(500, b'Internal Server Error', [(b'Content-Type', b'text/html')])
978 | import cgitb
979 | request.write(cgitb.html(sys.exc_info()).encode('latin-1'))
980 | else:
981 | errorpage = b"""
982 |
983 | Unhandled Exception
984 |
985 | Unhandled Exception
986 | An unhandled exception was thrown by the application.
987 |
988 | """
989 | request.startResponse(200, b'Internal Server Error', [(b'Content-Type', b'text/html')])
990 | request.write(errorpage)
991 |
--------------------------------------------------------------------------------