def _line(self, l): # {{{ if DEBUG > 4: log('Debug: Received line: %s' % l) if self.address is not None: if not l.strip(): self._handle_headers() return try: key, value = l.split(':', 1) except ValueError: log('Invalid header line: %s' % l) return self.headers[key.lower()] = value.strip() return else: try: self.method, url, self.standard = l.split() for prefix in self.proxy: if url.startswith('/' + prefix + '/') or url == '/' + prefix: self.prefix = '/' + prefix break else: self.prefix = '' address = urlparse(url) path = address.path[len(self.prefix):] or '/' self.url = path + url[len(address.path):] self.address = urlparse(self.url) self.query = parse_qs(self.address.query) except: traceback.print_exc() self.server.reply(self, 400) self.socket.close() return
def send(self, data, opcode = 1): # Send a WebSocket frame. {{{ '''Send a Websocket frame to the remote end of the connection. @param data: Data to send. @param opcode: Opcade to send. 0 = fragment, 1 = text packet, 2 = binary packet, 8 = close request, 9 = ping, 10 = pong. ''' if DEBUG > 3: log('websend:' + repr(data)) assert opcode in(0, 1, 2, 8, 9, 10) if self._is_closed: return None if opcode == 1: data = data.encode('utf-8') if self.mask[1]: maskchar = 0x80 # Masks are stupid, but the standard requires them. Don't waste time on encoding (or decoding, if also using this module). mask = b'\0\0\0\0' else: maskchar = 0 mask = b'' if len(data) < 126: l = bytes((maskchar | len(data),)) elif len(data) < 1 << 16: l = bytes((maskchar | 126,)) + struct.pack('!H', len(data)) else: l = bytes((maskchar | 127,)) + struct.pack('!Q', len(data)) try: self.socket.send(bytes((0x80 | opcode,)) + l + mask + data) except: # Something went wrong; close the socket(in case it wasn't yet). if DEBUG > 0: traceback.print_exc() log('closing socket due to problem while sending.') self.socket.close() if opcode == 8: self.socket.close()
def _line(self, l): # {{{ if DEBUG > 4: log('Debug: Received line: %s' % l) if self.address is not None: if not l.strip(): self._handle_headers() return try: key, value = l.split(':', 1) except ValueError: log('Invalid header line: %s' % l) return self.headers[key.lower()] = value.strip() return else: try: self.method, url, self.standard = l.split() for prefix in self.proxy: if url.startswith('/' + prefix + '/') or url == '/' + prefix: self.prefix = '/' + prefix break else: self.prefix = '' address = urlparse(url) path = address.path[len(self.prefix):] or '/' self.url = path + url[len(address.path):] self.address = urlparse(self.url) self.query = parse_qs(self.address.query) except: traceback.print_exc() self.server.reply(self, 400) self.socket.close() return
def send(self, data, opcode = 1): # Send a WebSocket frame. {{{ '''Send a Websocket frame to the remote end of the connection. @param data: Data to send. @param opcode: Opcade to send. 0 = fragment, 1 = text packet, 2 = binary packet, 8 = close request, 9 = ping, 10 = pong. ''' if DEBUG > 3: log('websend:' + repr(data)) assert opcode in(0, 1, 2, 8, 9, 10) if self._is_closed: return None if opcode == 1: data = data.encode('utf-8') if self.mask[1]: maskchar = 0x80 # Masks are stupid, but the standard requires them. Don't waste time on encoding (or decoding, if also using this module). mask = b'\0\0\0\0' else: maskchar = 0 mask = b'' if len(data) < 126: l = bytes((maskchar | len(data),)) elif len(data) < 1 << 16: l = bytes((maskchar | 126,)) + struct.pack('!H', len(data)) else: l = bytes((maskchar | 127,)) + struct.pack('!Q', len(data)) try: self.socket.send(bytes((0x80 | opcode,)) + l + mask + data) except: # Something went wrong; close the socket(in case it wasn't yet). if DEBUG > 0: traceback.print_exc() log('closing socket due to problem while sending.') self.socket.close() if opcode == 8: self.socket.close()
def run_user_code(): error = False try: global network #user_code.bl = network exec(open('./user_code.py').read(), globals()) print('2') #user_code.network = network print('3') except Exception as err: print('User code crashed. Error: %s' % err) network.log('Code crashed. Error: %s' % err) error = True if error: print('Revert to default code') network.log('Revert to default code') gc.collect() print(gc.mem_free()) try: while True: network.process() time.sleep_ms(100) except: pass #machine.reset()
def __init__(self, port, target, *a, **ka): # {{{ '''Start a new RPC HTTP server. Extra arguments are passed to the Httpd constructor, which passes its extra arguments to network.Server. @param port: Port to listen on. Same format as in python-network. @param target: Communication object class. A new object is created for every connection. Its constructor is called with the newly created RPC as an argument. @param log: If set, debugging is enabled and logging is sent to this file. If it is a directory, a log file with the current date and time as filename will be used. ''' ## Function to send an event to some or all connected # clients. # To send to some clients, add an identifier to all # clients in a group, and use that identifier in the # item operator, like so: # @code{.py} # connection0.groups.clear() # connection1.groups.add('foo') # connection2.groups.add('foo') # server.broadcast.bar(42) # This is sent to all clients. # server.broadcast['foo'].bar(42) # This is only sent to clients in group 'foo'. # @endcode self.broadcast = RPChttpd._Broadcast(self) if 'log' in ka: name = ka.pop('log') if name: global DEBUG if DEBUG < 2: DEBUG = 2 if os.path.isdir(name): n = os.path.join(name, time.strftime('%F %T%z')) old = n i = 0 while os.path.exists(n): i += 1 n = '%s.%d' % (old, i) else: n = name try: f = open(n, 'a') if n != name: sys.stderr.write('Logging to %s\n' % n) except IOError: fd, n = tempfile.mkstemp(prefix = os.path.basename(n) + '-' + time.strftime('%F %T%z') + '-', text = True) sys.stderr.write('Opening file %s failed, using tempfile instead: %s\n' % (name, n)) f = os.fdopen(fd, 'a') stderr_fd = sys.stderr.fileno() os.close(stderr_fd) os.dup2(f.fileno(), stderr_fd) log('Start logging to %s, commandline = %s' % (n, repr(sys.argv))) Httpd.__init__(self, port, target, websocket = RPC, *a, **ka)
def __init__(self, port, target, *a, **ka): # {{{ '''Start a new RPC HTTP server. Extra arguments are passed to the Httpd constructor, which passes its extra arguments to network.Server. @param port: Port to listen on. Same format as in python-network. @param target: Communication object class. A new object is created for every connection. Its constructor is called with the newly created RPC as an argument. @param log: If set, debugging is enabled and logging is sent to this file. If it is a directory, a log file with the current date and time as filename will be used. ''' ## Function to send an event to some or all connected # clients. # To send to some clients, add an identifier to all # clients in a group, and use that identifier in the # item operator, like so: # @code{.py} # connection0.groups.clear() # connection1.groups.add('foo') # connection2.groups.add('foo') # server.broadcast.bar(42) # This is sent to all clients. # server.broadcast['foo'].bar(42) # This is only sent to clients in group 'foo'. # @endcode self.broadcast = RPChttpd._Broadcast(self) if 'log' in ka: name = ka.pop('log') if name: global DEBUG if DEBUG < 2: DEBUG = 2 if os.path.isdir(name): n = os.path.join(name, time.strftime('%F %T%z')) old = n i = 0 while os.path.exists(n): i += 1 n = '%s.%d' % (old, i) else: n = name try: f = open(n, 'a') if n != name: sys.stderr.write('Logging to %s\n' % n) except IOError: fd, n = tempfile.mkstemp(prefix = os.path.basename(n) + '-' + time.strftime('%F %T%z') + '-', text = True) sys.stderr.write('Opening file %s failed, using tempfile instead: %s\n' % (name, n)) f = os.fdopen(fd, 'a') stderr_fd = sys.stderr.fileno() os.close(stderr_fd) os.dup2(f.fileno(), stderr_fd) log('Start logging to %s, commandline = %s' % (n, repr(sys.argv))) Httpd.__init__(self, port, target, websocket = RPC, *a, **ka)
def _send(self, type, object): # {{{ '''Send an RPC packet. @param type: The packet type. One of "return", "error", "call". @param object: The data to send. Return value, error message, or function arguments. ''' if DEBUG > 1: log('sending:' + repr(type) + repr(object)) Websocket.send(self, json.dumps((type, object)))
def _send(self, type, object): # {{{ '''Send an RPC packet. @param type: The packet type. One of "return", "error", "call". @param object: The data to send. Return value, error message, or function arguments. ''' if DEBUG > 1: log('sending:' + repr(type) + repr(object)) Websocket.send(self, json.dumps((type, object)))
def post(self, connection): # A non-WebSocket page was requested with POST. Same as page() above, plus connection.post, which is a dict of name:(headers, sent_filename, local_filename). When done, the local files are unlinked; remove the items from the dict to prevent this. The default is to return an error (so POST cannot be used to retrieve static pages!) {{{ '''Handle POST request. This function responds with an error by default. It must be overridden to handle POST requests. @param connection: Same as for page(), plus connection.post, which is a 2-tuple. The first element is a dict of name:['value', ...] for fields without a file. The second element is a dict of name:[(local_filename, remote_filename), ...] for fields with a file. When done, the local files are unlinked; remove the items from the dict to prevent this. @return True to keep connection open after this request, False to close it. ''' log('Warning: ignoring POST request.') self.reply(connection, 501) return False
def post(self, connection): # A non-WebSocket page was requested with POST. Same as page() above, plus connection.post, which is a dict of name:(headers, sent_filename, local_filename). When done, the local files are unlinked; remove the items from the dict to prevent this. The default is to return an error (so POST cannot be used to retrieve static pages!) {{{ '''Handle POST request. This function responds with an error by default. It must be overridden to handle POST requests. @param connection: Same as for page(), plus connection.post, which is a dict of name:(headers, sent_filename, local_filename). When done, the local files are unlinked; remove the items from the dict to prevent this. @return True to keep connection open after this request, False to close it. ''' log('Warning: ignoring POST request.') self.reply(connection, 501) return False
def _parse_args(self, header): # {{{ if ';' not in header: return (header.strip(), {}) pos = header.index(';') + 1 main = header[:pos].strip() ret = {} while pos < len(header): if '=' not in header[pos:]: if header[pos:].strip() != '': log('header argument %s does not have a value' % header[pos:].strip()) return main, ret p = header.index('=', pos) key = header[pos:p].strip().lower() pos = p + 1 value = '' quoted = False while True: first = (len(header), None) if not quoted and ';' in header[pos:]: s = header.index(';', pos) if s < first[0]: first = (s, ';') if '"' in header[pos:]: q = header.index('"', pos) if q < first[0]: first = (q, '"') if '\\' in header[pos:]: b = header.index('\\', pos) if b < first[0]: first = (b, '\\') value += header[pos:first[0]] pos = first[0] + 1 if first[1] == ';' or first[1] is None: break if first[1] == '\\': value += header[pos] pos += 1 continue quoted = not quoted ret[key] = value return main, ret
def _parse_args(self, header): # {{{ if ';' not in header: return (header.strip(), {}) pos = header.index(';') + 1 main = header[:pos].strip() ret = {} while pos < len(header): if '=' not in header[pos:]: if header[pos:].strip() != '': log('header argument %s does not have a value' % header[pos:].strip()) return main, ret p = header.index('=', pos) key = header[pos:p].strip().lower() pos = p + 1 value = '' quoted = False while True: first = (len(header), None) if not quoted and ';' in header[pos:]: s = header.index(';', pos) if s < first[0]: first = (s, ';') if '"' in header[pos:]: q = header.index('"', pos) if q < first[0]: first = (q, '"') if '\\' in header[pos:]: b = header.index('\\', pos) if b < first[0]: first = (b, '\\') value += header[pos:first[0]] pos = first[0] + 1 if first[1] == ';' or first[1] is None: break if first[1] == '\\': value += header[pos] pos += 1 continue quoted = not quoted ret[key] = value return main, ret
def _base64_decoder(self, data, final): # {{{ ret = b'' pos = 0 table = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' current = [] while len(data) >= pos + 4 - len(current): c = data[pos] pos += 1 if c not in table: if c not in b'\r\n': log('ignoring invalid character %s in base64 string' % c) continue current.append(table.index(c)) if len(current) == 4: # decode ret += bytes((current[0] << 2 | current[1] >> 4,)) if current[2] != 65: ret += bytes((((current[1] << 4) & 0xf0) | current[2] >> 2,)) if current[3] != 65: ret += bytes((((current[2] << 6) & 0xc0) | current[3],)) return (ret, data[pos:])
def _base64_decoder(self, data, final): # {{{ ret = b'' pos = 0 table = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' current = [] while len(data) >= pos + 4 - len(current): c = data[pos] pos += 1 if c not in table: if c not in b'\r\n': log('ignoring invalid character %s in base64 string' % c) continue current.append(table.index(c)) if len(current) == 4: # decode ret += bytes((current[0] << 2 | current[1] >> 4,)) if current[2] != 65: ret += bytes((((current[1] << 4) & 0xf0) | current[2] >> 2,)) if current[3] != 65: ret += bytes((((current[2] << 6) & 0xc0) | current[3],)) return (ret, data[pos:])
def _parse_headers(self, message): # {{{ lines = [] pos = 0 while True: p = message.index(b'\r\n', pos) ln = message[pos:p].decode('utf-8', 'replace') pos = p + 2 if ln == '': break if ln[0] in ' \t': if len(lines) == 0: log('header starts with continuation') else: lines[-1] += ln else: lines.append(ln) ret = {} for ln in lines: if ':' not in ln: log('ignoring header line without ":": %s' % ln) continue key, value = [x.strip() for x in ln.split(':', 1)] if key.lower() in ret: log('duplicate key in header: %s' % key) ret[key.lower()] = value return ret, message[pos:]
def _parse_headers(self, message): # {{{ lines = [] pos = 0 while True: p = message.index(b'\r\n', pos) ln = message[pos:p].decode('utf-8', 'replace') pos = p + 2 if ln == '': break if ln[0] in ' \t': if len(lines) == 0: log('header starts with continuation') else: lines[-1] += ln else: lines.append(ln) ret = {} for ln in lines: if ':' not in ln: log('ignoring header line without ":": %s' % ln) continue key, value = [x.strip() for x in ln.split(':', 1)] if key.lower() in ret: log('duplicate key in header: %s' % key) ret[key.lower()] = value return ret, message[pos:]
def page(self, connection, path = None): # A non-WebSocket page was requested. Use connection.address, connection.method, connection.query, connection.headers and connection.body (which should be empty) to find out more. {{{ '''Serve a non-websocket page. Overload this function for custom behavior. Call this function from the overloaded function if you want the default functionality in some cases. @param connection: The connection that requests the page. Attributes of interest are connection.address, connection.method, connection.query, connection.headers and connection.body (which should be empty). @param path: The requested file. @return True to keep the connection open after this request, False to close it. ''' if self.httpdirs is None: self.reply(connection, 501) return if path is None: path = connection.address.path if path == '/': address = 'index' else: address = '/' + unquote(path) + '/' while '/../' in address: # Don't handle this; just ignore it. pos = address.index('/../') address = address[:pos] + address[pos + 3:] address = address[1:-1] if '.' in address.rsplit('/', 1)[-1]: base, ext = address.rsplit('.', 1) base = base.strip('/') if ext not in self.exts and None not in self.exts: log('not serving unknown extension %s' % ext) self.reply(connection, 404) return for d in self.httpdirs: filename = os.path.join(d, base + os.extsep + ext) if os.path.exists(filename): break else: log('file %s not found in %s' % (base + os.extsep + ext, ', '.join(self.httpdirs))) self.reply(connection, 404) return else: base = address.strip('/') for ext in self.exts: for d in self.httpdirs: filename = os.path.join(d, base if ext is None else base + os.extsep + ext) if os.path.exists(filename): break else: continue break else: log('no file %s (with supported extension) found in %s' % (base, ', '.join(self.httpdirs))) self.reply(connection, 404) return return self.exts[ext](connection, open(filename, 'rb').read())
def page(self, connection, path = None): # A non-WebSocket page was requested. Use connection.address, connection.method, connection.query, connection.headers and connection.body (which should be empty) to find out more. {{{ '''Serve a non-websocket page. Overload this function for custom behavior. Call this function from the overloaded function if you want the default functionality in some cases. @param connection: The connection that requests the page. Attributes of interest are connection.address, connection.method, connection.query, connection.headers and connection.body (which should be empty). @param path: The requested file. @return True to keep the connection open after this request, False to close it. ''' if self.httpdirs is None: self.reply(connection, 501) return if path is None: path = connection.address.path if path == '/': address = 'index' else: address = '/' + path + '/' while '/../' in address: # Don't handle this; just ignore it. pos = address.index('/../') address = address[:pos] + address[pos + 3:] address = address[1:-1] if '.' in address: base, ext = address.rsplit('.', 1) base = base.strip('/') if ext not in self.exts and None not in self.exts: log('not serving unknown extension %s' % ext) self.reply(connection, 404) return for d in self.httpdirs: filename = os.path.join(d, base + os.extsep + ext) if os.path.exists(filename): break else: log('file %s not found in %s' % (base + os.extsep + ext, ', '.join(self.httpdirs))) self.reply(connection, 404) return else: base = address.strip('/') for ext in self.exts: for d in self.httpdirs: filename = os.path.join(d, base if ext is None else base + os.extsep + ext) if os.path.exists(filename): break else: continue break else: log('no file %s (with supported extension) found in %s' % (base, ', '.join(self.httpdirs))) self.reply(connection, 404) return return self.exts[ext](connection, open(filename, 'rb').read())
def _quopri_decoder(self, data, final): # {{{ ret = b'' pos = 0 while b'=' in data[pos:-2]: p = data.index(b'=', pos) ret += data[:p] if data[p + 1:p + 3] == b'\r\n': ret += b'\n' pos = p + 3 continue if any(x not in b'0123456789ABCDEFabcdef' for x in data[p + 1:p + 3]): log('invalid escaped sequence in quoted printable: %s' % data[p:p + 3].encode('utf-8', 'replace')) pos = p + 1 continue ret += bytes((int(data[p + 1:p + 3], 16),)) pos = p + 3 if final: ret += data[pos:] pos = len(data) elif len(pos) >= 2: ret += data[pos:-2] pos = len(data) - 2 return (ret, data[pos:])
def _quopri_decoder(self, data, final): # {{{ ret = b'' pos = 0 while b'=' in data[pos:-2]: p = data.index(b'=', pos) ret += data[:p] if data[p + 1:p + 3] == b'\r\n': ret += b'\n' pos = p + 3 continue if any(x not in b'0123456789ABCDEFabcdef' for x in data[p + 1:p + 3]): log('invalid escaped sequence in quoted printable: %s' % data[p:p + 3].encode('utf-8', 'replace')) pos = p + 1 continue ret += bytes((int(data[p + 1:p + 3], 16),)) pos = p + 3 if final: ret += data[pos:] pos = len(data) elif len(pos) >= 2: ret += data[pos:-2] pos = len(data) - 2 return (ret, data[pos:])
def _recv(self, frame): # {{{ '''Receive a websocket packet. @param frame: The packet. @return None. ''' data = self._parse_frame(frame) if DEBUG > 1: log('packet received: %s' % repr(data)) if data[0] is None: self._send('error', data[1]) return elif data[0] == 'error': if DEBUG > 0: traceback.print_stack() if self._error is not None: self._error(data[1]) else: raise ValueError(data[1]) elif data[0] == 'event': # Do nothing with this; the packet is already logged if DEBUG > 1. return elif data[0] == 'return': assert data[1][0] in RPC._calls RPC._calls[data[1][0]] (data[1][1]) return elif data[0] == 'call': try: if self._delayed_calls is not None: self._delayed_calls.append(data[1]) else: self._call(data[1][0], data[1][1], data[1][2], data[1][3]) except: traceback.print_exc() log('error: %s' % str(sys.exc_info()[1])) self._send('error', traceback.format_exc()) else: self._send('error', 'invalid RPC command')
def _recv(self, frame): # {{{ '''Receive a websocket packet. @param frame: The packet. @return None. ''' data = self._parse_frame(frame) if DEBUG > 1: log('packet received: %s' % repr(data)) if data[0] is None: self._send('error', data[1]) return elif data[0] == 'error': if DEBUG > 0: traceback.print_stack() if self._error is not None: self._error(data[1]) else: raise ValueError(data[1]) elif data[0] == 'event': # Do nothing with this; the packet is already logged if DEBUG > 1. return elif data[0] == 'return': assert data[1][0] in RPC._calls RPC._calls[data[1][0]] (data[1][1]) return elif data[0] == 'call': try: if self._delayed_calls is not None: self._delayed_calls.append(data[1]) else: self._call(data[1][0], data[1][1], data[1][2], data[1][3]) except: traceback.print_exc() log('error: %s' % str(sys.exc_info()[1])) self._send('error', traceback.format_exc()) else: self._send('error', 'invalid RPC command')
def _parse_frame(self, frame): # {{{ '''Decode an RPC packet. @param frame: The packet. @return (type, object) or (None, error_message). ''' try: # Don't choke on Chrome's junk at the end of packets. data = json.JSONDecoder().raw_decode(frame)[0] except ValueError: log('non-json frame: %s' % repr(frame)) return(None, 'non-json frame') if type(data) is not list or len(data) != 2 or not isinstance(data[0], str): log('invalid frame %s' % repr(data)) return(None, 'invalid frame') if data[0] == 'call': if self._delayed_calls is None and (not hasattr(self._target, data[1][1]) or not isinstance(getattr(self._target, data[1][1]), collections.Callable)): log('invalid call frame %s' % repr(data)) return(None, 'invalid frame') elif data[0] not in ('error', 'return'): log('invalid frame type %s' % repr(data)) return(None, 'invalid frame') return data
def _parse_frame(self, frame): # {{{ '''Decode an RPC packet. @param frame: The packet. @return (type, object) or (None, error_message). ''' try: # Don't choke on Chrome's junk at the end of packets. data = json.JSONDecoder().raw_decode(frame)[0] except ValueError: log('non-json frame: %s' % repr(frame)) return(None, 'non-json frame') if type(data) is not list or len(data) != 2 or not isinstance(data[0], str): log('invalid frame %s' % repr(data)) return(None, 'invalid frame') if data[0] == 'call': if self._delayed_calls is None and (not hasattr(self._target, data[1][1]) or not isinstance(getattr(self._target, data[1][1]), collections.Callable)): log('invalid call frame %s' % repr(data)) return(None, 'invalid frame') elif data[0] not in ('error', 'return'): log('invalid frame type %s' % repr(data)) return(None, 'invalid frame') return data
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): # {{{ '''When constructing a Websocket, a connection is made to the requested port, and the websocket handshake is performed. This constructor passes any extra arguments to the network.Socket constructor (if it is called), in particular "tls" can be used to control the use of encryption. There objects are also created by the websockets server. For that reason, there are some arguments that should not be used when calling it directly. @param port: Host and port to connect to, same format as python-network uses. @param url: The url to request at the host. @param recv: Function to call when a data packet is received asynchronously. @param method: Connection method to use. @param user: Username for authentication. Only plain text authentication is supported; this should only be used over a link with TLS encryption. @param password: Password for authentication. @param extra: Extra headers to pass to the host. @param socket: Existing socket to use for connection, or None to create a new socket. @param mask: Mostly for internal use by the server. Flag whether or not to send and receive masks. (None, True) is the default, which means to accept anything, and send masked packets. Note that the mask that is used for sending is always (0,0,0,0), which is effectively no mask. It is sent to follow the protocol. No real mask is sent, because masks give a false sense of security and provide no benefit. The unmasking implementation is rather slow. When communicating between two programs using this module, the non-mask is detected and the unmasking step is skipped. @param websockets: For interal use by the server. A set to remove the socket from on disconnect. @param data: For internal use by the server. Data to pass through to callback functions. @param real_remote: For internal use by the server. Override detected remote. Used to have proper remotes behind virtual proxy. ''' self.recv = recv self.mask = mask self.websockets = websockets self.websocket_buffer = b'' self.websocket_fragments = b'' self.opcode = None self._is_closed = False self._pong = True # If false, we're waiting for a pong. if socket is None: socket = network.Socket(port, *a, **ka) self.socket = socket self.remote = [real_remote or socket.remote[0], socket.remote[1]] hdrdata = b'' if url is not None: elist = [] for e in extra: elist.append('%s: %s\r\n' % (e, extra[e])) if user is not None: userpwd = user + ':' + password + '\r\n' else: userpwd = '' socket.send(('''\ %s %s HTTP/1.1\r Connection: Upgrade\r Upgrade: websocket\r Sec-WebSocket-Key: 0\r %s%s\r ''' % (method, url, userpwd, ''.join(elist))).encode('utf-8')) while b'\n' not in hdrdata: r = socket.recv() if r == b'': raise EOFError('EOF while reading reply') hdrdata += r pos = hdrdata.index(b'\n') assert int(hdrdata[:pos].split()[1]) == 101 hdrdata = hdrdata[pos + 1:] data = {} while True: while b'\n' not in hdrdata: r = socket.recv() if len(r) == 0: raise EOFError('EOF while reading reply') hdrdata += r pos = hdrdata.index(b'\n') line = hdrdata[:pos].strip() hdrdata = hdrdata[pos + 1:] if len(line) == 0: break key, value = [x.strip() for x in line.decode('utf-8', 'replace').split(':', 1)] data[key] = value self.data = data self.socket.read(self._websocket_read) def disconnect(socket, data): if not self._is_closed: self._is_closed = True if self.websockets is not None: self.websockets.remove(self) self.closed() return b'' if self.websockets is not None: self.websockets.add(self) self.socket.disconnect_cb(disconnect) self.opened() if len(hdrdata) > 0: self._websocket_read(hdrdata) if DEBUG > 2: log('opened websocket')
def _websocket_read(self, data, sync = False): # {{{ # Websocket data consists of: # 1 byte: # bit 7: 1 for last(or only) fragment; 0 for other fragments. # bit 6-4: extension stuff; must be 0. # bit 3-0: opcode. # 1 byte: # bit 7: 1 if masked, 0 otherwise. # bit 6-0: length or 126 or 127. # If 126: # 2 bytes: length # If 127: # 8 bytes: length # If masked: # 4 bytes: mask # length bytes: (masked) payload #log('received: ' + repr(data)) if DEBUG > 2: log('received %d bytes' % len(data)) if DEBUG > 3: log('waiting: ' + ' '.join(['%02x' % x for x in self.websocket_buffer]) + ''.join([chr(x) if 32 <= x < 127 else '.' for x in self.websocket_buffer])) log('data: ' + ' '.join(['%02x' % x for x in data]) + ''.join([chr(x) if 32 <= x < 127 else '.' for x in data])) self.websocket_buffer += data while len(self.websocket_buffer) > 0: if self.websocket_buffer[0] & 0x70: # Protocol error. log('extension stuff %x, not supported!' % self.websocket_buffer[0]) self.socket.close() return None if len(self.websocket_buffer) < 2: # Not enough data for length bytes. if DEBUG > 2: log('no length yet') return None b = self.websocket_buffer[1] have_mask = bool(b & 0x80) b &= 0x7f if have_mask and self.mask[0] is True or not have_mask and self.mask[0] is False: # Protocol error. log('mask error') self.socket.close() return None if b == 127: if len(self.websocket_buffer) < 10: # Not enough data for length bytes. if DEBUG > 2: log('no 4 length yet') return None l = struct.unpack('!Q', self.websocket_buffer[2:10])[0] pos = 10 elif b == 126: if len(self.websocket_buffer) < 4: # Not enough data for length bytes. if DEBUG > 2: log('no 2 length yet') return None l = struct.unpack('!H', self.websocket_buffer[2:4])[0] pos = 4 else: l = b pos = 2 if len(self.websocket_buffer) < pos + (4 if have_mask else 0) + l: # Not enough data for packet. if DEBUG > 2: log('no packet yet(%d < %d)' % (len(self.websocket_buffer), pos + (4 if have_mask else 0) + l)) # Long packets should not cause ping timeouts. self._pong = True return None header = self.websocket_buffer[:pos] opcode = header[0] & 0xf if have_mask: mask = [x for x in self.websocket_buffer[pos:pos + 4]] pos += 4 data = self.websocket_buffer[pos:pos + l] # The following is slow! # Don't do it if the mask is 0; this is always true if talking to another program using this module. if mask != [0, 0, 0, 0]: data = bytes([x ^ mask[i & 3] for i, x in enumerate(data)]) else: data = self.websocket_buffer[pos:pos + l] self.websocket_buffer = self.websocket_buffer[pos + l:] if self.opcode is None: self.opcode = opcode elif opcode != 0: # Protocol error. # Exception: pongs are sometimes sent asynchronously. # Theoretically the packet can be fragmented, but that should never happen; asynchronous pongs seem to be a protocol violation anyway... if opcode == 10: # Pong. self._pong = True else: log('invalid fragment') self.socket.close() return None if (header[0] & 0x80) != 0x80: # fragment found; not last. self._pong = True self.websocket_fragments += data if DEBUG > 2: log('fragment recorded') return None # Complete frame has been received. data = self.websocket_fragments + data self.websocket_fragments = b'' opcode = self.opcode self.opcode = None if opcode == 8: # Connection close request. self.close() return None elif opcode == 9: # Ping. self.send(data, 10) # Pong elif opcode == 10: # Pong. self._pong = True elif opcode == 1: # Text. data = data.decode('utf-8', 'replace') if sync: return data if self.recv: self.recv(self, data) else: log('warning: ignoring incoming websocket frame') elif opcode == 2: # Binary. if sync: return data if self.recv: self.recv(self, data) else: log('warning: ignoring incoming websocket frame (binary)') else: log('invalid opcode') self.socket.close()
def _handle_headers(self): # {{{ if DEBUG > 4: log('Debug: handling headers') 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() self.data = {} self.data['url'] = self.url self.data['address'] = self.address self.data['query'] = self.query self.data['headers'] = self.headers msg = self.server.auth_message(self, is_websocket) if callable(self.server.auth_message) else self.server.auth_message if msg: if 'authorization' not in self.headers: self.server.reply(self, 401, headers = {'WWW-Authenticate': 'Basic realm="%s"' % msg.replace('\n', ' ').replace('\r', ' ').replace('"', "'")}) if 'content-length' not in self.headers or self.headers['content-length'].strip() != '0': self.socket.close() return else: auth = self.headers['authorization'].split(None, 1) if auth[0].lower() != 'basic': self.server.reply(self, 400) self.socket.close() return pwdata = base64.b64decode(auth[1].encode('utf-8')).decode('utf-8', 'replace').split(':', 1) if len(pwdata) != 2: self.server.reply(self, 400) self.socket.close() return self.data['user'] = pwdata[0] self.data['password'] = pwdata[1] if not self.server.authenticate(self): self.server.reply(self, 401, headers = {'WWW-Authenticate': 'Basic realm="%s"' % msg.replace('\n', ' ').replace('\r', ' ').replace('"', "'")}) if 'content-length' not in self.headers or self.headers['content-length'].strip() != '0': self.socket.close() return if not is_websocket: if DEBUG > 4: log('Debug: not a websocket') self.body = self.socket.unread() if self.method.upper() == 'POST': if 'content-type' not in self.headers or self.headers['content-type'].lower().split(';')[0].strip() != 'multipart/form-data': 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')) self.server.reply(self, 500) self.socket.close() return args = self._parse_args(self.headers['content-type'])[1] if 'boundary' not in args: log('Invalid Content-Type for POST: missing boundary in %s\n' % (self.headers['content-type'] if 'content-type' in self.headers else 'undefined')) self.server.reply(self, 500) self.socket.close() return self.boundary = b'\r\n' + b'--' + args['boundary'].encode('utf-8') + b'\r\n' self.endboundary = b'\r\n' + b'--' + args['boundary'].encode('utf-8') + b'--\r\n' self.post_state = None self.post = [{}, {}] self.socket.read(self._post) self._post(b'') else: try: if not self.server.page(self): self.socket.close() except: if DEBUG > 0: traceback.print_exc() log('exception: %s\n' % repr(sys.exc_info()[1])) try: self.server.reply(self, 500) except: pass self.socket.close() return # Websocket. if self.method.upper() != 'GET' or 'sec-websocket-key' not in self.headers: if DEBUG > 2: log('Debug: invalid websocket') self.server.reply(self, 400) self.socket.close() return newkey = base64.b64encode(hashlib.sha1(self.headers['sec-websocket-key'].strip().encode('utf-8') + b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()).decode('utf-8') headers = {'Sec-WebSocket-Accept': newkey, 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': '13'} self.server.reply(self, 101, None, None, headers) self.websocket(None, recv = self.server.recv, url = None, socket = self.socket, error = self.error, mask = (None, False), websockets = self.server.websockets, data = self.data, real_remote = self.headers.get('x-forwarded-for'))
def _handle_headers(self): # {{{ if DEBUG > 4: log('Debug: handling headers') 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() self.data = {} self.data['url'] = self.url self.data['address'] = self.address self.data['query'] = self.query msg = self.server.auth_message(self, is_websocket) if callable(self.server.auth_message) else self.server.auth_message if msg: if 'authorization' not in self.headers: self.server.reply(self, 401, headers = {'WWW-Authenticate': 'Basic realm="%s"' % msg.replace('\n', ' ').replace('\r', ' ').replace('"', "'")}) if 'content-length' not in self.headers or self.headers['content-length'].strip() != '0': self.socket.close() return else: auth = self.headers['authorization'].split(None, 1) if auth[0].lower() != 'basic': self.server.reply(self, 400) self.socket.close() return pwdata = base64.b64decode(auth[1].encode('utf-8')).decode('utf-8', 'replace').split(':', 1) if len(pwdata) != 2: self.server.reply(self, 400) self.socket.close() return self.data['user'] = pwdata[0] self.data['password'] = pwdata[1] if not self.server.authenticate(self): self.server.reply(self, 401, headers = {'WWW-Authenticate': 'Basic realm="%s"' % msg.replace('\n', ' ').replace('\r', ' ').replace('"', "'")}) if 'content-length' not in self.headers or self.headers['content-length'].strip() != '0': self.socket.close() return if not is_websocket: if DEBUG > 4: log('Debug: not a websocket') self.body = self.socket.unread() if self.method.upper() == 'POST': if 'content-type' not in self.headers or self.headers['content-type'].lower().split(';')[0].strip() != 'multipart/form-data': 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')) self.server.reply(self, 500) self.socket.close() return args = self._parse_args(self.headers['content-type'])[1] if 'boundary' not in args: log('Invalid Content-Type for POST: missing boundary in %s\n' % (self.headers['content-type'] if 'content-type' in self.headers else 'undefined')) self.server.reply(self, 500) self.socket.close() return self.boundary = b'\r\n' + b'--' + args['boundary'].encode('utf-8') + b'\r\n' self.endboundary = b'\r\n' + b'--' + args['boundary'].encode('utf-8') + b'--\r\n' self.post_state = None self.post = [{}, {}] self.socket.read(self._post) self._post(b'') else: try: if not self.server.page(self): self.socket.close() except: if DEBUG > 0: traceback.print_exc() log('exception: %s\n' % repr(sys.exc_info()[1])) try: self.server.reply(self, 500) except: pass self.socket.close() return # Websocket. if self.method.upper() != 'GET' or 'sec-websocket-key' not in self.headers: if DEBUG > 2: log('Debug: invalid websocket') self.server.reply(self, 400) self.socket.close() return newkey = base64.b64encode(hashlib.sha1(self.headers['sec-websocket-key'].strip().encode('utf-8') + b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest()).decode('utf-8') headers = {'Sec-WebSocket-Accept': newkey, 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': '13'} self.server.reply(self, 101, None, None, headers) self.websocket(None, recv = self.server.recv, url = None, socket = self.socket, error = self.error, mask = (None, False), websockets = self.server.websockets, data = self.data, real_remote = self.headers.get('x-forwarded-for'))
def _websocket_read(self, data, sync = False): # {{{ # Websocket data consists of: # 1 byte: # bit 7: 1 for last(or only) fragment; 0 for other fragments. # bit 6-4: extension stuff; must be 0. # bit 3-0: opcode. # 1 byte: # bit 7: 1 if masked, 0 otherwise. # bit 6-0: length or 126 or 127. # If 126: # 2 bytes: length # If 127: # 8 bytes: length # If masked: # 4 bytes: mask # length bytes: (masked) payload #log('received: ' + repr(data)) if DEBUG > 2: log('received %d bytes' % len(data)) if DEBUG > 3: log('waiting: ' + ' '.join(['%02x' % x for x in self.websocket_buffer]) + ''.join([chr(x) if 32 <= x < 127 else '.' for x in self.websocket_buffer])) log('data: ' + ' '.join(['%02x' % x for x in data]) + ''.join([chr(x) if 32 <= x < 127 else '.' for x in data])) self.websocket_buffer += data while len(self.websocket_buffer) > 0: if self.websocket_buffer[0] & 0x70: # Protocol error. log('extension stuff %x, not supported!' % self.websocket_buffer[0]) self.socket.close() return None if len(self.websocket_buffer) < 2: # Not enough data for length bytes. if DEBUG > 2: log('no length yet') return None b = self.websocket_buffer[1] have_mask = bool(b & 0x80) b &= 0x7f if have_mask and self.mask[0] is True or not have_mask and self.mask[0] is False: # Protocol error. log('mask error') self.socket.close() return None if b == 127: if len(self.websocket_buffer) < 10: # Not enough data for length bytes. if DEBUG > 2: log('no 4 length yet') return None l = struct.unpack('!Q', self.websocket_buffer[2:10])[0] pos = 10 elif b == 126: if len(self.websocket_buffer) < 4: # Not enough data for length bytes. if DEBUG > 2: log('no 2 length yet') return None l = struct.unpack('!H', self.websocket_buffer[2:4])[0] pos = 4 else: l = b pos = 2 if len(self.websocket_buffer) < pos + (4 if have_mask else 0) + l: # Not enough data for packet. if DEBUG > 2: log('no packet yet(%d < %d)' % (len(self.websocket_buffer), pos + (4 if have_mask else 0) + l)) # Long packets should not cause ping timeouts. self._pong = True return None header = self.websocket_buffer[:pos] opcode = header[0] & 0xf if have_mask: mask = [x for x in self.websocket_buffer[pos:pos + 4]] pos += 4 data = self.websocket_buffer[pos:pos + l] # The following is slow! # Don't do it if the mask is 0; this is always true if talking to another program using this module. if mask != [0, 0, 0, 0]: data = bytes([x ^ mask[i & 3] for i, x in enumerate(data)]) else: data = self.websocket_buffer[pos:pos + l] self.websocket_buffer = self.websocket_buffer[pos + l:] if self.opcode is None: self.opcode = opcode elif opcode != 0: # Protocol error. # Exception: pongs are sometimes sent asynchronously. # Theoretically the packet can be fragmented, but that should never happen; asynchronous pongs seem to be a protocol violation anyway... if opcode == 10: # Pong. self._pong = True else: log('invalid fragment') self.socket.close() return None if (header[0] & 0x80) != 0x80: # fragment found; not last. self._pong = True self.websocket_fragments += data if DEBUG > 2: log('fragment recorded') return None # Complete frame has been received. data = self.websocket_fragments + data self.websocket_fragments = b'' opcode = self.opcode self.opcode = None if opcode == 8: # Connection close request. self.close() return None elif opcode == 9: # Ping. self.send(data, 10) # Pong elif opcode == 10: # Pong. self._pong = True elif opcode == 1: # Text. data = data.decode('utf-8', 'replace') if sync: return data if self.recv: self.recv(self, data) else: log('warning: ignoring incoming websocket frame') elif opcode == 2: # Binary. if sync: return data if self.recv: self.recv(self, data) else: log('warning: ignoring incoming websocket frame (binary)') else: log('invalid opcode') self.socket.close()
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): # {{{ '''When constructing a Websocket, a connection is made to the requested port, and the websocket handshake is performed. This constructor passes any extra arguments to the network.Socket constructor (if it is called), in particular "tls" can be used to control the use of encryption. There objects are also created by the websockets server. For that reason, there are some arguments that should not be used when calling it directly. @param port: Host and port to connect to, same format as python-network uses. @param url: The url to request at the host. @param recv: Function to call when a data packet is received asynchronously. @param method: Connection method to use. @param user: Username for authentication. Only plain text authentication is supported; this should only be used over a link with TLS encryption. @param password: Password for authentication. @param extra: Extra headers to pass to the host. @param socket: Existing socket to use for connection, or None to create a new socket. @param mask: Mostly for internal use by the server. Flag whether or not to send and receive masks. (None, True) is the default, which means to accept anything, and send masked packets. Note that the mask that is used for sending is always (0,0,0,0), which is effectively no mask. It is sent to follow the protocol. No real mask is sent, because masks give a false sense of security and provide no benefit. The unmasking implementation is rather slow. When communicating between two programs using this module, the non-mask is detected and the unmasking step is skipped. @param websockets: For interal use by the server. A set to remove the socket from on disconnect. @param data: For internal use by the server. Data to pass through to callback functions. @param real_remote: For internal use by the server. Override detected remote. Used to have proper remotes behind virtual proxy. ''' self.recv = recv self.mask = mask self.websockets = websockets self.websocket_buffer = b'' self.websocket_fragments = b'' self.opcode = None self._is_closed = False self._pong = True # If false, we're waiting for a pong. if socket is None: socket = network.Socket(port, *a, **ka) self.socket = socket # Use real_remote if it was provided. if real_remote: if isinstance(socket.remote, (tuple, list)): self.remote = [real_remote, socket.remote[1]] else: self.remote = [real_remote, None] else: self.remote = socket.remote hdrdata = b'' if url is not None: elist = [] for e in extra: elist.append('%s: %s\r\n' % (e, extra[e])) if user is not None: userpwd = user + ':' + password + '\r\n' else: userpwd = '' socket.send(('''\ %s %s HTTP/1.1\r Connection: Upgrade\r Upgrade: websocket\r Sec-WebSocket-Key: 0\r %s%s\r ''' % (method, url, userpwd, ''.join(elist))).encode('utf-8')) while b'\n' not in hdrdata: r = socket.recv() if r == b'': raise EOFError('EOF while reading reply') hdrdata += r pos = hdrdata.index(b'\n') assert int(hdrdata[:pos].split()[1]) == 101 hdrdata = hdrdata[pos + 1:] data = {} while True: while b'\n' not in hdrdata: r = socket.recv() if len(r) == 0: raise EOFError('EOF while reading reply') hdrdata += r pos = hdrdata.index(b'\n') line = hdrdata[:pos].strip() hdrdata = hdrdata[pos + 1:] if len(line) == 0: break key, value = [x.strip() for x in line.decode('utf-8', 'replace').split(':', 1)] data[key] = value self.data = data self.socket.read(self._websocket_read) def disconnect(socket, data): if not self._is_closed: self._is_closed = True if self.websockets is not None: self.websockets.remove(self) self.closed() return b'' if self.websockets is not None: self.websockets.add(self) self.socket.disconnect_cb(disconnect) self.opened() if len(hdrdata) > 0: self._websocket_read(hdrdata) if DEBUG > 2: log('opened websocket')