wsgi_test.py 14.7 KB
Newer Older
Denis Bilenko's avatar
Denis Bilenko committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
# @author Donovan Preston
#
# Copyright (c) 2007, Linden Research, Inc.
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from gevent import monkey
22
monkey.patch_all(thread=False)
Denis Bilenko's avatar
Denis Bilenko committed
23

24
import sys
Denis Bilenko's avatar
Denis Bilenko committed
25 26 27 28
import cgi
import os
import urllib2

29 30
import greentest

Denis Bilenko's avatar
Denis Bilenko committed
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
import gevent
from gevent import wsgi
from gevent import socket


try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO


def hello_world(env, start_response):
    if env['PATH_INFO'] == 'notexist':
        start_response('404 Not Found', [('Content-type', 'text/plain')])
        return ["not found"]

    start_response('200 OK', [('Content-type', 'text/plain')])
    return ["hello world"]


51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
def hello_world_explicit_content_length(env, start_response):
    if env['PATH_INFO'] == 'notexist':
        msg = 'not found'
        start_response('404 Not Found',
                       [('Content-type', 'text/plain'),
                        ('Content-Length', len(msg))])
        return [msg]

    msg = 'hello world'
    start_response('200 OK',
                   [('Content-type', 'text/plain'),
                    ('Content-Length', len(msg))])
    return [msg]


66 67 68 69 70 71 72 73 74
def hello_world_yield(env, start_response):
    if env['PATH_INFO'] == 'notexist':
        start_response('404 Not Found', [('Content-type', 'text/plain')])
        yield "not found"
    else:
        start_response('200 OK', [('Content-type', 'text/plain')])
        yield "hello world"


Denis Bilenko's avatar
Denis Bilenko committed
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
def chunked_app(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    yield "this"
    yield "is"
    yield "chunked"


def big_chunks(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    line = 'a' * 8192
    for x in range(10):
        yield line

def use_write(env, start_response):
    if env['PATH_INFO'] == '/a':
        write = start_response('200 OK', [('Content-type', 'text/plain'),
                                          ('Content-Length', '5')])
        write('abcde')
    if env['PATH_INFO'] == '/b':
        write = start_response('200 OK', [('Content-type', 'text/plain')])
        write('abcde')
    return []

def chunked_post(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    if env['PATH_INFO'] == '/a':
        return [env['wsgi.input'].read()]
    elif env['PATH_INFO'] == '/b':
        return [x for x in iter(lambda: env['wsgi.input'].read(4096), '')]
    elif env['PATH_INFO'] == '/c':
        return [x for x in iter(lambda: env['wsgi.input'].read(1), '')]

Denis Bilenko's avatar
Denis Bilenko committed
107

Denis Bilenko's avatar
Denis Bilenko committed
108
class Site(object):
109 110 111

    def __init__(self, application):
        self.application = application
Denis Bilenko's avatar
Denis Bilenko committed
112 113 114 115 116 117 118 119 120 121 122 123

    def __call__(self, env, start_response):
        return self.application(env, start_response)


CONTENT_LENGTH = 'content-length'


class ConnectionClosed(Exception):
    pass


124
def read_headers(fd):
Denis Bilenko's avatar
Denis Bilenko committed
125 126 127
    response_line = fd.readline()
    if not response_line:
        raise ConnectionClosed
128 129 130 131 132 133 134 135
    headers = {}
    while True:
        line = fd.readline().strip()
        if not line:
            break
        try:
            key, value = line.split(': ', 1)
        except:
136
            print 'Failed to split: %r' % (line, )
137
            raise
138 139
        key = key.lower()
        assert key not in headers, 'Header %r is sent more than once' % key
Denis Bilenko's avatar
Denis Bilenko committed
140
        headers[key.lower()] = value
141 142 143
    return response_line, headers


144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
def iread_chunks(fd):
    while True:
        chunk_size = fd.readline().strip()
        try:
            chunk_size = int(chunk_size, 16)
        except:
            print 'Failed to parse chunk size: %r' % chunk_size
            raise
        if chunk_size == 0:
            crlf = fd.read(2)
            assert crlf == '\r\n', repr(crlf)
            break
        data = fd.read(chunk_size)
        yield data
        crlf = fd.read(2)
        assert crlf == '\r\n', repr(crlf)


162 163
def read_http(fd):
    response_line, headers = read_headers(fd)
Denis Bilenko's avatar
Denis Bilenko committed
164 165 166 167 168

    if CONTENT_LENGTH in headers:
        num = int(headers[CONTENT_LENGTH])
        body = fd.read(num)
        #print body
169 170
    elif 'chunked' in headers.get('transfer-encoding', ''):
        body = ''.join(iread_chunks(fd))
Denis Bilenko's avatar
Denis Bilenko committed
171 172 173 174 175 176
    else:
        body = None

    return response_line, headers, body


177 178 179 180 181
class TestCase(greentest.TestCase):

    def listen(self):
        return socket.tcp_listener(('0.0.0.0', 0))

Denis Bilenko's avatar
Denis Bilenko committed
182
    def setUp(self):
183 184 185 186
        self.logfile = sys.stderr # StringIO()
        self.site = Site(self.application)
        listener = self.listen()
        self.port = listener.getsockname()[1]
Denis Bilenko's avatar
Denis Bilenko committed
187
        self.server = gevent.spawn(
188
            wsgi.server, listener, self.site, max_size=128, log=self.logfile)
Denis Bilenko's avatar
Denis Bilenko committed
189 190

    def tearDown(self):
Denis Bilenko's avatar
Denis Bilenko committed
191
        self.server.kill(block=True)
192 193 194 195 196 197 198
        # XXX server should have 'close' method which closes everything reliably
        # XXX currently listening socket is kept open


class TestHttpdBasic(TestCase):

    application = staticmethod(hello_world)
Denis Bilenko's avatar
Denis Bilenko committed
199 200

    def test_001_server(self):
201
        sock = socket.connect_tcp(('127.0.0.1', self.port))
202 203 204
        sock.sendall('GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
        result = sock.makefile().read()
        sock.close()
Denis Bilenko's avatar
Denis Bilenko committed
205 206 207 208 209
        ## The server responds with the maximum version it supports
        self.assert_(result.startswith('HTTP'), result)
        self.assert_(result.endswith('hello world'))

    def test_002_keepalive(self):
210
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
211
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
212
        read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
213
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
214
        read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
215 216 217 218
        fd.close()

    def test_003_passing_non_int_to_read(self):
        # This should go in greenio_test
219
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
220
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
221
        cancel = gevent.Timeout.start_new(1, RuntimeError)
Denis Bilenko's avatar
Denis Bilenko committed
222 223 224 225 226
        self.assertRaises(TypeError, fd.read, "This shouldn't work")
        cancel.cancel()
        fd.close()

    def test_004_close_keepalive(self):
227
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
228
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
229
        read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
230
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
231
        read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
232
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
233
        self.assertRaises(ConnectionClosed, read_http, fd)
Denis Bilenko's avatar
Denis Bilenko committed
234 235 236
        fd.close()

    def skip_test_005_run_apachebench(self):
237
        url = 'http://localhost:%s/' % self.port
Denis Bilenko's avatar
Denis Bilenko committed
238 239
        # ab is apachebench
        from gevent import processes
240
        out = processes.Process(greentest.find_command('ab'),
Denis Bilenko's avatar
Denis Bilenko committed
241 242 243 244
                                ['-c','64','-n','1024', '-k', url])
        print out.read()

    def test_006_reject_long_urls(self):
245
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
246 247 248 249 250 251 252 253 254 255 256
        path_parts = []
        for ii in range(3000):
            path_parts.append('path')
        path = '/'.join(path_parts)
        request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
        fd.write(request)
        result = fd.readline()
        status = result.split(' ')[1]
        self.assertEqual(status, '414')
        fd.close()

257 258 259
    def test_008_correctresponse(self):
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
Denis Bilenko's avatar
Denis Bilenko committed
260
        response_line_200, _, _ = read_http(fd)
261
        fd.write('GET /notexist HTTP/1.1\r\nHost: localhost\r\n\r\n')
Denis Bilenko's avatar
Denis Bilenko committed
262
        response_line_404, _, _ = read_http(fd)
263
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
Denis Bilenko's avatar
Denis Bilenko committed
264 265
        response_line_test, _, _ = read_http(fd)
        self.assertEqual(response_line_200, response_line_test)
266 267 268
        fd.close()


269 270 271 272 273
class TestExplicitContentLength(TestHttpdBasic):

    application = staticmethod(hello_world_explicit_content_length)


274 275 276 277 278
class TestYield(TestHttpdBasic):

    application = staticmethod(hello_world_yield)


279 280 281 282 283 284 285 286
class TestGetArg(TestCase):

    def application(self, env, start_response):
        body = env['wsgi.input'].read()
        a = cgi.parse_qs(body).get('a', [1])[0]
        start_response('200 OK', [('Content-type', 'text/plain')])
        return ['a is %s, body is %s' % (a, body)]

Denis Bilenko's avatar
Denis Bilenko committed
287 288
    def test_007_get_arg(self):
        # define a new handler that does a get_arg as well as a read_body
289
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
290 291 292 293 294 295 296 297 298 299
        request = '\r\n'.join((
            'POST / HTTP/1.0',
            'Host: localhost',
            'Content-Length: 3',
            '',
            'a=a'))
        fd.write(request)

        # send some junk after the actual request
        fd.write('01234567890123456789')
300
        reqline, headers, body = read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
301 302 303
        self.assertEqual(body, 'a is a, body is a=a')
        fd.close()

304 305 306 307

class TestChunkedApp(TestCase):

    application = staticmethod(chunked_app)
Denis Bilenko's avatar
Denis Bilenko committed
308 309

    def test_009_chunked_response(self):
310
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
311 312 313 314
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
        self.assert_('Transfer-Encoding: chunked' in fd.read())

    def test_010_no_chunked_http_1_0(self):
315
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
316 317 318
        fd.write('GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n')
        self.assert_('Transfer-Encoding: chunked' not in fd.read())

319 320 321 322 323

class TestBigChunks(TestCase):
        
    application = staticmethod(big_chunks)

Denis Bilenko's avatar
Denis Bilenko committed
324
    def test_011_multiple_chunks(self):
325
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
326
        fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
327 328
        _, headers = read_headers(fd)
        assert ('transfer-encoding', 'chunked') in headers.items(), headers
Denis Bilenko's avatar
Denis Bilenko committed
329 330 331 332 333 334 335 336 337
        chunks = 0
        chunklen = int(fd.readline(), 16)
        while chunklen:
            chunks += 1
            chunk = fd.read(chunklen)
            fd.readline()
            chunklen = int(fd.readline(), 16)
        self.assert_(chunks > 1)

338 339 340 341 342

class TestChunkedPost(TestCase):

    application = staticmethod(chunked_post)

Denis Bilenko's avatar
Denis Bilenko committed
343
    def test_014_chunked_post(self):
344
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
345 346 347
        fd.write('PUT /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
                 'Transfer-Encoding: chunked\r\n\r\n'
                 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n')
348
        read_headers(fd)
Denis Bilenko's avatar
Denis Bilenko committed
349 350 351
        response = fd.read()
        self.assert_(response == 'oh hai', 'invalid response %s' % response)

352
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
353 354 355
        fd.write('PUT /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
                 'Transfer-Encoding: chunked\r\n\r\n'
                 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n')
356
        read_headers(fd)
Denis Bilenko's avatar
Denis Bilenko committed
357 358 359
        response = fd.read()
        self.assert_(response == 'oh hai', 'invalid response %s' % response)

360
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
361 362 363
        fd.write('PUT /c HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
                 'Transfer-Encoding: chunked\r\n\r\n'
                 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n')
364 365
        #fd.readuntil('\r\n\r\n')
        read_headers(fd)
Denis Bilenko's avatar
Denis Bilenko committed
366 367 368
        response = fd.read(8192)
        self.assert_(response == 'oh hai', 'invalid response %s' % response)

369 370 371 372 373

class TestUseWrite(TestCase):

    application = staticmethod(use_write)

Denis Bilenko's avatar
Denis Bilenko committed
374
    def test_015_write(self):
375
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
376
        fd.write('GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
377
        response_line, headers, body = read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
378 379
        self.assert_('content-length' in headers)

380
        fd = socket.connect_tcp(('127.0.0.1', self.port)).makefile(bufsize=1)
Denis Bilenko's avatar
Denis Bilenko committed
381
        fd.write('GET /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
382
        response_line, headers, body = read_http(fd)
Denis Bilenko's avatar
Denis Bilenko committed
383 384 385 386
        self.assert_('transfer-encoding' in headers)
        self.assert_(headers['transfer-encoding'] == 'chunked')


387 388 389 390 391
class TestHttps(greentest.TestCase):

    def application(self, environ, start_response):
        start_response('200 OK', {})
        return [environ['wsgi.input'].read()]
392 393 394 395 396

    def test_012_ssl_server(self):
        certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
        private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')

397
        sock = socket.ssl_listener(('', 4201), private_key_file, certificate_file)
398

399
        g = gevent.spawn(wsgi.server, sock, self.application)
400 401 402 403 404 405
        try:
            req = HTTPRequest("https://localhost:4201/foo", method="POST", data='abc')
            f = urllib2.urlopen(req)
            result = f.read()
            self.assertEquals(result, 'abc')
        finally:
406
            g.kill(block=True)
407 408 409 410

    def test_013_empty_return(self):
        certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
        private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
411
        sock = socket.ssl_listener(('', 4202), private_key_file, certificate_file)
412
        g = gevent.spawn(wsgi.server, sock, self.application)
413 414 415 416 417 418
        try:
            req = HTTPRequest("https://localhost:4202/foo")
            f = urllib2.urlopen(req)
            result = f.read()
            self.assertEquals(result, '')
        finally:
419
            g.kill(block=True)
420 421


Denis Bilenko's avatar
Denis Bilenko committed
422 423 424 425 426 427 428 429 430 431 432 433 434
class HTTPRequest(urllib2.Request):
    """Hack urllib2.Request to support PUT and DELETE methods."""

    def __init__(self, url, method="GET", data=None, headers={},
                 origin_req_host=None, unverifiable=False):
        urllib2.Request.__init__(self,url,data,headers,origin_req_host,unverifiable)
        self.url = url
        self.method = method

    def get_method(self):
        return self.method

if __name__ == '__main__':
435
    greentest.main()