#!/usr/bin/perl
# The LearningOnline Network
# CrGrant.pl  - Grant a loncapa SSL certificate.
#
# $Id: CrGrant.pl,v 1.4 2004/07/09 09:11:48 foxr Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or 
# (at your option) any later version.
#
# LON-CAPA 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 General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#


# http://www.lon-capa.org/
#
# This script operates on a certificate request that has been
# extracted from the attachment sent to the loncapa certificate 
# administrator and:
#
#  1. Creates an ssl certificate corresponding to the request.
#  2. Constructs an installation script that will install
#     the certificate along with the certificate authority's
#     certificate in a loncapa system.
#  3. Constructs an email which contains a cover letter 
#     describing what to do with the attachment, and an
#     attachment that consists of the installation script
#     created in step 2.
#  4. Emails the message to the email address in the certificate
#     request.
#
#  There are some assumptions we need to make in order to
#  get this all to work:
#    - The certificate authority is installed on a 
#      loncapa system with configuration files that specify
#      the same certificate directory and certificate filenames
#      as the target system (otherwise we can't generate the
#      installation script).
#    - The loncapa certificate authority configuration file is
#      $SSLDir/loncapaca.cnf and that it specifies that:
#      o The certificate authority files are in $SSLDir/loncapaca
#      o The certificate authority certificate is in:
#         $SSLDir/loncapaca/cacert.pem
#      o Only one instance of this script will be run at a time in
#        this directory.
#      o The person that runs this script knows the passphrase
#        for the loncapa certificate authority's private key
#        which remains encrypted for security reasons.
#
#

# Import section:

use strict;
use lib '/home/httpd/lib/perl';	# An assumption!!!
use MIME::Entity;
use LONCAPA::Configuration;



# Global variable declarations


my $ssl_dir       = "/usr/share/ssl";    # Where ssl config files etc. live
my $ca_cert_file  = $ssl_dir."/loncapaca/cacert.pem"; # CA's certificate file.
my $ca_config_file= $ssl_dir."/loncapaca.cnf";      # CA's config file. 


#   LONCAPA Configuration global variables:

# Items read from our configuration file.

my $ssl_command   = "/usr/bin/openssl "; # Command to run openssl.
my $loncapa_cert_dir;		# Name of target cert dir (from config)
my $loncapa_hostcert_name;	# Name of host's signed cert file (config)
my $loncapa_cacert_name;        # Name of the CA's certificate file (config)
my $return_address;		# Email return address.

#  Items I just need to know:

my $loncapa_config = "loncapa.conf";   # User's override config file.
my $loncapa_apache_user = 'www';	# Name of apache daemon's user
my $loncapa_apache_group = 'www';	# Name of apache daemon's group



# Debug/log support

my $DEBUG=0;

sub Debug {
    my $msg = shift;
    if($DEBUG) {
	print STDERR "$msg\n";
    }
}
#  Support subs:

#
#   Print out program usage.
#
# Side effects:
#    Output goes to stderr.
#
sub Usage {
    print STDERR << "USAGE";

Usage:
   CrGrant.pl requestfile.pem

Where:
   requestfile.pem is a PEM formatted certificate extracted from an email 
                   to the LonCAPA certificate manager.
USAGE

}
#
#  Read the loncapa configuration file and pull out the items
#  we need:
#
# Implicit inputs:
#   $loncapa_config   - The name of the auxilliary config file.
# Side effects:
#    - On failure exits with an error message.
#    - On success set the following variables:
#      o loncapa_cert_dir      - Path to certificates.
#      o loncapa_hostcert_name - Name of host's cert file in that dir
#      o loncapa_cacert_name   - Name of CA's cert file in that dir.
#      o ssl_command           - Name of ssl utility command.
sub ReadConfig {
    Debug("Reading the config files");
    my $perlvarref = LONCAPA::Configuration::read_conf($loncapa_config);

    #  Pull out the individual variables or die:

    # SSL Command:

    if($perlvarref->{SSLProgram}) {
	$ssl_command = $perlvarref->{SSLProgram};
	Debug("SSL utility program is $ssl_command");
    } 
    else {
	die "LonCAPA configuration errror: Can't read SSLProgram variable";
    }
    # Certificate directory:
   
    if($perlvarref->{lonCertificateDirectory}) {
	$loncapa_cert_dir = $perlvarref->{lonCertificateDirectory};
	Debug("Certificates will be installed in $loncapa_cert_dir");
    } 
    else {
	die "LonCAPA configuration error can't read lonCertificateDirectory variable";

    }
    #  Get the name of the host's certificate:

    if($perlvarref->{lonnetCertificate}) {
	$loncapa_hostcert_name = $perlvarref->{lonnetCertificate};
	Debug("Host's certificate will be $loncapa_hostcert_name");
    }
    else {
	die "LonCAPA configuration error: Can't read lonnetCertificate variable";
    }
    #   Get the name of the certificate authority's certificate.

    if($perlvarref->{lonnetCertificateAuthority}) {
	$loncapa_cacert_name = $perlvarref->{lonnetCertificateAuthority};
	Debug("CA's certificate will be $loncapa_cacert_name");
    }
    else {
	die "LonCAPA configuration error: Can't read lonnetCertificateAuthority variable";
    }
    #  Get the email address of the certificate manager:
    #  this is the email return address:

    if($perlvarref->{SSLEmail}) {
	$return_address = $perlvarref->{SSLEmail};
	Debug("Return address will be $return_address");
    }
    else {
	die "LonCAPA configuration error can't read SSLEmail configuration item";
    }

}

#  Create a certificate from the request file.  The certificate
#  is used, in conjunction with the openssl command with the 
#  certificate authority configuration to produce a certificate
#  file.
#
#  The certificate is parsed to determine the email address
#  of the requestor, which is returned to the caller.
#
#Parameters:
#     request_file   - Name of the file containing the certificate request.
#Returns:
#     If the request file exists and is able to produce a certificate
#     the email address of the requester is returned to the caller.
#     If not, undef is returned.
#
sub CreateCertificate {
    my ($request_file) = @_;

    Debug("CreateCertificate");

    if(!(-e $request_file)) {
	Debug("Certificate file $request_file does not exist");
	return undef;
    }
    Debug("Certificate file $request_file exists");

    # Create the certificate:  The status of the openssl command
    # is used to determine if the certificate succeeded:

    my $create_command = $ssl_command." ca -config ".$ca_config_file
	                             ." -in ".$request_file
				     ." -out hostCertificate.pem";
    my $status = system($create_command);
    if($status) {
	Debug("openssl ca failed");
	print STDERR "Certificate generation failed... probably bad";
	print STDERR " request file!\n";
	return undef;
    }
    Debug("openssl ca succeeded");

    #  Now we have a shining new signed certificate in ./hostCertificate.pem
    #  we parse it to get the email address to which the certificate should
    #  be emailed.
    #   The certificate's return email address will be in the Subject line:
    #

    Debug("Parsing certificate file for Subject:");
    open CERTIFICATE, "<hostCertificate.pem";
    my $line;
    my $subject_found = 0;
    while ($line = <CERTIFICATE>) {
	Debug("Line = $line");
	if($line =~ /Subject:/) {
	    Debug("Found Subject: in $line");
	    $subject_found =1;
	    last;
	}
    }
    close CERTIFICATE;

    if(!$subject_found) {
	Debug("Did not find Subject line in cert");
	print STDERR "Output certificate parse failed: no Subject:\n";
	return undef;
    }
    #  The subject line contains an Email= string amidst the other stuff.
    #  First break in to comma separated stuff, then locate the piece that
    #  contains /Email=

    my @subject_fields = split(/,/, $line);
    my $email_found = 0;
    my $element;
    my $email_element;
    Debug("Parsing subject line for Email=");
    foreach $element (@subject_fields) {
	$email_element = $element;
	Debug("Parsing $element");
	if($element =~ /\/Email=/) {
	    Debug("Found /Email=");
	    $email_found = 1;
	    last;
	}
    }
    if(!$email_found) {
	Debug("Failed to fine Email=");
	print STDERR "Unable to find line with /Email= in cert. Subject\n";
	return undef;
    }

    #  The piece we found must first be split at the /
    #  to isolate the Email= part and then that part at the = to isolate
    #  the address:

    Debug("Splitting $email_element at /");
    my ($junk, $email) = split(/\//, $email_element);
    Debug("Email part is $email");
    my ($junk, $address) = split(/=/, $email);
    Debug("CreateCertificate Returning $address to caller");

    return $address;

}
#
#   Create the installation script.  This will be  bash script
#   that will install the certifiate and the CA's certificate with ownership
#   WebUser:WebGroup and permissions 0400.  I thought about using a perl
#   script in order to be able to get the certificate file/directory from
#   the configuration files.  Unfortunately this is not as easy as it looks.
#   Root has a chicken and egg problem.  In order to read the config file
#   you need to have added the ..../lib/perl to the perl lib path. To do
#   that correctly, you need to have read the config file to know where
#   it is...What we will do is read our local configuration file and
#   assume that our configuration is the same as the target's system in
#   all respects we care about.
# Implicit Inputs:
#    - Bash is in /bin/bash
#    - $loncapa_cert_dir             -  install target directory.
#    - $loncapa_hostcert_name        -  Name of installed host cert file.
#    - $loncapa_cacert_name          -  Name of installed ca cert file.
#    - $loncapa_apache_user          -  username under which httpd runs.
#    - $loncapa_apache_group         -  group under which httpd runs.
#    - 0400                          -  install permissions.
#    - The host's certificate is now in ./hostCertificate.pem
#    - The CA's certificate is now in  $ca_cert_file
#
# Implicit Outputs:
#    A file named CertInstall.sh
# Return
#    Name of the file we created.
#
sub CreateInstallScript {
    open INSTALLER,">CertInstall.sh";
    print INSTALLER <<BASH_HEADER;
#!/bin/bash
#
#    Installer for your lonCAPA certificates.  Please check the
#    configuration variables to be sure they match your installation.
#    Then run this script under a root shell to complete the 
#    installation of the certificates.
#
# Configuration Variables:
CERTDIR="$loncapa_cert_dir"        # Directory with your host key.
HOSTCERT="$loncapa_hostcert_name"   # Name of host's certificate file.
CACERT="$loncapa_cacert_name"     # Name of certifiate authority file.
HTTPDUID="$loncapa_apache_user"     # UID of httpd.
HTTPDGID="$loncapa_apache_group"    # GID of httpd.

#   End of configuration variables.

MODE=0444                           # certificates get this mode.
HOSTCERTPATH="\$CERTDIR/\$HOSTCERT"
CACERTPATH="\$CERTDIR/\$CACERT"

#  Create the host certificate file to install:

echo unpacking host certificate

cat <<-HOSTCERTTEXT   >\$HOSTCERT
BASH_HEADER

    #   Now copy the host certificate into the script:

    open HOSTCERT, "<hostCertificate.pem";
    while(my $line = <HOSTCERT>) {
	print INSTALLER $line;	# Line presumably has a \n.
    }
    close HOSTCERT;

    #  Close the here doc, and start up the cat of the ca cert:

    print INSTALLER "HOSTCERTTEXT\n";
    print INSTALLER "echo unpacking CA certificate\n";
    print INSTALLER "cat <<-CACERTTEXT >\$CACERT\n";
    open  CACERT, "<$ca_cert_file";
    while(my $line = <CACERT>) {
	print INSTALLER $line;
    }
    close CACERT;
    print INSTALLER "CACERTTEXT\n";

    #  Ok, the script can create the two files, now it must install
    # install them >and< clean up after itself.

    print INSTALLER <<BASH_TRAILER;

echo Installing certificates

install -m \$MODE -o \$HTTPDUID -g \$HTTPDGID \$CACERT \$CACERTPATH
install -m \$MODE -o \$HTTPDUID -g \$HTTPDGID \$HOSTCERT \$HOSTCERTPATH

echo done

rm -f \$CACERT
rm -f \$HOSTCERT

#    Do they want to restart loncapa:
#

echo In order to start running in secure mode you will need to start
echo lonCAPA.  If you want I can do that now for you.  Otherwise,
echo you will have to do it yourself later either by rebooting your
echo system or by typing:
echo
echo /etc/init.d/loncontrol restart
echo
read -p "Restart loncapa now [yN]?"  yesno

if [ "\${yesno:0:1}" = "Y" -o "\${yesno:0:1}"  = "y" ] 
then
   /etc/init.d/loncontrol restart
fi
BASH_TRAILER

    close INSTALLER;

    return "CertInstall.sh";
}
#
#    Create a mime Email that consists of a cover letter of installation
#    instructions and an attachment that is the installation script.
# Parameters:
#     script    - The name of the script that will be attached
#                 to the email.
#     send_address - Where the mail will be sent.
# Returns:
#     The MIME::Entity handle of the script.
#
sub CreateEmail {
    Debug("Creating Email");
    my ($installer_file, $send_address) = @_;

    #  The top level mime entity is the mail headers and the
    #  cover letter:

    my $mime_message = MIME::Entity->build(Type    => "multipart/mixed",
					   From    => $return_address,
					   To      => $send_address,
					   Subject =>"LonCAPA certificates");
    if(!$mime_message) {
	die "Unable to create top level MIME Message";
    }

     $mime_message->attach(Data =>["  This email contains your lonCAPA SSL certificates.  These\n",
     "certificates allow your system to interact with the world wide\n",
     "cluster of LonCAPA systems, and allow you to access and share\n",
     "public resources for courses you host.\n\n",
     "   The certificates are shipped as a self installing shell script\n",
     "To install these certificates:\n\n",
     "1. Extract the attachment to this email message\n",
     "2. Save the attachment where it can be recovered in case you need\n",
     "   to re-install these certificates later on for some reason\n",
     "3. As root execute the certificate request file:
           . $installer_file\n",
     "   (Note: If you used a Windows based email program to extract the\n",
     "   this file and then tranferred it to your unix lonCAPA system you \n",
     "   Will probably need to convert the file first e.g.: \n",
     "     dos2unix $installer_file\n",
     "     . $installer_file\n",
     "   The installer file will install the certificates and ask you\n",
     "   if you want to restart the LonCAPA system.  You must restart the\n",
     "   LonCAPA system for it to use the new certificates.\n\n",
     "      Thank you for choosing LonCAPA for your course delivery needs,\n",
     "      The LonCAPA team.\n"]);

    Debug("Main message body created");


    #  Attach the certificate intaller:

    $mime_message->attach(Type    => "text/plain",
			  Path    => $installer_file);
    Debug("Installer attached");

    return $mime_message;

}

#
#   Sends a mime message to an email address.
# Parameters:
#    message   - A MIME::Entity containing the message.
# Implicit inputs:
#   Mail is sent via /usr/lib/sendmail -t -oi -oem"
#   This should work on all systems with a properly configured
#   sendmail or compatible mail transfer agent.
sub SendEmail {
    my ($message) =  @_;

    Debug("Mailing");

    open MAILPIPE, "| /usr/lib/sendmail -t -oi -oem" or 
	die "Failed to open pipe to sendmail: $!";

    $message->print(\*MAILPIPE);
    Debug("Submitted to sendmail");
    close MAILPIPE;
}
#
#  Cleanup destroys the certificate file and its installer.
#
#
sub Cleanup {
    my ($installer) = @_;
    unlink($installer);
    unlink("hostcertificate.pem");
}


#  Program entry point
#   The usage is:
#     CrGrant.pl    {request_file}
#

my $argc = @ARGV;		# Count number of command parameters.
if($argc != 1) {
    Usage;
    exit -1;
}
my $CertificateRequest = $ARGV[0];

&ReadConfig;

my $email_address = &CreateCertificate($CertificateRequest);
Debug("CreateCertificate returned: $email_address");

if(!defined $email_address) {
    print STDERR "Bad or missing certificate file!!";
    Usage;
    exit -1;
}

my $script_name = &CreateInstallScript;
my $Message = &CreateEmail($script_name, $email_address);
&SendEmail($Message);
&Cleanup($script_name);

# POD documentation.
