Artifact Content
Not logged in

Artifact d3ef278c076cf5d1f39d20fbfc5b4a485377b6a5

File src/xfer.c part of check-in [dccd27c6da] - Added a global command line argument named -quiet. This will suppress some output (currently during sync) for those interfacing fossil with various development tools. It does not make eliminate all output, only the output that is meant for fancy display to the user on a console. You can now use this new flag anywhere by referencing if (!g.fQuiet) {...} by jeremy_c on 2009-12-14 23:18:54. Also file src/xfer.c part of check-in [76bc05d739] - merge with trunk by btheado on 2009-12-30 20:33:59.

/*
** Copyright (c) 2007 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public
** License version 2 as published by the Free Software Foundation.
**
** 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 library; if not, write to the
** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
** Boston, MA  02111-1307, USA.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code to implement the file transfer protocol.
*/
#include "config.h"
#include "xfer.h"

/*
** This structure holds information about the current state of either
** a client or a server that is participating in xfer.
*/
typedef struct Xfer Xfer;
struct Xfer {
  Blob *pIn;          /* Input text from the other side */
  Blob *pOut;         /* Compose our reply here */
  Blob line;          /* The current line of input */
  Blob aToken[5];     /* Tokenized version of line */
  Blob err;           /* Error message text */
  int nToken;         /* Number of tokens in line */
  int nIGotSent;      /* Number of "igot" cards sent */
  int nGimmeSent;     /* Number of gimme cards sent */
  int nFileSent;      /* Number of files sent */
  int nDeltaSent;     /* Number of deltas sent */
  int nFileRcvd;      /* Number of files received */
  int nDeltaRcvd;     /* Number of deltas received */
  int nDanglingFile;  /* Number of dangling deltas received */
  int mxSend;         /* Stop sending "file" with pOut reaches this size */
};


/*
** The input blob contains a UUID.  Convert it into a record ID.
** Create a phantom record if no prior record exists and
** phantomize is true.
**
** Compare to uuid_to_rid().  This routine takes a blob argument
** and does less error checking.
*/
static int rid_from_uuid(Blob *pUuid, int phantomize){
  static Stmt q;
  int rid;

  db_static_prepare(&q, "SELECT rid FROM blob WHERE uuid=:uuid");
  db_bind_str(&q, ":uuid", pUuid);
  if( db_step(&q)==SQLITE_ROW ){
    rid = db_column_int(&q, 0);
  }else{
    rid = 0;
  }
  db_reset(&q);
  if( rid==0 && phantomize ){
    rid = content_new(blob_str(pUuid));
  }
  return rid;
}

/*
** Remember that the other side of the connection already has a copy
** of the file rid.
*/
static void remote_has(int rid){
  if( rid ) db_multi_exec("INSERT OR IGNORE INTO onremote VALUES(%d)", rid);
}

/*
** The aToken[0..nToken-1] blob array is a parse of a "file" line 
** message.  This routine finishes parsing that message and does
** a record insert of the file.
**
** The file line is in one of the following two forms:
**
**      file UUID SIZE \n CONTENT
**      file UUID DELTASRC SIZE \n CONTENT
**
** The content is SIZE bytes immediately following the newline.
** If DELTASRC exists, then the CONTENT is a delta against the
** content of DELTASRC.
**
** If any error occurs, write a message into pErr which has already
** be initialized to an empty string.
**
** Any artifact successfully received by this routine is considered to
** be public and is therefore removed from the "private" table.
*/
static void xfer_accept_file(Xfer *pXfer){
  int n;
  int rid;
  int srcid = 0;
  Blob content, hash;
  
  if( pXfer->nToken<3 
   || pXfer->nToken>4
   || !blob_is_uuid(&pXfer->aToken[1])
   || !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &n)
   || n<0
   || (pXfer->nToken==4 && !blob_is_uuid(&pXfer->aToken[2]))
  ){
    blob_appendf(&pXfer->err, "malformed file line");
    return;
  }
  blob_zero(&content);
  blob_zero(&hash);
  blob_extract(pXfer->pIn, n, &content);
  if( uuid_is_shunned(blob_str(&pXfer->aToken[1])) ){
    /* Ignore files that have been shunned */
    return;
  }
  if( pXfer->nToken==4 ){
    Blob src;
    srcid = rid_from_uuid(&pXfer->aToken[2], 1);
    if( content_get(srcid, &src)==0 ){
      rid = content_put(&content, blob_str(&pXfer->aToken[1]), srcid);
      pXfer->nDanglingFile++;
      db_multi_exec("DELETE FROM phantom WHERE rid=%d", rid);
      content_make_public(rid);
      return;
    }
    pXfer->nDeltaRcvd++;
    blob_delta_apply(&src, &content, &content);
    blob_reset(&src);
  }else{
    pXfer->nFileRcvd++;
  }
  sha1sum_blob(&content, &hash);
  if( !blob_eq_str(&pXfer->aToken[1], blob_str(&hash), -1) ){
    blob_appendf(&pXfer->err, "content does not match sha1 hash");
  }
  rid = content_put(&content, blob_str(&hash), 0);
  blob_reset(&hash);
  if( rid==0 ){
    blob_appendf(&pXfer->err, "%s", g.zErrMsg);
  }else{
    content_make_public(rid);
    manifest_crosslink(rid, &content);
  }
  remote_has(rid);
}

/*
** Try to send a file as a delta against its parent.
** If successful, return the number of bytes in the delta.
** If we cannot generate an appropriate delta, then send
** nothing and return zero.
**
** Never send a delta against a private artifact.
*/
static int send_delta_parent(
  Xfer *pXfer,            /* The transfer context */
  int rid,                /* record id of the file to send */
  Blob *pContent,         /* The content of the file to send */
  Blob *pUuid             /* The UUID of the file to send */
){
  static const char *azQuery[] = {
    "SELECT pid FROM plink x"
    " WHERE cid=%d"
    "   AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)"
    "   AND NOT EXISTS(SELECT 1 FROM plink y"
                      " WHERE y.pid=x.cid AND y.cid=x.pid)",

    "SELECT pid FROM mlink x"
    " WHERE fid=%d"
    "   AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)"
    "   AND NOT EXISTS(SELECT 1 FROM mlink y"
                     "  WHERE y.pid=x.fid AND y.fid=x.pid)"
  };
  int i;
  Blob src, delta;
  int size = 0;
  int srcId = 0;

  for(i=0; srcId==0 && i<count(azQuery); i++){
    srcId = db_int(0, azQuery[i], rid);
  }
  if( srcId>0 && !content_is_private(srcId) && content_get(srcId, &src) ){
    char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", srcId);
    blob_delta_create(&src, pContent, &delta);
    size = blob_size(&delta);
    if( size>=blob_size(pContent)-50 ){
      size = 0;
    }else if( uuid_is_shunned(zUuid) ){
      size = 0;
    }else{
      blob_appendf(pXfer->pOut, "file %b %s %d\n", pUuid, zUuid, size);
      blob_append(pXfer->pOut, blob_buffer(&delta), size);
      /* blob_appendf(pXfer->pOut, "\n", 1); */
    }
    blob_reset(&delta);
    free(zUuid);
    blob_reset(&src);
  }
  return size;
}

/*
** Try to send a file as a native delta.  
** If successful, return the number of bytes in the delta.
** If we cannot generate an appropriate delta, then send
** nothing and return zero.
**
** Never send a delta against a private artifact.
*/
static int send_delta_native(
  Xfer *pXfer,            /* The transfer context */
  int rid,                /* record id of the file to send */
  Blob *pUuid             /* The UUID of the file to send */
){
  Blob src, delta;
  int size = 0;
  int srcId;

  srcId = db_int(0, "SELECT srcid FROM delta WHERE rid=%d", rid);
  if( srcId>0 && !content_is_private(srcId) ){
    blob_zero(&src);
    db_blob(&src, "SELECT uuid FROM blob WHERE rid=%d", srcId);
    if( uuid_is_shunned(blob_str(&src)) ){
      blob_reset(&src);
      return 0;
    }
    blob_zero(&delta);
    db_blob(&delta, "SELECT content FROM blob WHERE rid=%d", rid);
    blob_uncompress(&delta, &delta);
    blob_appendf(pXfer->pOut, "file %b %b %d\n",
                pUuid, &src, blob_size(&delta));
    blob_append(pXfer->pOut, blob_buffer(&delta), blob_size(&delta));
    size = blob_size(&delta);
    blob_reset(&delta);
    blob_reset(&src);
  }else{
    size = 0;
  }
  return size;
}

/*
** Send the file identified by rid.
**
** The pUuid can be NULL in which case the correct UUID is computed
** from the rid.
**
** Try to send the file as a native delta if nativeDelta is true, or
** as a parent delta if nativeDelta is false.
**
** It should never be the case that rid is a private artifact.  But
** as a precaution, this routine does check on rid and if it is private
** this routine becomes a no-op.
*/
static void send_file(Xfer *pXfer, int rid, Blob *pUuid, int nativeDelta){
  Blob content, uuid;
  int size = 0;

  if( content_is_private(rid) ) return;
  if( db_exists("SELECT 1 FROM onremote WHERE rid=%d", rid) ){
     return;
  }
  blob_zero(&uuid);
  db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d AND size>=0", rid);
  if( blob_size(&uuid)==0 ){
    return;
  }
  if( pUuid ){
    if( blob_compare(pUuid, &uuid)!=0 ){
      blob_reset(&uuid);
      return;
    }
  }else{
    pUuid = &uuid;
  }
  if( uuid_is_shunned(blob_str(pUuid)) ){
    blob_reset(&uuid);
    return;
  }
  if( pXfer->mxSend<=blob_size(pXfer->pOut) ){
    blob_appendf(pXfer->pOut, "igot %b\n", pUuid);
    pXfer->nIGotSent++;
    blob_reset(&uuid);
    return;
  }
  if( nativeDelta ){
    size = send_delta_native(pXfer, rid, pUuid);
    if( size ){
      pXfer->nDeltaSent++;
    }
  }
  if( size==0 ){
    content_get(rid, &content);

    if( !nativeDelta && blob_size(&content)>100 ){
      size = send_delta_parent(pXfer, rid, &content, pUuid);
    }
    if( size==0 ){
      int size = blob_size(&content);
      blob_appendf(pXfer->pOut, "file %b %d\n", pUuid, size);
      blob_append(pXfer->pOut, blob_buffer(&content), size);
      pXfer->nFileSent++;
    }else{
      pXfer->nDeltaSent++;
    }
  }
  remote_has(rid);
  blob_reset(&uuid);
}

/*
** Send a gimme message for every phantom.
**
** It should not be possible to have a private phantom.  But just to be
** sure, take care not to send any "gimme" messagse on private artifacts.
*/
static void request_phantoms(Xfer *pXfer, int maxReq){
  Stmt q;
  db_prepare(&q, 
    "SELECT uuid FROM phantom JOIN blob USING(rid)"
    " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
    "   AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)"
  );
  while( db_step(&q)==SQLITE_ROW && maxReq-- > 0 ){
    const char *zUuid = db_column_text(&q, 0);
    blob_appendf(pXfer->pOut, "gimme %s\n", zUuid);
    pXfer->nGimmeSent++;
  }
  db_finalize(&q);
}

/*
** Compute an SHA1 hash on the tail of pMsg.  Verify that it matches the
** the hash given in pHash.  Return 1 on a successful match.  Return 0
** if there is a mismatch.
*/
static int check_tail_hash(Blob *pHash, Blob *pMsg){
  Blob tail;
  Blob h2;
  int rc;
  blob_tail(pMsg, &tail);
  sha1sum_blob(&tail, &h2);
  rc = blob_compare(pHash, &h2);
  blob_reset(&h2);
  blob_reset(&tail);
  return rc==0;
}


/*
** Check the signature on an application/x-fossil payload received by
** the HTTP server.  The signature is a line of the following form:
**
**        login LOGIN NONCE SIGNATURE
**
** The NONCE is the SHA1 hash of the remainder of the input.  
** SIGNATURE is the SHA1 checksum of the NONCE concatenated 
** with the users password.
**
** The parameters to this routine are ephermeral blobs holding the
** LOGIN, NONCE and SIGNATURE.
**
** This routine attempts to locate the user and verify the signature.
** If everything checks out, the USER.CAP column for the USER table
** is consulted to set privileges in the global g variable.
**
** If anything fails to check out, no changes are made to privileges.
**
** Signature generation on the client side is handled by the 
** http_exchange() routine.
*/
void check_login(Blob *pLogin, Blob *pNonce, Blob *pSig){
  Stmt q;
  int rc = -1;
  char *zLogin = blob_terminate(pLogin);
  defossilize(zLogin);

  db_prepare(&q,
     "SELECT pw, cap, uid FROM user"
     " WHERE login=%Q"
     "   AND login NOT IN ('anonymous','nobody','developer','reader')"
     "   AND length(pw)>0",
     zLogin
  );
  if( db_step(&q)==SQLITE_ROW ){
    Blob pw, combined, hash;
    blob_zero(&pw);
    db_ephemeral_blob(&q, 0, &pw);
    blob_zero(&combined);
    blob_copy(&combined, pNonce);
    blob_append(&combined, blob_buffer(&pw), blob_size(&pw));
    /* CGIDEBUG(("presig=[%s]\n", blob_str(&combined))); */
    sha1sum_blob(&combined, &hash);
    rc = blob_compare(&hash, pSig);
    blob_reset(&hash);
    blob_reset(&combined);
    if( rc==0 ){
      const char *zCap;
      zCap = db_column_text(&q, 1);
      login_set_capabilities(zCap);
      g.userUid = db_column_int(&q, 2);
      g.zLogin = mprintf("%b", pLogin);
      g.zNonce = mprintf("%b", pNonce);
    }
  }
  db_finalize(&q);

  if( rc==0 ){
    /* If the login was successful. */
    login_set_anon_nobody_capabilities();
  }
}

/*
** Send the content of all files in the unsent table.
**
** This is really just an optimization.  If you clear the
** unsent table, all the right files will still get transferred.
** It just might require an extra round trip or two.
*/
static void send_unsent(Xfer *pXfer){
  Stmt q;
  db_prepare(&q, "SELECT rid FROM unsent EXCEPT SELECT rid FROM private");
  while( db_step(&q)==SQLITE_ROW ){
    int rid = db_column_int(&q, 0);
    send_file(pXfer, rid, 0, 0);
  }
  db_finalize(&q);
  db_multi_exec("DELETE FROM unsent");
}

/*
** Check to see if the number of unclustered entries is greater than
** 100 and if it is, form a new cluster.  Unclustered phantoms do not
** count toward the 100 total.  And phantoms are never added to a new
** cluster.
*/
static void create_cluster(void){
  Blob cluster, cksum;
  Stmt q;
  int nUncl;

  /* We should not ever get any private artifacts in the unclustered table.
  ** But if we do (because of a bug) now is a good time to delete them. */
  db_multi_exec(
    "DELETE FROM unclustered WHERE rid IN (SELECT rid FROM private)"
  );

  nUncl = db_int(0, "SELECT count(*) FROM unclustered"
                    " WHERE NOT EXISTS(SELECT 1 FROM phantom"
                                      " WHERE rid=unclustered.rid)");
  if( nUncl<100 ){
    return;
  }
  blob_zero(&cluster);
  db_prepare(&q, "SELECT uuid FROM unclustered, blob"
                 " WHERE NOT EXISTS(SELECT 1 FROM phantom"
                 "                   WHERE rid=unclustered.rid)"
                 "   AND unclustered.rid=blob.rid"
                 "   AND NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
                 " ORDER BY 1");
  while( db_step(&q)==SQLITE_ROW ){
    blob_appendf(&cluster, "M %s\n", db_column_text(&q, 0));
  }
  db_finalize(&q);
  md5sum_blob(&cluster, &cksum);
  blob_appendf(&cluster, "Z %b\n", &cksum);
  blob_reset(&cksum);
  db_multi_exec("DELETE FROM unclustered");
  content_put(&cluster, 0, 0);
  blob_reset(&cluster);
}

/*
** Send an igot message for every entry in unclustered table.
** Return the number of cards sent.
*/
static int send_unclustered(Xfer *pXfer){
  Stmt q;
  int cnt = 0;
  db_prepare(&q, 
    "SELECT uuid FROM unclustered JOIN blob USING(rid)"
    " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
    "   AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)"
  );
  while( db_step(&q)==SQLITE_ROW ){
    blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0));
    cnt++;
  }
  db_finalize(&q);
  return cnt;
}

/*
** Send an igot message for every artifact.
*/
static void send_all(Xfer *pXfer){
  Stmt q;
  db_prepare(&q, 
    "SELECT uuid FROM blob "
    " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)"
    "   AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)"
  );
  while( db_step(&q)==SQLITE_ROW ){
    blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0));
  }
  db_finalize(&q);
}

/*
** Send a single config card for configuration item zName
*/
static void send_config_card(Xfer *pXfer, const char *zName){
  if( zName[0]!='@' ){
    Blob val;
    blob_zero(&val);
    db_blob(&val, "SELECT value FROM config WHERE name=%Q", zName);
    if( blob_size(&val)>0 ){
      blob_appendf(pXfer->pOut, "config %s %d\n", zName, blob_size(&val));
      blob_append(pXfer->pOut, blob_buffer(&val), blob_size(&val));
      blob_reset(&val);
      blob_append(pXfer->pOut, "\n", 1);
    }
  }else{
    Blob content;
    blob_zero(&content);
    configure_render_special_name(zName, &content);
    blob_appendf(pXfer->pOut, "config %s %d\n%s\n", zName,
                 blob_size(&content), blob_str(&content));
    blob_reset(&content);
  }
}


/*
** If this variable is set, disable login checks.  Used for debugging
** only.
*/
static int disableLogin = 0;

/*
** The CGI/HTTP preprocessor always redirects requests with a content-type
** of application/x-fossil or application/x-fossil-debug to this page,
** regardless of what path was specified in the HTTP header.  This allows
** clone clients to specify a URL that omits default pathnames, such
** as "http://fossil-scm.morg/" instead of "http://fossil-scm.org/index.cgi".
**
** WEBPAGE: xfer
**
** This is the transfer handler on the server side.  The transfer
** message has been uncompressed and placed in the g.cgiIn blob.
** Process this message and form an appropriate reply.
*/
void page_xfer(void){
  int isPull = 0;
  int isPush = 0;
  int nErr = 0;
  Xfer xfer;
  int deltaFlag = 0;
  int isClone = 0;
  int nGimme = 0;
  int size;
  int recvConfig = 0;

  if( strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
     fossil_redirect_home();
  }
  memset(&xfer, 0, sizeof(xfer));
  blobarray_zero(xfer.aToken, count(xfer.aToken));
  cgi_set_content_type(g.zContentType);
  blob_zero(&xfer.err);
  xfer.pIn = &g.cgiIn;
  xfer.pOut = cgi_output_blob();
  xfer.mxSend = db_get_int("max-download", 5000000);
  g.xferPanic = 1;

  db_begin_transaction();
  db_multi_exec(
     "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
  );
  manifest_crosslink_begin();
  while( blob_line(xfer.pIn, &xfer.line) ){
    if( blob_buffer(&xfer.line)[0]=='#' ) continue;
    xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));

    /*   file UUID SIZE \n CONTENT
    **   file UUID DELTASRC SIZE \n CONTENT
    **
    ** Accept a file from the client.
    */
    if( blob_eq(&xfer.aToken[0], "file") ){
      if( !isPush ){
        cgi_reset_content();
        @ error not\sauthorized\sto\swrite
        nErr++;
        break;
      }
      xfer_accept_file(&xfer);
      if( blob_size(&xfer.err) ){
        cgi_reset_content();
        @ error %T(blob_str(&xfer.err))
        nErr++;
        break;
      }
    }else

    /*   gimme UUID
    **
    ** Client is requesting a file.  Send it.
    */
    if( blob_eq(&xfer.aToken[0], "gimme")
     && xfer.nToken==2
     && blob_is_uuid(&xfer.aToken[1])
    ){
      nGimme++;
      if( isPull ){
        int rid = rid_from_uuid(&xfer.aToken[1], 0);
        if( rid ){
          send_file(&xfer, rid, &xfer.aToken[1], deltaFlag);
        }
      }
    }else

    /*   igot UUID
    **
    ** Client announces that it has a particular file.
    */
    if( xfer.nToken==2
     && blob_eq(&xfer.aToken[0], "igot")
     && blob_is_uuid(&xfer.aToken[1])
    ){
      if( isPush ){
        rid_from_uuid(&xfer.aToken[1], 1);
      }
    }else
  
    
    /*    pull  SERVERCODE  PROJECTCODE
    **    push  SERVERCODE  PROJECTCODE
    **
    ** The client wants either send or receive.  The server should
    ** verify that the project code matches and that the server code
    ** does not match.
    */
    if( xfer.nToken==3
     && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push"))
     && blob_is_uuid(&xfer.aToken[1])
     && blob_is_uuid(&xfer.aToken[2])
    ){
      const char *zPCode;

#if 0
      /* This block checks to see if a server is trying to sync with itself.
      ** This used to be disallowed, but I cannot think of any significant
      ** harm, so I have disabled the check.
      **
      ** With this check disabled, it is sufficient to copy the repository
      ** database.  No need to run clone.
      */
      const char *zSCode;
      zSCode = db_get("server-code", 0);
      if( zSCode==0 ){
        fossil_panic("missing server code");
      }
      if( blob_eq_str(&xfer.aToken[1], zSCode, -1) ){
        cgi_reset_content();
        @ error server\sloop
        nErr++;
        break;
      }
#endif

      zPCode = db_get("project-code", 0);
      if( zPCode==0 ){
        fossil_panic("missing project code");
      }
      if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){
        cgi_reset_content();
        @ error wrong\sproject
        nErr++;
        break;
      }
      login_check_credentials();
      if( blob_eq(&xfer.aToken[0], "pull") ){
        if( !g.okRead ){
          cgi_reset_content();
          @ error not\sauthorized\sto\sread
          nErr++;
          break;
        }
        isPull = 1;
      }else{
        if( !g.okWrite ){
          if( !isPull ){
            cgi_reset_content();
            @ error not\sauthorized\sto\swrite
            nErr++;
          }else{
            @ message pull\sonly\s-\snot\sauthorized\sto\spush
          }
        }else{
          isPush = 1;
        }
      }
    }else

    /*    clone
    **
    ** The client knows nothing.  Tell all.
    */
    if( blob_eq(&xfer.aToken[0], "clone") ){
      login_check_credentials();
      if( !g.okClone ){
        cgi_reset_content();
        @ error not\sauthorized\sto\sclone
        nErr++;
        break;
      }
      isClone = 1;
      isPull = 1;
      deltaFlag = 1;
      @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
    }else

    /*    login  USER  NONCE  SIGNATURE
    **
    ** Check for a valid login.  This has to happen before anything else.
    ** The client can send multiple logins.  Permissions are cumulative.
    */
    if( blob_eq(&xfer.aToken[0], "login")
     && xfer.nToken==4
    ){
      if( disableLogin ){
        g.okRead = g.okWrite = 1;
      }else if( check_tail_hash(&xfer.aToken[2], xfer.pIn) ){
        check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]);
      }
    }else
    
    /*    reqconfig  NAME
    **
    ** Request a configuration value
    */
    if( blob_eq(&xfer.aToken[0], "reqconfig")
     && xfer.nToken==2
    ){
      if( g.okRead ){
        char *zName = blob_str(&xfer.aToken[1]);
        if( configure_is_exportable(zName) ){
          send_config_card(&xfer, zName);
        }
      }
    }else
    
    /*   config NAME SIZE \n CONTENT
    **
    ** Receive a configuration value from the client.  This is only
    ** permitted for high-privilege users.
    */
    if( blob_eq(&xfer.aToken[0],"config") && xfer.nToken==3
        && blob_is_int(&xfer.aToken[2], &size) ){
      const char *zName = blob_str(&xfer.aToken[1]);
      Blob content;
      blob_zero(&content);
      blob_extract(xfer.pIn, size, &content);
      if( !g.okAdmin ){
        cgi_reset_content();
        @ error not\sauthorized\sto\spush\sconfiguration
        nErr++;
        break;
      }
      if( zName[0]!='@' ){
        if( strcmp(zName, "logo-image")==0 ){
          Stmt ins;
          db_prepare(&ins,
            "REPLACE INTO config(name, value) VALUES(:name, :value)"
          );
          db_bind_text(&ins, ":name", zName);
          db_bind_blob(&ins, ":value", &content);
          db_step(&ins);
          db_finalize(&ins);
        }else{
          db_multi_exec(
              "REPLACE INTO config(name,value) VALUES(%Q,%Q)",
              zName, blob_str(&content)
          );
        }
      }else{
        /* Notice that we are evaluating arbitrary SQL received from the
        ** client.  But this can only happen if the client has authenticated
        ** as an administrator, so presumably we trust the client at this
        ** point.
        */
        if( !recvConfig ){
          configure_prepare_to_receive(0);
          recvConfig = 1;
        }
        db_multi_exec("%s", blob_str(&content));
      }
      blob_reset(&content);
      blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR);
    }else

      

    /*    cookie TEXT
    **
    ** A cookie contains a arbitrary-length argument that is server-defined.
    ** The argument must be encoded so as not to contain any whitespace.
    ** The server can optionally send a cookie to the client.  The client
    ** might then return the same cookie back to the server on its next
    ** communication.  The cookie might record information that helps
    ** the server optimize a push or pull.
    **
    ** The client is not required to return a cookie.  So the server
    ** must not depend on the cookie.  The cookie should be an optimization
    ** only.  The client might also send a cookie that came from a different
    ** server.  So the server must be prepared to distinguish its own cookie
    ** from cookies originating from other servers.  The client might send
    ** back several different cookies to the server.  The server should be
    ** prepared to sift through the cookies and pick the one that it wants.
    */
    if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){
      /* Process the cookie */
    }else

    /* Unknown message
    */
    {
      cgi_reset_content();
      @ error bad\scommand:\s%F(blob_str(&xfer.line))
    }
    blobarray_reset(xfer.aToken, xfer.nToken);
  }
  if( isPush ){
    request_phantoms(&xfer, 500);
  }
  if( isClone && nGimme==0 ){
    /* The initial "clone" message from client to server contains no
    ** "gimme" cards. On that initial message, send the client an "igot"
    ** card for every artifact currently in the respository.  This will
    ** cause the client to create phantoms for all artifacts, which will
    ** in turn make sure that the entire repository is sent efficiently
    ** and expeditiously.
    */
    send_all(&xfer);
  }else if( isPull ){
    create_cluster();
    send_unclustered(&xfer);
  }
  if( recvConfig ){
    configure_finalize_receive();
  }
  manifest_crosslink_end();
  db_end_transaction(0);
}

/*
** COMMAND: test-xfer
**
** This command is used for debugging the server.  There is a single
** argument which is the uncompressed content of an "xfer" message
** from client to server.  This command interprets that message as
** if had been received by the server.
**
** On the client side, run:
**
**      fossil push http://bogus/ --httptrace
**
** Or a similar command to provide the output.  The content of the
** message will appear on standard output.  Capture this message
** into a file named (for example) out.txt.  Then run the
** server in gdb:
**
**     gdb fossil
**     r test-xfer out.txt
*/
void cmd_test_xfer(void){
  int notUsed;
  if( g.argc!=2 && g.argc!=3 ){
    usage("?MESSAGEFILE?");
  }
  db_must_be_within_tree();
  blob_zero(&g.cgiIn);
  blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2]);
  disableLogin = 1;
  page_xfer();
  printf("%s\n", cgi_extract_content(&notUsed));
}

/*
** Format strings for progress reporting.
*/
static const char zLabelFormat[] = "%-10s %10s %10s %10s %10s\n";
static const char zValueFormat[] = "\r%-10s %10d %10d %10d %10d\n";


/*
** Sync to the host identified in g.urlName and g.urlPath.  This
** routine is called by the client.
**
** Records are pushed to the server if pushFlag is true.  Records
** are pulled if pullFlag is true.  A full sync occurs if both are
** true.
*/
void client_sync(
  int pushFlag,           /* True to do a push (or a sync) */
  int pullFlag,           /* True to do a pull (or a sync) */
  int cloneFlag,          /* True if this is a clone */
  int configRcvMask,      /* Receive these configuration items */
  int configSendMask      /* Send these configuration items */
){
  int go = 1;             /* Loop until zero */
  int nCardSent = 0;      /* Number of cards sent */
  int nCardRcvd = 0;      /* Number of cards received */
  int nCycle = 0;         /* Number of round trips to the server */
  int size;               /* Size of a config value */
  int nFileSend = 0;
  int origConfigRcvMask;  /* Original value of configRcvMask */
  int nFileRecv;          /* Number of files received */
  int mxPhantomReq = 200; /* Max number of phantoms to request per comm */
  const char *zCookie;    /* Server cookie */
  int nSent, nRcvd;       /* Bytes sent and received (after compression) */
  Blob send;              /* Text we are sending to the server */
  Blob recv;              /* Reply we got back from the server */
  Xfer xfer;              /* Transfer data */
  const char *zSCode = db_get("server-code", "x");
  const char *zPCode = db_get("project-code", 0);

  if( db_get_boolean("dont-push", 0) ) pushFlag = 0;
  if( pushFlag + pullFlag + cloneFlag == 0 
     && configRcvMask==0 && configSendMask==0 ) return;

  transport_stats(0, 0, 1);
  socket_global_init();
  memset(&xfer, 0, sizeof(xfer));
  xfer.pIn = &recv;
  xfer.pOut = &send;
  xfer.mxSend = db_get_int("max-upload", 250000);

  assert( pushFlag | pullFlag | cloneFlag | configRcvMask | configSendMask );
  db_begin_transaction();
  db_record_repository_filename(0);
  db_multi_exec(
    "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
  );
  blobarray_zero(xfer.aToken, count(xfer.aToken));
  blob_zero(&send);
  blob_zero(&recv);
  blob_zero(&xfer.err);
  blob_zero(&xfer.line);
  origConfigRcvMask = configRcvMask;

  /*
  ** Always begin with a clone, pull, or push message
  */
  if( cloneFlag ){
    blob_appendf(&send, "clone\n");
    pushFlag = 0;
    pullFlag = 0;
    nCardSent++;
    /* TBD: Request all transferable configuration values */
  }else if( pullFlag ){
    blob_appendf(&send, "pull %s %s\n", zSCode, zPCode);
    nCardSent++;
  }
  if( pushFlag ){
    blob_appendf(&send, "push %s %s\n", zSCode, zPCode);
    nCardSent++;
  }
  manifest_crosslink_begin();
  printf(zLabelFormat, "", "Bytes", "Cards", "Artifacts", "Deltas");

  while( go ){
    int newPhantom = 0;
    char *zRandomness;

    /* Send make the most recently received cookie.  Let the server
    ** figure out if this is a cookie that it cares about.
    */
    zCookie = db_get("cookie", 0);
    if( zCookie ){
      blob_appendf(&send, "cookie %s\n", zCookie);
    }
    
    /* Generate gimme cards for phantoms and leaf cards
    ** for all leaves.
    */
    if( pullFlag || cloneFlag ){
      request_phantoms(&xfer, mxPhantomReq);
    }
    if( pushFlag ){
      send_unsent(&xfer);
      nCardSent += send_unclustered(&xfer);
    }

    /* Send configuration parameter requests */
    if( configRcvMask ){
      const char *zName;
      zName = configure_first_name(configRcvMask);
      while( zName ){
        blob_appendf(&send, "reqconfig %s\n", zName);
        zName = configure_next_name(configRcvMask);
        nCardSent++;
      }
      if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){
        configure_prepare_to_receive(0);
      }
      configRcvMask = 0;
    }

    /* Send configuration parameters being pushed */
    if( configSendMask ){
      const char *zName;
      zName = configure_first_name(configSendMask);
      while( zName ){
        send_config_card(&xfer, zName);
        zName = configure_next_name(configSendMask);
        nCardSent++;
      }
      configSendMask = 0;
    }

    /* Append randomness to the end of the message */
    zRandomness = db_text(0, "SELECT hex(randomblob(20))");
    blob_appendf(&send, "# %s\n", zRandomness);
    free(zRandomness);

    /* Exchange messages with the server */
    nFileSend = xfer.nFileSent + xfer.nDeltaSent;
    printf(zValueFormat, "Send:",
            blob_size(&send), nCardSent+xfer.nGimmeSent+xfer.nIGotSent,
            xfer.nFileSent, xfer.nDeltaSent);
    nCardSent = 0;
    nCardRcvd = 0;
    xfer.nFileSent = 0;
    xfer.nDeltaSent = 0;
    xfer.nGimmeSent = 0;
    fflush(stdout);
    http_exchange(&send, &recv);
    blob_reset(&send);

    /* Begin constructing the next message (which might never be
    ** sent) by beginning with the pull or push cards
    */
    if( pullFlag ){
      blob_appendf(&send, "pull %s %s\n", zSCode, zPCode);
      nCardSent++;
    }
    if( pushFlag ){
      blob_appendf(&send, "push %s %s\n", zSCode, zPCode);
      nCardSent++;
    }

    /* Process the reply that came back from the server */
    while( blob_line(&recv, &xfer.line) ){
      if( blob_buffer(&xfer.line)[0]=='#' ){
        continue;
      }
      xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
      nCardRcvd++;
      if (!g.fQuiet) {
        printf("\r%d", nCardRcvd);
        fflush(stdout);
      }

      /*   file UUID SIZE \n CONTENT
      **   file UUID DELTASRC SIZE \n CONTENT
      **
      ** Receive a file transmitted from the server.
      */
      if( blob_eq(&xfer.aToken[0],"file") ){
        xfer_accept_file(&xfer);
      }else

      /*   gimme UUID
      **
      ** Server is requesting a file.  If the file is a manifest, assume
      ** that the server will also want to know all of the content files
      ** associated with the manifest and send those too.
      */
      if( blob_eq(&xfer.aToken[0], "gimme")
       && xfer.nToken==2
       && blob_is_uuid(&xfer.aToken[1])
      ){
        if( pushFlag ){
          int rid = rid_from_uuid(&xfer.aToken[1], 0);
          if( rid ) send_file(&xfer, rid, &xfer.aToken[1], 0);
        }
      }else
  
      /*   igot UUID
      **
      ** Server announces that it has a particular file.  If this is
      ** not a file that we have and we are pulling, then create a
      ** phantom to cause this file to be requested on the next cycle.
      ** Always remember that the server has this file so that we do
      ** not transmit it by accident.
      */
      if( xfer.nToken==2
       && blob_eq(&xfer.aToken[0], "igot")
       && blob_is_uuid(&xfer.aToken[1])
      ){
        int rid = rid_from_uuid(&xfer.aToken[1], 0);
        if( rid>0 ){
          content_make_public(rid);
        }else if( pullFlag || cloneFlag ){
          rid = content_new(blob_str(&xfer.aToken[1]));
          if( rid ) newPhantom = 1;
        }
        remote_has(rid);
      }else
    
      
      /*   push  SERVERCODE  PRODUCTCODE
      **
      ** Should only happen in response to a clone.  This message tells
      ** the client what product to use for the new database.
      */
      if( blob_eq(&xfer.aToken[0],"push")
       && xfer.nToken==3
       && cloneFlag
       && blob_is_uuid(&xfer.aToken[1])
       && blob_is_uuid(&xfer.aToken[2])
      ){
        if( blob_eq_str(&xfer.aToken[1], zSCode, -1) ){
          fossil_fatal("server loop");
        }
        if( zPCode==0 ){
          zPCode = mprintf("%b", &xfer.aToken[2]);
          db_set("project-code", zPCode, 0);
        }
        blob_appendf(&send, "clone\n");
        nCardSent++;
      }else
      
      /*   config NAME SIZE \n CONTENT
      **
      ** Receive a configuration value from the server.
      */
      if( blob_eq(&xfer.aToken[0],"config") && xfer.nToken==3
          && blob_is_int(&xfer.aToken[2], &size) ){
        const char *zName = blob_str(&xfer.aToken[1]);
        Blob content;
        blob_zero(&content);
        blob_extract(xfer.pIn, size, &content);
        g.okAdmin = g.okRdAddr = 1;
        if( configure_is_exportable(zName) & origConfigRcvMask ){
          if( zName[0]!='@' ){
            if( strcmp(zName, "logo-image")==0 ){
              Stmt ins;
              db_prepare(&ins,
                "REPLACE INTO config(name, value) VALUES(:name, :value)"
              );
              db_bind_text(&ins, ":name", zName);
              db_bind_blob(&ins, ":value", &content);
              db_step(&ins);
              db_finalize(&ins);
            }else{
              db_multi_exec(
                  "REPLACE INTO config(name,value) VALUES(%Q,%Q)",
                  zName, blob_str(&content)
              );
            }
          }else{
            /* Notice that we are evaluating arbitrary SQL received from the
            ** server.  But this can only happen if we have specifically
            ** requested configuration information from the server, so
            ** presumably the operator trusts the server.
            */
            db_multi_exec("%s", blob_str(&content));
          }
        }
        nCardSent++;
        blob_reset(&content);
        blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR);
      }else

      
      /*    cookie TEXT
      **
      ** The server might include a cookie in its reply.  The client
      ** should remember this cookie and send it back to the server
      ** in its next query.
      **
      ** Each cookie received overwrites the prior cookie from the
      ** same server.
      */
      if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){
        db_set("cookie", blob_str(&xfer.aToken[1]), 0);
      }else

      /*   message MESSAGE
      **
      ** Print a message.  Similar to "error" but does not stop processing
      */        
      if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
        char *zMsg = blob_terminate(&xfer.aToken[1]);
        defossilize(zMsg);
        printf("\rServer says: %s\n", zMsg);
      }else

      /*   error MESSAGE
      **
      ** Report an error and abandon the sync session
      */        
      if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){
        char *zMsg = blob_terminate(&xfer.aToken[1]);
        defossilize(zMsg);
        blob_appendf(&xfer.err, "server says: %s", zMsg);
        printf("Server Error: %s\n", zMsg);
      }else

      /* Unknown message */
      {
        if( blob_str(&xfer.aToken[0])[0]=='<' ){
          fossil_fatal(
            "server replies with HTML instead of fossil sync protocol:\n%b",
            &recv
          );
        }
        blob_appendf(&xfer.err, "unknown command: %b", &xfer.aToken[0]);
      }

      if( blob_size(&xfer.err) ){
        fossil_fatal("%b", &xfer.err);
      }
      blobarray_reset(xfer.aToken, xfer.nToken);
      blob_reset(&xfer.line);
    }
    if( origConfigRcvMask & (CONFIGSET_TKT|CONFIGSET_USER) ){
      configure_finalize_receive();
    }
    origConfigRcvMask = 0;
    if( nCardRcvd>0 ){
      printf(zValueFormat, "Received:",
              blob_size(&recv), nCardRcvd,
              xfer.nFileRcvd, xfer.nDeltaRcvd + xfer.nDanglingFile);
    }
    blob_reset(&recv);
    nCycle++;
    go = 0;

    /* If we received one or more files on the previous exchange but
    ** there are still phantoms, then go another round.
    */
    nFileRecv = xfer.nFileRcvd + xfer.nDeltaRcvd + xfer.nDanglingFile;
    if( (nFileRecv>0 || newPhantom) && db_exists("SELECT 1 FROM phantom") ){
      go = 1;
      mxPhantomReq = nFileRecv*2;
      if( mxPhantomReq<200 ) mxPhantomReq = 200;
    }
    nCardRcvd = 0;
    xfer.nFileRcvd = 0;
    xfer.nDeltaRcvd = 0;
    xfer.nDanglingFile = 0;

    /* If we have one or more files queued to send, then go
    ** another round 
    */
    if( xfer.nFileSent+xfer.nDeltaSent>0 ){
      go = 1;
    }
  };
  transport_stats(&nSent, &nRcvd, 1);
  printf("Total network traffic: %d bytes sent, %d bytes received\n",
         nSent, nRcvd);
  transport_close();
  socket_global_shutdown();
  db_multi_exec("DROP TABLE onremote");
  manifest_crosslink_end();
  db_end_transaction(0);
}