~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_server.py

  • Committer: Alexander Belchenko
  • Date: 2008-01-28 22:48:19 UTC
  • mto: This revision was merged to the branch mainline in revision 3230.
  • Revision ID: bialix@ukr.net-20080128224819-88buaniv5dwy9vx4
$BZR_LOG is used to contol location of .bzr.log (use /dev/null or NUL to suppress log).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 Canonical Ltd
 
1
# Copyright (C) 2006, 2007 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import BaseHTTPServer
18
17
import errno
 
18
import httplib
19
19
import os
20
 
from SimpleHTTPServer import SimpleHTTPRequestHandler
21
 
import socket
22
20
import posixpath
23
21
import random
24
22
import re
 
23
import SimpleHTTPServer
 
24
import socket
 
25
import SocketServer
25
26
import sys
26
27
import threading
27
28
import time
28
29
import urllib
29
30
import urlparse
30
31
 
31
 
from bzrlib.transport import Server
32
 
from bzrlib.transport.local import LocalURLServer
 
32
from bzrlib import transport
 
33
from bzrlib.transport import local
33
34
 
34
35
 
35
36
class WebserverNotAvailable(Exception):
41
42
        return 'path %s is not in %s' % self.args
42
43
 
43
44
 
44
 
class TestingHTTPRequestHandler(SimpleHTTPRequestHandler):
 
45
class TestingHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
45
46
    """Handles one request.
46
47
 
47
 
    A TestingHTTPRequestHandler is instantiated for every request
48
 
    received by the associated server.
 
48
    A TestingHTTPRequestHandler is instantiated for every request received by
 
49
    the associated server. Note that 'request' here is inherited from the base
 
50
    TCPServer class, for the HTTP server it is really a connection which itself
 
51
    will handle one or several HTTP requests.
49
52
    """
 
53
    # Default protocol version
 
54
    protocol_version = 'HTTP/1.1'
 
55
 
 
56
    # The Message-like class used to parse the request headers
 
57
    MessageClass = httplib.HTTPMessage
 
58
 
 
59
    def setup(self):
 
60
        SimpleHTTPServer.SimpleHTTPRequestHandler.setup(self)
 
61
        tcs = self.server.test_case_server
 
62
        if tcs.protocol_version is not None:
 
63
            # If the test server forced a protocol version, use it
 
64
            self.protocol_version = tcs.protocol_version
50
65
 
51
66
    def log_message(self, format, *args):
52
67
        tcs = self.server.test_case_server
60
75
    def handle_one_request(self):
61
76
        """Handle a single HTTP request.
62
77
 
63
 
        You normally don't need to override this method; see the class
64
 
        __doc__ string for information on how to handle specific HTTP
65
 
        commands such as GET and POST.
66
 
 
 
78
        We catch all socket errors occurring when the client close the
 
79
        connection early to avoid polluting the test results.
67
80
        """
68
 
        for i in xrange(1,11): # Don't try more than 10 times
69
 
            try:
70
 
                self.raw_requestline = self.rfile.readline()
71
 
            except socket.error, e:
72
 
                if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
73
 
                    # omitted for now because some tests look at the log of
74
 
                    # the server and expect to see no errors.  see recent
75
 
                    # email thread. -- mbp 20051021. 
76
 
                    ## self.log_message('EAGAIN (%d) while reading from raw_requestline' % i)
77
 
                    time.sleep(0.01)
78
 
                    continue
 
81
        try:
 
82
            SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self)
 
83
        except socket.error, e:
 
84
            # Any socket error should close the connection, but some errors are
 
85
            # due to the client closing early and we don't want to pollute test
 
86
            # results, so we raise only the others.
 
87
            self.close_connection = 1
 
88
            if (len(e.args) == 0
 
89
                or e.args[0] not in (errno.EPIPE, errno.ECONNRESET,
 
90
                                     errno.ECONNABORTED, errno.EBADF)):
79
91
                raise
80
 
            else:
81
 
                break
82
 
        if not self.raw_requestline:
83
 
            self.close_connection = 1
84
 
            return
85
 
        if not self.parse_request(): # An error code has been sent, just exit
86
 
            return
87
 
        mname = 'do_' + self.command
88
 
        if getattr(self, mname, None) is None:
89
 
            self.send_error(501, "Unsupported method (%r)" % self.command)
90
 
            return
91
 
        method = getattr(self, mname)
92
 
        method()
93
92
 
94
93
    _range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
95
94
    _tail_regexp = re.compile(r'^-(?P<tail>\d+)$')
127
126
                    return 0, []
128
127
        return tail, ranges
129
128
 
 
129
    def _header_line_length(self, keyword, value):
 
130
        header_line = '%s: %s\r\n' % (keyword, value)
 
131
        return len(header_line)
 
132
 
 
133
    def send_head(self):
 
134
        """Overrides base implementation to work around a bug in python2.5."""
 
135
        path = self.translate_path(self.path)
 
136
        if os.path.isdir(path) and not self.path.endswith('/'):
 
137
            # redirect browser - doing basically what apache does when
 
138
            # DirectorySlash option is On which is quite common (braindead, but
 
139
            # common)
 
140
            self.send_response(301)
 
141
            self.send_header("Location", self.path + "/")
 
142
            # Indicates that the body is empty for HTTP/1.1 clients 
 
143
            self.send_header('Content-Length', '0')
 
144
            self.end_headers()
 
145
            return None
 
146
 
 
147
        return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
 
148
 
130
149
    def send_range_content(self, file, start, length):
131
150
        file.seek(start)
132
151
        self.wfile.write(file.read(length))
147
166
    def get_multiple_ranges(self, file, file_size, ranges):
148
167
        self.send_response(206)
149
168
        self.send_header('Accept-Ranges', 'bytes')
150
 
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
151
 
        self.send_header("Content-Type",
152
 
                         "multipart/byteranges; boundary=%s" % boundary)
 
169
        boundary = '%d' % random.randint(0,0x7FFFFFFF)
 
170
        self.send_header('Content-Type',
 
171
                         'multipart/byteranges; boundary=%s' % boundary)
 
172
        boundary_line = '--%s\r\n' % boundary
 
173
        # Calculate the Content-Length
 
174
        content_length = 0
 
175
        for (start, end) in ranges:
 
176
            content_length += len(boundary_line)
 
177
            content_length += self._header_line_length(
 
178
                'Content-type', 'application/octet-stream')
 
179
            content_length += self._header_line_length(
 
180
                'Content-Range', 'bytes %d-%d/%d' % (start, end, file_size))
 
181
            content_length += len('\r\n') # end headers
 
182
            content_length += end - start # + 1
 
183
        content_length += len(boundary_line)
 
184
        self.send_header('Content-length', content_length)
153
185
        self.end_headers()
 
186
 
 
187
        # Send the multipart body
154
188
        for (start, end) in ranges:
155
 
            self.wfile.write("--%s\r\n" % boundary)
156
 
            self.send_header("Content-type", 'application/octet-stream')
157
 
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
158
 
                                                                  end,
159
 
                                                                  file_size))
 
189
            self.wfile.write(boundary_line)
 
190
            self.send_header('Content-type', 'application/octet-stream')
 
191
            self.send_header('Content-Range', 'bytes %d-%d/%d'
 
192
                             % (start, end, file_size))
160
193
            self.end_headers()
161
194
            self.send_range_content(file, start, end - start + 1)
162
 
            self.wfile.write("--%s\r\n" % boundary)
 
195
        # Final boundary
 
196
        self.wfile.write(boundary_line)
163
197
 
164
198
    def do_GET(self):
165
199
        """Serve a GET request.
166
200
 
167
201
        Handles the Range header.
168
202
        """
 
203
        # Update statistics
 
204
        self.server.test_case_server.GET_request_nb += 1
169
205
 
170
206
        path = self.translate_path(self.path)
171
207
        ranges_header_value = self.headers.get('Range')
172
208
        if ranges_header_value is None or os.path.isdir(path):
173
209
            # Let the mother class handle most cases
174
 
            return SimpleHTTPRequestHandler.do_GET(self)
 
210
            return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
175
211
 
176
212
        try:
177
213
            # Always read in binary mode. Opening files in text
248
284
        return self._translate_path(path)
249
285
 
250
286
    def _translate_path(self, path):
251
 
        return SimpleHTTPRequestHandler.translate_path(self, path)
 
287
        return SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(
 
288
            self, path)
252
289
 
253
290
    if sys.platform == 'win32':
254
291
        # On win32 you cannot access non-ascii filenames without
279
316
            return path
280
317
 
281
318
 
282
 
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
 
319
class TestingHTTPServerMixin:
283
320
 
284
 
    def __init__(self, server_address, RequestHandlerClass,
285
 
                 test_case_server):
286
 
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
287
 
                                           RequestHandlerClass)
 
321
    def __init__(self, test_case_server):
288
322
        # test_case_server can be used to communicate between the
289
323
        # tests and the server (or the request handler and the
290
324
        # server), allowing dynamic behaviors to be defined from
291
325
        # the tests cases.
292
326
        self.test_case_server = test_case_server
293
327
 
294
 
 
295
 
class HttpServer(Server):
 
328
    def tearDown(self):
 
329
         """Called to clean-up the server.
 
330
 
 
331
         Since the server may be (surely is, even) in a blocking listen, we
 
332
         shutdown its socket before closing it.
 
333
         """
 
334
         # Note that is this executed as part of the implicit tear down in the
 
335
         # main thread while the server runs in its own thread. The clean way
 
336
         # to tear down the server is to instruct him to stop accepting
 
337
         # connections and wait for the current connection(s) to end
 
338
         # naturally. To end the connection naturally, the http transports
 
339
         # should close their socket when they do not need to talk to the
 
340
         # server anymore. This happens naturally during the garbage collection
 
341
         # phase of the test transport objetcs (the server clients), so we
 
342
         # don't have to worry about them.  So, for the server, we must tear
 
343
         # down here, from the main thread, when the test have ended.  Note
 
344
         # that since the server is in a blocking operation and since python
 
345
         # use select internally, shutting down the socket is reliable and
 
346
         # relatively clean.
 
347
         try:
 
348
             self.socket.shutdown(socket.SHUT_RDWR)
 
349
         except socket.error, e:
 
350
             # WSAENOTCONN (10057) 'Socket is not connected' is harmless on
 
351
             # windows (occurs before the first connection attempt
 
352
             # vila--20071230)
 
353
             if not len(e.args) or e.args[0] != 10057:
 
354
                 raise
 
355
         # Let the server properly close the socket
 
356
         self.server_close()
 
357
 
 
358
class TestingHTTPServer(SocketServer.TCPServer, TestingHTTPServerMixin):
 
359
 
 
360
    def __init__(self, server_address, request_handler_class,
 
361
                 test_case_server):
 
362
        TestingHTTPServerMixin.__init__(self, test_case_server)
 
363
        SocketServer.TCPServer.__init__(self, server_address,
 
364
                                        request_handler_class)
 
365
 
 
366
 
 
367
class TestingThreadingHTTPServer(SocketServer.ThreadingTCPServer,
 
368
                                 TestingHTTPServerMixin):
 
369
    """A threading HTTP test server for HTTP 1.1.
 
370
 
 
371
    Since tests can initiate several concurrent connections to the same http
 
372
    server, we need an independent connection for each of them. We achieve that
 
373
    by spawning a new thread for each connection.
 
374
    """
 
375
 
 
376
    def __init__(self, server_address, request_handler_class,
 
377
                 test_case_server):
 
378
        TestingHTTPServerMixin.__init__(self, test_case_server)
 
379
        SocketServer.ThreadingTCPServer.__init__(self, server_address,
 
380
                                                 request_handler_class)
 
381
        # Decides how threads will act upon termination of the main
 
382
        # process. This is prophylactic as we should not leave the threads
 
383
        # lying around.
 
384
        self.daemon_threads = True
 
385
 
 
386
 
 
387
class HttpServer(transport.Server):
296
388
    """A test server for http transports.
297
389
 
298
390
    Subclasses can provide a specific request handler.
299
391
    """
300
392
 
 
393
    # The real servers depending on the protocol
 
394
    http_server_class = {'HTTP/1.0': TestingHTTPServer,
 
395
                         'HTTP/1.1': TestingThreadingHTTPServer,
 
396
                         }
 
397
 
301
398
    # Whether or not we proxy the requests (see
302
399
    # TestingHTTPRequestHandler.translate_path).
303
400
    proxy_requests = False
305
402
    # used to form the url that connects to this server
306
403
    _url_protocol = 'http'
307
404
 
308
 
    # Subclasses can provide a specific request handler
309
 
    def __init__(self, request_handler=TestingHTTPRequestHandler):
310
 
        Server.__init__(self)
 
405
    def __init__(self, request_handler=TestingHTTPRequestHandler,
 
406
                 protocol_version=None):
 
407
        """Constructor.
 
408
 
 
409
        :param request_handler: a class that will be instantiated to handle an
 
410
            http connection (one or several requests).
 
411
 
 
412
        :param protocol_version: if specified, will override the protocol
 
413
            version of the request handler.
 
414
        """
 
415
        transport.Server.__init__(self)
311
416
        self.request_handler = request_handler
312
417
        self.host = 'localhost'
313
418
        self.port = 0
314
419
        self._httpd = None
 
420
        self.protocol_version = protocol_version
 
421
        # Allows tests to verify number of GET requests issued
 
422
        self.GET_request_nb = 0
315
423
 
316
424
    def _get_httpd(self):
317
425
        if self._httpd is None:
318
 
            self._httpd = TestingHTTPServer((self.host, self.port),
319
 
                                            self.request_handler,
320
 
                                            self)
 
426
            rhandler = self.request_handler
 
427
            # Depending on the protocol version, we will create the approriate
 
428
            # server
 
429
            if self.protocol_version is None:
 
430
                # Use the request handler one
 
431
                proto_vers = rhandler.protocol_version
 
432
            else:
 
433
                # Use our own, it will be used to override the request handler
 
434
                # one too.
 
435
                proto_vers = self.protocol_version
 
436
            # Create the appropriate server for the required protocol
 
437
            serv_cls = self.http_server_class.get(proto_vers, None)
 
438
            if serv_cls is None:
 
439
                raise httplib.UnknownProtocol(proto_vers)
 
440
            else:
 
441
                self._httpd = serv_cls((self.host, self.port), rhandler, self)
321
442
            host, self.port = self._httpd.socket.getsockname()
322
443
        return self._httpd
323
444
 
324
445
    def _http_start(self):
325
 
        httpd = self._get_httpd()
326
 
        self._http_base_url = '%s://%s:%s/' % (self._url_protocol,
327
 
                                               self.host,
328
 
                                               self.port)
329
 
        self._http_starting.release()
330
 
        httpd.socket.settimeout(0.1)
 
446
        """Server thread main entry point. """
 
447
        self._http_running = False
 
448
        try:
 
449
            try:
 
450
                httpd = self._get_httpd()
 
451
                self._http_base_url = '%s://%s:%s/' % (self._url_protocol,
 
452
                                                       self.host, self.port)
 
453
                self._http_running = True
 
454
            except:
 
455
                # Whatever goes wrong, we save the exception for the main
 
456
                # thread. Note that since we are running in a thread, no signal
 
457
                # can be received, so we don't care about KeyboardInterrupt.
 
458
                self._http_exception = sys.exc_info()
 
459
        finally:
 
460
            # Release the lock or the main thread will block and the whole
 
461
            # process will hang.
 
462
            self._http_starting.release()
331
463
 
 
464
        # From now on, exceptions are taken care of by the
 
465
        # SocketServer.BaseServer or the request handler.
332
466
        while self._http_running:
333
467
            try:
 
468
                # Really an HTTP connection but the python framework is generic
 
469
                # and call them requests
334
470
                httpd.handle_request()
335
471
            except socket.timeout:
336
472
                pass
361
497
        # XXX: TODO: make the server back onto vfs_server rather than local
362
498
        # disk.
363
499
        assert backing_transport_server is None or \
364
 
            isinstance(backing_transport_server, LocalURLServer), \
 
500
            isinstance(backing_transport_server, local.LocalURLServer), \
365
501
            "HTTPServer currently assumes local transport, got %s" % \
366
502
            backing_transport_server
367
503
        self._home_dir = os.getcwdu()
368
504
        self._local_path_parts = self._home_dir.split(os.path.sep)
 
505
        self._http_base_url = None
 
506
 
 
507
        # Create the server thread
369
508
        self._http_starting = threading.Lock()
370
509
        self._http_starting.acquire()
371
 
        self._http_running = True
372
 
        self._http_base_url = None
373
510
        self._http_thread = threading.Thread(target=self._http_start)
374
511
        self._http_thread.setDaemon(True)
 
512
        self._http_exception = None
375
513
        self._http_thread.start()
 
514
 
376
515
        # Wait for the server thread to start (i.e release the lock)
377
516
        self._http_starting.acquire()
 
517
 
 
518
        if self._http_exception is not None:
 
519
            # Something went wrong during server start
 
520
            exc_class, exc_value, exc_tb = self._http_exception
 
521
            raise exc_class, exc_value, exc_tb
378
522
        self._http_starting.release()
379
523
        self.logs = []
380
524
 
381
525
    def tearDown(self):
382
526
        """See bzrlib.transport.Server.tearDown."""
 
527
        self._httpd.tearDown()
383
528
        self._http_running = False
384
 
        self._http_thread.join()
 
529
        # We don't need to 'self._http_thread.join()' here since the thread is
 
530
        # a daemonic one and will be garbage collected anyway. Joining just
 
531
        # slows us down for no added benefit.
385
532
 
386
533
    def get_url(self):
387
534
        """See bzrlib.transport.Server.get_url."""
391
538
        """See bzrlib.transport.Server.get_bogus_url."""
392
539
        # this is chosen to try to prevent trouble with proxies, weird dns,
393
540
        # etc
394
 
        return 'http://127.0.0.1:1/'
 
541
        return self._url_protocol + '://127.0.0.1:1/'
395
542
 
396
543
 
397
544
class HttpServer_urllib(HttpServer):