/*****************************************************************************
 * Author:   Valient Gough <vgough@pobox.com>
 *
 *****************************************************************************
 * Copyright (c) 2003-2004, Valient Gough
 *
 * This library is free software; you can distribute it and/or modify it under
 * the terms of the GNU General Public License (GPL), as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This library 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 GPL in the file COPYING for more
 * details.
 *
 */

#include "encfs.h"
#include "config.h"

#include <iostream>
#include <string>
#include <sstream>

#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

#include <getopt.h>

#include <rlog/rlog.h>
#include <rlog/Error.h>
#include <rlog/RLogChannel.h>
#include <rlog/SyslogNode.h>
#include <rlog/StdioNode.h>

#include "Config.h"
#include "Interface.h"
#include "MemoryPool.h"
#include "FileUtils.h"
#include "DirNode.h"

#ifdef HAVE_SSL
#define NO_DES
#include <openssl/ssl.h>
#include <openssl/rand.h>
#endif

#include <locale.h>

#include "i18n.h"

#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif

using namespace std;
using namespace rlog;
using namespace rel;

// Maximum number of arguments that we're going to pass on to fuse.  Doesn't
// affect how many arguments we can handle, just how many we can pass on..
const int MaxFuseArgs = 16;
struct EncFS_Args
{
    string rootDir; // where raw files are found
    string mountPoint; // where to make filesystem visible
    string passwordProgram; // program to provide password
    bool isDaemon; // true == spawn in background, log to syslog
    bool isThreaded; // true == threaded
    bool isVerbose; // false == only enable warning/error messages
    int idleTimeout; // 0 == idle time in minutes to trigger unmount
    bool keyCheck; // true == check key checksum
    bool forceDecode; // don't abort on MAC checksum failures for data blocks
    const char *fuseArgv[MaxFuseArgs];
    int fuseArgc;
};

/* Yeah, we can only deal with one filesystem at a time... */
DirNode *FSRoot = NULL;

static int oldStderr = STDERR_FILENO;

/*  Ask the user for permission to create directory.  If they say ok, then do
 *  it.
*/
static 
bool userAllowCreate( const char *fileName )
{
    // TODO: can we internationalize the y/n name??
    // xgroup(setup)
    cerr << autosprintf( _("The directory \"%s\" does not exist. Should it be created? (y,n) "), fileName );
    char answer[10];
    fgets( answer, sizeof(answer), stdin );

    if(toupper(answer[0]) == 'Y')
    {
	int result = mkdir( fileName, 0700 );
	if(result < 0)
	{
	    // mkdir failure..
	    perror( _("Unable to create directory: ") );
	    return false;
	} else
	{
	    return true;
	}
    } else
    {
	// Directory not created, by user request -- aborting...
	cerr << _("Directory not created.") << "\n";
	return false;
    }
}

static
void usage(const char *name)
{
    // xgroup(usage)
    cerr << autosprintf( _("Build: encfs version %s"), VERSION ) 
	<< "\n\n"
	// xgroup(usage)
	<< autosprintf(_("Usage: %s [options] rootDir mountPoint [-- [FUSE Mount Options]]"), name) << "\n\n"
	// xgroup(usage)
	<< _("Common Options:\n"
	"  -H\t\t\t"       "show optional FUSE Mount Options\n"
	"  -s\t\t\t"       "disable multithreaded operation\n"
	"  -f\t\t\t"       "run in foreground (don't spawn daemon).\n"
	             "\t\t\tError messages will be sent to stderr\n"
		     "\t\t\tinstead of syslog.\n")

	// xgroup(usage)
	<< _("  -v, --verbose\t\t"   "verbose: output encfs debug messages\n"
	"  -i, --idle=MINUTES\t""Auto unmount after period of inactivity\n"
	"  --anykey\t\t"        "Do not verify correct key is being used\n"
	"  --forcedecode\t\t"   "decode data even if an error is detected\n"
	                  "\t\t\t(for filesystems using MAC block headers)\n")

	// xgroup(usage)
	<< _("  --extpass=program\tUse external program for password prompt\n"
	"\n"
	"Example, to mount at ~/crypt with raw storage in ~/.crypt :\n"
	"    encfs ~/.crypt ~/crypt\n"
	"\n")
	// xgroup(usage)
	<< _("For more information, see the man page encfs(1)") << "\n"
	<< endl;
}

static
void FuseUsage()
{
    // xgroup(usage)
    cerr << _("encfs [options] rootDir mountPoint -- [FUSE Mount Options]\n"
	    "valid FUSE Mount Options follow:\n") << endl;

    int argc = 2;
    const char *argv[] = {"...", "-h"};
    fuse_main( argc, const_cast<char**>(argv), (fuse_operations*)NULL);
}

// in case someone sends me a log dump, I want to know how what options are in
// effect.  I'd rather not internationalize this, since it is something that is
// mostly useful for me!
string showFuseArgs( EncFS_Args &args )
{
    ostringstream ss;
    ss << (args.isDaemon ? "(daemon) " : "(fg) ");
    ss << (args.isThreaded ? "(threaded) " : "(UP) ");
    if(args.idleTimeout > 0)
	ss << "(timeout " << args.idleTimeout << ") ";
    for(int i=0; i<args.fuseArgc; ++i)
	ss << args.fuseArgv[i] << ' ';

    return ss.str();
}

#define PUSHARG(ARG) \
rAssert(out->fuseArgc < MaxFuseArgs); \
out->fuseArgv[out->fuseArgc++] = ARG

static 
bool processArgs(int argc, char *argv[], EncFS_Args *out)
{
    // set defaults
    out->isDaemon = true;
    out->isThreaded = true;
    out->isVerbose = false;
    out->idleTimeout = 0;
    out->keyCheck = true;
    out->fuseArgc = 0;
    out->forceDecode = false;

    // pass executable name through
    out->fuseArgv[0] = lastPathElement(argv[0]);
    ++out->fuseArgc;

    // leave a space for mount point, as FUSE expects the mount point before
    // any flags
    out->fuseArgv[1] = NULL;
    ++out->fuseArgc;
   
    // TODO: can flags be internationalized?
    static struct option long_options[] = {
	{"verbose", 0, 0, 'v'}, // verbose mode
	{"fuse-debug", 0, 0, 'd'}, // Fuse debug mode
	{"idle", 1, 0, 'i'}, // idle timeout
	{"anykey", 0, 0, 'k'}, // skip key checks
	{"forcedecode", 0, 0, 'D'}, // force decode
	{"extpass", 1, 0, 'p'}, // external password program
	{"version", 0, 0, 'V'}, //version
	{"fuse-help", 0, 0, 'H'}, // fuse_mount usage
	{0,0,0,0}
    };

    while (1)
    {
	int option_index = 0;

	// 's' : single-threaded mode
	// 'f' : foreground mode
	// 'v' : verbose mode (same as --verbose)
	// 'd' : fuse debug mode (same as --fusedebug)
	// 'i' : idle-timeout, takes argument
	int res = getopt_long( argc, argv, "Hsfvdi:",
		long_options, &option_index);

	if(res == -1)
	    break;

	switch( res )
	{
	case 's':
	    out->isThreaded = false;
	    PUSHARG("-s");
	    break;
	case 'f':
	    out->isDaemon = false;
	    // this option was added in fuse 2.x
	    PUSHARG("-f");
	    break;
	case 'v':
	    out->isVerbose = true;
	    break;
	case 'd':
	    PUSHARG("-d");
	    break;
	case 'i':
	    out->idleTimeout = strtol( optarg, (char**)NULL, 10);
	    break;
	case 'k':
	    out->keyCheck = false;
	    break;
	case 'D':
	    out->forceDecode = true;
	    break;
	case 'p':
	    out->passwordProgram.assign( optarg );
	    break;
	case 'V':
	    // xgroup(usage)
	    cerr << autosprintf(_("encfs version %s"), VERSION) << endl;
	    exit(EXIT_SUCCESS);
	    break;
	case 'H':
	    FuseUsage();
	    exit(EXIT_SUCCESS);
	    break;
	case '?':
	    // invalid options..
	    break;
	case ':':
	    // missing parameter for option..
	    break;
	default:
	    rWarning(_("getopt error: %i"), res);
	    break;
	}
    }
	    
    // we should have at least 2 arguments left over - the source directory and
    // the mount point.
    if(optind+2 <= argc)
    {
	if(!isDirectory( argv[optind] ) && !userAllowCreate( argv[optind] ))
	{
	    // root directory doesn't exist, and the user refused to allow us
	    // to create a non-existant one.
	    rWarning(_("Unable to locate root directory, aborting."));
	    return false;
	} else
	{
	    out->rootDir = argv[optind++];
	    if( out->rootDir[ out->rootDir.length()-1 ] != '/' )
		out->rootDir.append( "/" );
	}
		
	if(!isDirectory( argv[optind] ) && !userAllowCreate( argv[optind] ))
	{
	    // mount point doesn't exist, and the user refused to allow it to
	    // be created..
	    rWarning(_("Unable to locate mount point, aborting."));
	    return false;
	} else
	{
	    out->mountPoint = argv[optind++];
	}
    } else
    {
	// no mount point specified
	rWarning(_("Missing one or more arguments, aborting."));
	return false;
    }

    // If there are still extra unparsed arguments, pass them onto FUSE..
    if(optind < argc)
    {
	rAssert(out->fuseArgc < MaxFuseArgs);

	while(optind < argc)
	{
	    rAssert(out->fuseArgc < MaxFuseArgs);
	    out->fuseArgv[out->fuseArgc++] = argv[optind];
	    ++optind;
	}
    }

    // sanity check
    if(out->isDaemon && 
	    (!isAbsolutePath( out->mountPoint.c_str() ) ||
	    !isAbsolutePath( out->rootDir.c_str() ) ) 
      )
    {
	cerr << 
	    // xgroup(usage)
	    _("When specifying daemon mode, you must use absolute paths "
		    "(beginning with '/')")
	    << endl;
	return false;
    }

    // fill in mount path for fuse
    out->fuseArgv[1] = out->mountPoint.c_str();

    return true;
}

/*
    I think I nabbed this stuff from a test program from the OpenSSL
    distribution..
*/
#ifdef HAVE_SSL
unsigned long pthreads_thread_id()
{
    return (unsigned long)pthread_self();
}

static pthread_mutex_t *crypto_locks = NULL;
void pthreads_locking_callback( int mode, int n,
	const char *caller_file, int caller_line )
{
    (void)caller_file;
    (void)caller_line;

    if(!crypto_locks)
    {
	rDebug("Allocating %i locks for OpenSSL", CRYPTO_num_locks() );
	crypto_locks = new pthread_mutex_t[ CRYPTO_num_locks() ];
	for(int i=0; i<CRYPTO_num_locks(); ++i)
	    pthread_mutex_init( crypto_locks+i, 0 );
    }

    if(mode & CRYPTO_LOCK)
    {
	pthread_mutex_lock( crypto_locks + n );
    } else
    {
	pthread_mutex_unlock( crypto_locks + n );
    }
}

void pthreads_locking_cleanup()
{
    if(crypto_locks)
    {
	for(int i=0; i<CRYPTO_num_locks(); ++i)
	    pthread_mutex_destroy( crypto_locks+i );
	delete[] crypto_locks;
	crypto_locks = NULL;
    }
}
#endif

/*
    Idle monitoring thread.  This is only used when idle monitoring is enabled.
    It will cause the filesystem to be automatically unmounted (causing us to
    commit suicide) if the filesystem stays idle too long.  Idle time is only
    checked if there are no open files, as I don't want to risk problems by
    having the filesystem unmounted from underneath open files!
*/
static bool gRunning = true;
pthread_cond_t gWakeupCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t gWakeupMutex = PTHREAD_MUTEX_INITIALIZER;
const int MinActivityCheckInterval = 10;

void * idleMonitor(void *_arg)
{
    EncFS_Args *arg = (EncFS_Args*)_arg;

    int timeoutSeconds = 60 * arg->idleTimeout;

    pthread_mutex_lock( &gWakeupMutex );

    while(gRunning)
    {
	int nextCheck = MinActivityCheckInterval;

	int idleSeconds = FSRoot->idleSeconds();
	if(idleSeconds >= timeoutSeconds)
	{
	    if( !FSRoot->hasOpenFiles() )
	    {
		// Time to unmount!
		// xgroup(diag)
		rWarning(_("Unmounting filesystem %s due to inactivity"),
			arg->mountPoint.c_str());
		fuse_unmount( arg->mountPoint.c_str() );
		// wait for main thread to wake us up
		pthread_cond_wait( &gWakeupCond, &gWakeupMutex );
		break;
	    } else
	    {
		rDebug("open files: \n%s", FSRoot->openFileList().c_str());
	    }
	} else
	{
	    // don't need to check again until it could possibly timeout
	    nextCheck = MIN(timeoutSeconds - idleSeconds,
		            MinActivityCheckInterval);
	    rDebug("checking idle time again in %i seconds", nextCheck);
	}

	struct timeval currentTime;
	gettimeofday( &currentTime, 0 );
	struct timespec wakeupTime;
	wakeupTime.tv_sec = currentTime.tv_sec + nextCheck;
	wakeupTime.tv_nsec = currentTime.tv_usec * 1000;
	pthread_cond_timedwait( &gWakeupCond, &gWakeupMutex, &wakeupTime );
    }
    
    pthread_mutex_unlock( &gWakeupMutex );

    rDebug("Idle monitoring thread exiting");

    return 0;
}

int main(int argc, char *argv[])
{
    // initialize the logging library
    RLogInit( argc, argv );

#ifdef LOCALEDIR
    setlocale( LC_ALL, "" );
    bindtextdomain( PACKAGE, LOCALEDIR );
    textdomain( PACKAGE );
#endif

#ifdef HAVE_SSL
    // initialize the SSL library
    SSL_load_error_strings();
    SSL_library_init();

    unsigned int randSeed = 0;
    RAND_bytes( (unsigned char*)&randSeed, sizeof(randSeed) );
    srand( randSeed );
#endif

    // log to stderr by default..
    StdioNode *slog = new StdioNode( STDERR_FILENO );
    SyslogNode *logNode = NULL;

    // show error and warning output
    slog->subscribeTo( GetGlobalChannel("error") );
    slog->subscribeTo( GetGlobalChannel("warning") );

    // anything that comes from the user should be considered tainted until
    // we've processed it and only allowed through what we support.
    EncFS_Args encfsArgs;
    for(int i=0; i<MaxFuseArgs; ++i)
	encfsArgs.fuseArgv[i] = NULL; // libfuse expects null args..

    if(argc == 1 || !processArgs(argc, argv, &encfsArgs))
    {
	usage(argv[0]);
	return EXIT_FAILURE;
    }

    if(encfsArgs.isVerbose)
    {
	// subscribe to more logging channels..
	slog->subscribeTo( GetGlobalChannel("info") );
	slog->subscribeTo( GetGlobalChannel("debug") );
    }
  
#ifdef HAVE_SSL
    if(encfsArgs.isThreaded)
    {
	// provide locking functions to OpenSSL since we'll be running with
	// threads accessing openssl in parallel.
	CRYPTO_set_id_callback( pthreads_thread_id );
	CRYPTO_set_locking_callback( pthreads_locking_callback );
    }
#endif

    rDebug("Root directory: %s", encfsArgs.rootDir.c_str());
    rDebug("Fuse arguments: %s", showFuseArgs( encfsArgs ).c_str());
	
    fuse_operations encfs_oper;
    // in case this code is compiled against a newer FUSE library and new
    // members have been added to fuse_operations, make sure they get set to
    // 0..
    memset(&encfs_oper, 0, sizeof(fuse_operations));

    encfs_oper.getattr = encfs_getattr;
    encfs_oper.readlink = encfs_readlink;
    encfs_oper.getdir = encfs_getdir;
    encfs_oper.mknod = encfs_mknod;
    encfs_oper.mkdir = encfs_mkdir;
    encfs_oper.unlink = encfs_unlink;
    encfs_oper.rmdir = encfs_rmdir;
    encfs_oper.symlink = encfs_symlink;
    encfs_oper.rename = encfs_rename;
    encfs_oper.link = encfs_link;
    encfs_oper.chmod = encfs_chmod;
    encfs_oper.chown = encfs_chown;
    encfs_oper.truncate = encfs_truncate;
    encfs_oper.utime = encfs_utime;
    encfs_oper.open = encfs_open;
    encfs_oper.read = encfs_read;
    encfs_oper.write = encfs_write;
    encfs_oper.statfs = encfs_statfs;
    encfs_oper.release = encfs_release;
    encfs_oper.fsync = encfs_fsync;
#ifdef HAVE_XATTR
    encfs_oper.setxattr = encfs_setxattr;
    encfs_oper.getxattr = encfs_getxattr;
    encfs_oper.listxattr = encfs_listxattr;
    encfs_oper.removexattr = encfs_removexattr;
#endif // HAVE_XATTR

    const bool enableIdleChecking = (encfsArgs.idleTimeout > 0);
    const bool createFSIfNotFound = true;
    RootPtr rootInfo = initFS( encfsArgs.rootDir, enableIdleChecking,
	    createFSIfNotFound, encfsArgs.keyCheck, encfsArgs.forceDecode,
	    encfsArgs.passwordProgram );
    if( !rootInfo.isNull() )
    {
	// set the globally visible root directory node
	FSRoot = rootInfo->root.get();
	    
	if(encfsArgs.isThreaded == false && encfsArgs.idleTimeout > 0)
	{
	    // xgroup(usage)
	    cerr << _("Note: requested single-threaded mode, but an idle\n"
		    "timeout was specified.  The filesystem will operate\n"
		    "single-threaded, but threads will still be used to\n"
		    "implement idle checking.") << endl;
	}

	// reset umask now, since we don't want it to interfere with the
	// pass-thru calls..
	umask( 0 );

	if(encfsArgs.isDaemon)
	{
	    // switch to logging just warning and error messages via syslog
	    logNode = new SyslogNode( "encfs" );
	    logNode->subscribeTo( GetGlobalChannel("warning") );
	    logNode->subscribeTo( GetGlobalChannel("error") );

	    // disable stderr reporting..
	    delete slog;
	    slog = NULL;

	    // keep around a pointer just in case we end up needing it to
	    // report a fatal condition later (fuse_main exits unexpectedly)...
	    oldStderr = dup( STDERR_FILENO );
	}

	// if an idle timeout is specified, then setup a thread to monitor the
	// filesystem.
	pthread_t monitorThread;
	if(encfsArgs.idleTimeout > 0)
	{
	    rDebug("starting idle monitoring thread");
	    pthread_create( &monitorThread, 0, idleMonitor, 
		    (void*)&encfsArgs );
	}

	try
	{
	    // fuse_main returns an error code in newer versions of fuse..
	    int res = fuse_main( encfsArgs.fuseArgc, 
		    const_cast<char**>(encfsArgs.fuseArgv), 
		    &encfs_oper);

	    if(res != 0 && encfsArgs.isDaemon)
	    {
		// the users will not have seen any message from fuse, so say a
		// few words in libfuse's memory..
		FILE *out = fdopen( oldStderr, "a" );
		// xgroup(usage)
		fprintf(out, _("fuse failed.  Common problems:\n"
			" - fuse kernel module not installed (modprobe fuse)\n"
			" - invalid options -- see usage message\n"));
		fclose(out);
		exit(1);
	    }
	} catch(std::exception &ex)
	{
	    rError(_("Internal error: Caught exception from main loop: %s"), 
		    ex.what());
	} catch(...)
	{
	    rError(_("Internal error: Caught unexpected exception"));
	}
  
	if(encfsArgs.idleTimeout > 0)
	{
	    gRunning = false;

	    // wake up the thread if it is waiting..
	    rDebug("waking up monitoring thread");
	    pthread_mutex_lock( &gWakeupMutex );
	    pthread_cond_signal( &gWakeupCond );
	    pthread_mutex_unlock( &gWakeupMutex );
	    rDebug("joining with idle monitoring thread");
	    pthread_join( monitorThread , 0 );
	    rDebug("join done");
	}
    }

    // cleanup so that we can check for leaked resources..
    rootInfo.reset();
    FSRoot = NULL;

    MemoryPool::destroyAll();
#ifdef HAVE_SSL
    if(encfsArgs.isThreaded)
	pthreads_locking_cleanup();
#endif
    delete logNode;

    return 0;
}
