123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- """
- REF:: https://github.com/mtah/python-websocket (+ /.setup/websocket.py.patch)
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>
- """
- import sys, re, socket, asyncore
- if sys.version_info[0] < 3:
- import urlparse as urlparse
- else:
- import urllib.parse as urlparse
- class WebSocket(object):
- def __init__(self, url, **kwargs):
- self.host, self.port, self.resource, self.secure = WebSocket._parse_url(url)
- self.protocol = kwargs.pop('protocol', None)
- self.cookie_jar = kwargs.pop('cookie_jar', None)
- self.onopen = kwargs.pop('onopen', None)
- self.onmessage = kwargs.pop('onmessage', None)
- self.onerror = kwargs.pop('onerror', None)
- self.onclose = kwargs.pop('onclose', None)
- if kwargs: raise ValueError('Unexpected argument(s): %s' % ', '.join(list(kwargs.values())))
- self._dispatcher = _Dispatcher(self)
- def send(self, data):
- self._dispatcher.write('\x00' + _utf8(data) + '\xff')
- def close(self):
- self._dispatcher.handle_close()
- @classmethod
- def _parse_url(cls, url):
- p = urlparse.urlparse(url)
- if p.hostname:
- host = p.hostname
- else:
- raise ValueError('URL must be absolute')
- if p.fragment:
- raise ValueError('URL must not contain a fragment component')
- if p.scheme == 'ws':
- secure = False
- port = p.port or 80
- elif p.scheme == 'wss':
- raise NotImplementedError('Secure WebSocket not yet supported')
- # secure = True
- # port = p.port or 443
- else:
- raise ValueError('Invalid URL scheme')
- resource = p.path or '/'
- if p.query: resource += '?' + p.query
- return (host, port, resource, secure)
- #@classmethod
- #def _generate_key(cls):
- # spaces = random.randint(1, 12)
- # number = random.randint(0, 0xffffffff/spaces)
- # key = list(str(number*spaces))
- # chars = map(unichr, range(0x21, 0x2f) + range(0x3a, 0x7e))
- # random_inserts = random.sample(xrange(len(key)), random.randint(1,12))
- # for (i, c) in [(r, random.choice(chars)) for r in random_inserts]:
- # key.insert(i, c)
- # print key
- # return ''.join(key)
- class WebSocketError(Exception):
- def _init_(self, value):
- self.value = value
- def _str_(self):
- return str(self.value)
- class _Dispatcher(asyncore.dispatcher):
- def __init__(self, ws):
- asyncore.dispatcher.__init__(self)
- self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
- self.connect((ws.host, ws.port))
- self.ws = ws
- self._read_buffer = ''
- self._write_buffer = ''
- self._handshake_complete = False
- if self.ws.port != 80:
- hostport = '%s:%d' % (self.ws.host, self.ws.port)
- else:
- hostport = self.ws.host
- fields = [
- 'Upgrade: WebSocket',
- 'Connection: Upgrade',
- 'Host: ' + hostport,
- 'Origin: http://' + hostport,
- #'Sec-WebSocket-Key1: %s' % WebSocket.generate_key(),
- #'Sec-WebSocket-Key2: %s' % WebSocket.generate_key()
- ]
- if self.ws.protocol: fields['Sec-WebSocket-Protocol'] = self.ws.protocol
- if self.ws.cookie_jar:
- cookies = [c for c in self.ws.cookie_jar if _cookie_for_domain(c, _eff_host(self.ws.host)) and \
- _cookie_for_path(c, self.ws.resource) and \
- not c.is_expired()]
- for cookie in cookies:
- fields.append('Cookie: %s=%s' % (cookie.name, cookie.value))
- # key3 = ''.join(map(unichr, (random.randrange(256) for i in xrange(8))))
- self.write(_utf8('GET %s HTTP/1.1\r\n' \
- '%s\r\n\r\n' % (self.ws.resource,
- '\r\n'.join(fields))))
- # key3)))
- def handl_expt(self):
- self.handle_error()
- def handle_error(self):
- self.close()
- t, e, trace = sys.exc_info()
- if self.ws.onerror:
- self.ws.onerror(e)
- else:
- asyncore.dispatcher.handle_error(self)
- def handle_close(self):
- self.close()
- if self.ws.onclose:
- self.ws.onclose()
- def handle_read(self):
- if self._handshake_complete:
- self._read_until('\xff', self._handle_frame)
- else:
- self._read_until('\r\n\r\n', self._handle_header)
- def handle_write(self):
- sent = self.send(self._write_buffer)
- self._write_buffer = self._write_buffer[sent:]
- def writable(self):
- return len(self._write_buffer) > 0
- def write(self, data):
- self._write_buffer += data # TODO: separate buffer for handshake from data to
- # prevent mix-up when send() is called before
- # handshake is complete?
- def _read_until(self, delimiter, callback):
- def lookForAndHandleCompletedFrame():
- pos = self._read_buffer.find(delimiter)
- if pos >= 0:
- pos += len(delimiter)
- data = self._read_buffer[:pos]
- self._read_buffer = self._read_buffer[pos:]
- if data:
- callback(data)
- lookForAndHandleCompletedFrame()
- self._read_buffer += self.recv(4096)
- lookForAndHandleCompletedFrame()
- def _handle_frame(self, frame):
- assert frame[-1] == '\xff'
- if frame[0] != '\x00':
- raise WebSocketError('WebSocket stream error')
- if self.ws.onmessage:
- self.ws.onmessage(frame[1:-1])
- # TODO: else raise WebSocketError('No message handler defined')
- def _handle_header(self, header):
- assert header[-4:] == '\r\n\r\n'
- start_line, fields = _parse_http_header(header)
- if start_line != 'HTTP/1.1 101 Web Socket Protocol Handshake' or \
- fields.get('Connection', None) != 'Upgrade' or \
- fields.get('Upgrade', None) != 'WebSocket':
- raise WebSocketError('Invalid server handshake')
- self._handshake_complete = True
- if self.ws.onopen:
- self.ws.onopen()
- _IPV4_RE = re.compile(r'\.\d+$')
- _PATH_SEP = re.compile(r'/+')
- def _parse_http_header(header):
- def split_field(field):
- k, v = field.split(':', 1)
- return (k, v.strip())
- lines = header.strip().split('\r\n')
- if len(lines) > 0:
- start_line = lines[0]
- else:
- start_line = None
- return (start_line, dict(map(split_field, lines[1:])))
- def _eff_host(host):
- if host.find('.') == -1 and not _IPV4_RE.search(host):
- return host + '.local'
- return host
- def _cookie_for_path(cookie, path):
- if not cookie.path or path == '' or path == '/':
- return True
- path = _PATH_SEP.split(path)[1:]
- cookie_path = _PATH_SEP.split(cookie.path)[1:]
- for p1, p2 in map(lambda *ps: ps, path, cookie_path):
- if p1 == None:
- return True
- elif p1 != p2:
- return False
- return True
- def _cookie_for_domain(cookie, domain):
- if not cookie.domain:
- return True
- elif cookie.domain[0] == '.':
- return domain.endswith(cookie.domain)
- else:
- return cookie.domain == domain
- def _utf8(s):
- return s.encode('utf-8')
|