~toddy/bzr/bzr.i18n

« back to all changes in this revision

Viewing changes to bzrlib/tests/http_server.py

  • Committer: Tobias Toedter
  • Date: 2008-02-10 08:01:48 UTC
  • mfrom: (2438.1.783 +trunk)
  • Revision ID: t.toedter@gmx.net-20080210080148-bg5rh61oq2zk2xw3
Merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
64
79
        connection early to avoid polluting the test results.
65
80
        """
66
81
        try:
67
 
            SimpleHTTPRequestHandler.handle_one_request(self)
 
82
            SimpleHTTPServer.SimpleHTTPRequestHandler.handle_one_request(self)
68
83
        except socket.error, e:
69
 
            if (len(e.args) > 0
70
 
                and e.args[0] in (errno.EPIPE, errno.ECONNRESET,
71
 
                                  errno.ECONNABORTED,)):
72
 
                self.close_connection = 1
73
 
                pass
74
 
            else:
 
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)):
75
91
                raise
76
92
 
77
93
    _range_regexp = re.compile(r'^(?P<start>\d+)-(?P<end>\d+)$')
110
126
                    return 0, []
111
127
        return tail, ranges
112
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
 
113
149
    def send_range_content(self, file, start, length):
114
150
        file.seek(start)
115
151
        self.wfile.write(file.read(length))
130
166
    def get_multiple_ranges(self, file, file_size, ranges):
131
167
        self.send_response(206)
132
168
        self.send_header('Accept-Ranges', 'bytes')
133
 
        boundary = "%d" % random.randint(0,0x7FFFFFFF)
134
 
        self.send_header("Content-Type",
135
 
                         "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)
136
185
        self.end_headers()
 
186
 
 
187
        # Send the multipart body
137
188
        for (start, end) in ranges:
138
 
            self.wfile.write("--%s\r\n" % boundary)
139
 
            self.send_header("Content-type", 'application/octet-stream')
140
 
            self.send_header("Content-Range", "bytes %d-%d/%d" % (start,
141
 
                                                                  end,
142
 
                                                                  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))
143
193
            self.end_headers()
144
194
            self.send_range_content(file, start, end - start + 1)
145
195
        # Final boundary
146
 
        self.wfile.write("--%s\r\n" % boundary)
 
196
        self.wfile.write(boundary_line)
147
197
 
148
198
    def do_GET(self):
149
199
        """Serve a GET request.
157
207
        ranges_header_value = self.headers.get('Range')
158
208
        if ranges_header_value is None or os.path.isdir(path):
159
209
            # Let the mother class handle most cases
160
 
            return SimpleHTTPRequestHandler.do_GET(self)
 
210
            return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
161
211
 
162
212
        try:
163
213
            # Always read in binary mode. Opening files in text
234
284
        return self._translate_path(path)
235
285
 
236
286
    def _translate_path(self, path):
237
 
        return SimpleHTTPRequestHandler.translate_path(self, path)
 
287
        return SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(
 
288
            self, path)
238
289
 
239
290
    if sys.platform == 'win32':
240
291
        # On win32 you cannot access non-ascii filenames without
265
316
            return path
266
317
 
267
318
 
268
 
class TestingHTTPServer(BaseHTTPServer.HTTPServer):
 
319
class TestingHTTPServerMixin:
269
320
 
270
 
    def __init__(self, server_address, RequestHandlerClass,
271
 
                 test_case_server):
272
 
        BaseHTTPServer.HTTPServer.__init__(self, server_address,
273
 
                                           RequestHandlerClass)
 
321
    def __init__(self, test_case_server):
274
322
        # test_case_server can be used to communicate between the
275
323
        # tests and the server (or the request handler and the
276
324
        # server), allowing dynamic behaviors to be defined from
277
325
        # the tests cases.
278
326
        self.test_case_server = test_case_server
279
327
 
280
 
    def server_close(self):
281
 
        """Called to clean-up the server.
282
 
 
283
 
        Since the server may be in a blocking read, we shutdown the socket
284
 
        before closing it.
285
 
        """
286
 
        self.socket.shutdown(socket.SHUT_RDWR)
287
 
        BaseHTTPServer.HTTPServer.server_close(self)
288
 
 
289
 
 
290
 
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):
291
388
    """A test server for http transports.
292
389
 
293
390
    Subclasses can provide a specific request handler.
294
391
    """
295
392
 
 
393
    # The real servers depending on the protocol
 
394
    http_server_class = {'HTTP/1.0': TestingHTTPServer,
 
395
                         'HTTP/1.1': TestingThreadingHTTPServer,
 
396
                         }
 
397
 
296
398
    # Whether or not we proxy the requests (see
297
399
    # TestingHTTPRequestHandler.translate_path).
298
400
    proxy_requests = False
300
402
    # used to form the url that connects to this server
301
403
    _url_protocol = 'http'
302
404
 
303
 
    # Subclasses can provide a specific request handler
304
 
    def __init__(self, request_handler=TestingHTTPRequestHandler):
305
 
        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)
306
416
        self.request_handler = request_handler
307
417
        self.host = 'localhost'
308
418
        self.port = 0
309
419
        self._httpd = None
 
420
        self.protocol_version = protocol_version
310
421
        # Allows tests to verify number of GET requests issued
311
422
        self.GET_request_nb = 0
312
423
 
313
424
    def _get_httpd(self):
314
425
        if self._httpd is None:
315
 
            self._httpd = TestingHTTPServer((self.host, self.port),
316
 
                                            self.request_handler,
317
 
                                            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)
318
442
            host, self.port = self._httpd.socket.getsockname()
319
443
        return self._httpd
320
444
 
321
445
    def _http_start(self):
322
 
        httpd = self._get_httpd()
323
 
        self._http_base_url = '%s://%s:%s/' % (self._url_protocol,
324
 
                                               self.host,
325
 
                                               self.port)
326
 
        self._http_starting.release()
 
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()
327
463
 
 
464
        # From now on, exceptions are taken care of by the
 
465
        # SocketServer.BaseServer or the request handler.
328
466
        while self._http_running:
329
467
            try:
 
468
                # Really an HTTP connection but the python framework is generic
 
469
                # and call them requests
330
470
                httpd.handle_request()
331
471
            except socket.timeout:
332
472
                pass
357
497
        # XXX: TODO: make the server back onto vfs_server rather than local
358
498
        # disk.
359
499
        assert backing_transport_server is None or \
360
 
            isinstance(backing_transport_server, LocalURLServer), \
 
500
            isinstance(backing_transport_server, local.LocalURLServer), \
361
501
            "HTTPServer currently assumes local transport, got %s" % \
362
502
            backing_transport_server
363
503
        self._home_dir = os.getcwdu()
364
504
        self._local_path_parts = self._home_dir.split(os.path.sep)
 
505
        self._http_base_url = None
 
506
 
 
507
        # Create the server thread
365
508
        self._http_starting = threading.Lock()
366
509
        self._http_starting.acquire()
367
 
        self._http_running = True
368
 
        self._http_base_url = None
369
510
        self._http_thread = threading.Thread(target=self._http_start)
370
511
        self._http_thread.setDaemon(True)
 
512
        self._http_exception = None
371
513
        self._http_thread.start()
 
514
 
372
515
        # Wait for the server thread to start (i.e release the lock)
373
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
374
522
        self._http_starting.release()
375
523
        self.logs = []
376
524
 
377
525
    def tearDown(self):
378
526
        """See bzrlib.transport.Server.tearDown."""
379
 
        self._httpd.server_close()
 
527
        self._httpd.tearDown()
380
528
        self._http_running = False
381
 
        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.
382
532
 
383
533
    def get_url(self):
384
534
        """See bzrlib.transport.Server.get_url."""
388
538
        """See bzrlib.transport.Server.get_bogus_url."""
389
539
        # this is chosen to try to prevent trouble with proxies, weird dns,
390
540
        # etc
391
 
        return 'http://127.0.0.1:1/'
 
541
        return self._url_protocol + '://127.0.0.1:1/'
392
542
 
393
543
 
394
544
class HttpServer_urllib(HttpServer):