/*
 * 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 <repos/Repository.h>
#include <repos/FSRepos.h>
#include <common/SSLcommon.h>

#define REPOS_ECACHE_SIZE 20
EntityCache *repos_ecache = 0;

Repository *
repository_open(const char *argURI)
{
  URI *uri;
  Repository *r = (Repository *) GC_MALLOC(sizeof(Repository));
  
  r->uri = uri = uri_create(argURI);
  r->storeType = NULL;
  r->store = NULL;
  r->repositoryID = NULL;
  r->svrPubKey = 0;
  r->authMutable = 0;
  r->authUser = 0;
  r->authAccess = 0x0;
  r->adminGroup = 0;
  r->info = 0;
  r->entityCache = 0;
  
  if (repos_ecache == 0)
    repos_ecache = ecache_create(REPOS_ECACHE_SIZE);

  if (uri->path == NULL && (strcmp(uri->scheme, CM_REPOS_SSL) != 0))
    THROW(ExBadValue, 
	  format("No repository path specified in %s", argURI));

  if (!uri->well_formed)
    THROW(ExBadValue, 
	  format("Malformed server URI %s", argURI));

  if (uri->relative)
    THROW(ExBadValue, format("Relative URI (%s) not appropriate", argURI));

  /* If we're dealing with a file-system based repository (non-network)
   * then we need to determine the underlying flavor:  plain-jane,
   * sxdfs, etc.  So, look for the type file in the actual repository
   * path and then initialize according to its contents. */
  if (strcmp(uri->scheme, CM_REPOS_FILE) == 0) {
    const char *configPath = path_join(uri->path, R_CONFIG);
    const char *typefile = path_join(configPath, R_STORETYPE);

    if (!path_exists(typefile))
      THROW(ExNoObject, 
            format("Repository is corrupted: no type file %s",
                   typefile));

    r->storeType = ReadOneLineFile(typefile);
    if (strcmp(r->storeType, CM_REPOS_FS) == 0 || 
	strcmp(r->storeType, CM_REPOS_GZFS) == 0 || 
	strcmp(r->storeType, CM_REPOS_SXD2) == 0) {
      fsrepos_init(r);
    } else {
      THROW(ExBadValue, 
	    format("Don't know how to open this type: %s.\n", 
		   r->storeType));
    }

  } else if (strcmp(uri->scheme, CM_REPOS_SSL) == 0) {
    netrepository_init(r);
  } else {
    THROW(ExBadValue, 
	  format("Unknown repository scheme: %s.\n", uri->scheme));
  }
  
  return r;
}

Repository *
repository_dup(Repository *r)
{
  Repository *newR = (Repository *) GC_MALLOC(sizeof(Repository));
  memcpy(newR, r, sizeof(*r));

  return newR;
}

static OC_bool
repos_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;
}

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

Serializable *
repos_GetMutableContent(Repository *r, Mutable *m)
{
  Revision *rev = NULL;

  assert(m != 0);

  /* First get the current revision */
  rev = (Revision *) repos_GetTopRev(r, m);

  if (!rev || !rev->newObject)
    THROW(ExNoObject, format("Mutable %s has no content", m->uri));

  /* Now get the Serializable that the Revision references */
  return repos_GetEntity(r, m->uri, rev->newObject);
}


Mutable *
repos_GetMutable(Repository *r, const char *uri)
{
  Mutable *m;
 
  /* Retrieve the mutable */
  m = r->GetMutable(r, uri);

  /* Validate signature */
  if (!mutable_VerifySignature(m))
    THROW(ExIntegrityFail, 
	  format("Signature did not check on mutable %s", uri));

  return m;
}

void
repos_FlushCache(Repository *r)
{
  repos_ecache = ecache_create(REPOS_ECACHE_SIZE);
}

void *
repos_GetEntity(Repository *r, const char *mURI, const char *tn)
{
  void *ent;

  ent = ecache_lookup(repos_ecache, tn);
  if (ent) {
    log_trace(DBG_CACHE, "EntityCache hit for %s (%s)\n", tn,
	      ((Serializable *)ent)->ser_type->tyName);
    return ent;
  }

#if 0
  /* Retrieve the entity */
  ent = r->GetEntity(r, mURI, tn);

  /* Integrity (of contents) check. This check is relatively
     expensive, and we should be seeking to avoid it on the server. */

  /* Look it up again. If we are layered on a lower-level repository,
     we can rely on the fact that the repos_GetEntity() call on THAT
     repository would not have cached the object until it was
     checked. */
  if (ecache_lookup(repos_ecache, tn))
    return ent;

  {
    const char * check_tn = ser_getTrueName(ent);
    if (!nmequal(tn, check_tn))
      THROW(ExIntegrityFail, 
	    format("SHA1 did not check (%s) for entity %s", check_tn, tn));
  }
#else
  {
    WireDelta *wd = repos_GetEntityDelta(r, mURI, tn, 0);
    Buffer *buf = wd->delta;
    SDR_stream *strm;

    if (wd->baseTrueName != 0)
      THROW(ExAssert, "Full entity requested, but delta format returned.");

    /* GetEntityDelta() is specified to always return a buffer that is
     * serialized in BINARY format. This means we can cheaply
     * recompute the SHA1. */

    strm = stream_fromBuffer(buf, SDR_AUTO);

    ent = sdr_read("ENTITY", strm);

    stream_close(strm);

    {
      /* Incoming buffer was BINARY format. Canonical truename is
	 defined as sha1 of buffer resulting from BINARY
	 serialization. Skip the reserialization and just compute the
	 SHA1 directly from the buffer. */
      const char *check_tn = buffer_getContentTrueName(buf);
      if (!nmequal(tn, check_tn))
	THROW(ExIntegrityFail, 
	      format("SHA1 did not check (%s) for entity %s", check_tn, tn));
    }
  }
#endif

  log_trace(DBG_CACHE, "EntityCache miss for %s (%s)\n", tn, 
	 ((Serializable *)ent)->ser_type->tyName);

  /* No point cacheing the entity bits, as we never fetch them more
   * than once in a given run and they will tend to crowd other things
   * out of the cache. */
  if (GETTYPE(ent) != TY_Buffer)
    ecache_insert(repos_ecache, tn, ent);

  return ent;
}

void
repos_ReviseEntity(Repository *r, const char *mURI, 
		   const char *prevTrueName, void *ent)
{
  r->ReviseEntity(r,mURI,prevTrueName,ent);
}


static OC_bool
is_contained(const char *member, StrVec *set)
{
  unsigned u;
  for (u = 0; u < vec_size(set); u++) {
    if (nmequal(member, vec_fetch(set, u, const char *)))
      return TRUE;
  }
  return FALSE;
}

/* Recursive function to test group membership.  Only tests a group
 * once, based on the 'marked' vector. */
static OC_bool
recursive_user_in_group(Repository *r, const char *gname, StrVec *marked)
{
  unsigned u;
  Mutable *m = 0;
  Group *g = 0;

  if (nmequal(gname, r->authMutable->uri))
    return TRUE;

  TRY {
    m = repos_GetMutable(r, gname);
    g = (Group *)repos_GetMutableContent(r, m);
  } 
  DEFAULT(any) {
  } 
  END_CATCH;

  if (m == NULL || g == NULL)
    return FALSE;

  if (GETTYPE(g) != TY_Group)
    return FALSE;

  /* Skip groups that we've already examined */
  if (is_contained(gname, marked)) 
    return FALSE;

  strvec_append(marked, gname);

  /* Try depth-first: */
  for (u = 0; u < vec_size(g->members); u++) {
    const char *member = vec_fetch(g->members, u, const char *);

    if (recursive_user_in_group(r, member, marked))
      return TRUE;
  }

  return FALSE;
}

OC_bool
repos_user_in_group(Repository *r, const char *gname, unsigned access)
{
  StrVec *marked = strvec_create();

  assert (access == ACC_READ || access == ACC_WRITE);

  /* Anyone operating with the server's private key has access to the
     underlying native file system or database. Grant such a user READ
     access on anything. We actually do use this. It lets us offload
     change notification from the server by running it in a
     server-side client.

     6/18/2003: Also grant this key WRITE access, since it is
     necessary for upgrade purposes that the server key be able to
     rewrite existing mutables. */

  if (nmequal(pubkey_getid(r->svrPubKey), 
	      pubkey_getid(r->authUser->pubKey)))
    return TRUE;

  /* Use a recursive check since groups may contain groups */
  return (recursive_user_in_group(r, gname, marked));
}

OC_bool
repos_check_ACL(Repository *r, Mutable *m, unsigned access) 
{
  OC_bool result = TRUE;

  if (m == NULL)
    return FALSE;

  assert (access == ACC_READ || access == ACC_WRITE);

  /* Someone who has write access has sufficient authority to alter or
   * replace the read ACL. Therefore, write access dominates read
   * access. I (shap) find the implications of this unsettling, but
   * it was definitely unnatural that prior to this realization the
   * system supported blind writes.
   *
   * Translation:
   *
   *   all writers are readers
   *   not all readers are writers
   *   some pigs are more equal than others.
   */

  /* Membership in the write group is sufficient for either read
   * or write: */
  result = repos_user_in_group(r, m->modGroupURI, access);

  if (result == FALSE && access == ACC_READ)
    result = repos_user_in_group(r, m->readGroupURI, access);

  return result;
}

#if 0
Revision *
repos_GetThisRev(Repository *r, Mutable *m, uint64_t revNo)
{
  ObVec *vec = repos_GetRevisions(r, m->uri, revNo, revNo);
  Revision *rv;

  if (vec_size(vec) == 0)
    THROW(ExNoObject, 
	  format("Revision %s/%s not available", m->uri, 
		 xunsigned64_str(revNo)));

  rv = (Revision *) vec_fetch(vec, 0);

  if (GETTYPE(rv) != TY_Revision)
    THROW(ExMalformed, 
	  format("Fetched revision %s:%s is malformed",
		 m->uri, xunsigned64_str(revNo)));

  return rv;
}
#endif 

Revision *
repos_GetTopRev(Repository *r, Mutable *m)
{
  uint64_t topRev = m->nRevisions - 1;

  /* It is now legal for a Mutable to have no revisions! */
  if (m->nRevisions == 0)
    return NULL;

  return repos_GetRevision(r, m->uri, topRev);
}

/* Helper function:  we don't want this in the wire protocol */
ObVec *
repos_GetEntityVec(Repository *r, const char *mURI, 
	           StrVec *names)
{
  void *obj = NULL;
  ObVec * vec = obvec_create();
  unsigned u;

  if (names) {

    for (u = 0; u < vec_size(names); u++) {
      TRY {
	obj = repos_GetEntity(r, mURI, vec_fetch(names, u, const char *));
	obvec_append(vec, (Serializable *)obj);
      }
      DEFAULT(AnyException) {
	obvec_append(vec, NULL);
      }
      END_CATCH;
    }

  }

  return vec;
}

/* Helper function: we don't really want this in the 
 * wire protocol. */
ObVec *
repos_GetRevisions(Repository *r, const char *mURI,
		   uint64_t first,
		   uint64_t last)
{
  ObVec * vec = obvec_create();
  Revision *rev;
  uint64_t revNo;
  Mutable *m = repos_GetMutable(r, mURI);

  last = min(last, m->nRevisions - 1);

  /* Trail-less mutables are no longer really trail-less. Instead, we
   * will return only the most recent revision and let the background
   * repository garbage collector get rid of the old revisions. */
  if (m->flags & MF_NOTRAIL)
    first = last = m->nRevisions - 1;

  for (revNo = first; revNo <= last; revNo++) {

    TRY {
      rev = (Revision *) repos_GetRevision(r, mURI, revNo);
      obvec_append(vec, rev);
    }
    DEFAULT(ex) {
    }
    END_CATCH;
  }

  return vec;
}

