38 This module can be used to create websockets servers and clients. A websocket
39 client is an HTTP connection which uses the headers to initiate a protocol
40 change. The server is a web server which serves web pages, and also responds
41 to the protocol change headers that clients can use to set up a websocket.
43 Note that the server is not optimized for high traffic. If you need that, use
44 something like Apache to handle all the other content and set up a virtual
45 proxy to this server just for the websocket.
47 In addition to implementing the protocol, this module contains a simple system
48 to use websockets for making remote procedure calls (RPC). This system allows
49 the called procedures to be generators, so they can yield control to the main
50 program and continue running when they need to. This system can also be used
51 locally by using call().
54 '''@package websocketd Client WebSockets and webserver with WebSockets support
55 This module can be used to create websockets servers and clients. A websocket
56 client is an HTTP connection which uses the headers to initiate a protocol
57 change. The server is a web server which serves web pages, and also responds
58 to the protocol change headers that clients can use to set up a websocket.
60 Note that the server is not optimized for high traffic. If you need that, use
61 something like Apache to handle all the other content and set up a virtual
62 proxy to this server just for the websocket.
64 In addition to implementing the protocol, this module contains a simple system
65 to use websockets for making remote procedure calls (RPC). This system allows
66 the called procedures to be generators, so they can yield control to the main
67 program and continue running when they need to. This system can also be used
68 locally by using call().
76 from network
import endloop, log, set_log_output, add_read, add_write, add_timeout, add_idle, remove_read, remove_write, remove_timeout, remove_idle
88 from urllib.parse
import urlparse, parse_qs, unquote
89 from http.client
import responses
as httpcodes
104 DEBUG = 0
if os.getenv(
'NODEBUG')
else int(os.getenv(
'DEBUG', 1))
145 def __init__(self, port, url = '/', recv = None, method = 'GET', user = None, password = None, extra = {}, socket = None, mask = (
None,
True), websockets =
None, data =
None, real_remote =
None, *a, **ka):
153 self.
_pong_pong =
True
155 socket = network.Socket(port, *a, **ka)
159 if isinstance(socket.remote, (tuple, list)):
160 self.
remoteremote = [real_remote, socket.remote[1]]
162 self.
remoteremote = [real_remote,
None]
164 self.
remoteremote = socket.remote
169 elist.append(
'%s: %s\r\n' % (e, extra[e]))
171 userpwd = user +
':' + password +
'\r\n'
176 Connection: Upgrade\r
178 Sec-WebSocket-Key: 0\r
180 ''' % (method, url, userpwd,
''.join(elist))).encode(
'utf-8'))
181 while b
'\n' not in hdrdata:
184 raise EOFError(
'EOF while reading reply')
186 pos = hdrdata.index(b
'\n')
187 assert int(hdrdata[:pos].split()[1]) == 101
188 hdrdata = hdrdata[pos + 1:]
191 while b
'\n' not in hdrdata:
194 raise EOFError(
'EOF while reading reply')
196 pos = hdrdata.index(b
'\n')
197 line = hdrdata[:pos].strip()
198 hdrdata = hdrdata[pos + 1:]
201 key, value = [x.strip()
for x
in line.decode(
'utf-8',
'replace').split(
':', 1)]
205 def disconnect(socket, data):
214 self.
socketsocket.disconnect_cb(disconnect)
219 log(
'opened websocket')
221 def _websocket_read(self, data, sync = False):
240 log(
'received %d bytes' % len(data))
242 log(
'waiting: ' +
' '.join([
'%02x' % x
for x
in self.
websocket_bufferwebsocket_buffer]) +
''.join([chr(x)
if 32 <= x < 127
else '.' for x
in self.
websocket_bufferwebsocket_buffer]))
243 log(
'data: ' +
' '.join([
'%02x' % x
for x
in data]) +
''.join([chr(x)
if 32 <= x < 127
else '.' for x
in data]))
248 log(
'extension stuff %x, not supported!' % self.
websocket_bufferwebsocket_buffer[0])
257 have_mask = bool(b & 0x80)
259 if have_mask
and self.
maskmask[0]
is True or not have_mask
and self.
maskmask[0]
is False:
268 log(
'no 4 length yet')
276 log(
'no 2 length yet')
283 if len(self.
websocket_bufferwebsocket_buffer) < pos + (4
if have_mask
else 0) + l:
286 log(
'no packet yet(%d < %d)' % (len(self.
websocket_bufferwebsocket_buffer), pos + (4
if have_mask
else 0) + l))
288 self.
_pong_pong =
True
291 opcode = header[0] & 0xf
298 if mask != [0, 0, 0, 0]:
299 data = bytes([x ^ mask[i & 3]
for i, x
in enumerate(data)])
303 if self.
opcodeopcode
is None:
304 self.
opcodeopcode = opcode
311 self.
_pong_pong =
True
313 log(
'invalid fragment')
316 if (header[0] & 0x80) != 0x80:
318 self.
_pong_pong =
True
321 log(
'fragment recorded')
326 opcode = self.
opcodeopcode
334 self.
sendsend(data, 10)
337 self.
_pong_pong =
True
340 data = data.decode(
'utf-8',
'replace')
344 self.
recvrecv(self, data)
346 log(
'warning: ignoring incoming websocket frame')
352 self.
recvrecv(self, data)
354 log(
'warning: ignoring incoming websocket frame (binary)')
356 log(
'invalid opcode')
363 def send(self, data, opcode = 1):
365 log(
'websend:' + repr(data))
366 assert opcode in(0, 1, 2, 8, 9, 10)
370 data = data.encode(
'utf-8')
379 l = bytes((maskchar | len(data),))
380 elif len(data) < 1 << 16:
381 l = bytes((maskchar | 126,)) + struct.pack(
'!H', len(data))
383 l = bytes((maskchar | 127,)) + struct.pack(
'!Q', len(data))
385 self.
socketsocket.
send(bytes((0x80 | opcode,)) + l + mask + data)
389 traceback.print_exc()
390 log(
'closing socket due to problem while sending.')
400 ret = self.
_pong_pong
401 self.
_pong_pong =
False
402 self.
sendsend(data, opcode = 9)
409 self.
sendsend(b
'', 8)
435 _activation = [set(),
None]
447 def call(reply, target, *a, **ka):
448 ret = target(*a, **ka)
449 if type(ret)
is not RPC._generatortype:
450 if reply
is not None:
454 def wake(arg = None):
457 except StopIteration
as result:
458 if reply
is not None:
484 _generatortype = type((
lambda: (
yield))())
512 def __init__(self, port, recv = None, error = None, *a, **ka):
513 _activation[0].add(self)
514 if _activation[1]
is None:
515 _activation[1] = add_idle(_activate_all)
519 Websocket.__init__(self, port, recv = RPC._recv, *a, **ka)
521 self.
_target_target =
recv(self)
if recv
is not None else None
532 if not hasattr(self.
_target_target, call[1])
or not isinstance(getattr(self.
_target_target, call[1]), collections.Callable):
533 self.
_send_send(
'error',
'invalid delayed call frame %s' % repr(call))
535 self.
_call_call(call[0], call[1], call[2], call[3])
538 def __init__(self, base, attr):
542 def __call__(self, *a, **ka):
543 my_id = RPC._get_index()
544 self.base._send(
'call', (my_id, self.attr, a, ka))
546 RPC._calls[my_id] =
lambda x: my_call.__setitem__(0, (x,))
547 while my_call[0]
is None:
548 data = self.base._websocket_read(self.base.socket.recv(),
True)
549 while data
is not None:
550 self.base._recv(data)
551 data = self.base._websocket_read(b
'')
552 del RPC._calls[my_id]
555 def __getitem__(self, *a, **ka):
556 self.base._send(
'call', (
None, self.attr, a, ka))
558 def bg(self, reply, *a, **ka):
559 my_id = RPC._get_index()
560 self.base._send(
'call', (my_id, self.attr, a, ka))
561 RPC._calls[my_id] =
lambda x: self.do_reply(reply, my_id, x)
563 def do_reply(self, reply, my_id, ret):
564 del RPC._calls[my_id]
568 def call(self, *a, **ka):
570 def event(self, *a, **ka):
571 self.__getitem__(*a, **ka)
582 def _send(self, type, object):
584 log(
'sending:' + repr(type) + repr(object))
585 Websocket.send(self, json.dumps((type, object)))
593 def _parse_frame(self, frame):
596 data = json.JSONDecoder().raw_decode(frame)[0]
598 log(
'non-json frame: %s' % repr(frame))
599 return (
None,
'non-json frame')
600 if type(data)
is not list
or len(data) != 2
or not isinstance(data[0], str):
601 log(
'invalid frame %s' % repr(data))
602 return (
None,
'invalid frame')
603 if data[0] ==
'call':
604 if not isinstance(data[1], list):
605 log(
'invalid call frame (no list) %s' % repr(data))
606 return (
None,
'invalid frame')
607 if len(data[1]) != 4:
608 log(
'invalid call frame (list length is not 4) %s' % repr(data))
609 return (
None,
'invalid frame')
610 if (data[1][0]
is not None and not isinstance(data[1][0], int)):
611 log(
'invalid call frame (invalid id) %s' % repr(data))
612 return (
None,
'invalid frame')
613 if not isinstance(data[1][1], str):
614 log(
'invalid call frame (no string target) %s' % repr(data))
615 return (
None,
'invalid frame')
616 if not isinstance(data[1][2], list):
617 log(
'invalid call frame (no list args) %s' % repr(data))
618 return (
None,
'invalid frame')
619 if not isinstance(data[1][3], dict):
620 log(
'invalid call frame (no dict kwargs) %s' % repr(data))
621 return (
None,
'invalid frame')
622 if (self.
_delayed_calls_delayed_calls
is None and (
not hasattr(self.
_target_target, data[1][1])
or not isinstance(getattr(self.
_target_target, data[1][1]), collections.Callable))):
623 log(
'invalid call frame (no callable) %s' % repr(data))
624 return (
None,
'invalid frame')
625 elif data[0]
not in (
'error',
'return'):
626 log(
'invalid frame type %s' % repr(data))
627 return (
None,
'invalid frame')
636 def _recv(self, frame):
639 log(
'packet received: %s' % repr(data))
641 self.
_send_send(
'error', data[1])
643 elif data[0] ==
'error':
645 traceback.print_stack()
646 if self.
_error_error
is not None:
647 self.
_error_error(data[1])
649 raise ValueError(data[1])
650 elif data[0] ==
'event':
653 elif data[0] ==
'return':
654 assert data[1][0]
in RPC._calls
655 RPC._calls[data[1][0]] (data[1][1])
657 elif data[0] ==
'call':
662 self.
_call_call(data[1][0], data[1][1], data[1][2], data[1][3])
664 traceback.print_exc()
665 log(
'error: %s' % str(sys.exc_info()[1]))
666 self.
_send_send(
'error', traceback.format_exc())
668 self.
_send_send(
'error',
'invalid RPC command')
681 def _call(self, reply, member, a, ka):
682 call((
lambda ret: self.
_send_send(
'return', (reply, ret)))
if reply
is not None else None, getattr(self.
_target_target, member), *a, **ka)
688 def __getattr__(self, attr):
689 if attr.startswith(
'_'):
690 raise AttributeError(
'invalid RPC function name %s' % attr)
691 return RPC._wrapper(self, attr)
701 if _activation[0]
is not None:
702 for s
in _activation[0]:
704 _activation[0].clear()
705 _activation[1] =
None
716 class _Httpd_connection:
729 def __init__(self, server, socket, websocket = Websocket, proxy = (), error =
None):
731 self.
socketsocket = socket
732 self.websocket = websocket
733 self.proxy = (proxy,)
if isinstance(proxy, str)
else proxy
737 self.
socketsocket.disconnect_cb(
lambda socket, data: b
'')
738 self.
socketsocket.readlines(self._line)
743 log(
'Debug: Received line: %s' % l)
744 if self.address
is not None:
746 self._handle_headers()
749 key, value = l.split(
':', 1)
751 log(
'Invalid header line: %s' % l)
753 self.headers[key.lower()] = value.strip()
757 self.method, url, self.standard = l.split()
758 for prefix
in self.proxy:
759 if url.startswith(
'/' + prefix +
'/')
or url ==
'/' + prefix:
760 self.prefix =
'/' + prefix
764 address = urlparse(url)
765 path = address.path[len(self.prefix):]
or '/'
766 self.url = path + url[len(address.path):]
767 self.address = urlparse(self.url)
768 self.query = parse_qs(self.address.query)
770 traceback.print_exc()
771 self.server.reply(self, 400, close =
True)
774 def _handle_headers(self):
776 log(
'Debug: handling headers')
777 is_websocket =
'connection' in self.headers
and 'upgrade' in self.headers
and 'upgrade' in self.headers[
'connection'].lower()
and 'websocket' in self.headers[
'upgrade'].lower()
779 self.
datadata[
'url'] = self.url
780 self.
datadata[
'address'] = self.address
781 self.
datadata[
'query'] = self.query
782 self.
datadata[
'headers'] = self.headers
783 msg = self.server.auth_message(self, is_websocket)
if callable(self.server.auth_message)
else self.server.auth_message
785 if 'authorization' not in self.headers:
786 self.server.reply(self, 401, headers = {
'WWW-Authenticate':
'Basic realm="%s"' % msg.replace(
'\n',
' ').replace(
'\r',
' ').replace(
'"',
"'")}, close =
True)
789 auth = self.headers[
'authorization'].split(
None, 1)
790 if auth[0].lower() !=
'basic':
791 self.server.reply(self, 400, close =
True)
793 pwdata = base64.b64decode(auth[1].encode(
'utf-8')).decode(
'utf-8',
'replace').split(
':', 1)
795 self.server.reply(self, 400, close =
True)
797 self.
datadata[
'user'] = pwdata[0]
798 self.
datadata[
'password'] = pwdata[1]
799 if not self.server.authenticate(self):
800 self.server.reply(self, 401, headers = {
'WWW-Authenticate':
'Basic realm="%s"' % msg.replace(
'\n',
' ').replace(
'\r',
' ').replace(
'"',
"'")}, close =
True)
804 log(
'Debug: not a websocket')
805 self.body = self.
socketsocket.unread()
806 if self.method.upper() ==
'POST':
807 if 'content-type' not in self.headers
or self.headers[
'content-type'].lower().split(
';')[0].strip() !=
'multipart/form-data':
808 log(
'Invalid Content-Type for POST; must be multipart/form-data (not %s)\n' % (self.headers[
'content-type']
if 'content-type' in self.headers
else 'undefined'))
809 self.server.reply(self, 500, close =
True)
811 args = self._parse_args(self.headers[
'content-type'])[1]
812 if 'boundary' not in args:
813 log(
'Invalid Content-Type for POST: missing boundary in %s\n' % (self.headers[
'content-type']
if 'content-type' in self.headers
else 'undefined'))
814 self.server.reply(self, 500, close =
True)
816 self.boundary = b
'\r\n' + b
'--' + args[
'boundary'].encode(
'utf-8') + b
'\r\n'
817 self.endboundary = b
'\r\n' + b
'--' + args[
'boundary'].encode(
'utf-8') + b
'--\r\n'
818 self.post_state =
None
820 self.
socketsocket.read(self._post)
824 if not self.server.page(self):
828 traceback.print_exc()
829 log(
'exception: %s\n' % repr(sys.exc_info()[1]))
831 self.server.reply(self, 500, close =
True)
836 if self.method.upper() !=
'GET' or 'sec-websocket-key' not in self.headers:
838 log(
'Debug: invalid websocket')
839 self.server.reply(self, 400, close =
True)
841 newkey = base64.b64encode(hashlib.sha1(self.headers[
'sec-websocket-key'].strip().encode(
'utf-8') + b
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()).decode(
'utf-8')
842 headers = {
'Sec-WebSocket-Accept': newkey,
'Connection':
'Upgrade',
'Upgrade':
'websocket',
'Sec-WebSocket-Version':
'13'}
843 self.server.reply(self, 101,
None,
None, headers, close =
False)
844 self.websocket(
None, recv = self.server.recv, url =
None, socket = self.
socketsocket, error = self.error, mask = (
None,
False), websockets = self.server.websockets, data = self.
datadata, real_remote = self.headers.get(
'x-forwarded-for'))
846 def _parse_headers(self, message):
850 p = message.index(b
'\r\n', pos)
851 ln = message[pos:p].decode(
'utf-8',
'replace')
857 log(
'header starts with continuation')
865 log(
'ignoring header line without ":": %s' % ln)
867 key, value = [x.strip()
for x
in ln.split(
':', 1)]
868 if key.lower()
in ret:
869 log(
'duplicate key in header: %s' % key)
870 ret[key.lower()] = value
871 return ret, message[pos:]
873 def _parse_args(self, header):
874 if ';' not in header:
875 return (header.strip(), {})
876 pos = header.index(
';') + 1
877 main = header[:pos].strip()
879 while pos < len(header):
880 if '=' not in header[pos:]:
881 if header[pos:].strip() !=
'':
882 log(
'header argument %s does not have a value' % header[pos:].strip())
884 p = header.index(
'=', pos)
885 key = header[pos:p].strip().lower()
890 first = (len(header),
None)
891 if not quoted
and ';' in header[pos:]:
892 s = header.index(
';', pos)
895 if '"' in header[pos:]:
896 q = header.index(
'"', pos)
899 if '\\' in header[pos:]:
900 b = header.index(
'\\', pos)
903 value += header[pos:first[0]]
905 if first[1] ==
';' or first[1]
is None:
915 def _post(self, data):
918 if self.post_state
is None:
920 if self.boundary
not in b
'\r\n' + self.body:
921 if self.endboundary
in b
'\r\n' + self.body:
924 self.body = b
'\r\n' + self.body
925 self.body = self.body[self.body.index(self.boundary) + len(self.boundary):]
930 if self.post_state == 0:
932 if b
'\r\n\r\n' not in self.body:
934 headers, self.body = self._parse_headers(self.body)
936 if 'content-type' not in headers:
937 post_type = (
'text/plain', {
'charset':
'us-ascii'})
939 post_type = self._parse_args(headers[
'content-type'])
940 if 'content-transfer-encoding' not in headers:
941 self.post_encoding =
'7bit'
943 self.post_encoding = self._parse_args(headers[
'content-transfer-encoding'])[0].lower()
945 if self.post_encoding ==
'base64':
946 self._post_decoder = self._base64_decoder
947 elif self.post_encoding ==
'quoted-printable':
948 self._post_decoder = self._quopri_decoder
950 self._post_decoder =
lambda x, final: (x, b
'')
951 if 'content-disposition' in headers:
952 args = self._parse_args(headers[
'content-disposition'])[1]
954 self.post_name = args[
'name']
956 self.post_name =
None
957 if 'filename' in args:
958 fd, self.post_file = tempfile.mkstemp()
959 self.post_handle = os.fdopen(fd,
'wb')
960 if self.post_name
not in self.post[1]:
961 self.post[1][self.post_name] = []
962 self.post[1][self.post_name].append((self.post_file, args[
'filename'], headers, post_type))
964 self.post_handle =
None
966 self.post_name =
None
967 if self.post_handle
is None:
968 self.post[0][self.post_name] = [b
'', headers, post_type]
970 if self.post_state == 1:
972 if self.endboundary
in self.body:
973 p = self.body.index(self.endboundary)
976 if self.boundary
in self.body
and (p
is None or self.body.index(self.boundary) < p):
978 rest = self.body[self.body.index(self.boundary) + len(self.boundary):]
979 self.body = self.body[:self.body.index(self.boundary)]
981 self.body = self.body[:p]
982 self.post_state =
None
984 if len(self.body) <= len(self.boundary):
986 rest = self.body[-len(self.boundary):]
987 self.body = self.body[:-len(rest)]
988 decoded, self.body = self._post_decoder(self.body, self.post_state != 1)
989 if self.post_handle
is not None:
990 self.post_handle.write(decoded)
991 if self.post_state != 1:
992 self.post_handle.
close()
994 self.post[0][self.post_name][0] += decoded
995 if self.post_state != 1:
996 if self.post[0][self.post_name][2][0] ==
'text/plain':
997 self.post[0][self.post_name][0] = self.post[0][self.post_name][0].decode(self.post[0][self.post_name][2][1].get(
'charset',
'utf-8'),
'replace')
998 if self.post_state
is None:
1003 def _finish_post(self):
1004 if not self.server.post(self):
1006 for f
in self.post[1]:
1007 for g
in self.post[1][f]:
1011 def _base64_decoder(self, data, final):
1014 table = b
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
1016 while len(data) >= pos + 4 - len(current):
1020 if c
not in b
'\r\n':
1021 log(
'ignoring invalid character %s in base64 string' % c)
1023 current.append(table.index(c))
1024 if len(current) == 4:
1026 ret += bytes((current[0] << 2 | current[1] >> 4,))
1027 if current[2] != 65:
1028 ret += bytes((((current[1] << 4) & 0xf0) | current[2] >> 2,))
1029 if current[3] != 65:
1030 ret += bytes((((current[2] << 6) & 0xc0) | current[3],))
1031 return (ret, data[pos:])
1033 def _quopri_decoder(self, data, final):
1036 while b
'=' in data[pos:-2]:
1037 p = data.index(b
'=', pos)
1039 if data[p + 1:p + 3] == b
'\r\n':
1043 if any(x
not in b
'0123456789ABCDEFabcdef' for x
in data[p + 1:p + 3]):
1044 log(
'invalid escaped sequence in quoted printable: %s' % data[p:p + 3].encode(
'utf-8',
'replace'))
1047 ret += bytes((int(data[p + 1:p + 3], 16),))
1055 return (ret, data[pos:])
1076 def __init__(self, port, recv = None, httpdirs = None, server = None, proxy = (), http_connection = _Httpd_connection, websocket = Websocket, error =
None, *a, **ka):
1083 self.
_proxy_proxy = proxy
1085 self.
_error_error = error
if error
is not None else lambda msg: print(msg)
1092 with open(
'/etc/mime.types')
as f:
1095 for ext
in items[1:]:
1097 exts[ext] = items[0]
1101 except FileNotFoundError:
1103 exts = {
'html':
'text/html',
'css':
'text/css',
'js':
'text/javascript',
'jpg':
'image/jpeg',
'jpeg':
'image/jpeg',
'png':
'image/png',
'bmp':
'image/bmp',
'gif':
'image/gif',
'pdf':
'application/pdf',
'svg':
'image/svg+xml',
'txt':
'text/plain'}
1105 if exts[ext]
is not False:
1106 if exts[ext].startswith(
'text/')
or exts[ext] ==
'application/javascript':
1107 self.
handle_exthandle_ext(ext, exts[ext] +
';charset=utf-8')
1114 self.
serverserver = network.Server(port, self, *a, **ka)
1116 self.
serverserver = server
1124 def __call__(self, socket):
1137 self.
extsexts[ext] =
lambda socket, message: self.
replyreply(socket, 200, message, mime)
1201 def reply(self, connection, code, message = None, content_type = None, headers = None, close = False):
1202 assert code
in httpcodes
1204 connection.socket.send((
'HTTP/1.1 %d %s\r\n' % (code, httpcodes[code])).encode(
'utf-8'))
1207 if message
is None and code != 101:
1208 assert content_type
is None
1209 content_type =
'text/html; charset=utf-8'
1210 message = (
'<!DOCTYPE html><html><head><title>%d: %s</title></head><body><h1>%d: %s</h1></body></html>' % (code, httpcodes[code], code, httpcodes[code])).encode(
'utf-8')
1211 if close
and 'Connection' not in headers:
1212 headers[
'Connection'] =
'close'
1213 if content_type
is not None:
1214 headers[
'Content-Type'] = content_type
1215 headers[
'Content-Length'] =
'%d' % len(message)
1219 connection.socket.send((
''.join([
'%s: %s\r\n' % (x, headers[x])
for x
in headers]) +
'\r\n').encode(
'utf-8') + message)
1221 connection.socket.close()
1237 def page(self, connection, path = None):
1239 self.
replyreply(connection, 501)
1242 path = connection.address.path
1246 address =
'/' + unquote(path) +
'/'
1247 while '/../' in address:
1249 pos = address.index(
'/../')
1250 address = address[:pos] + address[pos + 3:]
1251 address = address[1:-1]
1252 if '.' in address.rsplit(
'/', 1)[-1]:
1253 base, ext = address.rsplit(
'.', 1)
1254 base = base.strip(
'/')
1255 if ext
not in self.
extsexts
and None not in self.
extsexts:
1256 log(
'not serving unknown extension %s' % ext)
1257 self.
replyreply(connection, 404)
1260 filename = os.path.join(d, base + os.extsep + ext)
1261 if os.path.exists(filename):
1264 log(
'file %s not found in %s' % (base + os.extsep + ext,
', '.join(self.
httpdirshttpdirs)))
1265 self.
replyreply(connection, 404)
1268 base = address.strip(
'/')
1269 for ext
in self.
extsexts:
1271 filename = os.path.join(d, base
if ext
is None else base + os.extsep + ext)
1272 if os.path.exists(filename):
1278 log(
'no file %s (with supported extension) found in %s' % (base,
', '.join(self.
httpdirshttpdirs)))
1279 self.
replyreply(connection, 404)
1281 return self.
extsexts[ext](connection, open(filename,
'rb').read())
1294 log(
'Warning: ignoring POST request.')
1295 self.
replyreply(connection, 501)
1303 def __init__(self, server, group = None):
1304 self.
serverserver = server
1306 def __getitem__(self, item):
1307 return RPChttpd._Broadcast(self.
serverserver, item)
1308 def __getattr__(self, key):
1309 if key.startswith(
'_'):
1310 raise AttributeError(
'invalid member name')
1312 for c
in self.
serverserver.websockets.copy():
1313 if self.group
is None or self.group
in c.groups:
1314 getattr(c, key).event(*a, **ka)
1331 def __init__(self, port, target, *a, **ka):
1346 name = ka.pop(
'log')
1351 if os.path.isdir(name):
1352 n = os.path.join(name, time.strftime(
'%F %T%z'))
1355 while os.path.exists(n):
1357 n =
'%s.%d' % (old, i)
1363 sys.stderr.write(
'Logging to %s\n' % n)
1365 fd, n = tempfile.mkstemp(prefix = os.path.basename(n) +
'-' + time.strftime(
'%F %T%z') +
'-', text =
True)
1366 sys.stderr.write(
'Opening file %s failed, using tempfile instead: %s\n' % (name, n))
1367 f = os.fdopen(fd,
'a')
1368 stderr_fd = sys.stderr.fileno()
1370 os.dup2(f.fileno(), stderr_fd)
1371 log(
'Start logging to %s, commandline = %s' % (n, repr(sys.argv)))
1372 Httpd.__init__(self, port, target, websocket = RPC, *a, **ka)
1381 return network.fgloop(*a, **ka)
1389 return network.bgloop(*a, **ka)
1397 return network.iteration(*a, **ka)
def page(self, connection, path=None)
Serve a non-websocket page.
recv
Communication object for new websockets.
httpdirs
Sequence of directories that that are searched to serve.
def post(self, connection)
Handle POST request.
def reply(self, connection, code, message=None, content_type=None, headers=None, close=False)
Reply to a request for a document.
server
network.Server object.
def authenticate(self, connection)
Handle user authentication.
def handle_ext(self, ext, mime)
Add file extension to handle successfully.
exts
Extensions which are handled from httpdirs.
websockets
Currently connected websocket connections.
groups
Groups are used to do selective broadcast() events.
Remote Procedure Call over Websocket.
broadcast
Function to send an event to some or all connected clients.
Http server which serves websockets that implement RPC.
def closed(self)
This function does nothing by default, but can be overridden by the application.
def opened(self)
This function does nothing by default, but can be overridden by the application.
def send(self, data, opcode=1)
Send a Websocket frame to the remote end of the connection.
def close(self)
Send close request, and close the connection.
def ping(self, data=b'')
Send a ping, return if a pong was received since last ping.
Main class implementing the websocket protocol.
def call(reply, target, *a, **ka)
Make a call to a function or generator.
def bgloop(*a, **ka)
Activate all websockets and start the main loop in the background.
def iteration(*a, **ka)
Activate all websockets and run one loop iteration.
def fgloop(*a, **ka)
Activate all websockets and start the main loop.