def body_args(self) -> Union[TolerantMagicDict, HTTPMultipartBody, Mapping[Any, Any], List[Any]]: """ Parse body arguments and return body arguments in a proper instance. """ if not hasattr(self, "_body_args"): content_type = self.headers.get_first("content-type") if content_type.lower().strip() in ( "application/x-www-form-urlencoded", "application/x-url-encoded"): self._body_args = TolerantMagicDict( urllib.parse.parse_qsl( ensure_str(self.body), keep_blank_values=True, strict_parsing=True)) elif content_type.lower().startswith( "multipart/form-data"): self._body_args = HTTPMultipartBody.parse( content_type=content_type, data=self.body) elif content_type.lower().strip() == "application/json": self._body_args = json.loads(ensure_str(self.body)) else: # Unknown Content Type. raise ProtocolError("Unknown Body Type.") return self._body_args
def set_secure_cookie(self, name: str, value: str, expires_days: int=30, **kwargs): """ Set a secure cookie. By default, FutureFinity will use AES GCM Security Object as the backend of secure cookie. You must set a security_secret in Application Settings before you use this method. It can be generated by:: futurefinity.security.get_random_str(length=32) Once security_secret is generated, treat it like a password, change security_secret will cause all secure_cookie become invalid. :arg name: is the name of the secure cookie. :arg value: is the value of the secure cookie. :arg expires_days: is the lifetime of the cookie. :arg \*\*kwargs: all the other keyword arguments will be passed to ``RequestHandler.set_cookie``. """ if "security_secret" not in self.settings.keys(): raise ValueError( "Cannot found security_secret. " "Please provide security_secret through Application Settings.") content = self.app.security_object.generate_secure_text(value) self.set_cookie(ensure_str(name), ensure_str(content), expires_days=expires_days, **kwargs)
def redirect(self, url: str, permanent: bool=False, status: typing.Optional[int]=None): """ Rediect request to other location. :arg url: is the relative url or absolute url that the client will be redirected to. :arg permanent: True if this is 301 or 302. :arg status: Custom the status code. """ if self._initial_written: raise HTTPError(400, "Cannot redirect after initial written.") if status is None: status = 301 if permanent else 302 else: assert isinstance(status, int) and 300 <= status <= 399 self._status_code = status self.set_header("location", ensure_str(url)) self.finish("<!DOCTYPE HTML>" "<html>" "<head>" " <meta charset=\"utf-8\">" " <title>%(status_code)d %(status_message)s</title>" "</head>" "<body>" " <h1>%(status_code)d %(status_message)s</h1>" " The document has moved <a href=\"%(url)s\">here</a>." "</body>" "</html>" % { "status_code": status, "status_message": protocol.status_code_text[status], "url": ensure_str(url) })
def lookup_origin_text( self, secure_text: str, valid_length: Optional[int]=None) -> str: try: encrypted_text_reader = io.BytesIO(base64.b64decode(secure_text)) except: # Unable to decode the data. return None try: iv = encrypted_text_reader.read(16) length = struct.unpack("l", encrypted_text_reader.read(8))[0] ciphertext = encrypted_text_reader.read(length) tag = encrypted_text_reader.read(16) except: # Unable to split the data. return None decryptor = AESCipher( aes_algorithms.AES(self._security_secret), aes_modes.GCM(iv, tag), backend=aes_backend() ).decryptor() try: content = decryptor.update(ciphertext) + decryptor.finalize() except: # Unable to decrypt and/or verify the data. return None timestamp = struct.unpack("l", content[:8])[0] text = content[8:] if valid_length and int(time.time()) - timestamp > valid_length: return None # Data Expired. return ensure_str(text)
def generate_secure_text(self, origin_text: str) -> str: """ Encrypt and sign the origin text by ``cryptography`` library using AES GCM algorithm. :arg origin_text: is a string that will be encrypted by the provided security secret. """ iv = os.urandom(16) content = struct.pack( "l", int(time.time())) + ensure_bytes(origin_text) encryptor = AESCipher( aes_algorithms.AES(ensure_bytes(self.__security_secret)), aes_modes.GCM(iv), backend=aes_backend() ).encryptor() ciphertext = encryptor.update(content) + encryptor.finalize() final_encrypted_text = iv final_encrypted_text += struct.pack("l", len(ciphertext)) final_encrypted_text += ciphertext final_encrypted_text += encryptor.tag return ensure_str(base64.b64encode(final_encrypted_text))
def assemble(self) -> Tuple[bytes, str]: """ Generate HTTP v1 Body to bytes. It will return the body in bytes and the content-type in str. """ body = b"" boundary = "----------FutureFinityFormBoundary" boundary += ensure_str(security.get_random_str(8)).lower() content_type = "multipart/form-data; boundary=" + boundary full_boundary = b"--" + ensure_bytes(boundary) for field_name, field_value in self.items(): body += full_boundary + _CRLF_BYTES_MARK if isinstance(field_value, str): body += b"Content-Disposition: form-data; " body += ensure_bytes("name=\"%s\"\r\n" % field_name) body += _CRLF_BYTES_MARK body += ensure_bytes(field_value) body += _CRLF_BYTES_MARK else: raise ProtocolError("Unknown Field Type") for file_field in self.files.values(): body += full_boundary + _CRLF_BYTES_MARK body += file_field.assemble() body += full_boundary + b"--" + _CRLF_BYTES_MARK return body, content_type
def lookup_origin_text(self, secure_text: str, valid_length: Optional[int]=None) -> str: try: signed_text_reader = io.BytesIO(base64.b64decode(secure_text)) except: # Unable to decode base64 into bytes. return None try: iv = signed_text_reader.read(16) length = struct.unpack("l", signed_text_reader.read(8))[0] content = signed_text_reader.read(length) signature = signed_text_reader.read(32) except: # Unable to split the secure_text. return None hash = hmac.new(iv + self._security_secret, digestmod=hashlib.sha256) hash.update(content) if not hmac.compare_digest(signature, hash.digest()): return None # Signature Mismatch. timestamp = struct.unpack("l", content[:8])[0] text = content[8:] if valid_length and int(time.time()) - timestamp > valid_length: return None # Data Expired return ensure_str(text)
def load_headers(self, data: Union[str, bytes, list, TolerantMagicDict]): """ Load HTTP Headers from another object. It will raise an Error if the header is invalid. """ # For dict-like object. if hasattr(data, "items"): for (key, value) in data.items(): self.add(key.strip(), value.strip()) return if isinstance(data, (str, bytes)): # For string-like object. splitted_data = ensure_str(data).split(_CRLF_MARK) for header in splitted_data: if not header: continue (key, value) = header.split(":", 1) self.add(key.strip(), value.strip()) return # For list-like object. if hasattr(data, "__iter__"): for (key, value) in data: self.add(key.strip(), value.strip()) return raise ValueError("Unknown Type of input data.")
def add_header(self, name: str, value: str): """ Add a response header with the name and value, this will not override any former value(s) with the same name. """ if self._initial_written: raise HTTPError(500, "You cannot add a new header after the " "initial is written.") self._headers.add(name, ensure_str(value))
def set_header(self, name: str, value: str): """ Set a response header with the name and value, this will override any former value(s) with the same name. """ if self._initial_written: raise HTTPError(500, "You cannot set a new header after the " "initial is written.") self._headers[name] = ensure_str(value)
def test_jinja2_template_request(self): @self.app.add_handler("/template_test") class TestHandler(futurefinity.web.RequestHandler): @render_template("main.htm") async def get(self, *args, **kwargs): return {"name": "John Smith"} server = self.app.listen(8888) async def get_requests_result(self): try: self.requests_result = await self.loop.run_in_executor( None, functools.partial( requests.get, "http://127.0.0.1:8888/template_test" ) ) except: traceback.print_exc() finally: server.close() await server.wait_closed() self.loop.stop() asyncio.ensure_future(get_requests_result(self)) self.loop.run_forever() jinja2_envir = jinja2.Environment(loader=jinja2.FileSystemLoader( "examples/template", encoding="utf-8" )) template = jinja2_envir.get_template("main.htm") self.assertEqual(self.requests_result.status_code, 200, "Wrong Status Code") self.assertEqual(ensure_str(self.requests_result.text), ensure_str(template.render(name="John Smith")))
def generate_secure_text(self, origin_text: str) -> str: if not isinstance(origin_text, str): raise TypeError("origin_text should be a string.") iv = os.urandom(16) content = struct.pack("l", int(time.time())) content += ensure_bytes(origin_text) hash = hmac.new(iv + self._security_secret, digestmod=hashlib.sha256) hash.update(content) signature = hash.digest() final_signed_text = iv final_signed_text += struct.pack("l", len(content)) final_signed_text += content final_signed_text += signature return ensure_str(base64.b64encode(final_signed_text))
def write_error(self, error_code: int, message: typing.Optional[typing.Union[str, bytes]]=None, exc_info: typing.Optional[tuple]=None): """ Respond an error to client. You may override this page if you want to custom the error page. """ self._status_code = error_code self.set_header("Content-Type", "text/html; charset=UTF-8") if self._status_code >= 400: self.set_header("Connection", "Close") self.write("<!DOCTYPE HTML>" "<html>" "<head>" " <meta charset=\"UTF-8\">" " <title>%(error_code)d: %(status_code_detail)s</title>" "</head>" "<body>" " <div>%(error_code)d: %(status_code_detail)s</div>" % { "error_code": error_code, "status_code_detail": protocol.status_code_text[ error_code] }, clear_text=True) if message: self.write("" " <div>%(message)s</div>" % { "message": ensure_str(message)}) if self.settings.get("debug", False) and exc_info: print(self.request, file=sys.stderr) traceback.print_exception(*exc_info) for line in traceback.format_exception(*exc_info): self.write(" <div>%s</div>" % html.escape(line).replace( " ", " ")) self.write("" "</body>" "</html>")
def lookup_origin_text(self, secure_text: str, valid_length: int=None) -> str: """ Decrypt the encrypted text, validate the signature and return the origin text. If the content and the signature mismatches, ``None`` will be returned. :arg secure_text: is a base64 encoded string, which contains the generated time, the content, and a AES GCM tag. :arg valid_length: the length that the content should be valid. The unit is second. It you want the content be always valid, set the length to ``None``. """ encrypted_text_reader = io.BytesIO(base64.b64decode(secure_text)) iv = encrypted_text_reader.read(16) length = struct.unpack("l", encrypted_text_reader.read(8))[0] ciphertext = encrypted_text_reader.read(length) tag = encrypted_text_reader.read(16) decryptor = AESCipher( aes_algorithms.AES(ensure_bytes(self.__security_secret)), aes_modes.GCM(iv, tag), backend=aes_backend() ).decryptor() try: content = decryptor.update(ciphertext) + decryptor.finalize() except: return None timestamp = struct.unpack("l", content[:8])[0] text = content[8:] if valid_length and int(time.time()) - timestamp > valid_length: return None try: return ensure_str(text) except: return None
def do_POST(server): content_type = server.headers.get("Content-Type") content_length = server.headers.get("Content-Length") if content_type.lower().find("json") == -1: body = cgi.FieldStorage(fp=server.rfile, environ={ "REQUEST_METHOD": "POST", "CONTENT_TYPE": content_type, "CONTENT_LENGTH": content_length }) else: body = json.loads( ensure_str(server.rfile.read(int(content_length)))) self.received_body = body server.send_response(http.server.HTTPStatus.OK) server.send_header("Content-Length", "13") server.send_header("Content-Type", "text/plain") server.end_headers() server.wfile.write(b"Hello, World!")
def generate_secure_text(self, origin_text: str) -> str: """ Generate a signed text by ``hmac`` library using sha256 algorithm. :arg origin_text: is a string that will be signed by the provided security secret. """ iv = os.urandom(16) content = struct.pack( "l", int(time.time())) + ensure_bytes(origin_text) hash = hmac.new(iv + ensure_bytes(self.__security_secret), digestmod=hashlib.sha256) hash.update(ensure_bytes(content)) signature = hash.digest() final_signed_text = iv final_signed_text += struct.pack("l", len(content)) final_signed_text += content final_signed_text += signature return ensure_str(base64.b64encode(final_signed_text))
def generate_secure_text(self, origin_text: str) -> str: if not isinstance(origin_text, str): raise TypeError("origin_text should be a string.") iv = os.urandom(16) content = struct.pack("l", int(time.time())) content += ensure_bytes(origin_text) encryptor = AESCipher( aes_algorithms.AES(self._security_secret), aes_modes.GCM(iv), backend=aes_backend() ).encryptor() ciphertext = encryptor.update(content) + encryptor.finalize() final_encrypted_text = iv final_encrypted_text += struct.pack("l", len(ciphertext)) final_encrypted_text += ciphertext final_encrypted_text += encryptor.tag return ensure_str(base64.b64encode(final_encrypted_text))
def lookup_origin_text(self, secure_text: str, valid_length: int=None) -> str: """ Validate the signed text and return the origin text. If the content and the signature mismatches, ``None`` will be returned. :arg secure_text: is a base64 encoded string, which contains the generated time, the content, and a sha256 signature. :arg valid_length: the length that the content should be valid. The unit is second. It you want the content be always valid, set the length to ``None``. """ signed_text_reader = io.BytesIO(base64.b64decode(secure_text)) iv = signed_text_reader.read(16) length = struct.unpack("l", signed_text_reader.read(8))[0] content = signed_text_reader.read(length) signature = signed_text_reader.read(32) hash = hmac.new(iv + ensure_bytes(self.__security_secret), digestmod=hashlib.sha256) hash.update(ensure_bytes(content)) if not hmac.compare_digest(signature, hash.digest()): return None timestamp = struct.unpack("l", content[:8])[0] text = content[8:] if valid_length and int(time.time()) - timestamp > valid_length: return None try: return ensure_str(text) except: return None
def test_ensure_str_from_none(self): self.assertEqual("", ensure_str(None))
def add_header(self, name: str, value: str): """ Add a response header with the name and value, this will not override any former value(s) with the same name. """ self._headers.add(name, ensure_str(value))
def set_header(self, name: str, value: str): """ Set a response header with the name and value, this will override any former value(s) with the same name. """ self._headers[name] = ensure_str(value)
def test_ensure_str_from_other(self): self.assertIsInstance(ensure_str(object()), str)
def test_ensure_str_from_str(self): random_str = futurefinity.security.get_random_str(10) ensured_str = ensure_str(random_str) self.assertEqual(random_str, ensured_str)
def _parse_initial(self): initial_end = self._pending_bytes.find(_CRLF_BYTES_MARK * 2) if initial_end == -1: if len(self._pending_bytes) > self.max_initial_length: raise ConnectionEntityTooLarge( "Initial Exceed its Maximum Length.") return initial_end += 2 if initial_end > self.max_initial_length: raise ConnectionEntityTooLarge( "Initial Exceed its Maximum Length.") return pending_initial = ensure_bytes(self._pending_bytes[:initial_end]) del self._pending_bytes[:initial_end + 2] basic_info, origin_headers = ensure_str(pending_initial).split( _CRLF_MARK, 1) basic_info = basic_info.split(" ") if self.is_client: http_version = basic_info[0] if not basic_info[1].isdecimal(): raise ConnectionBadMessage("Bad Initial Received.") self._parsed_incoming_info["status_code"] = int(basic_info[1]) else: if len(basic_info) != 3: raise ConnectionBadMessage("Bad Initial Received.") method, origin_path, http_version = basic_info self._parsed_incoming_info["method"] = basic_info[0] self._parsed_incoming_info["origin_path"] = basic_info[1] if http_version.lower() == "http/1.1": self.http_version = 11 elif http_version.lower() == "http/1.0": self.http_version = 10 else: raise ConnectionBadMessage("Unknown HTTP Version.") self._parsed_incoming_info["http_version"] = self.http_version try: headers = HTTPHeaders.parse(origin_headers) except: raise ConnectionBadMessage("Bad Headers Received.") if self._can_keep_alive and "connection" in headers: self._use_keep_alive = headers.get_first( "connection").lower() == "keep-alive" self._parsed_incoming_info["headers"] = headers if self.is_client: try: self.incoming = HTTPIncomingResponse( **self._parsed_incoming_info) except: raise ConnectionBadMessage("Bad Initial Received.") else: try: self.incoming = HTTPIncomingRequest( **self._parsed_incoming_info, connection=self) except: raise ConnectionBadMessage("Bad Initial Received.") self.stage = _CONN_INITIAL_PARSED
def test_ensure_str_from_bytearray(self): random_str = futurefinity.security.get_random_str(10) encoded_bytearray = bytearray(random_str.encode()) self.assertEqual(random_str, ensure_str(encoded_bytearray))