/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <opencm.h>
#include <opencm-version.h>
#include <obspace/ObSpace.h>

#if 0
#define R_KEY_FILE      "server.key"
#define R_MUTABLES      "mutables"
#define R_ENTITIES      "entities"
#define R_REVISIONS     "revisions"
#define R_LOCKFILE      "LOCK"
#define R_SELF          "self"
#define R_EVERYONE      "everyone"
#define R_TYPE_FILE     "store_type"
#define R_USR_LINK      "usr"
#endif

#define R_LOCKDIR       "locks"
#define R_CONFIG        "config"
#define R_IDENTITY      "identity"
#define R_EXCLUSIVELOCK "EXCLUSIVE"
#define R_METALOCK      "METALOCK"
#define R_ADMIN_GROUP   "admin_group"
#define R_CERT_FILE     "server.pem"
#define R_USERS         "users"
#define R_USR_ACCESS    "access"

#define CM_VERSION_FILE "OPENCM-VERSION"

static void ls_maybe_upgrade_repository(LocalObSpace *los);

OC_bool repos_validate_pubkey(const char *reposName, PubKey *pk);

static void *deserialize_buffer(Buffer *b)
{
  return 0;
}

static OC_bool
validate_x509_cert(const char *reposName, X509 *cert)
{
  const char *srv_dir;
  const char *srv_file;
  
  srv_dir = path_join(opt_ConfigDir, CM_SERVER_SUBDIR);
  srv_file = path_join(srv_dir, xstrcat(reposName, ".pem"));

  if (!path_exists(srv_dir))
    path_smkdir(srv_dir);

  if (path_exists(srv_file)) {
    long num = 0;
    X509 *saved_cert = ssl_read_cert_file(srv_file);
    BIO *saved_cert_mem = BIO_new(BIO_s_mem());
    BIO *cert_mem = BIO_new(BIO_s_mem());
    void *vbuf;
    char *saved_cert_data;
    char *cert_data;

    if (!PEM_write_bio_X509(saved_cert_mem, saved_cert))
      return FALSE;

    if ((num = BIO_get_mem_data(saved_cert_mem, &vbuf)) == 0)
      return FALSE;

    saved_cert_data = (char *)GC_MALLOC_ATOMIC(sizeof(char) * (num + 1));
    memcpy(saved_cert_data, vbuf, num);
    saved_cert_data[num] = 0;

    if (!PEM_write_bio_X509(cert_mem, cert))
      return FALSE;

    if ((num = BIO_get_mem_data(cert_mem, &vbuf)) == 0)
      return FALSE;

    cert_data = (char *)GC_MALLOC_ATOMIC(sizeof(char) * (num + 1));
    memcpy(cert_data, vbuf, num);
    cert_data[num] = 0;

    if ( !nmequal(cert_data, saved_cert_data) )
      return FALSE;
  }
  else {
    if (!path_exists(srv_dir))
      path_mkdir(srv_dir);

    ssl_write_cert_file(srv_file, cert);
  }

  return TRUE;
}

static OC_bool
validate_pubkey(const char *reposName, PubKey *pk)
{
  switch(pk->type) {
  case pk_X509:
    return validate_x509_cert(reposName, pk->keys.x509_cert);
  default:
    THROW(ExBadValue, "<unknown key type>\n");
    return FALSE;		/* not reached */
  }
}

static int
WriteOneLineFile(const char *path, const char *string)
{
  int result;
  FILE *f = NULL;

  TRY {
    f = xfopen(path, 'c', 't');
  }
  DEFAULT(ex) {
    THROW(ex, format("Could not write file \"%s\": errno %d", path,
		     errno));
  }
  END_CATCH;

  result = fprintf(f, "%s\n", string);
  fclose(f);

  return result;
}

static char *
ReadOneLineFile(const char *path)
{
  char *buf;
  size_t len = path_file_length(path);
  FILE *f = xfopen(path, 'r', 't');

  buf = GC_MALLOC_ATOMIC(len + 1);

  /* Length may get truncated if this is a WIN32 text file */
  len = fread(buf, 1, len, f);

  buf[len--] = 0;
  while (buf[len] == '\n' || buf[len] == '\r')
    buf[len--] = 0;

  fclose(f);

  return xstrdup(buf);
}

static const char *
keyBindingName(PubKey *pk)
{
  /* Prefix the id with "PKB_" (Public Key Binding), because one of
     the possible leading characters from the hash is '-', and UNIX
     utilities have a hard time with files that begin with '-'. */
  return xstrcat("PKB_", pubkey_getid(pk));
}

static const char *
keyBindingPath(LocalObSpace *los, PubKey *pk)
{
  const char *path = path_join(los->uri->path, R_USERS);
  return path_join(path, keyBindingName(pk));
}

#if 0
static void
store_user_access(LocalObSpace *los, PubKey *pk, unsigned int access)
{
  SDR_stream *strm = NULL;
  const char *path = keyBindingPath(los, pk);

  if (!path_exists(path))
    THROW(ExObjectExists, 
	  format("User not found (path=%s)\n", path));

  TRY {
    strm = stream_createfile(path_join(path, R_USR_ACCESS), SDR_BINARY);
    sdr_w_u32(R_USR_ACCESS, strm, access);
  }
  DEFAULT(ex) {
    if (strm) stream_close(strm);
    RETHROW(ex);
  }
  END_CATCH;
  stream_close(strm);
}
#endif

static unsigned int
get_user_access(LocalObSpace *los, PubKey *pk)
{
  unsigned int access = 0x0;
  SDR_stream *strm = NULL;
  const char *path = keyBindingPath(los, pk);

  if (!path_exists(path))
    THROW(ExObjectExists, 
	  format("User not found (path=%s)\n", path));

  strm = stream_fromfile(path_join(path, R_USR_ACCESS), SDR_AUTO);
  access = sdr_r_u32(R_USR_ACCESS, strm);
  stream_close(strm);

  return access;
}

static OC_bool
other_locks_exist(const char *dirPath)
{
  OC_bool result = FALSE;
  OC_DIR* dir;
  const char* ent;
  
  dir = path_opendir(dirPath, TRUE);

  while ((ent = path_readdir(dir))) {
    if (path_should_skip_dirent(ent))
      continue;

    if (strcmp(ent, R_EXCLUSIVELOCK) == 0)
      continue;

    result = TRUE;
    break;
  }

  path_closedir(dir);
  return result;
}

static void
acquire_exclusive_los_lock(LocalObSpace *los)
{
  const char *lockDir = path_join(los->uri->path, R_LOCKDIR);
  const char *metaLock = path_join(lockDir, R_METALOCK);
  const char *exclLock = path_join(lockDir, R_EXCLUSIVELOCK);

  os_create_local_lockfile(metaLock);

  os_create_local_lockfile(exclLock);

  while (other_locks_exist(lockDir)) {
    log_trace(DBG_LOCK, "Waiting for other clients to exit...\n");
    sleep(1);
  }

  los->activityLock = exclLock;

  path_remove(metaLock);
}

static OC_bool
acquire_activity_lock(LocalObSpace *los)
{
  OC_bool result = FALSE;

  const char *lockDir = path_join(los->uri->path, R_LOCKDIR);
  const char *metaLock = path_join(lockDir, R_METALOCK);
  const char *exclLock = path_join(lockDir, R_EXCLUSIVELOCK);
  const char *myLock = path_join(lockDir, xunsigned_str(os_GetPid()));

  os_create_local_lockfile(metaLock);

  if (os_validate_local_lockfile(exclLock)) {
    path_remove(metaLock);
    THROW(ExLockFail, 
	  format("An exclusive lock is currently held on repository %s", 
		 los->uri->URI));
  }

  os_create_local_lockfile(myLock);

  los->activityLock = myLock;

  path_remove(metaLock);
  return result;
}

static const char *
ls_GetVersion(ObSpace *s)
{
  LocalObSpace *los = (LocalObSpace *)s;
  const char *version = ReadOneLineFile(path_join(los->uri->path,
					path_join(R_CONFIG, CM_VERSION_FILE)));

  return version;
}

static void
ls_connect(ObSpace *s, PubKey *pk, OC_bool exclusive)
{
  LocalObSpace *los = (LocalObSpace *)s;
  const char *configPath = path_join(los->uri->path, R_CONFIG);

  if (!path_exists(los->uri->path))
    THROW(ExNoObject, 
	  format("Repository %s does not exist", los->uri->URI));

  if (os_is_local_path(configPath) == FALSE)
    THROW(ExNoConnect, 
	  format("Repository %s is not mounted on a local file system", los->uri->URI));

  if (exclusive)
    acquire_exclusive_los_lock(los);
  else
    acquire_activity_lock(los);

  if (!path_exists(path_join(configPath, CM_VERSION_FILE)))
    THROW(ExNoObject, 
	  format("Cannot determine repository %s schema version",
		 los->uri->URI));

  /* The checks above must continue to hold for all versions of the
     repository schema. If you can't even get far enough to
     determine the repository version, it's hardly possible to
     upgrade, now is it?

     The following function will upgrade the repository schema if
     that is called for. */
  if (exclusive) ls_maybe_upgrade_repository(los);

  {
    const char *schema_v = ls_GetVersion((ObSpace *) los);

    if (compare_rpm_versions(schema_v, VERSION_STRING) != 0) {
      THROW(ExBadValue, 
	    format("Repository schema is not current for %s",
		   los->uri->URI));
    }
  }
    
  if (!path_exists(path_join(configPath, R_IDENTITY)))
    THROW(ExNoObject, 
	  format("Cannot determine repository %s identity",
		 los->uri->URI));

  if (!path_exists(path_join(configPath, R_ADMIN_GROUP)))
    THROW(ExNoObject, 
	  format("Cannot determine repository %s admin group",
		 los->uri->URI));

  los->repositoryID = ReadOneLineFile(path_join(configPath, R_IDENTITY));
  los->pubKey = pubkey_from_PemFile(path_join(configPath, R_CERT_FILE));

  /* This is here to ensure that the cert gets validated in the
     users cert records. */
  validate_pubkey(los->repositoryID, los->pubKey);

  if (pk) {
    assert(los->base.authUser == 0);
    assert(los->base.authMutable == 0);

    /* Until proven otherwise: */
    los->base.authAccess = 0;

    TRY {
      Buffer *b;
      los->base.authMutable = ObSpace_GetUser(los, pk);
      b = ObSpace_GetMutableContent(los, los->base.authMutable);

      los->base.authUser = deserialize_buffer(b);
      los->base.authAccess = get_user_access(los, pk);
    } 
    CATCH(ExNoObject) {
      RETHROW(ExNoAuth);
    }
    END_CATCH;
  }

  los->adminGroup = ReadOneLineFile(path_join(configPath, R_ADMIN_GROUP));
}

static void
ls_disconnect(ObSpace *s)
{
  LocalObSpace *los = (LocalObSpace *)s;

  path_remove(los->activityLock);

}

static void
UpdateRepositoryVersionTo(LocalObSpace *los, const char *version_string)
{
  const char *verfile_path = 
    path_join(los->uri->path,
	      path_join(R_CONFIG, CM_VERSION_FILE));

  const char *vertmpfile_path =
    xstrcat(verfile_path, ".new");

  onthrow_remove(vertmpfile_path);

  WriteOneLineFile(vertmpfile_path, VERSION_STRING);

  path_rename(vertmpfile_path, verfile_path);
}

/* fsrepos_maybe_upgrade_repository(r) -- decide whether the
   repository schema needs to be updated, and if so, perform the update. */
#define OLDEST_RECOGNIZED_SCHEMA "0.1.0alpha5"
void 
ls_maybe_upgrade_repository(LocalObSpace *los)
{
  struct sigaction old_sigint;
  struct sigaction old_sighup;
  struct sigaction old_sigquit;
  struct sigaction old_sigterm;
  struct sigaction sig_ignore;

  const char *schema_v = ObSpace_GetVersion(los);

  if (compare_rpm_versions(schema_v, VERSION_STRING) == 0) {
    log_trace(TRC_UPGRADE, "Repository schema version is current\n");
    return;
  }

  memset(&sig_ignore, 0, sizeof(sig_ignore));
  sig_ignore.sa_handler = SIG_IGN;
  sig_ignore.sa_flags = 0;

  log_trace(TRC_UPGRADE, "Repository version %s being upgraded...\n"
	     "    Do not interrupt this.\n", schema_v);

  if (compare_rpm_versions(schema_v, OLDEST_RECOGNIZED_SCHEMA) < 0)
    THROW(ExVersionError, "Repository schema is too old to be upgraded by OpenCM");

  sigaction(SIGINT, &sig_ignore, &old_sigint);
  sigaction(SIGHUP, &sig_ignore, &old_sighup);
  sigaction(SIGQUIT, &sig_ignore, &old_sigquit);
  sigaction(SIGTERM, &sig_ignore, &old_sigterm);

  /* The general idea here is to apply, sequentially, all of the
     necessary upgrade procedures. It is typical that the repository
     schema does NOT change with a given version of OpenCM, and in
     that case we can just blast the number and declare victory. 

     A word to the maintainer: this is a VERY sensitive sequence of
     operations. If there is just ONE place in the OpenCM code where
     you document obsessively, this should be it.

     The sigaction() calls notwithstanding, a proper upgrade procedure
     should endeavour to be robust against system crashes and/or
     shutdowns. Also, the user could always send us SIGKILL, which
     would stop us pretty much dead in our tracks. 

     FIX: Perhaps we should just fail the connect rather than do an
     in-place upgrade.  It would give the administrator a chance to
     backup the repository first.  The problem is that this would
     break things for naive users. I am considering adding a test for
     the presence of
    
        {REPOS-ROOT}/config/NO_AUTO_UPGRADE

     If present, it means don't do any upgrades by magic. Require
     instead that the administrator do an upgrade explicitly via the
     (not yet implemented) 'opencm upgrade' command.
  */

  /* Now that we have applied all of the required upgrades, rewrite
     the repository version to be the current one. */

  UpdateRepositoryVersionTo(los, VERSION_STRING);
  log_trace(TRC_UPGRADE, "Repository schema has been successfully upgraded to %s\n", VERSION_STRING);

  sigaction(SIGINT, &old_sigint, 0);
  sigaction(SIGHUP, &old_sighup, 0);
  sigaction(SIGQUIT, &old_sigquit, 0);
  sigaction(SIGTERM, &old_sigterm, 0);
}

void
LocalObSpace_init(LocalObSpace *ls, const char *storeType)
{
  ObSpace *s = (ObSpace *)ls;

  bzero(ls, sizeof(ls));

  s->GetVersion        = ls_GetVersion;
  s->Connect           = ls_connect;
  s->Disconnect        = ls_disconnect;

#if 0
  /* Entity management:  */
  s->ReviseEntity      = diskrepos_ReviseEntity;
  s->GetEntity         = diskrepos_GetEntity;

  /* Mutable management: */
  s->GetMutable        = diskrepos_GetMutable;
  s->CreateMutable     = diskrepos_CreateMutable;
  s->DupMutable        = diskrepos_DupMutable;
  s->ReviseMutable     = diskrepos_ReviseMutable;
  s->GetRevision       = diskrepos_GetRevision;
  s->PutRevision       = diskrepos_PutRevision;
  s->SetMutableACL     = diskrepos_SetMutableACL;
  s->SetMutableFlags   = diskrepos_SetMutableFlags;
  s->SetMutableName    = diskrepos_SetMutableName;
  s->SetMutableDesc    = diskrepos_SetMutableDesc;

  s->BindUser          = diskrepos_BindUser;
  s->RebindUser        = diskrepos_RebindUser;
  s->GetUser           = diskrepos_GetUser;
  s->GetUserAccess     = diskrepos_GetUserAccess;
  s->ShutDown          = diskrepos_ShutDown;
  s->GetParents        = diskrepos_GetParents;
  s->SetCompression    = diskrepos_SetCompression;
#endif
}
