<reponame>jsspric/aws-deepracer-webserver-pkg
#################################################################################
#   Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.          #
#                                                                               #
#   Licensed under the Apache License, Version 2.0 (the "License").             #
#   You may not use this file except in compliance with the License.            #
#   You may obtain a copy of the License at                                     #
#                                                                               #
#       http://www.apache.org/licenses/LICENSE-2.0                              #
#                                                                               #
#   Unless required by applicable law or agreed to in writing, software         #
#   distributed under the License is distributed on an "AS IS" BASIS,           #
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.    #
#   See the License for the specific language governing permissions and         #
#   limitations under the License.                                              #
#################################################################################

"""
login.py

This is the module that manages the authentication, routing and login/logout
functionality for the console.
"""

import datetime
import hmac
import os
import uuid
import hashlib
from flask import (Blueprint,
                   request,
                   make_response,
                   render_template,
                   flash,
                   jsonify)

from webserver_pkg.constants import (TOKEN_PATH,
                                     PASSWORD_PATH,
                                     DEEPRACER_TOKEN,
                                     DESTINATION_PATH,
                                     DEFAULT_PASSWORD_PATH)

from webserver_pkg import webserver_publisher_node


LOGIN_BLUEPRINT = Blueprint("login", __name__)


@LOGIN_BLUEPRINT.route("/", methods=["GET"])
@LOGIN_BLUEPRINT.route("/home", methods=["GET"])
def home_page():
    """API to load home page when if authenticated else redirect to login page.

    Returns:
        flask.Response: Response object with login page or home page details.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    status, _ = check_authentication()
    if status:
        webserver_node.get_logger().info("Authenticated")
        return render_template("index.html")
    else:
        resp = make_response(render_template("login.html"), 200)
        return resp


@LOGIN_BLUEPRINT.route("/login", methods=["GET", "POST"])
def login():
    """API to load login page when requested as GET request and authenticate
       with password passed as parameter when requested as POST request.

    Returns:
        flask.Response: Response object with login page or home page details.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()
    webserver_node.get_logger().info(f"Called /login with {request.method}")
    if request.method == "POST":
        response = make_response(jsonify({"redirect": "/home"}), 200)
        try:
            with open(PASSWORD_PATH, "r") as file_ptr:
                pwd = file_ptr.readline()
            user_provided_pwd_hash = \
                str(compute_password_digest(request.form["password"].encode("utf-8")))
            if hmac.compare_digest(user_provided_pwd_hash, str(pwd)):
                webserver_node.get_logger().info("Password check passed")
                token = str(uuid.uuid4())
                try:
                    with open(TOKEN_PATH, "w") as token_file:
                        token_file.write(token)
                    webserver_node.get_logger().info("Token set")
                    response.set_cookie("deepracer_token",
                                        token,
                                        max_age=datetime.timedelta(hours=1),
                                        secure=True)
                    return response
                except IOError:
                    webserver_node.get_logger().error("Token path incorrect")
                    flash("Token path incorrect")
                    return "failure"
            else:
                webserver_node.get_logger().error("Incorrect password. Try again.")
                flash("Incorrect password. Try again.")
            return "failure"
        except IOError:
            webserver_node.get_logger().error("File not found")
            reset_default()
            flash("Password has been reset to default. "
                  "Please look at your the bottom of your vehicle for the default password.")
            return "failure"
    resp = make_response(render_template("login.html"), 200)
    return resp


@LOGIN_BLUEPRINT.route("/auth", methods=["POST", "GET"])
def auth():
    """Main authorization API which is always called from nginx to check the
       authentication for all requests. This uses the cookie as token to provide access.

    Returns:
        tuple: A tuple of response object and return status code.
    """
    if request.content_type == "application/json":
        resp = jsonify("")
    else:
        resp = make_response("")
    status, cookie_val = check_authentication()
    if not status:
        return resp, 401
    resp.set_cookie("deepracer_token",
                    cookie_val,
                    max_age=datetime.timedelta(hours=1),
                    secure=True)
    return resp, 200


@LOGIN_BLUEPRINT.route("/logout", methods=["GET"])
@LOGIN_BLUEPRINT.route("/redirect_login", methods=["GET", "POST"])
def logout():
    """API to redirect to login page with reset deepracer_token in the cookie.

    Returns:
        flask.Response: Response object with login page details.
    """
    response = make_response(render_template("login.html"), 200)
    response.set_cookie("deepracer_token", expires=0)
    return response


@LOGIN_BLUEPRINT.route("/api/password", methods=["POST", "GET"])
def update_password_api():
    """API that updates the console password to the new one sent as a parameter.

    Returns:
        dict: Execution status if the API call was successful, flag to indicate
              if password update was successful and error reason if call fails.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    old_password = request.json.get("old_password").encode("utf-8")
    new_password = request.json.get("new_password").encode("utf-8")

    try:
        with open(PASSWORD_PATH, "r") as pwd_file:
            password_digest = pwd_file.readline()

        if hmac.compare_digest(compute_password_digest(old_password), password_digest):
            with open(PASSWORD_PATH, "w") as pwd_file:
                pwd_file.write(compute_password_digest(new_password))
                pwd_file.close()
            return jsonify({"success": True})
        else:
            return jsonify({"success": False,
                            "reason": "The password is incorrect. Provide your current password."})
    except IOError:
        webserver_node.get_logger().error("Password.txt not found in given path")
        return jsonify({"success": False, "reason": "Password.txt not found."})


def check_authentication():
    """Helper function to validate the token sent in the request object.

    Returns:
        tuple: A tuple of validation flag and cookie value.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()
    if not os.path.exists(TOKEN_PATH):
        webserver_node.get_logger().error("Token path not found")
        return False, None

    token = ""
    with open(TOKEN_PATH, "r") as file_ptr:
        token = file_ptr.readline()

    cookie_val = str(request.cookies.get(DEEPRACER_TOKEN))
    status = True if(hmac.compare_digest(cookie_val, token)) else False
    webserver_node.get_logger().info(f"Cookie compare status: {status}")
    return status, cookie_val


def compute_password_digest(message):
    """Helper method to compute the message digest for the given string.
    """
    return hashlib.sha224(message).hexdigest()


def reset_default():
    """Helper method to reset the password to the default password found on vehicle.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    if os.path.exists(DEFAULT_PASSWORD_PATH):
        webserver_node.get_logger().info("Default password file found")
        with open(DEFAULT_PASSWORD_PATH, "r") as pwd_file:
            default_pass = pwd_file.readline().strip()
    else:
        default_pass = "<PASSWORD>"
        webserver_node.get_logger().info("Default password file not found")
    with open(DESTINATION_PATH, "w") as pwd_file:
        pwd_file.write(compute_password_digest(default_pass))
    webserver_node.get_logger().info("Password reset to default")

#################################################################################
#   Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.          #
#                                                                               #
#   Licensed under the Apache License, Version 2.0 (the "License").             #
#   You may not use this file except in compliance with the License.            #
#   You may obtain a copy of the License at                                     #
#                                                                               #
#       http://www.apache.org/licenses/LICENSE-2.0                              #
#                                                                               #
#   Unless required by applicable law or agreed to in writing, software         #
#   distributed under the License is distributed on an "AS IS" BASIS,           #
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.    #
#   See the License for the specific language governing permissions and         #
#   limitations under the License.                                              #
#################################################################################

"""
login.py

This is the module that manages the authentication, routing and login/logout
functionality for the console.
"""

import datetime
import hmac
import os
import uuid
import hashlib
from flask import (Blueprint,
                   request,
                   make_response,
                   render_template,
                   flash,
                   jsonify)

from webserver_pkg.constants import (TOKEN_PATH,
                                     PASSWORD_PATH,
                                     DEEPRACER_TOKEN,
                                     DESTINATION_PATH,
                                     DEFAULT_PASSWORD_PATH)

from webserver_pkg import webserver_publisher_node


LOGIN_BLUEPRINT = Blueprint("login", __name__)


@LOGIN_BLUEPRINT.route("/", methods=["GET"])
@LOGIN_BLUEPRINT.route("/home", methods=["GET"])
def home_page():
    """API to load home page when if authenticated else redirect to login page.

    Returns:
        flask.Response: Response object with login page or home page details.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    status, _ = check_authentication()
    if status:
        webserver_node.get_logger().info("Authenticated")
        return render_template("index.html")
    else:
        resp = make_response(render_template("login.html"), 200)
        return resp


@LOGIN_BLUEPRINT.route("/login", methods=["GET", "POST"])
def login():
    """API to load login page when requested as GET request and authenticate
       with password passed as parameter when requested as POST request.

    Returns:
        flask.Response: Response object with login page or home page details.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()
    webserver_node.get_logger().info(f"Called /login with {request.method}")
    if request.method == "POST":
        response = make_response(jsonify({"redirect": "/home"}), 200)
        try:
            with open(PASSWORD_PATH, "r") as file_ptr:
                pwd = file_ptr.readline()
            user_provided_pwd_hash = \
                str(compute_password_digest(request.form["password"].encode("utf-8")))
            if hmac.compare_digest(user_provided_pwd_hash, str(pwd)):
                webserver_node.get_logger().info("Password check passed")
                token = str(uuid.uuid4())
                try:
                    with open(TOKEN_PATH, "w") as token_file:
                        token_file.write(token)
                    webserver_node.get_logger().info("Token set")
                    response.set_cookie("deepracer_token",
                                        token,
                                        max_age=datetime.timedelta(hours=1),
                                        secure=True)
                    return response
                except IOError:
                    webserver_node.get_logger().error("Token path incorrect")
                    flash("Token path incorrect")
                    return "failure"
            else:
                webserver_node.get_logger().error("Incorrect password. Try again.")
                flash("Incorrect password. Try again.")
            return "failure"
        except IOError:
            webserver_node.get_logger().error("File not found")
            reset_default()
            flash("Password has been reset to default. "
                  "Please look at your the bottom of your vehicle for the default password.")
            return "failure"
    resp = make_response(render_template("login.html"), 200)
    return resp


@LOGIN_BLUEPRINT.route("/auth", methods=["POST", "GET"])
def auth():
    """Main authorization API which is always called from nginx to check the
       authentication for all requests. This uses the cookie as token to provide access.

    Returns:
        tuple: A tuple of response object and return status code.
    """
    if request.content_type == "application/json":
        resp = jsonify("")
    else:
        resp = make_response("")
    status, cookie_val = check_authentication()
    if not status:
        return resp, 401
    resp.set_cookie("deepracer_token",
                    cookie_val,
                    max_age=datetime.timedelta(hours=1),
                    secure=True)
    return resp, 200


@LOGIN_BLUEPRINT.route("/logout", methods=["GET"])
@LOGIN_BLUEPRINT.route("/redirect_login", methods=["GET", "POST"])
def logout():
    """API to redirect to login page with reset deepracer_token in the cookie.

    Returns:
        flask.Response: Response object with login page details.
    """
    response = make_response(render_template("login.html"), 200)
    response.set_cookie("deepracer_token", expires=0)
    return response


@LOGIN_BLUEPRINT.route("/api/password", methods=["POST", "GET"])
def update_password_api():
    """API that updates the console password to the new one sent as a parameter.

    Returns:
        dict: Execution status if the API call was successful, flag to indicate
              if password update was successful and error reason if call fails.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    old_password = request.json.get("old_password").encode("utf-8")
    new_password = request.json.get("new_password").encode("utf-8")

    try:
        with open(PASSWORD_PATH, "r") as pwd_file:
            password_digest = pwd_file.readline()

        if hmac.compare_digest(compute_password_digest(old_password), password_digest):
            with open(PASSWORD_PATH, "w") as pwd_file:
                pwd_file.write(compute_password_digest(new_password))
                pwd_file.close()
            return jsonify({"success": True})
        else:
            return jsonify({"success": False,
                            "reason": "The password is incorrect. Provide your current password."})
    except IOError:
        webserver_node.get_logger().error("Password.txt not found in given path")
        return jsonify({"success": False, "reason": "Password.txt not found."})


def check_authentication():
    """Helper function to validate the token sent in the request object.

    Returns:
        tuple: A tuple of validation flag and cookie value.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()
    if not os.path.exists(TOKEN_PATH):
        webserver_node.get_logger().error("Token path not found")
        return False, None

    token = ""
    with open(TOKEN_PATH, "r") as file_ptr:
        token = file_ptr.readline()

    cookie_val = str(request.cookies.get(DEEPRACER_TOKEN))
    status = True if(hmac.compare_digest(cookie_val, token)) else False
    webserver_node.get_logger().info(f"Cookie compare status: {status}")
    return status, cookie_val


def compute_password_digest(message):
    """Helper method to compute the message digest for the given string.
    """
    return hashlib.sha224(message).hexdigest()


def reset_default():
    """Helper method to reset the password to the default password found on vehicle.
    """
    webserver_node = webserver_publisher_node.get_webserver_node()

    if os.path.exists(DEFAULT_PASSWORD_PATH):
        webserver_node.get_logger().info("Default password file found")
        with open(DEFAULT_PASSWORD_PATH, "r") as pwd_file:
            default_pass = pwd_file.readline().strip()
    else:
        default_pass = "<PASSWORD>"
        webserver_node.get_logger().info("Default password file not found")
    with open(DESTINATION_PATH, "w") as pwd_file:
        pwd_file.write(compute_password_digest(default_pass))
    webserver_node.get_logger().info("Password reset to default")
################################################################################# #   Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.          # #                                                                               # #   Licensed under the Apache License, Version 2.0 (the "License").             # #   You may not use this file except in compliance with the License.            # #   You may obtain a copy of the License at                                     # #                                                                               # #       http://www.apache.org/licenses/LICENSE-2.0                              # #                                                                               # #   Unless required by applicable law or agreed to in writing, software         # #   distributed under the License is distributed on an "AS IS" BASIS,           # #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.    # #   See the License for the specific language governing permissions and         # #   limitations under the License.                                              # ################################################################################# login.py  This is the module that manages the authentication, routing and login/logout functionality for the console. API to load home page when if authenticated else redirect to login page.      Returns:         flask.Response: Response object with login page or home page details. API to load login page when requested as GET request and authenticate        with password passed as parameter when requested as POST request.      Returns:         flask.Response: Response object with login page or home page details. Main authorization API which is always called from nginx to check the        authentication for all requests. This uses the cookie as token to provide access.      Returns:         tuple: A tuple of response object and return status code. API to redirect to login page with reset deepracer_token in the cookie.      Returns:         flask.Response: Response object with login page details. API that updates the console password to the new one sent as a parameter.      Returns:         dict: Execution status if the API call was successful, flag to indicate               if password update was successful and error reason if call fails. Helper function to validate the token sent in the request object.      Returns:         tuple: A tuple of validation flag and cookie value. Helper method to compute the message digest for the given string. Helper method to reset the password to the default password found on vehicle.