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 36 import logging 37 import os 38 import signal 39 import socket 40 import subprocess 41 import sys 42 import time 43 import unittest 44 45 import set_sys_path # Update sys.path to locate mod_pywebsocket module. 46 47 from test import client_for_testing 48 from 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 61 def _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 75 def _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 89 def _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 104 def _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 113 def _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 122 def _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 156 class 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 537 if __name__ == '__main__': 538 unittest.main() 539 540 541 # vi:sts=4 sw=4 et 542