#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
#
# Client usage examples;
# Getting password:
#   wget -q -t 3 -T 20 -O - --header 'DomU_Request: send_my_password' <routerIP>:8080
# Send ack:
#   wget -t 3 -T 20 -O - --header 'DomU_Request: saved_password' localhost:8080
# Save password only from within router:
#   /opt/cloud/bin/savepassword.sh -v <IP> -p <password>
#   curl --header 'DomU_Request: save_password' http://localhost:8080/ -F ip=<IP> -F password=<passwd>

import binascii
import cgi
import os
import socketserver
import sys
import syslog
import threading
import urllib.parse

from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn


passMap = {}
secureToken = None
listeningAddress = '127.0.0.1'
allowAddresses = ['localhost', '127.0.0.1']
lock = threading.RLock()

def getTokenFile():
    return '/tmp/passwdsrvrtoken'

def getPasswordFile():
    return '/var/cache/cloud/passwords-%s' % listeningAddress

def initToken():
    global secureToken
    if os.path.exists(getTokenFile()):
        with open(getTokenFile(), 'r') as f:
            secureToken = f.read()
    if not secureToken:
        secureToken = binascii.hexlify(os.urandom(16)).decode()
        with open(getTokenFile(), 'w') as f:
            f.write(secureToken)

def checkToken(token):
    return token == secureToken

def loadPasswordFile():
    try:
        with open(getPasswordFile()) as f:
            for line in f:
                if '=' not in line: continue
                key, value = line.strip().split('=', 1)
                passMap[key] = value
    except IOError:
        pass

def savePasswordFile():
    with lock:
        try:
            with open(getPasswordFile(), 'w') as f:
                for ip in passMap:
                    f.write('%s=%s\n' % (ip, passMap[ip]))
            f.close()
        except IOError as e:
            syslog.syslog('serve_password: Unable to save to password file %s' % e)

def getPassword(ip):
    return passMap.get(ip, None)

def setPassword(ip, password):
    if not ip or not password:
        return
    with lock:
        passMap[ip] = password

def removePassword(ip):
    with lock:
        if ip in passMap:
            del passMap[ip]


class CloudStackPasswordServer(socketserver.TCPServer):
    allow_reuse_address = 1
    def server_bind(self):
        """Override server_bind to store the server name."""
        socketserver.TCPServer.server_bind(self)
        host, port = self.server_address[:2]
        self.server_name = host
        self.server_port = port

class ThreadedHTTPServer(ThreadingMixIn, CloudStackPasswordServer):
    pass

class PasswordRequestHandler(BaseHTTPRequestHandler):
    server_version = 'CloudStack Password Server'
    sys_version = '4.x'
    def do_GET(self):
        self.send_response(200)
        self.send_header('content-type', 'text/plain')
        self.send_header('Server', 'CloudStack Password Server')
        self.end_headers()
        requestType = self.headers.get('DomU_Request')
        clientAddress = self.client_address[0]
        if requestType == 'send_my_password':
            password = getPassword(clientAddress)
            if not password:
                self.wfile.write('saved_password'.encode())
                syslog.syslog('serve_password: requested password not found for %s' % clientAddress)
            else:
                self.wfile.write(password.encode())
                syslog.syslog('serve_password: password sent to %s' % clientAddress)
        elif requestType == 'saved_password':
            removePassword(clientAddress)
            savePasswordFile()
            self.wfile.write('saved_password'.encode())
            syslog.syslog('serve_password: saved_password ack received from %s' % clientAddress)
        else:
            self.send_response(400)
            self.wfile.write('bad_request'.encode())
            syslog.syslog('serve_password: bad_request from IP %s' % clientAddress)
        return

    def do_POST(self):
        form = cgi.FieldStorage(
                    fp=self.rfile,
                    headers=self.headers,
                    environ={'REQUEST_METHOD':'POST',
                             'CONTENT_TYPE':self.headers['content-type'],
                    })
        self.send_response(200)
        self.end_headers()
        clientAddress = self.client_address[0]
        if clientAddress not in allowAddresses:
            syslog.syslog('serve_password: non-localhost IP trying to save password: %s' % clientAddress)
            self.send_response(403)
            return
        if 'ip' not in form or 'password' not in form or 'token' not in form or self.headers.get('DomU_Request') != 'save_password':
            syslog.syslog('serve_password: request trying to save password does not contain both ip and password')
            self.send_response(403)
            return
        token = form['token'].value
        if not checkToken(token):
            syslog.syslog('serve_password: invalid save_password token received from %s' % clientAddress)
            self.send_response(403)
            return
        ip = form['ip'].value
        password = form['password'].value
        if not ip or not password:
            syslog.syslog('serve_password: empty ip/password[%s/%s] received from savepassword' % (ip, password))
            return
        syslog.syslog('serve_password: password saved for VM IP %s' % ip)
        setPassword(ip, password)
        savePasswordFile()
        return

    def log_message(self, format, *args):
            return


def serve(HandlerClass = PasswordRequestHandler,
          ServerClass = ThreadedHTTPServer):

    global listeningAddress
    global allowAddresses
    if len(sys.argv) > 1:
        addresses = sys.argv[1].split(",")
        if len(addresses) > 0:
            listeningAddress = addresses[0]
            allowAddresses.append(addresses[0])
        if len(addresses) > 1:
            allowAddresses.append(addresses[1])

    server_address = (listeningAddress, 8080)
    passwordServer = ServerClass(server_address, HandlerClass)
    passwordServer.allow_reuse_address = True
    sa = passwordServer.socket.getsockname()
    initToken()
    loadPasswordFile()
    syslog.syslog('serve_password running on %s:%s' % (sa[0], sa[1]))
    try:
        passwordServer.serve_forever()
    except KeyboardInterrupt:
        syslog.syslog('serve_password shutting down')
        passwordServer.socket.close()
    except Exception as e:
        syslog.syslog('serve_password hit exception %s -- died' % e)
        passwordServer.socket.close()


if __name__ == '__main__':
    serve()
