#!/usr/bin/env python2.2
# Startup from single-user installation
# Copyright (C) 2002 John Goerzen
# <jgoerzen@complete.org>
#
#    This program 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.
#
#    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 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

import sys, re, os, pwd, time, gruntlib, md5, fcntl
from sys import stdin, stdout
import GnuPGInterface

"""File format:

Plain:
:GRUNT:INSECUREHEADER:FORMAT-1I:
:USER:username
:DATA:

Signed/Encrypted:
:GRUNT:SECUREHEADER:FORMAT-1S:
:USER:username
:SENDER:senderstr
:RANDOM:randomstr
:MODE:mode
:DEST:destination
:OUTPUT:destination:encryptkey  (if one is specified, so must the other, but encryptkey may be blank)
:DATA:
--data follows here--

*DATA MUST OCCUR IN THIS ORDER, NO UNSPECIFIED WHITESPACE PERMITTED*

Where:

  "username" is the name of the user to run as
  "mode" is one of:
     EXEC -- execute a command -- attached data to be used as stdin
     PUT  -- copy a file into place
  "destination" is:
     a filename (for PUT mode)
     a command (for EXEC mode)
  "senderstr" is:
     username:pid:time    time is seconds since epoch and must not have fractional part.
  "randomstr" is a base64-encoded random string and is ignored
  by the receiver.

All replaceable data (the lowercase stuff) must be base64-encoded.

No line, post-base64ing, may exceed 1023 characters.

     """

# Start processing.

def setuid(username):
    if pwd.getpwuid(os.getuid())[0] != username:
        destuid, destgid = pwd.getpwnam(username)[2:4]
        os.setgroups([])
        os.setregid(destgid, destgid)
        os.setreuid(destuid, destuid)

def processinsecureheader(fd):
    gruntlib.findfirstheader(fd)
    username = gruntlib.readwithcheck(fd, ':USER:')
    setuid(username)
    gruntlib.usernamecheck(username)
    gruntlib.sanitizeenviron()
    return username

def decryptit(inputfd, tmpfilename):
    gnupg = GnuPGInterface.GnuPG()
    gnupg.options.meta_interactive = 0
    gnupg.options.quiet = 1
    gnupg.options.no_verbose = 1
    gnupg.options.no_greeting = 1
    process = gnupg.run(['--decrypt', '--output', tmpfilename],
                        create_fhs=['status', 'stdin', 'passphrase'])
    # We create this and then close it, so if some decrypted data comes in,
    # we don't try to read a passphrase from stdin.  Just fail.
    process.handles['passphrase'].close()
    if (os.fork() == 0):
        # Copy process.
        gruntlib.copy(inputfd, process.handles['stdin'])
        inputfd.close()
        process.handles['stdin'].close()
        process.handles['status'].close()
        sys.exit(0)
    else:
        inputfd.close()
        process.handles['stdin'].close()
    goodsig = None
    signatory = None
    while 1:
        status = process.handles['status'].readline()
        if len(status) == 0:
            break
        statuswords = status.strip().split(' ')
        if statuswords[1] == 'GOODSIG':
            goodsig = 1
            signatory = statuswords[2]
        if statuswords[1] == 'BADSIG':
            goodsig = 0
    process.wait()        
    if not goodsig:
        try:
            os.unlink(tmpfilename)
        except OSError:
            pass
        raise ValueError, "FAILURE: no signature detected!"
    return (signatory, signatory[-8:])

def process_and_validate(fd):
    processinsecureheader(fd)
    os.chdir(gruntlib.getuserhome())
    gruntlib.checkforvalidsigsfile()
    gruntlib.makegruntwork()
    gruntlib.readwithcheck(fd, ':DATA:')
    tmpfilename = gruntlib.gettmpfilename()
    sigchecks = decryptit(fd, tmpfilename)
    # Now, check to see if this signature is in the user's list of acceptable ones.
    foundvalid = gruntlib.scanfileforlines(gruntlib.getvalidsigsfile(), sigchecks)

    if not foundvalid:
        try:
            os.unlink(tmpfilename)
        except OSError:
            pass
        raise ValueError, "FAILURE: Key %s (%s) not found in list of valid keys."%\
              (signatory, shortsignatory)
    return tmpfilename

def checkmd5(datafile):
    thismd5 = gruntlib.computemd5(datafile)
    datafile.seek(0, 0)

    existingmd5s = []
    maxmd5age = 60L # days

    efd = gruntlib.openwithlock(gruntlib.getgrunthome() + '/seenmd5s.txt')
    firstline = efd.readline()
    if len(firstline):
        maxmd5age = long(firstline.strip())
        for line in efd.xreadlines():
            recordedmd5, dateepoch, datestr = line.strip().split('|')
            if recordedmd5 == thismd5:
                raise ValueError,\
                   "ERROR: Detected attempt to resubmit existing request. DENIED."
            existingmd5s.append({'md5': recordedmd5,
                                 'dateepoch': long(dateepoch),
                                 'datestr': datestr})

    existingmd5s.append({'md5': thismd5,
                         'dateepoch': long(time.time()),
                         'datestr': time.asctime()})

    efd.seek(0, 0)
    efd.write("%d\n" % maxmd5age)
    now = long(time.time())
    multiplier = long(60 * 60 * 24)           # seconds in a day
    for entry in existingmd5s:
        if abs(now - entry['dateepoch']) < (maxmd5age * multiplier):
            efd.write("%s|%d|%s\n" % (entry['md5'], entry['dateepoch'], entry['datestr']))
    efd.flush()
    efd.truncate()
    efd.close()
    return maxmd5age

def checksecureuser(datafile, insecureuser):
    susername = gruntlib.readwithcheck(datafile, ':USER:')
    if susername != insecureuser:
        raise ValueError, \
              'FAILURE: secure packet username %s differs from regular username %s' %\
              (susername, insecureuser)

def checkage(fd, maxage):
    senderstr = gruntlib.readwithcheck(fd, ':SENDER:')

    senderusername, senderpid, sendertime = senderstr.split(':')
    if abs(long(time.time()) - long(sendertime)) > (maxage * 60 * 60 * 24):
        raise ValueError,\
              "Requests older than one third the configured storage age, or %d days, are rejected.  This request is %d days old, so it is REJECTED." % \
              (maxage, abs(long(time.time()) - long(sendertime)) / 60 / 60 / 24)

def handleexec(infile, dest):
    outputfd = os.popen(dest, 'w')
    gruntlib.copy(infile, outputfd)
    return (not outputfd.close() == None)

def handleputfile(infile, dest):
    outputfd = open(dest + '.gruntput', 'wb')
    gruntlib.copy(infile, outputfd)
    outputfd.close()
    os.rename(dest + '.gruntput', dest)

def handleputdir(infile, dest):
    os.mkdir(dest + '.gruntput', 0777)
    os.chdir(dest + '.gruntput')
    outputfd = os.popen('tar -xvSpf -', 'w')
    gruntlib.copy(infile, outputfd)
    retval = outputfd.close()
    os.chdir('..')
    os.rename(dest + '.gruntput', dest)
    return retval

tmpfilename = process_and_validate(stdin)
##################### PAST HERE, the sig is good.
datafile = open(tmpfilename)

try:
    maxage = checkmd5(datafile)
    #### Compute the md5 and see if we already have it.
    # Good sig AND new request.  Process the data.
    gruntlib.headercheck(datafile, 1)
    checksecureuser(datafile, gruntlib.getusername())
    checkage(datafile, maxage / 3)
    gruntlib.readwithcheck(datafile, ':RANDOM:')
    mode = gruntlib.readwithcheck(datafile, ':MODE:')
    dest = gruntlib.readwithcheck(datafile, ':DEST:')
    gruntlib.readwithcheck(datafile, ':DATA:')

    if mode == 'EXEC':
        exitstatus = handleexec(datafile, dest)
    elif mode == 'PUTFILE':
        exitstatus = handleputfile(datafile, dest)
    elif mode == 'PUTDIR':
        exitstatus = handleputdir(datafile, dest)
finally:
    datafile.close()
    os.unlink(tmpfilename)
sys.exit(exitstatus)
