| #!/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 |