~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_server.py

(jameinel) Allow 'bzr serve' to interpret SIGHUP as a graceful shutdown.
 (bug #795025) (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
import errno
18
18
import httplib
20
20
import posixpath
21
21
import random
22
22
import re
23
 
import select
24
23
import SimpleHTTPServer
25
24
import socket
26
 
import SocketServer
27
 
import sys
28
 
import threading
29
 
import time
30
25
import urllib
31
26
import urlparse
32
27
 
33
 
from bzrlib import transport
34
 
from bzrlib.transport import local
 
28
from bzrlib.tests import test_server
35
29
 
36
30
 
37
31
class BadWebserverPath(ValueError):
70
64
                self.headers.get('referer', '-'),
71
65
                self.headers.get('user-agent', '-'))
72
66
 
 
67
    def handle(self):
 
68
        SimpleHTTPServer.SimpleHTTPRequestHandler.handle(self)
 
69
        # Some client (pycurl, I'm looking at you) are more picky than others
 
70
        # and require that the socket itself is closed
 
71
        # (SocketServer.StreamRequestHandler only close the two associated
 
72
        # 'makefile' objects)
 
73
        self.connection.close()
 
74
 
73
75
    def handle_one_request(self):
74
76
        """Handle a single HTTP request.
75
77
 
77
79
        connection early to avoid polluting the test results.
78
80
        """
79
81
        try:
80
 
            SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self)
 
82
            self._handle_one_request()
81
83
        except socket.error, e:
82
84
            # Any socket error should close the connection, but some errors are
83
85
            # due to the client closing early and we don't want to pollute test
88
90
                                     errno.ECONNABORTED, errno.EBADF)):
89
91
                raise
90
92
 
91
 
    _range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
 
93
    error_content_type = 'text/plain'
 
94
    error_message_format = '''\
 
95
Error code: %(code)s.
 
96
Message: %(message)s.
 
97
'''
 
98
 
 
99
    def send_error(self, code, message=None):
 
100
        """Send and log an error reply.
 
101
 
 
102
        We redefine the python-provided version to be able to set a 
 
103
        ``Content-Length`` header as some http/1.1 clients complain otherwise
 
104
        (see bug #568421).
 
105
 
 
106
        :param code: The HTTP error code.
 
107
 
 
108
        :param message: The explanation of the error code, Defaults to a short
 
109
             entry.
 
110
        """
 
111
 
 
112
        if message is None:
 
113
            try:
 
114
                message = self.responses[code][0]
 
115
            except KeyError:
 
116
                message = '???'
 
117
        self.log_error("code %d, message %s", code, message)
 
118
        content = (self.error_message_format %
 
119
                   {'code': code, 'message': message})
 
120
        self.send_response(code, message)
 
121
        self.send_header("Content-Type", self.error_content_type)
 
122
        self.send_header("Content-Length", "%d" % len(content))
 
123
        self.send_header('Connection', 'close')
 
124
        self.end_headers()
 
125
        if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
 
126
            self.wfile.write(content)
 
127
 
 
128
    def _handle_one_request(self):
 
129
        SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self)
 
130
 
 
131
    _range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)?$')
92
132
    _tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
93
133
 
94
 
    def parse_ranges(self, ranges_header):
95
 
        """Parse the range header value and returns ranges and tail.
96
 
 
97
 
        RFC2616 14.35 says that syntactically invalid range
98
 
        specifiers MUST be ignored. In that case, we return 0 for
99
 
        tail and [] for ranges.
 
134
    def _parse_ranges(self, ranges_header, file_size):
 
135
        """Parse the range header value and returns ranges.
 
136
 
 
137
        RFC2616 14.35 says that syntactically invalid range specifiers MUST be
 
138
        ignored. In that case, we return None instead of a range list.
 
139
 
 
140
        :param ranges_header: The 'Range' header value.
 
141
 
 
142
        :param file_size: The size of the requested file.
 
143
 
 
144
        :return: A list of (start, end) tuples or None if some invalid range
 
145
            specifier is encountered.
100
146
        """
101
 
        tail = 0
102
 
        ranges = []
103
147
        if not ranges_header.startswith('bytes='):
104
148
            # Syntactically invalid header
105
 
            return 0, []
 
149
            return None
106
150
 
 
151
        tail = None
 
152
        ranges = []
107
153
        ranges_header = ranges_header[len('bytes='):]
108
154
        for range_str in ranges_header.split(','):
109
 
            # FIXME: RFC2616 says end is optional and default to file_size
110
155
            range_match = self._range_regexp.match(range_str)
111
156
            if range_match is not None:
112
157
                start = int(range_match.group('start'))
113
 
                end = int(range_match.group('end'))
 
158
                end_match = range_match.group('end')
 
159
                if end_match is None:
 
160
                    # RFC2616 says end is optional and default to file_size
 
161
                    end = file_size
 
162
                else:
 
163
                    end = int(end_match)
114
164
                if start > end:
115
165
                    # Syntactically invalid range
116
 
                    return 0, []
 
166
                    return None
117
167
                ranges.append((start, end))
118
168
            else:
119
169
                tail_match = self._tail_regexp.match(range_str)
121
171
                    tail = int(tail_match.group('tail'))
122
172
                else:
123
173
                    # Syntactically invalid range
124
 
                    return 0, []
125
 
        return tail, ranges
 
174
                    return None
 
175
        if tail is not None:
 
176
            # Normalize tail into ranges
 
177
            ranges.append((max(0, file_size - tail), file_size))
 
178
 
 
179
        checked_ranges = []
 
180
        for start, end in ranges:
 
181
            if start >= file_size:
 
182
                # RFC2616 14.35, ranges are invalid if start >= file_size
 
183
                return None
 
184
            # RFC2616 14.35, end values should be truncated
 
185
            # to file_size -1 if they exceed it
 
186
            end = min(end, file_size - 1)
 
187
            checked_ranges.append((start, end))
 
188
        return checked_ranges
126
189
 
127
190
    def _header_line_length(self, keyword, value):
128
191
        header_line = '%s: %s\r\n' % (keyword, value)
137
200
            # common)
138
201
            self.send_response(301)
139
202
            self.send_header("Location", self.path + "/")
140
 
            # Indicates that the body is empty for HTTP/1.1 clients 
 
203
            # Indicates that the body is empty for HTTP/1.1 clients
141
204
            self.send_header('Content-Length', '0')
142
205
            self.end_headers()
143
206
            return None
212
275
            # mode may cause newline translations, making the
213
276
            # actual size of the content transmitted *less* than
214
277
            # the content-length!
215
 
            file = open(path, 'rb')
 
278
            f = open(path, 'rb')
216
279
        except IOError:
217
280
            self.send_error(404, "File not found")
218
281
            return
219
282
 
220
 
        file_size = os.fstat(file.fileno())[6]
221
 
        tail, ranges = self.parse_ranges(ranges_header_value)
222
 
        # Normalize tail into ranges
223
 
        if tail != 0:
224
 
            ranges.append((file_size - tail, file_size))
225
 
 
226
 
        self._satisfiable_ranges = True
227
 
        if len(ranges) == 0:
228
 
            self._satisfiable_ranges = False
229
 
        else:
230
 
            def check_range(range_specifier):
231
 
                start, end = range_specifier
232
 
                # RFC2616 14.35, ranges are invalid if start >= file_size
233
 
                if start >= file_size:
234
 
                    self._satisfiable_ranges = False # Side-effect !
235
 
                    return 0, 0
236
 
                # RFC2616 14.35, end values should be truncated
237
 
                # to file_size -1 if they exceed it
238
 
                end = min(end, file_size - 1)
239
 
                return start, end
240
 
 
241
 
            ranges = map(check_range, ranges)
242
 
 
243
 
        if not self._satisfiable_ranges:
 
283
        file_size = os.fstat(f.fileno())[6]
 
284
        ranges = self._parse_ranges(ranges_header_value, file_size)
 
285
        if not ranges:
244
286
            # RFC2616 14.16 and 14.35 says that when a server
245
287
            # encounters unsatisfiable range specifiers, it
246
288
            # SHOULD return a 416.
247
 
            file.close()
 
289
            f.close()
248
290
            # FIXME: We SHOULD send a Content-Range header too,
249
291
            # but the implementation of send_error does not
250
292
            # allows that. So far.
253
295
 
254
296
        if len(ranges) == 1:
255
297
            (start, end) = ranges[0]
256
 
            self.get_single_range(file, file_size, start, end)
 
298
            self.get_single_range(f, file_size, start, end)
257
299
        else:
258
 
            self.get_multiple_ranges(file, file_size, ranges)
259
 
        file.close()
 
300
            self.get_multiple_ranges(f, file_size, ranges)
 
301
        f.close()
260
302
 
261
303
    def translate_path(self, path):
262
304
        """Translate a /-separated PATH to the local filename syntax.
318
360
        self.test_case_server = test_case_server
319
361
        self._home_dir = test_case_server._home_dir
320
362
 
321
 
    def tearDown(self):
322
 
         """Called to clean-up the server.
323
 
 
324
 
         Since the server may be (surely is, even) in a blocking listen, we
325
 
         shutdown its socket before closing it.
326
 
         """
327
 
         # Note that is this executed as part of the implicit tear down in the
328
 
         # main thread while the server runs in its own thread. The clean way
329
 
         # to tear down the server is to instruct him to stop accepting
330
 
         # connections and wait for the current connection(s) to end
331
 
         # naturally. To end the connection naturally, the http transports
332
 
         # should close their socket when they do not need to talk to the
333
 
         # server anymore. This happens naturally during the garbage collection
334
 
         # phase of the test transport objetcs (the server clients), so we
335
 
         # don't have to worry about them.  So, for the server, we must tear
336
 
         # down here, from the main thread, when the test have ended.  Note
337
 
         # that since the server is in a blocking operation and since python
338
 
         # use select internally, shutting down the socket is reliable and
339
 
         # relatively clean.
340
 
         try:
341
 
             self.socket.shutdown(socket.SHUT_RDWR)
342
 
         except socket.error, e:
343
 
             # WSAENOTCONN (10057) 'Socket is not connected' is harmless on
344
 
             # windows (occurs before the first connection attempt
345
 
             # vila--20071230)
346
 
 
347
 
             # 'Socket is not connected' can also occur on OSX, with a
348
 
             # "regular" ENOTCONN (when something went wrong during test case
349
 
             # setup leading to self.setUp() *not* being called but
350
 
             # self.tearDown() still being called -- vila20081106
351
 
             if not len(e.args) or e.args[0] not in (errno.ENOTCONN, 10057):
352
 
                 raise
353
 
         # Let the server properly close the socket
354
 
         self.server_close()
355
 
 
356
 
 
357
 
class TestingHTTPServer(SocketServer.TCPServer, TestingHTTPServerMixin):
 
363
 
 
364
class TestingHTTPServer(test_server.TestingTCPServer, TestingHTTPServerMixin):
358
365
 
359
366
    def __init__(self, server_address, request_handler_class,
360
367
                 test_case_server):
 
368
        test_server.TestingTCPServer.__init__(self, server_address,
 
369
                                              request_handler_class)
361
370
        TestingHTTPServerMixin.__init__(self, test_case_server)
362
 
        SocketServer.TCPServer.__init__(self, server_address,
363
 
                                        request_handler_class)
364
 
 
365
 
 
366
 
class TestingThreadingHTTPServer(SocketServer.ThreadingTCPServer,
 
371
 
 
372
 
 
373
class TestingThreadingHTTPServer(test_server.TestingThreadingTCPServer,
367
374
                                 TestingHTTPServerMixin):
368
375
    """A threading HTTP test server for HTTP 1.1.
369
376
 
371
378
    server, we need an independent connection for each of them. We achieve that
372
379
    by spawning a new thread for each connection.
373
380
    """
374
 
 
375
381
    def __init__(self, server_address, request_handler_class,
376
382
                 test_case_server):
 
383
        test_server.TestingThreadingTCPServer.__init__(self, server_address,
 
384
                                                       request_handler_class)
377
385
        TestingHTTPServerMixin.__init__(self, test_case_server)
378
 
        SocketServer.ThreadingTCPServer.__init__(self, server_address,
379
 
                                                 request_handler_class)
380
 
        # Decides how threads will act upon termination of the main
381
 
        # process. This is prophylactic as we should not leave the threads
382
 
        # lying around.
383
 
        self.daemon_threads = True
384
 
 
385
 
 
386
 
class HttpServer(transport.Server):
 
386
 
 
387
 
 
388
class HttpServer(test_server.TestingTCPServerInAThread):
387
389
    """A test server for http transports.
388
390
 
389
391
    Subclasses can provide a specific request handler.
411
413
        :param protocol_version: if specified, will override the protocol
412
414
            version of the request handler.
413
415
        """
414
 
        transport.Server.__init__(self)
415
 
        self.request_handler = request_handler
 
416
        # Depending on the protocol version, we will create the approriate
 
417
        # server
 
418
        if protocol_version is None:
 
419
            # Use the request handler one
 
420
            proto_vers = request_handler.protocol_version
 
421
        else:
 
422
            # Use our own, it will be used to override the request handler
 
423
            # one too.
 
424
            proto_vers = protocol_version
 
425
        # Get the appropriate server class for the required protocol
 
426
        serv_cls = self.http_server_class.get(proto_vers, None)
 
427
        if serv_cls is None:
 
428
            raise httplib.UnknownProtocol(proto_vers)
416
429
        self.host = 'localhost'
417
430
        self.port = 0
418
 
        self._httpd = None
419
 
        self.protocol_version = protocol_version
 
431
        super(HttpServer, self).__init__((self.host, self.port),
 
432
                                         serv_cls,
 
433
                                         request_handler)
 
434
        self.protocol_version = proto_vers
420
435
        # Allows tests to verify number of GET requests issued
421
436
        self.GET_request_nb = 0
422
 
 
423
 
    def create_httpd(self, serv_cls, rhandler_cls):
424
 
        return serv_cls((self.host, self.port), self.request_handler, self)
425
 
 
426
 
    def __repr__(self):
427
 
        return "%s(%s:%s)" % \
428
 
            (self.__class__.__name__, self.host, self.port)
429
 
 
430
 
    def _get_httpd(self):
431
 
        if self._httpd is None:
432
 
            rhandler = self.request_handler
433
 
            # Depending on the protocol version, we will create the approriate
434
 
            # server
435
 
            if self.protocol_version is None:
436
 
                # Use the request handler one
437
 
                proto_vers = rhandler.protocol_version
438
 
            else:
439
 
                # Use our own, it will be used to override the request handler
440
 
                # one too.
441
 
                proto_vers = self.protocol_version
442
 
            # Create the appropriate server for the required protocol
443
 
            serv_cls = self.http_server_class.get(proto_vers, None)
444
 
            if serv_cls is None:
445
 
                raise httplib.UnknownProtocol(proto_vers)
446
 
            else:
447
 
                self._httpd = self.create_httpd(serv_cls, rhandler)
448
 
            host, self.port = self._httpd.socket.getsockname()
449
 
        return self._httpd
450
 
 
451
 
    def _http_start(self):
452
 
        """Server thread main entry point. """
453
 
        self._http_running = False
454
 
        try:
455
 
            try:
456
 
                httpd = self._get_httpd()
457
 
                self._http_base_url = '%s://%s:%s/' % (self._url_protocol,
458
 
                                                       self.host, self.port)
459
 
                self._http_running = True
460
 
            except:
461
 
                # Whatever goes wrong, we save the exception for the main
462
 
                # thread. Note that since we are running in a thread, no signal
463
 
                # can be received, so we don't care about KeyboardInterrupt.
464
 
                self._http_exception = sys.exc_info()
465
 
        finally:
466
 
            # Release the lock or the main thread will block and the whole
467
 
            # process will hang.
468
 
            self._http_starting.release()
469
 
 
470
 
        # From now on, exceptions are taken care of by the
471
 
        # SocketServer.BaseServer or the request handler.
472
 
        while self._http_running:
473
 
            try:
474
 
                # Really an HTTP connection but the python framework is generic
475
 
                # and call them requests
476
 
                httpd.handle_request()
477
 
            except socket.timeout:
478
 
                pass
479
 
            except (socket.error, select.error), e:
480
 
               if e[0] == errno.EBADF:
481
 
                   # Starting with python-2.6, handle_request may raise socket
482
 
                   # or select exceptions when the server is shut down (as we
483
 
                   # do).
484
 
                   pass
485
 
               else:
486
 
                   raise
 
437
        self._http_base_url = None
 
438
        self.logs = []
 
439
 
 
440
    def create_server(self):
 
441
        return self.server_class(
 
442
            (self.host, self.port), self.request_handler_class, self)
487
443
 
488
444
    def _get_remote_url(self, path):
489
445
        path_parts = path.split(os.path.sep)
501
457
        """Capture Server log output."""
502
458
        self.logs.append(format % args)
503
459
 
504
 
    def setUp(self, backing_transport_server=None):
505
 
        """See bzrlib.transport.Server.setUp.
506
 
        
 
460
    def start_server(self, backing_transport_server=None):
 
461
        """See bzrlib.transport.Server.start_server.
 
462
 
507
463
        :param backing_transport_server: The transport that requests over this
508
464
            protocol should be forwarded to. Note that this is currently not
509
465
            supported for HTTP.
510
466
        """
511
467
        # XXX: TODO: make the server back onto vfs_server rather than local
512
468
        # disk.
513
 
        if not (backing_transport_server is None or \
514
 
                isinstance(backing_transport_server, local.LocalURLServer)):
 
469
        if not (backing_transport_server is None
 
470
                or isinstance(backing_transport_server,
 
471
                              test_server.LocalURLServer)):
515
472
            raise AssertionError(
516
 
                "HTTPServer currently assumes local transport, got %s" % \
 
473
                "HTTPServer currently assumes local transport, got %s" %
517
474
                backing_transport_server)
518
475
        self._home_dir = os.getcwdu()
519
476
        self._local_path_parts = self._home_dir.split(os.path.sep)
520
 
        self._http_base_url = None
521
 
 
522
 
        # Create the server thread
523
 
        self._http_starting = threading.Lock()
524
 
        self._http_starting.acquire()
525
 
        self._http_thread = threading.Thread(target=self._http_start)
526
 
        self._http_thread.setDaemon(True)
527
 
        self._http_exception = None
528
 
        self._http_thread.start()
529
 
 
530
 
        # Wait for the server thread to start (i.e release the lock)
531
 
        self._http_starting.acquire()
532
 
 
533
 
        if self._http_exception is not None:
534
 
            # Something went wrong during server start
535
 
            exc_class, exc_value, exc_tb = self._http_exception
536
 
            raise exc_class, exc_value, exc_tb
537
 
        self._http_starting.release()
538
477
        self.logs = []
539
478
 
540
 
    def tearDown(self):
541
 
        """See bzrlib.transport.Server.tearDown."""
542
 
        self._httpd.tearDown()
543
 
        self._http_running = False
544
 
        # We don't need to 'self._http_thread.join()' here since the thread is
545
 
        # a daemonic one and will be garbage collected anyway. Joining just
546
 
        # slows us down for no added benefit.
 
479
        super(HttpServer, self).start_server()
 
480
        self._http_base_url = '%s://%s:%s/' % (
 
481
            self._url_protocol, self.host, self.port)
547
482
 
548
483
    def get_url(self):
549
484
        """See bzrlib.transport.Server.get_url."""