Beispiel #1
0
    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
Beispiel #2
0
    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)
Beispiel #3
0
    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)
                     })
Beispiel #4
0
    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)
Beispiel #5
0
    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))
Beispiel #6
0
    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
Beispiel #7
0
    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)
Beispiel #8
0
    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.")
Beispiel #9
0
 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))
Beispiel #10
0
 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)
Beispiel #11
0
    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")))
Beispiel #12
0
    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))
Beispiel #13
0
    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(
                    " ", "&nbsp;"))

        self.write(""
                   "</body>"
                   "</html>")
Beispiel #14
0
    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
Beispiel #15
0
            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!")
Beispiel #16
0
    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))
Beispiel #17
0
    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))
Beispiel #18
0
    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
Beispiel #19
0
 def test_ensure_str_from_none(self):
     self.assertEqual("", ensure_str(None))
Beispiel #20
0
 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))
Beispiel #21
0
 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)
Beispiel #22
0
 def test_ensure_str_from_other(self):
     self.assertIsInstance(ensure_str(object()), str)
Beispiel #23
0
 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)
Beispiel #24
0
    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
Beispiel #25
0
 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))