1#!/usr/bin/env python
2#
3# Copyright 2012, Google Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are
8# met:
9#
10#     * Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12#     * Redistributions in binary form must reproduce the above
13# copyright notice, this list of conditions and the following disclaimer
14# in the documentation and/or other materials provided with the
15# distribution.
16#     * Neither the name of Google Inc. nor the names of its
17# contributors may be used to endorse or promote products derived from
18# this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
32
33"""Test for end-to-end."""
34
35
36import logging
37import os
38import signal
39import socket
40import subprocess
41import sys
42import time
43import unittest
44
45import set_sys_path  # Update sys.path to locate mod_pywebsocket module.
46
47from test import client_for_testing
48from test import mux_client_for_testing
49
50
51# Special message that tells the echo server to start closing handshake
52_GOODBYE_MESSAGE = 'Goodbye'
53
54# If you want to use external server to run end to end tests, set following
55# parameters correctly.
56_use_external_server = False
57_external_server_port = 0
58
59
60# Test body functions
61def _echo_check_procedure(client):
62    client.connect()
63
64    client.send_message('test')
65    client.assert_receive('test')
66    client.send_message('helloworld')
67    client.assert_receive('helloworld')
68
69    client.send_close()
70    client.assert_receive_close()
71
72    client.assert_connection_closed()
73
74
75def _echo_check_procedure_with_binary(client):
76    client.connect()
77
78    client.send_message('binary', binary=True)
79    client.assert_receive('binary', binary=True)
80    client.send_message('\x00\x80\xfe\xff\x00\x80', binary=True)
81    client.assert_receive('\x00\x80\xfe\xff\x00\x80', binary=True)
82
83    client.send_close()
84    client.assert_receive_close()
85
86    client.assert_connection_closed()
87
88
89def _echo_check_procedure_with_goodbye(client):
90    client.connect()
91
92    client.send_message('test')
93    client.assert_receive('test')
94
95    client.send_message(_GOODBYE_MESSAGE)
96    client.assert_receive(_GOODBYE_MESSAGE)
97
98    client.assert_receive_close()
99    client.send_close()
100
101    client.assert_connection_closed()
102
103
104def _echo_check_procedure_with_code_and_reason(client, code, reason):
105    client.connect()
106
107    client.send_close(code, reason)
108    client.assert_receive_close(code, reason)
109
110    client.assert_connection_closed()
111
112
113def _unmasked_frame_check_procedure(client):
114    client.connect()
115
116    client.send_message('test', mask=False)
117    client.assert_receive_close(client_for_testing.STATUS_PROTOCOL_ERROR, '')
118
119    client.assert_connection_closed()
120
121
122def _mux_echo_check_procedure(mux_client):
123    mux_client.connect()
124    mux_client.send_flow_control(1, 1024)
125
126    logical_channel_options = client_for_testing.ClientOptions()
127    logical_channel_options.server_host = 'localhost'
128    logical_channel_options.server_port = 80
129    logical_channel_options.origin = 'http://localhost'
130    logical_channel_options.resource = '/echo'
131    mux_client.add_channel(2, logical_channel_options)
132    mux_client.send_flow_control(2, 1024)
133
134    mux_client.send_message(2, 'test')
135    mux_client.assert_receive(2, 'test')
136
137    mux_client.add_channel(3, logical_channel_options)
138    mux_client.send_flow_control(3, 1024)
139
140    mux_client.send_message(2, 'hello')
141    mux_client.send_message(3, 'world')
142    mux_client.assert_receive(2, 'hello')
143    mux_client.assert_receive(3, 'world')
144
145    # Don't send close message on channel id 1 so that server-initiated
146    # closing handshake won't occur.
147    mux_client.send_close(2)
148    mux_client.send_close(3)
149    mux_client.assert_receive_close(2)
150    mux_client.assert_receive_close(3)
151
152    mux_client.send_physical_connection_close()
153    mux_client.assert_physical_connection_receive_close()
154
155
156class EndToEndTest(unittest.TestCase):
157    """An end-to-end test that launches pywebsocket standalone server as a
158    separate process, connects to it using the client_for_testing module, and
159    checks if the server behaves correctly by exchanging opening handshake and
160    frames over a TCP connection.
161    """
162
163    def setUp(self):
164        self.server_stderr = None
165        self.top_dir = os.path.join(os.path.split(__file__)[0], '..')
166        os.putenv('PYTHONPATH', os.path.pathsep.join(sys.path))
167        self.standalone_command = os.path.join(
168            self.top_dir, 'mod_pywebsocket', 'standalone.py')
169        self.document_root = os.path.join(self.top_dir, 'example')
170        s = socket.socket()
171        s.bind(('localhost', 0))
172        (_, self.test_port) = s.getsockname()
173        s.close()
174
175        self._options = client_for_testing.ClientOptions()
176        self._options.server_host = 'localhost'
177        self._options.origin = 'http://localhost'
178        self._options.resource = '/echo'
179
180        # TODO(toyoshim): Eliminate launching a standalone server on using
181        # external server.
182
183        if _use_external_server:
184            self._options.server_port = _external_server_port
185        else:
186            self._options.server_port = self.test_port
187
188    def _run_python_command(self, commandline, stdout=None, stderr=None):
189        return subprocess.Popen([sys.executable] + commandline, close_fds=True,
190                                stdout=stdout, stderr=stderr)
191
192    def _run_server(self, allow_draft75=False):
193        args = [self.standalone_command,
194                '-H', 'localhost',
195                '-V', 'localhost',
196                '-p', str(self.test_port),
197                '-P', str(self.test_port),
198                '-d', self.document_root]
199
200        # Inherit the level set to the root logger by test runner.
201        root_logger = logging.getLogger()
202        log_level = root_logger.getEffectiveLevel()
203        if log_level != logging.NOTSET:
204            args.append('--log-level')
205            args.append(logging.getLevelName(log_level).lower())
206
207        if allow_draft75:
208            args.append('--allow-draft75')
209
210        return self._run_python_command(args,
211                                        stderr=self.server_stderr)
212
213    def _kill_process(self, pid):
214        if sys.platform in ('win32', 'cygwin'):
215            subprocess.call(
216                ('taskkill.exe', '/f', '/pid', str(pid)), close_fds=True)
217        else:
218            os.kill(pid, signal.SIGKILL)
219
220    def _run_hybi_test_with_client_options(self, test_function, options):
221        server = self._run_server()
222        try:
223            # TODO(tyoshino): add some logic to poll the server until it
224            # becomes ready
225            time.sleep(0.2)
226
227            client = client_for_testing.create_client(options)
228            try:
229                test_function(client)
230            finally:
231                client.close_socket()
232        finally:
233            self._kill_process(server.pid)
234
235    def _run_hybi_test(self, test_function):
236        self._run_hybi_test_with_client_options(test_function, self._options)
237
238    def _run_hybi_deflate_test(self, test_function):
239        server = self._run_server()
240        try:
241            time.sleep(0.2)
242
243            self._options.enable_deflate_stream()
244            client = client_for_testing.create_client(self._options)
245            try:
246                test_function(client)
247            finally:
248                client.close_socket()
249        finally:
250            self._kill_process(server.pid)
251
252    def _run_hybi_deflate_frame_test(self, test_function):
253        server = self._run_server()
254        try:
255            time.sleep(0.2)
256
257            self._options.enable_deflate_frame()
258            client = client_for_testing.create_client(self._options)
259            try:
260                test_function(client)
261            finally:
262                client.close_socket()
263        finally:
264            self._kill_process(server.pid)
265
266    def _run_hybi_close_with_code_and_reason_test(self, test_function, code,
267                                                  reason):
268        server = self._run_server()
269        try:
270            time.sleep(0.2)
271
272            client = client_for_testing.create_client(self._options)
273            try:
274                test_function(client, code, reason)
275            finally:
276                client.close_socket()
277        finally:
278            self._kill_process(server.pid)
279
280    def _run_hybi_http_fallback_test(self, options, status):
281        server = self._run_server()
282        try:
283            time.sleep(0.2)
284
285            client = client_for_testing.create_client(options)
286            try:
287                client.connect()
288                self.fail('Could not catch HttpStatusException')
289            except client_for_testing.HttpStatusException, e:
290                self.assertEqual(status, e.status)
291            except Exception, e:
292                self.fail('Catch unexpected exception')
293            finally:
294                client.close_socket()
295        finally:
296            self._kill_process(server.pid)
297
298    def _run_hybi_mux_test(self, test_function):
299        server = self._run_server()
300        try:
301            time.sleep(0.2)
302
303            client = mux_client_for_testing.MuxClient(self._options)
304            try:
305                test_function(client)
306            finally:
307                client.close_socket()
308        finally:
309            self._kill_process(server.pid)
310
311    def test_echo(self):
312        self._run_hybi_test(_echo_check_procedure)
313
314    def test_echo_binary(self):
315        self._run_hybi_test(_echo_check_procedure_with_binary)
316
317    def test_echo_server_close(self):
318        self._run_hybi_test(_echo_check_procedure_with_goodbye)
319
320    def test_unmasked_frame(self):
321        self._run_hybi_test(_unmasked_frame_check_procedure)
322
323    def test_echo_deflate(self):
324        self._run_hybi_deflate_test(_echo_check_procedure)
325
326    def test_echo_deflate_server_close(self):
327        self._run_hybi_deflate_test(_echo_check_procedure_with_goodbye)
328
329    def test_echo_deflate_frame(self):
330        self._run_hybi_deflate_frame_test(_echo_check_procedure)
331
332    def test_echo_deflate_frame_server_close(self):
333        self._run_hybi_deflate_frame_test(
334            _echo_check_procedure_with_goodbye)
335
336    def test_echo_close_with_code_and_reason(self):
337        self._options.resource = '/close'
338        self._run_hybi_close_with_code_and_reason_test(
339            _echo_check_procedure_with_code_and_reason, 3333, 'sunsunsunsun')
340
341    def test_echo_close_with_empty_body(self):
342        self._options.resource = '/close'
343        self._run_hybi_close_with_code_and_reason_test(
344            _echo_check_procedure_with_code_and_reason, None, '')
345
346    def test_mux_echo(self):
347        self._run_hybi_mux_test(_mux_echo_check_procedure)
348
349    def test_close_on_protocol_error(self):
350        """Tests that the server sends a close frame with protocol error status
351        code when the client sends data with some protocol error.
352        """
353
354        def test_function(client):
355            client.connect()
356
357            # Intermediate frame without any preceding start of fragmentation
358            # frame.
359            client.send_frame_of_arbitrary_bytes('\x80\x80', '')
360            client.assert_receive_close(
361                client_for_testing.STATUS_PROTOCOL_ERROR)
362
363        self._run_hybi_test(test_function)
364
365    def test_close_on_unsupported_frame(self):
366        """Tests that the server sends a close frame with unsupported operation
367        status code when the client sends data asking some operation that is
368        not supported by the server.
369        """
370
371        def test_function(client):
372            client.connect()
373
374            # Text frame with RSV3 bit raised.
375            client.send_frame_of_arbitrary_bytes('\x91\x80', '')
376            client.assert_receive_close(
377                client_for_testing.STATUS_UNSUPPORTED_DATA)
378
379        self._run_hybi_test(test_function)
380
381    def test_close_on_invalid_frame(self):
382        """Tests that the server sends a close frame with invalid frame payload
383        data status code when the client sends an invalid frame like containing
384        invalid UTF-8 character.
385        """
386
387        def test_function(client):
388            client.connect()
389
390            # Text frame with invalid UTF-8 string.
391            client.send_message('\x80', raw=True)
392            client.assert_receive_close(
393                client_for_testing.STATUS_INVALID_FRAME_PAYLOAD_DATA)
394
395        self._run_hybi_test(test_function)
396
397    def _run_hybi00_test(self, test_function):
398        server = self._run_server()
399        try:
400            time.sleep(0.2)
401
402            client = client_for_testing.create_client_hybi00(self._options)
403            try:
404                test_function(client)
405            finally:
406                client.close_socket()
407        finally:
408            self._kill_process(server.pid)
409
410    def test_echo_hybi00(self):
411        self._run_hybi00_test(_echo_check_procedure)
412
413    def test_echo_server_close_hybi00(self):
414        self._run_hybi00_test(_echo_check_procedure_with_goodbye)
415
416    def _run_hixie75_test(self, test_function):
417        server = self._run_server(allow_draft75=True)
418        try:
419            time.sleep(0.2)
420
421            client = client_for_testing.create_client_hixie75(self._options)
422            try:
423                test_function(client)
424            finally:
425                client.close_socket()
426        finally:
427            self._kill_process(server.pid)
428
429    def test_echo_hixie75(self):
430        """Tests that the server can talk draft-hixie-thewebsocketprotocol-75
431        protocol.
432        """
433
434        def test_function(client):
435            client.connect()
436
437            client.send_message('test')
438            client.assert_receive('test')
439
440        self._run_hixie75_test(test_function)
441
442    def test_echo_server_close_hixie75(self):
443        """Tests that the server can talk draft-hixie-thewebsocketprotocol-75
444        protocol. At the end of message exchanging, the client sends a keyword
445        message that requests the server to close the connection, and then
446        checks if the connection is really closed.
447        """
448
449        def test_function(client):
450            client.connect()
451
452            client.send_message('test')
453            client.assert_receive('test')
454
455            client.send_message(_GOODBYE_MESSAGE)
456            client.assert_receive(_GOODBYE_MESSAGE)
457
458        self._run_hixie75_test(test_function)
459
460    # TODO(toyoshim): Add tests to verify invalid absolute uri handling like
461    # host unmatch, port unmatch and invalid port description (':' without port
462    # number).
463
464    def test_absolute_uri(self):
465        """Tests absolute uri request."""
466
467        options = self._options
468        options.resource = 'ws://localhost:%d/echo' % options.server_port
469        self._run_hybi_test_with_client_options(_echo_check_procedure, options)
470
471    def test_origin_check(self):
472        """Tests http fallback on origin check fail."""
473
474        options = self._options
475        options.resource = '/origin_check'
476        # Server shows warning message for http 403 fallback. This warning
477        # message is confusing. Following pipe disposes warning messages.
478        self.server_stderr = subprocess.PIPE
479        self._run_hybi_http_fallback_test(options, 403)
480
481    def test_version_check(self):
482        """Tests http fallback on version check fail."""
483
484        options = self._options
485        options.version = 99
486        self.server_stderr = subprocess.PIPE
487        self._run_hybi_http_fallback_test(options, 400)
488
489    def _check_example_echo_client_result(
490        self, expected, stdoutdata, stderrdata):
491        actual = stdoutdata.decode("utf-8")
492        if actual != expected:
493            raise Exception('Unexpected result on example echo client: '
494                            '%r (expected) vs %r (actual)' %
495                            (expected, actual))
496        if stderrdata is not None:
497            raise Exception('Unexpected error message on example echo '
498                            'client: %r' % stderrdata)
499
500    def test_example_echo_client(self):
501        """Tests that the echo_client.py example can talk with the server."""
502
503        server = self._run_server()
504        try:
505            time.sleep(0.2)
506
507            client_command = os.path.join(
508                self.top_dir, 'example', 'echo_client.py')
509
510            args = [client_command,
511                    '-p', str(self._options.server_port)]
512            client = self._run_python_command(args, stdout=subprocess.PIPE)
513            stdoutdata, stderrdata = client.communicate()
514            expected = ('Send: Hello\n' 'Recv: Hello\n'
515                u'Send: \u65e5\u672c\n' u'Recv: \u65e5\u672c\n'
516                'Send close\n' 'Recv ack\n')
517            self._check_example_echo_client_result(
518                expected, stdoutdata, stderrdata)
519
520            # Process a big message for which extended payload length is used.
521            # To handle extended payload length, ws_version attribute will be
522            # accessed. This test checks that ws_version is correctly set.
523            big_message = 'a' * 1024
524            args = [client_command,
525                    '-p', str(self._options.server_port),
526                    '-m', big_message]
527            client = self._run_python_command(args, stdout=subprocess.PIPE)
528            stdoutdata, stderrdata = client.communicate()
529            expected = ('Send: %s\nRecv: %s\nSend close\nRecv ack\n' %
530                        (big_message, big_message))
531            self._check_example_echo_client_result(
532                expected, stdoutdata, stderrdata)
533        finally:
534            self._kill_process(server.pid)
535
536
537if __name__ == '__main__':
538    unittest.main()
539
540
541# vi:sts=4 sw=4 et
542