#!/usr/bin/python3
#-*- coding: utf-8 -*-
"""
Cassh WEB Client

Copyright 2017-2020 Nicolas BEGUIER
Licensed under the Apache License, Version 2.0
Written by Nicolas BEGUIER (nicolas_beguier@hotmail.com)

"""

# Standard library imports
from __future__ import print_function
from base64 import urlsafe_b64decode, urlsafe_b64encode
from datetime import datetime
from functools import wraps
from json import loads
from os import environ, getenv, path
from ssl import PROTOCOL_TLSv1_2, SSLContext
import sys

# Third party library imports
from flask import Flask, render_template, request, Response, redirect, send_from_directory
from requests import post, put
from requests.exceptions import ConnectionError
from urllib3 import disable_warnings

# Disable HTTPs warnings
disable_warnings()

# Debug
# from pdb import set_trace as st

VERSION = '1.2.0'
APP = Flask(__name__)
# Read settings file by default, but can be missing
try:
    APP.config.from_pyfile('settings.txt')
except FileNotFoundError:
    pass

# Override optionnal settings.txt file
for env_var in [
        'CASSH_URL',
        'DEBUG',
        'ENABLE_LDAP',
        'ENCRYPTION_KEY',
        'LOGIN_BANNER',
        'PORT',
        'SSL_PRIV_KEY',
        'SSL_PUB_KEY',
        'UPLOAD_FOLDER',
    ]:
    if environ.get(env_var):
        if env_var in ['ENABLE_LDAP', 'DEBUG']:
            APP.config[env_var] = environ.get(env_var) == 'True'
        else:
            APP.config[env_var] = environ.get(env_var)
    elif env_var not in APP.config:
        print('Error: {} is not present in configuration...'.format(env_var))
        sys.exit(1)
# These are the extension that we are accepting to be uploaded
APP.config['ALLOWED_EXTENSIONS'] = set(['pub'])
APP.config['HEADERS'] = {
    'User-Agent': 'CASSH-WEB-CLIENT v%s' % VERSION,
    'CLIENT_VERSION': VERSION,
}

def allowed_file(filename):
    """ For a given file, return whether it's an allowed type or not """
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in APP.config['ALLOWED_EXTENSIONS']

def self_decode(key, enc):
    dec = []
    # Try to use urlsafe_b64decode before encoding
    try:
        encoded = urlsafe_b64decode(enc).decode()
    except TypeError:
        encoded = urlsafe_b64decode(enc.encode())
    for i in range(len(encoded)):
        key_c = key[i % len(key)]
        dec_c = chr((256 + ord(encoded[i]) - ord(key_c)) % 256)
        dec.append(dec_c)
    return "".join(dec)

def self_encode(key, clear):
    enc = []
    for i in range(len(clear)):
        key_c = key[i % len(key)]
        enc_c = chr((ord(clear[i]) + ord(key_c)) % 256)
        enc.append(enc_c)
    # Try to encode in unicode before urlsafe_b64encode
    try:
        encoded = urlsafe_b64encode("".join(enc).encode()).decode()
    except UnicodeDecodeError:
        encoded = urlsafe_b64encode("".join(enc))
    return encoded

def requires_auth(func):
    """ Wrapper which force authentication """
    @wraps(func)
    def decorated(*args, **kwargs):
        """ Authentication wrapper """
        current_user = {}
        current_user['name'] = request.cookies.get('username')
        try:
            current_user['password'] = self_decode(APP.config['ENCRYPTION_KEY'], request.cookies.get('password'))
        except:
            current_user['password'] = 'Unknown'
        current_user['is_authenticated'] = request.cookies.get('last_attempt_error') == 'False'
        if current_user['name'] == 'Unknown' and current_user['password'] == 'Unknown':
            current_user['is_authenticated'] = False
        return func(current_user=current_user, *args, **kwargs)
    return decorated

@APP.route('/')
@requires_auth
def index(current_user=None):
    """ Display home page """
    return render_template('homepage.html', username=current_user['name'], \
        logged_in=current_user['is_authenticated'], \
        display_error=request.cookies.get('last_attempt_error') == 'True', \
        login_banner=APP.config['LOGIN_BANNER'])

@APP.route('/login', methods=['POST'])
@requires_auth
def login(current_user=None):
    """
    Authentication
    """
    del current_user
    username = request.form['username']
    password = request.form['password']
    last_attempt_error = False
    redirect_to_index = redirect('/')
    response = APP.make_response(redirect_to_index)
    try:
        payload = {}
        payload.update({'realname': username, 'password': password})
        req = post(APP.config['CASSH_URL'] + '/test_auth', \
                data=payload, \
                headers=APP.config['HEADERS'], \
                verify=False)
    except:
        return Response('Connection error : %s' % APP.config['CASSH_URL'])
    if 'OK' in req.text:
        response.set_cookie('username', value=username)
        response.set_cookie('password', value=self_encode(APP.config['ENCRYPTION_KEY'], password))
    else:
        last_attempt_error = True
    response.set_cookie('last_attempt_error', value=str(last_attempt_error))
    return response

@APP.route('/logout', methods=['POST'])
@requires_auth
def logout(current_user=None):
    redirect_to_index = redirect('/')
    response = APP.make_response(redirect_to_index)
    response.set_cookie('username', value='Unknown')
    response.set_cookie('password', value='Unknown')
    response.set_cookie('last_attempt_error', value='False')
    return response

@APP.route('/add/')
@requires_auth
def cassh_add(current_user=None):
    """ Display add key page """
    return render_template('add.html', username=current_user['name'], \
        logged_in=current_user['is_authenticated'])

@APP.route('/sign/')
@requires_auth
def cassh_sign(current_user=None):
    """ Display sign page """
    return render_template('sign.html', username=current_user['name'], \
        logged_in=current_user['is_authenticated'])

@APP.route('/status/')
@requires_auth
def cassh_status(current_user=None):
    """
    CASSH status
    """
    try:
        payload = {}
        payload.update({'realname': current_user['name'], 'password': current_user['password']})
        req = post(APP.config['CASSH_URL'] + '/client/status', \
                data=payload, \
                headers=APP.config['HEADERS'], \
                verify=False)
    except ConnectionError:
        return Response('Connection error : %s' % APP.config['CASSH_URL'])
    try:
        result = loads(req.text)
        is_expired = datetime.strptime(result['expiration'], '%Y-%m-%d %H:%M:%S') < datetime.now()
        if result['status'] == 'ACTIVE':
            if is_expired:
                result['status'] = 'EXPIRED'
            else:
                result['status'] = 'SIGNED'
    except:
        result = req.text

    return render_template('status.html', username=current_user['name'], result=result, \
        logged_in=current_user['is_authenticated'])

# Route that will process the file upload
@APP.route('/sign/upload', methods=['POST'])
@requires_auth
def upload(current_user=None):
    """
    CASSH sign
    """
    pubkey = request.files['file']
    username = request.form['username']
    payload = {}
    payload.update({'realname': current_user['name'], 'password': current_user['password']})
    payload.update({'username': username})
    payload.update({'pubkey': pubkey.read().decode('UTF-8')})
    try:
        req = post(APP.config['CASSH_URL'] + '/client', \
                data=payload, \
                headers=APP.config['HEADERS'], \
                verify=False)
    except ConnectionError:
        return Response('Connection error : %s' % APP.config['CASSH_URL'])
    if 'Error' in req.text:
        return Response(req.text)

    with open(path.join(APP.config['UPLOAD_FOLDER'], current_user['name']), 'w') as f:
        f.write(req.text)

    return send_from_directory(APP.config['UPLOAD_FOLDER'], current_user['name'], \
        attachment_filename='id_rsa-cert.pub', as_attachment=True)

# Route that will process the file upload
@APP.route('/add/send', methods=['POST'])
@requires_auth
def send(current_user=None):
    """
    CASSH add
    """
    pubkey = request.files['file']
    username = request.form['username']
    payload = {}
    payload.update({'realname': current_user['name'], 'password': current_user['password']})
    payload.update({'username': username})
    payload.update({'pubkey': pubkey.read().decode('UTF-8')})
    try:
        req = put(APP.config['CASSH_URL'] + '/client', \
                data=payload, \
                headers=APP.config['HEADERS'], \
                verify=False)
    except ConnectionError:
        return Response('Connection error : %s' % APP.config['CASSH_URL'])
    if 'Error' in req.text:
        return Response(req.text)
    return redirect('/status')

@APP.errorhandler(404)
def page_not_found(_):
    """ Display error page """
    return render_template('404.html'), 404

if __name__ == '__main__':
    CONTEXT = SSLContext(PROTOCOL_TLSv1_2)
    CONTEXT.load_cert_chain(APP.config['SSL_PUB_KEY'], APP.config['SSL_PRIV_KEY'])
    PORT = int(getenv('PORT', APP.config['PORT']))
    APP.run(debug=APP.config['DEBUG'], host='0.0.0.0', port=PORT, ssl_context=CONTEXT)