#!/usr/bin/env python # # Copyright 2009, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Web Socket Echo client. This is an example Web Socket client that talks with echo_wsh.py. This may be useful for checking mod_pywebsocket installation. Note: This code is far from robust, e.g., we cut corners in handshake. """ import codecs from optparse import OptionParser import socket import sys _TIMEOUT_SEC = 10 _DEFAULT_PORT = 80 _DEFAULT_SECURE_PORT = 443 _UNDEFINED_PORT = -1 _UPGRADE_HEADER = 'Upgrade: WebSocket\r\n' _CONNECTION_HEADER = 'Connection: Upgrade\r\n' _EXPECTED_RESPONSE = ( 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' + _UPGRADE_HEADER + _CONNECTION_HEADER) _GOODBYE_MESSAGE = 'Goodbye' def _method_line(resource): return 'GET %s HTTP/1.1\r\n' % resource def _origin_header(origin): return 'Origin: %s\r\n' % origin class _TLSSocket(object): """Wrapper for a TLS connection.""" def __init__(self, raw_socket): self._ssl = socket.ssl(raw_socket) def send(self, bytes): return self._ssl.write(bytes) def recv(self, size=-1): return self._ssl.read(size) def close(self): # Nothing to do. pass class EchoClient(object): """Web Socket echo client.""" def __init__(self, options): self._options = options self._socket = None def run(self): """Run the client. Shake hands and then repeat sending message and receiving its echo. """ self._socket = socket.socket() self._socket.settimeout(self._options.socket_timeout) try: self._socket.connect((self._options.server_host, self._options.server_port)) if self._options.use_tls: self._socket = _TLSSocket(self._socket) self._handshake() for line in self._options.message.split(',') + [_GOODBYE_MESSAGE]: frame = '\x00' + line.encode('utf-8') + '\xff' self._socket.send(frame) if self._options.verbose: print 'Send: %s' % line received = self._socket.recv(len(frame)) if received != frame: raise Exception('Incorrect echo: %r' % received) if self._options.verbose: print 'Recv: %s' % received[1:-1].decode('utf-8', 'replace') finally: self._socket.close() def _handshake(self): self._socket.send(_method_line(self._options.resource)) self._socket.send(_UPGRADE_HEADER) self._socket.send(_CONNECTION_HEADER) self._socket.send(self._format_host_header()) self._socket.send(_origin_header(self._options.origin)) self._socket.send('\r\n') for expected_char in _EXPECTED_RESPONSE: received = self._socket.recv(1)[0] if expected_char != received: raise Exception('Handshake failure') # We cut corners and skip other headers. self._skip_headers() def _skip_headers(self): terminator = '\r\n\r\n' pos = 0 while pos < len(terminator): received = self._socket.recv(1)[0] if received == terminator[pos]: pos += 1 elif received == terminator[0]: pos = 1 else: pos = 0 def _format_host_header(self): host = 'Host: ' + self._options.server_host if ((not self._options.use_tls and self._options.server_port != _DEFAULT_PORT) or (self._options.use_tls and self._options.server_port != _DEFAULT_SECURE_PORT)): host += ':' + str(self._options.server_port) host += '\r\n' return host def main(): sys.stdout = codecs.getwriter('utf-8')(sys.stdout) parser = OptionParser() parser.add_option('-s', '--server_host', dest='server_host', type='string', default='localhost', help='server host') parser.add_option('-p', '--server_port', dest='server_port', type='int', default=_UNDEFINED_PORT, help='server port') parser.add_option('-o', '--origin', dest='origin', type='string', default='http://localhost/', help='origin') parser.add_option('-r', '--resource', dest='resource', type='string', default='/echo', help='resource path') parser.add_option('-m', '--message', dest='message', type='string', help=('comma-separated messages to send excluding "%s" ' 'that is always sent at the end' % _GOODBYE_MESSAGE)) parser.add_option('-q', '--quiet', dest='verbose', action='store_false', default=True, help='suppress messages') parser.add_option('-t', '--tls', dest='use_tls', action='store_true', default=False, help='use TLS (wss://)') parser.add_option('-k', '--socket_timeout', dest='socket_timeout', type='int', default=_TIMEOUT_SEC, help='Timeout(sec) for sockets') (options, unused_args) = parser.parse_args() # Default port number depends on whether TLS is used. if options.server_port == _UNDEFINED_PORT: if options.use_tls: options.server_port = _DEFAULT_SECURE_PORT else: options.server_port = _DEFAULT_PORT # optparse doesn't seem to handle non-ascii default values. # Set default message here. if not options.message: options.message = u'Hello,\u65e5\u672c' # "Japan" in Japanese EchoClient(options).run() if __name__ == '__main__': main() # vi:sts=4 sw=4 et