예제 #1
0
class Websocket():
	"""Handle a websocket connection"""

	# TODO: Make opcode its own object?
	# These are the possible opcodes
	OP_CONT = 0x0	# Continuation
	OP_TEXT = 0x1	# Text frame
	OP_BIN = 0x2	# Binary frame
	OP_CLOS = 0x8	# Close request
	OP_PING = 0x9	# Ping frame
	OP_PONG = 0xA	# Pong frame
	OP_OTH = 0xF	# Other - reserved

	# These are the receiver states
	ST_FILESIZE = "wait for filesize"
	ST_FILESEGM = "waiting for file segment metadata"
	ST_FILESEGD = "waiting for file segment data"
	ST_CLOSE = "close the connection"
	ST_FINISH = "file finish command received"

	def __init__(self, conn, addr, url, settings):
		self._conn = conn
		self._addr = addr
		self._url = url
		self._permitted_upload_dir = None
		if "uploaddir" in settings.keys():
			# Make sure there's one / at the end of the directory
			self._permitted_upload_dir = settings["uploaddir"].rstrip("/ ") + "/"
		self._filewriter = None
		self._curbuf=bytearray()	# Store the buffer of received bytes

	def handle_websocket(self):
		"""Handle a websocket connection"""
		self._send_header()
		state = self.ST_FILESIZE  # First, wait for the file size
		# Handle file upload.  Close connection nicely in any case
		try:
			while state != self.ST_FINISH:
				opcode, data = self._get_frame()
				if opcode == self.OP_PING:
					# Handle a ping in any state.  Don't do anything else with the data
					self._send_msg(data, self.OP_PONG)
				elif state == self.ST_FILESIZE:
					state = self._state_filesize(opcode, data)
				elif state == self.ST_FILESEGM:
					state = self._state_filesegm(opcode, data)
				elif state == self.ST_FILESEGD:
					state = self._state_filesegd(opcode, data)
				else:	
					raise StateError("Invalid state")
			# ST_FINISH state
			self._filewriter.finish()
		except (ProtocolError, StateError) as e:
			logging.error("%s: %s", self._addr, e.msg)
			if e.response is not None:
				self._send_msg(e.response.encode())
		else:
			self._send_msg(b"Finished")
		finally:
			self._send_msg(None, self.OP_CLOS)

	def handle_invalid_url(self):
		"""Handle a websocket request for an invalid URL."""
		self._send_header()
		self._send_msg("Invalid: -1")
		self._send_msg(None, self.OP_CLOS)
		
	# Send the header responding to the websocket connection request
	def _send_header(self):
		header = ("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n"
							"Connection: Upgrade\r\nSec-WebSocket-Accept: " +
							self._url.accept_key + "\r\n\r\n")
		self._conn.sendall(header.encode())

	# Handle the ST_FILESIZE state
	def _state_filesize(self, opcode, data):
		# Sanity check the frame
		if opcode != self.OP_TEXT:
			raise ProtocolError("Received non-text frame while waiting for filesize",
													"Invalid: -1")
		elif not data.startswith(b"Filesize: "):
			raise ProtocolError("Received invalid text while waiting for filesize",
													"Invalid: -1")

		# Retrieve the file size
		filesize = data.decode("ascii").replace("Filesize: ", "", 1).strip()
		if not filesize.isdigit():
			raise ProtocolError("Invalid filesize received", "Invalid: -1")
		# Make sure the upload is to a permitted destination
		elif (self._permitted_upload_dir is not None and 
				not self._url.filename.startswith(self._permitted_upload_dir)):
			raise ProtocolError("Invalid upload directory, upload denied",
													"Not Permitted: -2")
		# Now check if the file exists
		elif not os.path.isfile(self._url.filename):
			# We're go for file upload!
			self._send_msg(b"Permitted")
			self._filewriter = FileWriter(self._url.filename, int(filesize))
		else:
			# File already exists - let the client know it messed up
			raise ProtocolError("File already exists, upload denied",
													"Not Permitted: -1")
		return self.ST_FILESEGM

	# Handle the ST_FILESEGM state
	def _state_filesegm(self, opcode, data):
		if opcode != self.OP_TEXT:
			raise ProtocolError("Received non-text frame while waiting "
													"for segment metadata", "Invalid: -1")
		state = self.ST_FILESEGM
		if data.startswith(b"File Finish"):
			# The sender indicated that the file is done
			state = self.ST_FINISH
		elif data.startswith(b"Segment Start:"):
			#TODO: Parse out, verify, the segment start and finish data here
			# A file data segment will follow
			state = self.ST_FILESEGD
		else:
			raise ProtocolError("Received invalid segment metadata",
													"Invalid: -1")
		return state

	# Handle the ST_FILESEGD state
	def _state_filesegd(self, opcode, data):
		if opcode != self.OP_BIN and opcode != self.OP_TEXT:
			raise ProtocolError("Invalid opcode while waiting for file data",
													"Invalid: -1")
		if opcode == self.OP_TEXT:
			# In this case, the data is base64 encoded.  Decode it
			data = base64.b64decode(bytes(data))
		self._filewriter.append(data)
		if self._filewriter.test_size() > 0:
			raise ProtocolError("Received data after file met specified size",
													"Segment Error: -1")
		return self.ST_FILESEGM

	# Send a message over the websocket connection
	def _send_msg(self, data, opcode=None):
		if opcode == None:
			opcode = self.OP_TEXT
		# Start with a final message, blank opcode, no masking, 0 data length
		frame = bytearray(b"\x80\x00")
		# Place the opcode
		frame[0] = frame[0] | opcode
		# Place the size of the data segment
		if data is None:
			pass
		elif len(data) < 126:
			frame[1] = frame[1] | len(data)
		elif len(data) < 65535:
			frame[1] = frame[1] | 126
			packed = struct.pack(">I", len(data))
			frame.extend(packed)
		else:
			frame[1] = frame[1] | 127
			packed = struct.pack(">Q", len(data))
			frame.extend(packed)
		# Append the data
		if data is not None:
			frame.extend(data)
		# Send the frame
		self._conn.sendall(frame)

	# Receive a chunk of data into the buffer.  Get at least "amount" of data
	def _recv_data(self, amount, maxtimeouts=2, maxtries=1000):
		tocount = 0
		tries = 0
		while (len(self._curbuf) < amount and tocount < maxtimeouts and
					 tries < maxtries):
			tries += 1
			try:
				self._curbuf.extend(bytearray(self._conn.recv(1024*256)))
			except socket.error:
				tocount += 1
		if len(self._curbuf) < amount:
			raise ProtocolError("Socket timeout waiting for frame header",
													"Timeout: -1")

	# Return a byte array representing one single websocket frame
	def _get_frame(self):
		# TODO: handle fin set case - namely, if it's not set
		# Receive the header
		self._recv_data(2)
		headerlen = self._get_headerlen(self._curbuf)
		self._recv_data(headerlen)
		datalen = self._get_datalen(self._curbuf)
		framesize = headerlen + datalen

		# Receive the entire frame
		self._recv_data(framesize)
		frame = self._curbuf[0:framesize]
		del self._curbuf[0:framesize]
		opcode = self._get_opcode(frame)
		data = frame[headerlen:]
		# If there's a mask bit, unmask the frame
		if self._test_mask(frame):
			maskpos = headerlen - 4
			mask = frame[maskpos:(maskpos+4)]
			for i, byte in enumerate(data):
				data[i] = byte ^ mask[i%4]
		else:
			raise ProtocolError("Mask bit was not set", "Invalid: -1")
		return opcode, data

	# Return true if the mask bit is set
	def _test_mask(self, data):
		if len(data) >= 2 and data[1] & 0x80:
			return True
		return False

	# Return true if the fin bit is set
	def _test_fin(self, data):
		if len(data) >= 1 and data[0] & 0x80:
			return True
		return False

	# Return true if the specified rsv bit is set
	def _test_rsv(self, data, index=1):
		if (len(data) >= 1 and index > 1 and index < 3 and
				data[0] & (0x80 >> index)):
			return True
		return False

	# Return the value in the length field
	def _get_length_field(self, data):
		if len(data) < 2:
			raise ProtocolError("Invalid header", "Invalid: -1")
		# Remove the mask bit...
		return int(data[1] & 0x7f)

	# Determine the frame's opcode
	def _get_opcode(self, data):
		if len(data) < 1:
			raise StateError("Not enough data to get opcode")
		val = data[0] & 0x0f  # Remove the fin and rsv bits
		opcode = self.OP_OTH
		if val == self.OP_CONT: opcode = self.OP_CONT
		elif val == self.OP_TEXT: opcode = self.OP_TEXT
		elif val == self.OP_BIN: opcode = self.OP_BIN
		elif val == self.OP_CLOS: opcode = self.OP_CLOS
		elif val == self.OP_PING: opcode = self.OP_PING
		elif val == self.OP_PONG: opcode = self.OP_PONG
		else: opcode = self.OP_OTH
		return opcode

	# Determine the frame's length based on the header
	def _get_datalen(self, data):
		length = self._get_length_field(data)
		# The length field has magic values 126 and 127.  Handle them
		if length == 126:
			if len(data) < 4:
				raise ProtocolError("Invalid header", "Invalid: -1")
			# Unpack bytes 2 and 3 into an integer.  Unpack returns a tuple
			length, = struct.unpack(">H", bytes(data[2:4]))
		elif length == 127:
			if len(data) < 10:
				raise ProtocolError("Invalid header", "Invalid: -1")
			# Unpack bytes 2 thru 9 into an integer.  Unpack returns a tuple
			length, = struct.unpack(">Q", bytes(data[2:10]))
		return length

	# Determine the header's length based on the header
	def _get_headerlen(self, data):
		length = self._get_length_field(data)
		headerlen = 2
		# The length field has magic values 126 and 127.  Handle them
		if length == 126:
			headerlen = 4  # Length field is 3 bytes
		elif length == 127:
			headerlen = 10  # Length field is 9 bytes
		if self._test_mask(data):
			headerlen += 4  # Compensate for mask size
		return headerlen