#!/usr/bin/python
#  Copyright 2007, One Laptop per Child
#  Author: Nelson Elhage and John Watlington
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
"""This server accepts registration requests from laptops, and stores
the information in a database.

See http://wiki.laptop.org/go/School_Identity_Manager

To run as a daemon, provide a PID_file argument.
"""

import os, syslog, sys
import subprocess
import re
from SimpleXMLRPCServer import SimpleXMLRPCServer
import traceback
import idmanager

# idmanager.Config reads /etc/idmgr.conf
config = idmanager.Config()

# Daemonisation function.
#
# Default maximum for the number of available file descriptors.
MAXFD = 1024

def createDaemon(pidfilename):
    """Detach a process from the controlling terminal and run it in the
    background as a daemon.
    """
    pid = os.fork()
    if (pid == 0):   # The first child.
        pid = os.fork()
        if (pid == 0):   # The second child.
            # Since the current working directory may be a mounted filesystem,
            # we avoid the issue of not being able to unmount the filesystem at
            # shutdown time by changing it to the root directory.
            # (Actually, we use /home/idmgr...)
            os.chdir(config.WORKDIR)
            os.umask(config.UMASK)
        else:
            #  write out pid of child
            try:
                pidfile = open( pidfilename, "w" );
                pidfile.write( str( pid ) );
                pidfile.write( "\n" );
                pidfile.close();
            except OSError, e:
                syslog.openlog( 'olpc_idmgr', 0, syslog.LOG_USER )
                syslog.syslog( syslog.LOG_ALERT, "Writing PID file: %s [%d]" % (e.strerror, e.errno) )
                syslog.closelog()
                raise
            os._exit(0)  # Exit parent (the first child) of the second child.
    else:
        os._exit(0)# Exit parent of the first child.

    # Close all open file descriptors.
    # Use the getrlimit method to retrieve the maximum file descriptor
    # number that can be opened by this process.  If there is not limit
    # on the resource, use the default value.
    #
    import resource     # Resource usage information.
    maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
    if (maxfd == resource.RLIM_INFINITY):
        maxfd = MAXFD

    # Iterate through and close all file descriptors.
    for fd in range(0, maxfd):
        try:
            os.close(fd)
        except OSError: # ERROR, fd wasn't open to begin with (ignored)
            pass

    # Redirect the standard I/O file descriptors to the specified file.  Since
    # the daemon has no controlling terminal, most daemons redirect stdin,
    # stdout, and stderr to /dev/null.  This is done to prevent side-effects
    # from reads and writes to the standard I/O file descriptors.

    # This call to open is guaranteed to return the lowest file descriptor,
    # which will be 0 (stdin), since it was closed above.
    os.open(os.devnull, os.O_RDWR)    # standard input (0)

    # Duplicate standard input to standard output and standard error.
    os.dup2(0, 1)# standard output (1)
    os.dup2(0, 2)# standard error (2)
    return(0)


class ServerError(Exception):
    pass

# The specs are a little unclear on the encoding of UUIDs, so be
# flexible in what we accept
uuidre = re.compile(r'[a-fA-F0-9-]{32,40}')
serialre   = re.compile(r'^[A-Z]{3}[A-F0-9]{8}$')
base64re   = re.compile(r'^[A-Fa-z0-9+/=]+$') #XXX unused, but should match pubkeys.


def register(serial, nickname, uuid, pubkey):
    try:
        #Sanitise all input
        if not serialre.match(serial):
            raise ServerError("Invalid serial: %s" % (serial,))
        if not uuidre.match(uuid):
            raise ServerError( "Invalid UUID: %s" % (uuid,))
        if "\n" in nickname:
            raise ServerError( "Invalid nickname: %s" % nickname)
        if "\n" in pubkey:
            raise ServerError("Invalid public key: %s" % pubkey)

        # first try creating the system user. If this fails, the
        # database will be untouched.
        _create_new_user(serial, nickname, uuid, pubkey)
        database.save_laptop({'serial':    serial,
                              'nickname':  nickname,
                              'full_name': '',
                              'pubkey':    pubkey,
                              'uuid':      uuid,
                              })

    except Exception, e:
        log(syslog.LOG_ERR, str(e))
        return {'success': 'ERR',
                'error': str(e),
                }

    # OK, the laptop is registered. Now we just need to let it know.
    log(syslog.LOG_NOTICE, "Registered user %s with serial %s"
        % (nickname.encode('utf-8'), serial))

    response = {
        'success': 'OK',
        'backupurl': "%s@%s:%s" % (serial, config.BACKUP, config.BACKUP_PATH),
        'jabberserver': config.PRESENCE,
        'backuppath': config.BACKUP_PATH,
        }
    return response


def _create_new_user(username, name, password, pubkey):
    """Create a system user using a special script. It raises
    ServerError if unsuccessful."""
    script = config.NEW_USER_SCRIPT
    proc = subprocess.Popen([script], shell=False, stdin=subprocess.PIPE, bufsize=1)
    proc.stdin.write(username + "\n")
    proc.stdin.write(name.encode('utf-8') + "\n")
    proc.stdin.write(password + "\n")
    proc.stdin.write(pubkey + "\n")
    ret = proc.wait()
    if ret != 0:
        raise ServerError( "create_user terminated with code %d" % (ret,) )




#
# If run without a pidfile, log to stderr; otherwise to syslog.

if len(sys.argv) > 1:
    if sys.argv[1] in ('-h', '--help'):
        print __doc__
        sys.exit()
    retCode = createDaemon( sys.argv[1] )
    syslog.openlog( 'olpc_idmgr', 0, syslog.LOG_USER )
    log = syslog.syslog
else:
    def log(*args):
        print >> sys.stderr, args

#Now that we've forked, its ok to open the database!
database = idmanager.Database(config.SQLITE3_FILE, config.USE_GROUPS_THRESHOLD)

log(syslog.LOG_NOTICE, 'Starting OLPC ID Manager')
server = SimpleXMLRPCServer((config.BIND_ADDRESS, config.PORT))
server.register_function(register)
server.serve_forever()
