3 # Copyright 2009, Google Inc.
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are
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
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.
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.
33 """Web Socket Echo client.
35 This is an example Web Socket client that talks with echo_wsh.py.
36 This may be useful for checking mod_pywebsocket installation.
39 This code is far from robust, e.g., we cut corners in handshake.
44 from optparse import OptionParser
52 _DEFAULT_SECURE_PORT = 443
55 _UPGRADE_HEADER = 'Upgrade: WebSocket\r\n'
56 _CONNECTION_HEADER = 'Connection: Upgrade\r\n'
57 _EXPECTED_RESPONSE = (
58 'HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
62 _GOODBYE_MESSAGE = 'Goodbye'
65 def _method_line(resource):
66 return 'GET %s HTTP/1.1\r\n' % resource
69 def _origin_header(origin):
70 return 'Origin: %s\r\n' % origin
73 class _TLSSocket(object):
74 """Wrapper for a TLS connection."""
76 def __init__(self, raw_socket):
77 self._ssl = socket.ssl(raw_socket)
79 def send(self, bytes):
80 return self._ssl.write(bytes)
82 def recv(self, size=-1):
83 return self._ssl.read(size)
90 class EchoClient(object):
91 """Web Socket echo client."""
93 def __init__(self, options):
94 self._options = options
100 Shake hands and then repeat sending message and receiving its echo.
102 self._socket = socket.socket()
103 self._socket.settimeout(self._options.socket_timeout)
105 self._socket.connect((self._options.server_host,
106 self._options.server_port))
107 if self._options.use_tls:
108 self._socket = _TLSSocket(self._socket)
110 for line in self._options.message.split(',') + [_GOODBYE_MESSAGE]:
111 frame = '\x00' + line.encode('utf-8') + '\xff'
112 self._socket.send(frame)
113 if self._options.verbose:
114 print 'Send: %s' % line
115 received = self._socket.recv(len(frame))
116 if received != frame:
117 raise Exception('Incorrect echo: %r' % received)
118 if self._options.verbose:
119 print 'Recv: %s' % received[1:-1].decode('utf-8',
124 def _handshake(self):
125 self._socket.send(_method_line(self._options.resource))
126 self._socket.send(_UPGRADE_HEADER)
127 self._socket.send(_CONNECTION_HEADER)
128 self._socket.send(self._format_host_header())
129 self._socket.send(_origin_header(self._options.origin))
130 self._socket.send('\r\n')
132 for expected_char in _EXPECTED_RESPONSE:
133 received = self._socket.recv(1)[0]
134 if expected_char != received:
135 raise Exception('Handshake failure')
136 # We cut corners and skip other headers.
139 def _skip_headers(self):
140 terminator = '\r\n\r\n'
142 while pos < len(terminator):
143 received = self._socket.recv(1)[0]
144 if received == terminator[pos]:
146 elif received == terminator[0]:
151 def _format_host_header(self):
152 host = 'Host: ' + self._options.server_host
153 if ((not self._options.use_tls and
154 self._options.server_port != _DEFAULT_PORT) or
155 (self._options.use_tls and
156 self._options.server_port != _DEFAULT_SECURE_PORT)):
157 host += ':' + str(self._options.server_port)
163 sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
165 parser = OptionParser()
166 parser.add_option('-s', '--server_host', dest='server_host', type='string',
167 default='localhost', help='server host')
168 parser.add_option('-p', '--server_port', dest='server_port', type='int',
169 default=_UNDEFINED_PORT, help='server port')
170 parser.add_option('-o', '--origin', dest='origin', type='string',
171 default='http://localhost/', help='origin')
172 parser.add_option('-r', '--resource', dest='resource', type='string',
173 default='/echo', help='resource path')
174 parser.add_option('-m', '--message', dest='message', type='string',
175 help=('comma-separated messages to send excluding "%s" '
176 'that is always sent at the end' %
178 parser.add_option('-q', '--quiet', dest='verbose', action='store_false',
179 default=True, help='suppress messages')
180 parser.add_option('-t', '--tls', dest='use_tls', action='store_true',
181 default=False, help='use TLS (wss://)')
182 parser.add_option('-k', '--socket_timeout', dest='socket_timeout',
183 type='int', default=_TIMEOUT_SEC,
184 help='Timeout(sec) for sockets')
186 (options, unused_args) = parser.parse_args()
188 # Default port number depends on whether TLS is used.
189 if options.server_port == _UNDEFINED_PORT:
191 options.server_port = _DEFAULT_SECURE_PORT
193 options.server_port = _DEFAULT_PORT
195 # optparse doesn't seem to handle non-ascii default values.
196 # Set default message here.
197 if not options.message:
198 options.message = u'Hello,\u65e5\u672c' # "Japan" in Japanese
200 EchoClient(options).run()
203 if __name__ == '__main__':