diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/xfer.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/xfer.c')
| -rw-r--r-- | src/xfer.c | 3083 |
1 files changed, 3083 insertions, 0 deletions
diff --git a/src/xfer.c b/src/xfer.c new file mode 100644 index 0000000..190dfcf --- /dev/null +++ b/src/xfer.c @@ -0,0 +1,3083 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** 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. +** +** 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" + +#include <time.h> + +/* +** Maximum number of HTTP redirects that any http_exchange() call will +** follow before throwing a fatal error. Most browsers use a limit of 20. +*/ +#define MAX_REDIRECTS 20 + +/* +** 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[6]; /* Tokenized version of line */ + Blob err; /* Error message text */ + int nToken; /* Number of tokens in line */ + int nIGotSent; /* Number of "igot" cards sent */ + int nPrivIGot; /* Number of private "igot" cards */ + 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" when pOut reaches this size */ + int resync; /* Send igot cards for all holdings */ + u8 syncPrivate; /* True to enable syncing private content */ + u8 nextIsPrivate; /* If true, next "file" received is a private */ + u32 remoteVersion; /* Version of fossil running on the other side */ + u32 remoteDate; /* Date for specific client software edition */ + u32 remoteTime; /* Time of date corresponding on remoteDate */ + time_t maxTime; /* Time when this transfer should be finished */ +}; + + +/* +** The input blob contains an artifact. 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, int isPrivate){ + 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), isPrivate); + } + 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 ){ + static Stmt q; + db_static_prepare(&q, "INSERT OR IGNORE INTO onremote VALUES(:r)"); + db_bind_int(&q, ":r", rid); + db_step(&q); + db_reset(&q); + } +} + +/* +** Remember that the other side of the connection lacks a copy of +** the artifact with the given hash. +*/ +static void remote_unk(Blob *pHash){ + static Stmt q; + db_static_prepare(&q, "INSERT OR IGNORE INTO unk VALUES(:h)"); + db_bind_text(&q, ":h", blob_str(pHash)); + db_step(&q); + db_reset(&q); +} + +/* +** 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 HASH SIZE \n CONTENT +** file HASH 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 cloneFlag, + char **pzUuidList, + int *pnUuidList +){ + int n; + int rid; + int srcid = 0; + Blob content; + int isPriv; + Blob *pUuid; + + isPriv = pXfer->nextIsPrivate; + pXfer->nextIsPrivate = 0; + if( pXfer->nToken<3 + || pXfer->nToken>4 + || !blob_is_hname(&pXfer->aToken[1]) + || !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &n) + || n<0 + || (pXfer->nToken==4 && !blob_is_hname(&pXfer->aToken[2])) + ){ + blob_appendf(&pXfer->err, "malformed file line"); + return; + } + blob_zero(&content); + blob_extract(pXfer->pIn, n, &content); + pUuid = &pXfer->aToken[1]; + if( !cloneFlag && uuid_is_shunned(blob_str(pUuid)) ){ + /* Ignore files that have been shunned */ + blob_reset(&content); + return; + } + if( isPriv && !g.perm.Private ){ + /* Do not accept private files if not authorized */ + blob_reset(&content); + return; + } + if( cloneFlag ){ + if( pXfer->nToken==4 ){ + srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); + pXfer->nDeltaRcvd++; + }else{ + srcid = 0; + pXfer->nFileRcvd++; + } + rid = content_put_ex(&content, blob_str(pUuid), srcid, + 0, isPriv); + Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), + blob_size(pUuid)); + remote_has(rid); + blob_reset(&content); + return; + } + if( pXfer->nToken==4 ){ + Blob src, next; + srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); + if( content_get(srcid, &src)==0 ){ + rid = content_put_ex(&content, blob_str(pUuid), srcid, + 0, isPriv); + Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), + blob_size(pUuid)); + pXfer->nDanglingFile++; + db_multi_exec("DELETE FROM phantom WHERE rid=%d", rid); + if( !isPriv ) content_make_public(rid); + blob_reset(&src); + blob_reset(&content); + return; + } + pXfer->nDeltaRcvd++; + blob_delta_apply(&src, &content, &next); + blob_reset(&src); + blob_reset(&content); + content = next; + }else{ + pXfer->nFileRcvd++; + } + if( hname_verify_hash(&content, blob_buffer(pUuid), blob_size(pUuid))==0 ){ + blob_appendf(&pXfer->err, "wrong hash on received artifact: %b", pUuid); + } + rid = content_put_ex(&content, blob_str(pUuid), 0, 0, isPriv); + Th_AppendToList(pzUuidList, pnUuidList, blob_str(pUuid), blob_size(pUuid)); + if( rid==0 ){ + blob_appendf(&pXfer->err, "%s", g.zErrMsg); + blob_reset(&content); + }else{ + if( !isPriv ) content_make_public(rid); + manifest_crosslink(rid, &content, MC_NO_ERRORS); + } + assert( blob_is_reset(&content) ); + remote_has(rid); +} + +/* +** The aToken[0..nToken-1] blob array is a parse of a "cfile" line +** message. This routine finishes parsing that message and does +** a record insert of the file. The difference between "file" and +** "cfile" is that with "cfile" the content is already compressed. +** +** The file line is in one of the following two forms: +** +** cfile HASH USIZE CSIZE \n CONTENT +** cfile HASH DELTASRC USIZE CSIZE \n CONTENT +** +** The content is CSIZE bytes immediately following the newline. +** If DELTASRC exists, then the CONTENT is a delta against the +** content of DELTASRC. +** +** The original size of the HASH artifact is USIZE. +** +** 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_compressed_file( + Xfer *pXfer, + char **pzUuidList, + int *pnUuidList +){ + int szC; /* CSIZE */ + int szU; /* USIZE */ + int rid; + int srcid = 0; + Blob content; + int isPriv; + + isPriv = pXfer->nextIsPrivate; + pXfer->nextIsPrivate = 0; + if( pXfer->nToken<4 + || pXfer->nToken>5 + || !blob_is_hname(&pXfer->aToken[1]) + || !blob_is_int(&pXfer->aToken[pXfer->nToken-2], &szU) + || !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &szC) + || szC<0 || szU<0 + || (pXfer->nToken==5 && !blob_is_hname(&pXfer->aToken[2])) + ){ + blob_appendf(&pXfer->err, "malformed cfile line"); + return; + } + if( isPriv && !g.perm.Private ){ + /* Do not accept private files if not authorized */ + return; + } + blob_zero(&content); + blob_extract(pXfer->pIn, szC, &content); + if( uuid_is_shunned(blob_str(&pXfer->aToken[1])) ){ + /* Ignore files that have been shunned */ + blob_reset(&content); + return; + } + if( pXfer->nToken==5 ){ + srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); + pXfer->nDeltaRcvd++; + }else{ + srcid = 0; + pXfer->nFileRcvd++; + } + rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, + szC, isPriv); + Th_AppendToList(pzUuidList, pnUuidList, blob_str(&pXfer->aToken[1]), + blob_size(&pXfer->aToken[1])); + remote_has(rid); + blob_reset(&content); +} + +/* +** The aToken[0..nToken-1] blob array is a parse of a "uvfile" line +** message. This routine finishes parsing that message and adds the +** unversioned file to the "unversioned" table. +** +** The file line is in one of the following two forms: +** +** uvfile NAME MTIME HASH SIZE FLAGS +** uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT +** +** If the 0x0001 bit of FLAGS is set, that means the file has been +** deleted, SIZE is zero, the HASH is "-", and the "\n CONTENT" is omitted. +** +** SIZE is the number of bytes of CONTENT. The CONTENT is uncompressed. +** HASH is the artifact hash of CONTENT. +** +** If the 0x0004 bit of FLAGS is set, that means the CONTENT is omitted. +** The sender might have omitted the content because it is too big to +** transmit, or because it is unchanged and this record exists purely +** to update the MTIME. +*/ +static void xfer_accept_unversioned_file(Xfer *pXfer, int isWriter){ + sqlite3_int64 mtime; /* The MTIME */ + Blob *pHash; /* The HASH value */ + int sz; /* The SIZE */ + int flags; /* The FLAGS */ + Blob content; /* The CONTENT */ + Blob x; /* Compressed content */ + Stmt q; /* SQL statements for comparison and insert */ + int isDelete; /* HASH is "-" indicating this is a delete */ + int nullContent; /* True of CONTENT is NULL */ + int iStatus; /* Result from unversioned_status() */ + + pHash = &pXfer->aToken[3]; + if( pXfer->nToken==5 + || !blob_is_filename(&pXfer->aToken[1]) + || !blob_is_int64(&pXfer->aToken[2], &mtime) + || (!blob_eq(pHash,"-") && !blob_is_hname(pHash)) + || !blob_is_int(&pXfer->aToken[4], &sz) + || !blob_is_int(&pXfer->aToken[5], &flags) + || (mtime<0 || sz<0 || flags<0) + ){ + blob_appendf(&pXfer->err, "malformed uvfile line"); + return; + } + blob_init(&content, 0, 0); + blob_init(&x, 0, 0); + if( sz>0 && (flags & 0x0005)==0 ){ + blob_extract(pXfer->pIn, sz, &content); + nullContent = 0; + if( hname_verify_hash(&content, blob_buffer(pHash), blob_size(pHash))==0 ){ + blob_appendf(&pXfer->err, "in uvfile line, HASH does not match CONTENT"); + goto end_accept_unversioned_file; + } + }else{ + nullContent = 1; + } + + /* The isWriter flag must be true in order to land the new file */ + if( !isWriter ){ + blob_appendf(&pXfer->err,"Write permissions for unversioned files missing"); + goto end_accept_unversioned_file; + } + + /* Make sure we have a valid g.rcvid marker */ + content_rcvid_init(0); + + /* Check to see if current content really should be overwritten. Ideally, + ** a uvfile card should never have been sent unless the overwrite should + ** occur. But do not trust the sender. Double-check. + */ + iStatus = unversioned_status(blob_str(&pXfer->aToken[1]), mtime, + blob_str(pHash)); + if( iStatus>=3 ) goto end_accept_unversioned_file; + + /* Store the content */ + isDelete = blob_eq(pHash, "-"); + if( isDelete ){ + db_prepare(&q, + "UPDATE unversioned" + " SET rcvid=:rcvid, mtime=:mtime, hash=NULL," + " sz=0, encoding=0, content=NULL" + " WHERE name=:name" + ); + db_bind_int(&q, ":rcvid", g.rcvid); + }else if( iStatus==2 ){ + db_prepare(&q, "UPDATE unversioned SET mtime=:mtime WHERE name=:name"); + }else{ + db_prepare(&q, + "REPLACE INTO unversioned(name,rcvid,mtime,hash,sz,encoding,content)" + " VALUES(:name,:rcvid,:mtime,:hash,:sz,:encoding,:content)" + ); + db_bind_int(&q, ":rcvid", g.rcvid); + db_bind_text(&q, ":hash", blob_str(pHash)); + db_bind_int(&q, ":sz", blob_size(&content)); + if( !nullContent ){ + blob_compress(&content, &x); + if( blob_size(&x) < 0.8*blob_size(&content) ){ + db_bind_blob(&q, ":content", &x); + db_bind_int(&q, ":encoding", 1); + }else{ + db_bind_blob(&q, ":content", &content); + db_bind_int(&q, ":encoding", 0); + } + }else{ + db_bind_int(&q, ":encoding", 0); + } + } + db_bind_text(&q, ":name", blob_str(&pXfer->aToken[1])); + db_bind_int64(&q, ":mtime", mtime); + db_step(&q); + db_finalize(&q); + db_unset("uv-hash", 0); + +end_accept_unversioned_file: + blob_reset(&x); + blob_reset(&content); +} + +/* +** 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 */ + int isPrivate, /* True if rid is a private artifact */ + Blob *pContent, /* The content of the file to send */ + Blob *pUuid /* The HASH of the file to send */ +){ + static const char *const azQuery[] = { + "SELECT pid FROM plink x" + " WHERE cid=%d" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=pid)", + + "SELECT pid, min(mtime) FROM mlink, event ON mlink.mid=event.objid" + " WHERE fid=%d" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=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] /*works-like:"%d"*/, rid); + } + if( srcId>0 + && (pXfer->syncPrivate || !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>=(int)blob_size(pContent)-50 ){ + size = 0; + }else if( uuid_is_shunned(zUuid) ){ + size = 0; + }else{ + if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); + blob_appendf(pXfer->pOut, "file %b %s %d\n", pUuid, zUuid, size); + blob_append(pXfer->pOut, blob_buffer(&delta), size); + } + 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 */ + int isPrivate, /* True if rid is a private artifact */ + Blob *pUuid /* The HASH 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 + && (pXfer->syncPrivate || !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); + if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); + 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; +} + +/* +** Push an error message to alert the older client that the repository +** has SHA3 content and cannot be synced or cloned. +*/ +static void xfer_cannot_send_sha3_error(Xfer *pXfer){ + blob_appendf(pXfer->pOut, + "error Fossil\\sversion\\s2.0\\sor\\slater\\srequired.\n" + ); +} + + +/* +** Send the file identified by rid. +** +** The pUuid can be NULL in which case the correct hash 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; + int isPriv = content_is_private(rid); + + if( isPriv && pXfer->syncPrivate==0 ){ + if( pXfer->remoteDate>=20200413 && pUuid && blob_size(pUuid)>0 ){ + /* If the artifact is private and we are not doing a private sync, + ** at least tell the other side that the artifact exists and is + ** known to be private. But only do this for newer clients since + ** older ones will throw an error if they get a private igot card + ** and private syncing is disallowed */ + blob_appendf(pXfer->pOut, "igot %b 1\n", pUuid); + pXfer->nIGotSent++; + pXfer->nPrivIGot++; + } + 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( blob_size(&uuid)>HNAME_LEN_SHA1 && pXfer->remoteVersion<20000 ){ + xfer_cannot_send_sha3_error(pXfer); + 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->maxTime != -1 && time(NULL) >= pXfer->maxTime) || + pXfer->mxSend<=(int)blob_size(pXfer->pOut) ){ + const char *zFormat = isPriv ? "igot %b 1\n" : "igot %b\n"; + blob_appendf(pXfer->pOut, zFormat /*works-like:"%b"*/, pUuid); + pXfer->nIGotSent++; + blob_reset(&uuid); + return; + } + if( nativeDelta ){ + size = send_delta_native(pXfer, rid, isPriv, pUuid); + if( size ){ + pXfer->nDeltaSent++; + } + } + if( size==0 ){ + content_get(rid, &content); + + if( !nativeDelta && blob_size(&content)>100 ){ + size = send_delta_parent(pXfer, rid, isPriv, &content, pUuid); + } + if( size==0 ){ + int size = blob_size(&content); + if( isPriv ) blob_append(pXfer->pOut, "private\n", -1); + blob_appendf(pXfer->pOut, "file %b %d\n", pUuid, size); + blob_append(pXfer->pOut, blob_buffer(&content), size); + pXfer->nFileSent++; + }else{ + pXfer->nDeltaSent++; + } + blob_reset(&content); + } + remote_has(rid); + blob_reset(&uuid); +#if 0 + if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ + blob_append(pXfer->pOut, "\n", 1); + } +#endif +} + +/* +** Send the file identified by rid as a compressed artifact. Basically, +** send the content exactly as it appears in the BLOB table using +** a "cfile" card. +*/ +static void send_compressed_file(Xfer *pXfer, int rid){ + const char *zContent; + const char *zUuid; + const char *zDelta; + int szU; + int szC; + int rc; + int isPrivate; + int srcIsPrivate; + static Stmt q1; + Blob fullContent; + + isPrivate = content_is_private(rid); + if( isPrivate && pXfer->syncPrivate==0 ) return; + db_static_prepare(&q1, + "SELECT uuid, size, content, delta.srcid IN private," + " (SELECT uuid FROM blob WHERE rid=delta.srcid)" + " FROM blob LEFT JOIN delta ON (blob.rid=delta.rid)" + " WHERE blob.rid=:rid" + " AND blob.size>=0" + " AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)" + ); + db_bind_int(&q1, ":rid", rid); + rc = db_step(&q1); + if( rc==SQLITE_ROW ){ + zUuid = db_column_text(&q1, 0); + szU = db_column_int(&q1, 1); + szC = db_column_bytes(&q1, 2); + zContent = db_column_raw(&q1, 2); + srcIsPrivate = db_column_int(&q1, 3); + zDelta = db_column_text(&q1, 4); + if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); + if( pXfer->remoteVersion<20000 && db_column_bytes(&q1,0)!=HNAME_LEN_SHA1 ){ + xfer_cannot_send_sha3_error(pXfer); + db_reset(&q1); + return; + } + blob_appendf(pXfer->pOut, "cfile %s ", zUuid); + if( !isPrivate && srcIsPrivate ){ + content_get(rid, &fullContent); + szU = blob_size(&fullContent); + blob_compress(&fullContent, &fullContent); + szC = blob_size(&fullContent); + zContent = blob_buffer(&fullContent); + zDelta = 0; + } + if( zDelta ){ + blob_appendf(pXfer->pOut, "%s ", zDelta); + pXfer->nDeltaSent++; + }else{ + pXfer->nFileSent++; + } + blob_appendf(pXfer->pOut, "%d %d\n", szU, szC); + blob_append(pXfer->pOut, zContent, szC); + if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ + blob_append(pXfer->pOut, "\n", 1); + } + if( !isPrivate && srcIsPrivate ){ + blob_reset(&fullContent); + } + } + db_reset(&q1); +} + +/* +** Send the unversioned file identified by zName by generating the +** appropriate "uvfile" card. +** +** uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT +** +** If the noContent flag is set, omit the CONTENT and set the 0x0004 +** flag in FLAGS. +*/ +static void send_unversioned_file( + Xfer *pXfer, /* Transfer context */ + const char *zName, /* Name of unversioned file to be sent */ + int noContent /* True to omit the content */ +){ + Stmt q1; + + if( (int)blob_size(pXfer->pOut)>=pXfer->mxSend ) noContent = 1; + if( noContent ){ + db_prepare(&q1, + "SELECT mtime, hash, encoding, sz FROM unversioned WHERE name=%Q", + zName + ); + }else{ + db_prepare(&q1, + "SELECT mtime, hash, encoding, sz, content FROM unversioned" + " WHERE name=%Q", + zName + ); + } + if( db_step(&q1)==SQLITE_ROW ){ + sqlite3_int64 mtime = db_column_int64(&q1, 0); + const char *zHash = db_column_text(&q1, 1); + if( pXfer->remoteVersion<20000 && db_column_bytes(&q1,1)>HNAME_LEN_SHA1 ){ + xfer_cannot_send_sha3_error(pXfer); + db_reset(&q1); + return; + } + if( (int)blob_size(pXfer->pOut)>=pXfer->mxSend ){ + /* If we have already reached the send size limit, send a (short) + ** uvigot card rather than a uvfile card. This only happens on the + ** server side. The uvigot card will provoke the client to resend + ** another uvgimme on the next cycle. */ + blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", + zName, mtime, zHash, db_column_int(&q1,3)); + }else{ + blob_appendf(pXfer->pOut, "uvfile %s %lld", zName, mtime); + if( zHash==0 ){ + blob_append(pXfer->pOut, " - 0 1\n", -1); + }else if( noContent ){ + blob_appendf(pXfer->pOut, " %s %d 4\n", zHash, db_column_int(&q1,3)); + }else{ + Blob content; + blob_init(&content, 0, 0); + db_column_blob(&q1, 4, &content); + if( db_column_int(&q1, 2) ){ + blob_uncompress(&content, &content); + } + blob_appendf(pXfer->pOut, " %s %d 0\n", zHash, blob_size(&content)); + blob_append(pXfer->pOut, blob_buffer(&content), blob_size(&content)); + blob_reset(&content); + } + } + } + db_finalize(&q1); +} + +/* +** Send a gimme message for every phantom. +** +** Except: do not request shunned artifacts. And do not request +** private artifacts if we are not doing a private transfer. +*/ +static void request_phantoms(Xfer *pXfer, int maxReq){ + Stmt q; + db_prepare(&q, + "SELECT uuid FROM phantom CROSS JOIN blob USING(rid) /*scan*/" + " WHERE NOT EXISTS(SELECT 1 FROM unk WHERE unk.uuid=blob.uuid)" + " AND NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid) %s", + (pXfer->syncPrivate ? "" : + " 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 hash on the tail of pMsg. Verify that it matches the +** the hash given in pHash. Return non-zero for an error and 0 on success. +** +** The type of hash computed (SHA1, SHA3-256) is determined by +** the length of the input hash in pHash. +*/ +static int check_tail_hash(Blob *pHash, Blob *pMsg){ + Blob tail; + int rc; + blob_tail(pMsg, &tail); + rc = hname_verify_hash(&tail, blob_buffer(pHash), blob_size(pHash)); + blob_reset(&tail); + return rc==HNAME_ERROR; +} + +/* +** 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 sha1_shared_secret() encoding of the users password. +** +** SIGNATURE = sha1_sum( NONCE + sha1_shared_secret(PASSWORD) ); +** +** The parameters to this routine are ephemeral 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. +** +** Return non-zero for a login failure and zero for success. +*/ +static int check_login(Blob *pLogin, Blob *pNonce, Blob *pSig){ + Stmt q; + int rc = -1; + char *zLogin = blob_terminate(pLogin); + defossilize(zLogin); + + if( fossil_strcmp(zLogin, "nobody")==0 + || fossil_strcmp(zLogin, "anonymous")==0 + ){ + return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ + } + if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 + && db_get_boolean("remote_user_ok",0) ){ + return 0; /* Accept Basic Authorization */ + } + 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 ){ + int szPw; + Blob pw, combined, hash; + blob_zero(&pw); + db_ephemeral_blob(&q, 0, &pw); + szPw = blob_size(&pw); + blob_zero(&combined); + blob_copy(&combined, pNonce); + blob_append(&combined, blob_buffer(&pw), szPw); + sha1sum_blob(&combined, &hash); + assert( blob_size(&hash)==40 ); + rc = blob_constant_time_cmp(&hash, pSig); + blob_reset(&hash); + blob_reset(&combined); + if( rc!=0 && szPw!=40 ){ + /* If this server stores cleartext passwords and the password did not + ** match, then perhaps the client is sending SHA1 passwords. Try + ** again with the SHA1 password. + */ + const char *zPw = db_column_text(&q, 0); + char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); + blob_zero(&combined); + blob_copy(&combined, pNonce); + blob_append(&combined, zSecret, -1); + fossil_free(zSecret); + sha1sum_blob(&combined, &hash); + rc = blob_constant_time_cmp(&hash, pSig); + blob_reset(&hash); + blob_reset(&combined); + } + if( rc==0 ){ + const char *zCap; + zCap = db_column_text(&q, 1); + login_set_capabilities(zCap, 0); + g.userUid = db_column_int(&q, 2); + g.zLogin = mprintf("%b", pLogin); + g.zNonce = mprintf("%b", pNonce); + } + } + db_finalize(&q); + return rc; +} + +/* +** 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. +*/ +void create_cluster(void){ + Blob cluster, cksum; + Blob deleteWhere; + Stmt q; + int nUncl; + int nRow = 0; + int rid; + +#if 0 + /* 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)" + ); +#endif + + nUncl = db_int(0, "SELECT count(*) FROM unclustered /*scan*/" + " WHERE NOT EXISTS(SELECT 1 FROM phantom" + " WHERE rid=unclustered.rid)"); + if( nUncl>=100 ){ + blob_zero(&cluster); + blob_zero(&deleteWhere); + 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)); + nRow++; + if( nRow>=800 && nUncl>nRow+100 ){ + md5sum_blob(&cluster, &cksum); + blob_appendf(&cluster, "Z %b\n", &cksum); + blob_reset(&cksum); + rid = content_put(&cluster); + manifest_crosslink(rid, &cluster, MC_NONE); + blob_reset(&cluster); + nUncl -= nRow; + nRow = 0; + blob_append_sql(&deleteWhere, ",%d", rid); + } + } + db_finalize(&q); + db_multi_exec( + "DELETE FROM unclustered WHERE rid NOT IN (0 %s)" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=unclustered.rid)", + blob_sql_text(&deleteWhere) + ); + blob_reset(&deleteWhere); + if( nRow>0 ){ + md5sum_blob(&cluster, &cksum); + blob_appendf(&cluster, "Z %b\n", &cksum); + blob_reset(&cksum); + rid = content_put(&cluster); + manifest_crosslink(rid, &cluster, MC_NONE); + blob_reset(&cluster); + } + } +} + +/* +** Send igot messages for every private artifact +*/ +static int send_private(Xfer *pXfer){ + int cnt = 0; + Stmt q; + if( pXfer->syncPrivate ){ + db_prepare(&q, "SELECT uuid FROM private JOIN blob USING(rid)"); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(pXfer->pOut, "igot %s 1\n", db_column_text(&q,0)); + cnt++; + } + db_finalize(&q); + } + return cnt; +} + +/* +** Send an igot message for every entry in unclustered table. +** Return the number of cards sent. +** +** Except: +** * Do not send igot cards for shunned artifacts +** * Do not send igot cards for phantoms +** * Do not send igot cards for private artifacts +** * Do not send igot cards for any artifact that is in the +** ONREMOTE table, if that table exists. +** +** If the pXfer->resync flag is set, that means we are doing a "--verily" +** sync and all artifacts that don't meet the restrictions above should +** be sent. +*/ +static int send_unclustered(Xfer *pXfer){ + Stmt q; + int cnt = 0; + const char *zExtra; + if( db_table_exists("temp","onremote") ){ + zExtra = " AND NOT EXISTS(SELECT 1 FROM onremote WHERE rid=blob.rid)"; + }else{ + zExtra = ""; + } + if( pXfer->resync ){ + db_prepare(&q, + "SELECT uuid, rid FROM blob" + " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s" + " AND blob.rid<=%d" + " ORDER BY blob.rid DESC", + zExtra /*safe-for-%s*/, pXfer->resync + ); + }else{ + db_prepare(&q, + "SELECT uuid FROM unclustered JOIN blob USING(rid) /*scan*/" + " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s", + zExtra /*safe-for-%s*/ + ); + } + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); + cnt++; + if( pXfer->resync && pXfer->mxSend<(int)blob_size(pXfer->pOut) ){ + pXfer->resync = db_column_int(&q, 1)-1; + } + } + db_finalize(&q); + if( cnt==0 ) pXfer->resync = 0; + return cnt; +} + +/* +** Send an igot message for every cluster artifact that is not a phantom, +** is not shunned, is not private, and that is not in the UNCLUSTERED table. +** Return the number of cards sent. +*/ +static int send_all_clusters(Xfer *pXfer){ + Stmt q; + int cnt = 0; + const char *zExtra; + if( db_table_exists("temp","onremote") ){ + zExtra = " AND NOT EXISTS(SELECT 1 FROM onremote WHERE rid=blob.rid)"; + }else{ + zExtra = ""; + } + db_prepare(&q, + "SELECT uuid" + " FROM tagxref JOIN blob ON tagxref.rid=blob.rid AND tagxref.tagid=%d" + " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" + " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" + " AND NOT EXISTS(SELECT 1 FROM unclustered WHERE rid=blob.rid)" + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)%s", + TAG_CLUSTER, zExtra /*safe-for-%s*/ + ); + while( db_step(&q)==SQLITE_ROW ){ + if( cnt==0 ) blob_appendf(pXfer->pOut, "# sending-clusters\n"); + blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); + cnt++; + } + db_finalize(&q); + if( cnt ) blob_appendf(pXfer->pOut, "# end-of-clusters\n"); + 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)" + " AND NOT EXISTS(SELECT 1 FROM phantom 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); +} + +/* +** pXfer is a "pragma uv-hash HASH" card. +** +** If HASH is different from the unversioned content hash on this server, +** then send a bunch of uvigot cards, one for each entry unversioned file +** on this server. +*/ +static void send_unversioned_catalog(Xfer *pXfer){ + Stmt uvq; + unversioned_schema(); + db_prepare(&uvq, + "SELECT name, mtime, hash, sz FROM unversioned" + ); + while( db_step(&uvq)==SQLITE_ROW ){ + const char *zName = db_column_text(&uvq,0); + sqlite3_int64 mtime = db_column_int64(&uvq,1); + const char *zHash = db_column_text(&uvq,2); + int sz = db_column_int(&uvq,3); + if( zHash==0 ){ sz = 0; zHash = "-"; } + blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", + zName, mtime, zHash, sz); + } + db_finalize(&uvq); +} + +/* +** Return a string that contains supplemental information about a +** "not authorized" error. The string might be empty if no additional +** information is available. +*/ +static char *whyNotAuth(void){ + if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){ + return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled"; + } + return ""; +} + +/* +** Called when there is an attempt to transfer private content to and +** from a server without authorization. +*/ +static void server_private_xfer_not_authorized(void){ + @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth()) +} + +/* +** Return the common TH1 code to evaluate prior to evaluating any other +** TH1 transfer notification scripts. +*/ +const char *xfer_common_code(void){ + return db_get("xfer-common-script", 0); +} + +/* +** Return the TH1 code to evaluate when a push is processed. +*/ +const char *xfer_push_code(void){ + return db_get("xfer-push-script", 0); +} + +/* +** Return the TH1 code to evaluate when a commit is processed. +*/ +const char *xfer_commit_code(void){ + return db_get("xfer-commit-script", 0); +} + +/* +** Return the TH1 code to evaluate when a ticket change is processed. +*/ +const char *xfer_ticket_code(void){ + return db_get("xfer-ticket-script", 0); +} + +/* +** Reset the CGI content, roll back any pending db transaction, and +** emit an "error" xfer message. The message text gets fossil-encoded +** by this function. This is only intended for use with +** fail-fast/fatal errors, not ones which can be skipped over. +*/ +static void xfer_fatal_error(const char *zMsg){ + cgi_reset_content(); + if( db_transaction_nesting_depth()>0 ){ + db_rollback_transaction(); + } + @ error %F(zMsg) +} + +/* +** Run the specified TH1 script, if any, and returns 1 on error. +*/ +int xfer_run_script( + const char *zScript, + const char *zUuidOrList, + int bIsList +){ + int rc = TH_OK; + if( !zScript ) return rc; + Th_FossilInit(TH_INIT_DEFAULT); + Th_Store(bIsList ? "uuids" : "uuid", zUuidOrList ? zUuidOrList : ""); + rc = Th_Eval(g.interp, 0, zScript, -1); + if( rc!=TH_OK ){ + fossil_error(1, "%s", Th_GetResult(g.interp, 0)); + } + return rc; +} + +/* +** Runs the pre-transfer TH1 script, if any, and returns its return code. +** This script may be run multiple times. If the script performs actions +** that cannot be redone, it should use an internal [if] guard similar to +** the following: +** +** if {![info exists common_done]} { +** # ... code here +** set common_done 1 +** } +*/ +int xfer_run_common_script(void){ + return xfer_run_script(xfer_common_code(), 0, 0); +} + +/* +** This routine makes a "syncwith:URL" entry in the CONFIG table to +** indicate that a sync is occurring with zUrl. +** +** Add a "syncfrom:URL" entry instead of "syncwith:URL" if bSyncFrom is true. +*/ +static void xfer_syncwith(const char *zUrl, int bSyncFrom){ + UrlData x; + memset(&x, 0, sizeof(x)); + url_parse_local(zUrl, URL_OMIT_USER, &x); + if( x.protocol && strncmp(x.protocol,"http",4)==0 + && x.name && sqlite3_strlike("%localhost%", x.name, 0)!=0 + ){ + db_unprotect(PROTECT_CONFIG); + db_multi_exec("REPLACE INTO config(name,value,mtime)" + "VALUES('sync%q:%q','{}',now())", + bSyncFrom ? "from" : "with", x.canonical); + db_protect_pop(); + } + url_unparse(&x); +} + +/* +** If this variable is set, disable login checks. Used for debugging +** only. +*/ +static int disableLogin = 0; + +/* +** Must be passed the version info from pragmas +** client-version/server-version cards. If the version info is "new +** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login +** card flag, else this is a no-op. +*/ +static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, + int fLoginCardMode){ + if( iRemoteVersion>=22700 + && (iDate > 20250727 + || (iDate == 20250727 && iTime >= 110500)) ){ + g.syncInfo.fLoginCardMode |= fLoginCardMode; + } +} + +/* +** 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.org/" instead of "http://fossil-scm.org/index.cgi". +** +** WEBPAGE: xfer raw-content loadavg-exempt +** +** 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; + char *zNow; + int rc; + const char *zScript = 0; + char *zUuidList = 0; + int nUuidList = 0; + char **pzUuidList = 0; + int *pnUuidList = 0; + int uvCatalogSent = 0; + int bSendLinks = 0; + int nLogin = 0; + + if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ + fossil_redirect_home(); + } + g.zLogin = "anonymous"; + login_set_anon_nobody_capabilities(); + login_check_credentials(); + if( g.perm.Clone ) g.perm.Read = 1; + cgi_check_for_malice(); + memset(&xfer, 0, sizeof(xfer)); + blobarray_zero(xfer.aToken, count(xfer.aToken)); + cgi_set_content_type(g.zContentType); + cgi_reset_content(); + if( db_schema_is_outofdate() ){ + @ error database\sschema\sis\sout-of-date\son\sthe\sserver. + return; + } + blob_zero(&xfer.err); + xfer.pIn = &g.cgiIn; + xfer.pOut = cgi_output_blob(); + xfer.mxSend = db_get_int("max-download", 5000000); + xfer.maxTime = db_get_int("max-download-time", 30); + if( xfer.maxTime<1 ) xfer.maxTime = 1; + xfer.maxTime += time(NULL); + g.xferPanic = 1; + + db_begin_write(); + db_multi_exec( + "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" + "CREATE TEMP TABLE unk(uuid TEXT PRIMARY KEY) WITHOUT ROWID;" + ); + manifest_crosslink_begin(); + rc = xfer_run_common_script(); + if( rc==TH_ERROR ){ + cgi_reset_content(); + @ error common\sscript\sfailed:\s%F(g.zErrMsg) + nErr++; + } + zScript = xfer_push_code(); + if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ + pzUuidList = &zUuidList; + pnUuidList = &nUuidList; + } + if( g.syncInfo.zLoginCard ){ + /* Login card received via HTTP Cookie header */ + blob_zero(&xfer.line); + blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); + xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, + count(xfer.aToken)); + fossil_free( g.syncInfo.zLoginCard ); + g.syncInfo.zLoginCard = 0; + if( xfer.nToken==4 + && blob_eq(&xfer.aToken[0], "login") ){ + goto handle_login_card; + } + } + while( blob_line(xfer.pIn, &xfer.line) ){ + if( blob_buffer(&xfer.line)[0]=='#' ) continue; + if( blob_size(&xfer.line)==0 ) continue; + xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); + + /* file HASH SIZE \n CONTENT + ** file HASH DELTASRC SIZE \n CONTENT + ** + ** Server accepts a file from the client. + */ + if( blob_eq(&xfer.aToken[0], "file") ){ + if( !isPush ){ + cgi_reset_content(); + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) + nErr++; + break; + } + xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList); + if( blob_size(&xfer.err) ){ + cgi_reset_content(); + @ error %T(blob_str(&xfer.err)) + nErr++; + break; + } + }else + + /* cfile HASH USIZE CSIZE \n CONTENT + ** cfile HASH DELTASRC USIZE CSIZE \n CONTENT + ** + ** Server accepts a compressed file from the client. + */ + if( blob_eq(&xfer.aToken[0], "cfile") ){ + if( !isPush ){ + cgi_reset_content(); + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) + nErr++; + break; + } + xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList); + if( blob_size(&xfer.err) ){ + cgi_reset_content(); + @ error %T(blob_str(&xfer.err)) + nErr++; + break; + } + }else + + /* uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT + ** + ** Server accepts an unversioned file from the client. + */ + if( blob_eq(&xfer.aToken[0], "uvfile") ){ + xfer_accept_unversioned_file(&xfer, g.perm.WrUnver); + if( blob_size(&xfer.err) ){ + cgi_reset_content(); + @ error %T(blob_str(&xfer.err)) + fossil_print("%%%%%%%% xfer.err: '%s'\n", blob_str(&xfer.err)); + nErr++; + break; + } + }else + + /* gimme HASH + ** + ** Client is requesting a file from the server. Send it. + */ + if( blob_eq(&xfer.aToken[0], "gimme") + && xfer.nToken==2 + && blob_is_hname(&xfer.aToken[1]) + ){ + nGimme++; + remote_unk(&xfer.aToken[1]); + if( isPull ){ + int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); + if( rid ){ + send_file(&xfer, rid, &xfer.aToken[1], deltaFlag); + } + } + }else + + /* uvgimme NAME + ** + ** Client is requesting an unversioned file from the server. Send it. + */ + if( blob_eq(&xfer.aToken[0], "uvgimme") + && xfer.nToken==2 + && blob_is_filename(&xfer.aToken[1]) + ){ + send_unversioned_file(&xfer, blob_str(&xfer.aToken[1]), 0); + }else + + /* igot HASH ?ISPRIVATE? + ** + ** Client announces that it has a particular file. If the ISPRIVATE + ** argument exists and is "1", then the file is a private file. + */ + if( xfer.nToken>=2 + && blob_eq(&xfer.aToken[0], "igot") + && blob_is_hname(&xfer.aToken[1]) + ){ + if( isPush ){ + int rid = 0; + int isPriv = 0; + if( xfer.nToken==2 || blob_eq(&xfer.aToken[2],"1")==0 ){ + /* Client says the artifact is public */ + rid = rid_from_uuid(&xfer.aToken[1], 1, 0); + }else if( g.perm.Private ){ + /* Client says the artifact is private and the client has + ** permission to push private content. Create a new phantom + ** artifact that is marked private. */ + rid = rid_from_uuid(&xfer.aToken[1], 1, 1); + isPriv = 1; + }else{ + /* Client says the artifact is private and the client is unable + ** or unwilling to send us the artifact. If we already hold the + ** artifact here on the server as a phantom, make sure that + ** phantom is marked as private so that we don't keep asking about + ** it in subsequent sync requests. */ + rid = rid_from_uuid(&xfer.aToken[1], 0, 1); + isPriv = 1; + } + if( rid ){ + remote_has(rid); + if( isPriv ){ + content_make_private(rid); + }else{ + content_make_public(rid); + } + } + } + }else + + + /* pull SERVERCODE PROJECTCODE + ** push SERVERCODE PROJECTCODE + ** + ** The client wants either send or receive. The server should + ** verify that the project code matches. The server code is ignored. + */ + if( xfer.nToken==3 + && (blob_eq(&xfer.aToken[0], "pull") || blob_eq(&xfer.aToken[0], "push")) + && blob_is_hname(&xfer.aToken[2]) + ){ + const char *zPCode; + zPCode = db_get("project-code", 0); + if( zPCode==0 ){ + fossil_fatal("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.perm.Read ){ + cgi_reset_content(); + @ error not\sauthorized\sto\sread%s(whyNotAuth()) + nErr++; + break; + } + isPull = 1; + }else{ + if( !g.perm.Write ){ + if( !isPull ){ + cgi_reset_content(); + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) + nErr++; + }else{ + @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth()) + } + }else{ + isPush = 1; + } + } + }else + + /* clone ?PROTOCOL-VERSION? ?SEQUENCE-NUMBER? + ** + ** The client knows nothing. Tell all. + */ + if( blob_eq(&xfer.aToken[0], "clone") ){ + int iVers; + login_check_credentials(); + if( !g.perm.Clone ){ + cgi_reset_content(); + @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) + @ error not\sauthorized\sto\sclone%s(whyNotAuth()) + nErr++; + break; + } + if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){ + @ pragma uv-pull-only + send_unversioned_catalog(&xfer); + uvCatalogSent = 1; + } + if( xfer.nToken==3 + && blob_is_int(&xfer.aToken[1], &iVers) + && iVers>=2 + ){ + int seqno, max; + if( iVers>=3 ){ + cgi_set_content_type("application/x-fossil-uncompressed"); + } + blob_is_int(&xfer.aToken[2], &seqno); + if( seqno<=0 ){ + xfer_fatal_error("invalid clone sequence number"); + db_rollback_transaction(); + return; + } + max = db_int(0, "SELECT max(rid) FROM blob"); + while( xfer.mxSend>(int)blob_size(xfer.pOut) && seqno<=max){ + if( time(NULL) >= xfer.maxTime ) break; + if( iVers>=3 ){ + send_compressed_file(&xfer, seqno); + }else{ + send_file(&xfer, seqno, 0, 1); + } + seqno++; + } + if( seqno>max ) seqno = 0; + @ clone_seqno %d(seqno) + }else{ + isClone = 1; + isPull = 1; + deltaFlag = 1; + } + @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) + }else + + /* login USER NONCE SIGNATURE + ** + ** The client has sent login credentials to the server. + ** Validate the login. This has to happen before anything else. + ** + ** For many years, Fossil would accept multiple login cards with + ** cumulative permissions. But that feature was never used. Hence + ** it is now prohibited. Any login card after the first generates + ** a fatal error. + */ + if( blob_eq(&xfer.aToken[0], "login") + && xfer.nToken==4 + ){ + handle_login_card: + nLogin++; + if( disableLogin ){ + g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; + }else if( nLogin > 1 ){ + cgi_reset_content(); + @ error multiple\slogin\cards + nErr++; + break; + }else{ + if( check_tail_hash(&xfer.aToken[2], xfer.pIn) + || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) + ){ + cgi_reset_content(); + @ error login\sfailed + nErr++; + break; + } + } + }else + + /* reqconfig NAME + ** + ** Client is requesting a configuration value from the server + */ + if( blob_eq(&xfer.aToken[0], "reqconfig") + && xfer.nToken==2 + ){ + if( g.perm.Read ){ + char *zName = blob_str(&xfer.aToken[1]); + if( zName[0]=='/' ){ + /* New style configuration transfer */ + int groupMask = configure_name_to_mask(&zName[1], 0); + if( !g.perm.Admin ) groupMask &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER); + if( !g.perm.RdAddr ) groupMask &= ~CONFIGSET_ADDR; + configure_send_group(xfer.pOut, groupMask, 0); + } + } + }else + + /* config NAME SIZE \n CONTENT + ** + ** Client has sent a configuration value to the server. + ** 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; + if( size<0 ){ + xfer_fatal_error("invalid config record"); + db_rollback_transaction(); + return; + } + blob_zero(&content); + blob_extract(xfer.pIn, size, &content); + if( !g.perm.Admin ){ + cgi_reset_content(); + @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth()) + nErr++; + break; + } + configure_receive(zName, &content, CONFIGSET_ALL); + blob_reset(&content); + blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR); + }else + + + /* cookie TEXT + ** + ** A cookie contains an 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 + + + /* private + ** + ** The client card indicates that the next "file" or "cfile" will contain + ** private content. + */ + if( blob_eq(&xfer.aToken[0], "private") ){ + if( !g.perm.Private ){ + server_private_xfer_not_authorized(); + }else{ + xfer.nextIsPrivate = 1; + } + }else + + /* pragma NAME VALUE... + ** + ** The client issues pragmas to try to influence the behavior of the + ** server. These are requests only. Unknown pragmas are silently + ** ignored. + */ + if( blob_eq(&xfer.aToken[0], "pragma") && xfer.nToken>=2 ){ + + /* pragma send-private + ** + ** The client is requesting private artifacts. + ** + ** If the user has the "x" privilege (which must be set explicitly - + ** it is not automatic with "a" or "s") then this pragma causes + ** private information to be pulled in addition to public records. + */ + if( blob_eq(&xfer.aToken[1], "send-private") ){ + login_check_credentials(); + if( !g.perm.Private ){ + server_private_xfer_not_authorized(); + }else{ + xfer.syncPrivate = 1; + } + }else + + /* pragma send-catalog + ** + ** The client wants to see igot cards for all known artifacts. + ** This is used as part of "sync --verily" to help ensure that + ** no artifacts have been missed on prior syncs. + */ + if( blob_eq(&xfer.aToken[1], "send-catalog") ){ + xfer.resync = 0x7fffffff; + }else + + /* pragma client-version VERSION ?DATE? ?TIME? + ** + ** The client announces to the server what version of Fossil it + ** is running. The DATE and TIME are a pure numeric ISO8601 time + ** for the specific check-in of the client. + */ + if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ + xfer.remoteVersion = g.syncInfo.remoteVersion = + atoi(blob_str(&xfer.aToken[2])); + if( xfer.nToken>=5 ){ + xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); + xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); + @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ + @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) + } + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, + xfer.remoteTime, 0x04 ); + }else + + /* pragma uv-hash HASH + ** + ** The client wants to make sure that unversioned files are all synced. + ** If the HASH does not match, send a complete catalog of + ** "uvigot" cards. + */ + if( blob_eq(&xfer.aToken[1], "uv-hash") + && blob_is_hname(&xfer.aToken[2]) + ){ + if( !uvCatalogSent + && g.perm.Read + && !blob_eq_str(&xfer.aToken[2], unversioned_content_hash(0),-1) + ){ + if( g.perm.WrUnver ){ + @ pragma uv-push-ok + }else if( g.perm.Read ){ + @ pragma uv-pull-only + } + send_unversioned_catalog(&xfer); + } + uvCatalogSent = 1; + }else + + /* pragma ci-lock CHECKIN-HASH CLIENT-ID + ** + ** The client wants to make non-branch commit against the check-in + ** identified by CHECKIN-HASH. The server will remember this and + ** subsequent ci-lock requests from different clients will generate + ** a ci-lock-fail pragma in the reply. + */ + if( blob_eq(&xfer.aToken[1], "ci-lock") + && xfer.nToken==4 + && blob_is_hname(&xfer.aToken[2]) + ){ + Stmt q; + sqlite3_int64 iNow = time(0); + sqlite3_int64 maxAge = db_get_int("lock-timeout",60); + int seenFault = 0; + db_prepare(&q, + "SELECT value->>'login'," + " mtime," + " value->>'clientid'," + " (SELECT rid FROM blob WHERE uuid=substr(name,9))," + " name" + " FROM config" + " WHERE name GLOB 'ci-lock-*'" + " AND json_valid(value)" + ); + while( db_step(&q)==SQLITE_ROW ){ + int x = db_column_int(&q,3); + const char *zName = db_column_text(&q,4); + if( db_column_int64(&q,1)<=iNow-maxAge || !is_a_leaf(x) ){ + /* check-in locks expire after maxAge seconds, or when the + ** check-in is no longer a leaf */ + db_unprotect(PROTECT_CONFIG); + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + db_protect_pop(); + continue; + } + if( fossil_strcmp(zName+8, blob_str(&xfer.aToken[2]))==0 ){ + const char *zClientId = db_column_text(&q, 2); + const char *zLogin = db_column_text(&q,0); + sqlite3_int64 mtime = db_column_int64(&q, 1); + if( fossil_strcmp(zClientId, blob_str(&xfer.aToken[3]))!=0 ){ + @ pragma ci-lock-fail %F(zLogin) %lld(mtime) + } + seenFault = 1; + } + } + db_finalize(&q); + if( !seenFault ){ + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "REPLACE INTO config(name,value,mtime)" + "VALUES('ci-lock-%q',json_object('login',%Q,'clientid',%Q),now())", + blob_str(&xfer.aToken[2]), g.zLogin, + blob_str(&xfer.aToken[3]) + ); + db_protect_pop(); + } + if( db_get_boolean("forbid-delta-manifests",0) ){ + @ pragma avoid-delta-manifests + } + }else + + /* pragma ci-unlock CLIENT-ID + ** + ** Remove any locks previously held by CLIENT-ID. Clients send this + ** pragma with their own ID whenever they know that they no longer + ** have any commits pending. + */ + if( blob_eq(&xfer.aToken[1], "ci-unlock") + && xfer.nToken==3 + && blob_is_hname(&xfer.aToken[2]) + ){ + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "DELETE FROM config" + " WHERE name GLOB 'ci-lock-*'" + " AND (NOT json_valid(value) OR value->>'clientid'==%Q)", + blob_str(&xfer.aToken[2]) + ); + db_protect_pop(); + }else + + /* pragma client-url URL + ** + ** This pragma is an informational notification to the server that + ** their relationship could, in theory, be inverted by having the + ** server call the client at URL. + */ + if( blob_eq(&xfer.aToken[1], "client-url") + && xfer.nToken==3 + && g.perm.Write + ){ + xfer_syncwith(blob_str(&xfer.aToken[2]), 1); + }else + + /* pragma req-links + ** + ** The client sends this message to the server to ask the server + ** to tell it about alternative repositories in the reply. + */ + if( blob_eq(&xfer.aToken[1], "req-links") ){ + bSendLinks = 1; + }else + + /* pragma req-clusters + ** + ** This pragma requests that the server send igot cards for every + ** cluster artifact that it knows about. + */ + if( blob_eq(&xfer.aToken[1], "req-clusters") ){ + send_all_clusters(&xfer); + } + + }else + + /* Unknown message + */ + { + cgi_reset_content(); + @ error bad\scommand:\s%F(blob_str(&xfer.line)) + } + blobarray_reset(xfer.aToken, xfer.nToken); + blob_reset(&xfer.line); + } + if( isPush ){ + if( rc==TH_OK ){ + rc = xfer_run_script(zScript, zUuidList, 1); + if( rc==TH_ERROR ){ + cgi_reset_content(); + @ error push\sscript\sfailed:\s%F(g.zErrMsg) + nErr++; + } + } + request_phantoms(&xfer, 500); + } + if( zUuidList ){ + Th_Free(g.interp, zUuidList); + } + 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 repository. 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); + if( xfer.syncPrivate ) send_private(&xfer); + }else if( isPull ){ + create_cluster(); + send_unclustered(&xfer); + if( xfer.syncPrivate ) send_private(&xfer); + } + hook_expecting_more_artifacts(xfer.nGimmeSent?60:0); + db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); + manifest_crosslink_end(MC_PERMIT_HOOKS); + + /* Send URLs for alternative repositories for the same project, + ** if requested by the client. */ + if( bSendLinks && g.zBaseURL ){ + Stmt q; + db_prepare(&q, + "WITH remote(mtime, url, arg) AS (\n" + " SELECT mtime, substr(name,10), '{}' FROM config\n" + " WHERE name GLOB 'syncwith:http*'\n" + " UNION ALL\n" + " SELECT mtime, substr(name,10), '{}' FROM config\n" + " WHERE name GLOB 'syncfrom:http*'\n" + " UNION ALL\n" + " SELECT mtime, substr(name,9), '{\"type\":\"git\"}' FROM config\n" + " WHERE name GLOB 'gitpush:*'\n" + ")\n" + "SELECT url, json_insert(arg,'$.src',%Q), max(mtime)\n" + " FROM remote WHERE mtime>unixepoch('now','-1 month')\n" + " GROUP BY url;", + g.zBaseURL + ); + while( db_step(&q)==SQLITE_ROW ){ + UrlData x; + const char *zUrl = db_column_text(&q, 0); + const char *zArg = db_column_text(&q, 1); + i64 iMtime = db_column_int64(&q, 2); + memset(&x, 0, sizeof(x)); + url_parse_local(zUrl, URL_OMIT_USER, &x); + if( x.name!=0 && sqlite3_strlike("%localhost%", x.name, 0)!=0 ){ + @ pragma link %F(x.canonical) %F(zArg) %lld(iMtime) + } + url_unparse(&x); + } + db_finalize(&q); + } + + /* Send the server timestamp last, in case prior processing happened + ** to use up a significant fraction of our time window. + */ + zNow = db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%S', 'now')"); + @ # timestamp %s(zNow) errors %d(nErr) + fossil_free(zNow); + + db_commit_transaction(); + configure_rebuild(); +} + +/* +** COMMAND: test-xfer +** +** Usage: %fossil test-xfer ?OPTIONS? XFERFILE +** +** Pass the sync-protocol input file XFERFILE into the server-side sync +** protocol handler. Generate a reply on standard output. +** +** This command was original created to help debug the server side of +** sync messages. The XFERFILE is the uncompressed content of an +** "xfer" HTTP request from client to server. This command interprets +** that message and generates the content of an HTTP reply (without any +** encoding and without the HTTP reply headers) and writes that reply +** on standard output. +** +** One possible usages scenario is to capture some XFERFILE examples +** using a command like: +** +** fossil push http://bogus/ --httptrace +** +** The complete HTTP requests are stored in files named "http-request-N.txt". +** Find one of those requests, remove the HTTP header, and make other edits +** as necessary to generate an appropriate XFERFILE test case. Then run: +** +** fossil test-xfer xferfile.txt +** +** Options: +** --host HOSTNAME Supply a server hostname used to populate +** g.zBaseURL and similar. +*/ +void cmd_test_xfer(void){ + const char *zHost; + db_find_and_open_repository(0,0); + zHost = find_option("host",0,1); + verify_all_options(); + if( g.argc!=2 && g.argc!=3 ){ + usage("?MESSAGEFILE?"); + } + if( zHost==0 ) zHost = "localhost:8080"; + g.zBaseURL = mprintf("http://%s", zHost); + g.zHttpsURL = mprintf("https://%s", zHost); + g.zTop = mprintf(""); + blob_zero(&g.cgiIn); + blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2], ExtFILE); + disableLogin = 1; + page_xfer(); + fossil_print("%s", cgi_extract_content()); +} + +/* +** 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"; +static const char zBriefFormat[] = + "Round-trips: %d Artifacts sent: %d received: %d\r"; + +#if INTERFACE +/* +** Flag options for controlling client_sync() +*/ +#define SYNC_PUSH 0x00001 /* push content client to server */ +#define SYNC_PULL 0x00002 /* pull content server to client */ +#define SYNC_CLONE 0x00004 /* clone the repository */ +#define SYNC_PRIVATE 0x00008 /* Also transfer private content */ +#define SYNC_VERBOSE 0x00010 /* Extra diagnostics */ +#define SYNC_RESYNC 0x00020 /* --verily */ +#define SYNC_FROMPARENT 0x00040 /* Pull from the parent project */ +#define SYNC_UNVERSIONED 0x00100 /* Sync unversioned content */ +#define SYNC_UV_REVERT 0x00200 /* Copy server unversioned to client */ +#define SYNC_UV_TRACE 0x00400 /* Describe UV activities */ +#define SYNC_UV_DRYRUN 0x00800 /* Do not actually exchange files */ +#define SYNC_IFABLE 0x01000 /* Inability to sync is not fatal */ +#define SYNC_CKIN_LOCK 0x02000 /* Lock the current check-in */ +#define SYNC_NOHTTPCOMPRESS 0x04000 /* Do not compression HTTP messages */ +#define SYNC_ALLURL 0x08000 /* The --all flag - sync to all URLs */ +#define SYNC_SHARE_LINKS 0x10000 /* Request alternate repo links */ +#define SYNC_XVERBOSE 0x20000 /* Extra verbose. Network traffic */ +#define SYNC_PING 0x40000 /* Verify server is alive */ +#define SYNC_QUIET 0x80000 /* No output */ +#endif + +/* +** Floating-point absolute value +*/ +static double fossil_fabs(double x){ + return x>0.0 ? x : -x; +} + +/* +** Sync to the host identified in g.url.name and g.url.path. 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. +*/ +int client_sync( + unsigned syncFlags, /* Mask of SYNC_* flags */ + unsigned configRcvMask, /* Receive these configuration items */ + unsigned configSendMask, /* Send these configuration items */ + const char *zAltPCode, /* Alternative project code (usually NULL) */ + int *pnRcvd /* Set to # received artifacts, if not NULL */ +){ + 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 or uvfile */ + int origConfigRcvMask; /* Original value of configRcvMask */ + int nFileRecv = 0; /* Number of files received */ + int mxPhantomReq = 200; /* Max number of phantoms to request per comm */ + const char *zCookie; /* Server cookie */ + i64 nUncSent, nUncRcvd; /* Bytes sent and received (before compression) */ + i64 nSent, nRcvd; /* Bytes sent and received (after compression) */ + int cloneSeqno = 1; /* Sequence number for clones */ + Blob send; /* Text we are sending to the server */ + Blob recv; /* Reply we got back from the server */ + Xfer xfer; /* Transfer data */ + int pctDone; /* Percentage done with a message */ + int lastPctDone = -1; /* Last displayed pctDone */ + double rArrivalTime; /* Time at which a message arrived */ + const char *zSCode = db_get("server-code", "x"); + const char *zPCode = db_get("project-code", 0); + int nErr = 0; /* Number of errors */ + int nRoundtrip= 0; /* Number of HTTP requests */ + int nArtifactSent = 0; /* Total artifacts sent */ + int nArtifactRcvd = 0; /* Total artifacts received */ + int nPriorArtifact = 0; /* Artifacts received on prior round-trips */ + const char *zOpType = 0;/* Push, Pull, Sync, Clone */ + double rSkew = 0.0; /* Maximum time skew */ + int uvHashSent = 0; /* The "pragma uv-hash" message has been sent */ + int uvDoPush = 0; /* Generate uvfile messages to send to server */ + int uvPullOnly = 0; /* 1: pull-only. 2: pull-only warning issued */ + int nUvGimmeSent = 0; /* Number of uvgimme cards sent on this cycle */ + int nUvFileRcvd = 0; /* Number of uvfile cards received on this cycle */ + int nGimmeRcvd = 0; /* Number of gimme cards recevied on the prev cycle */ + sqlite3_int64 mtime; /* Modification time on a UV file */ + int autopushFailed = 0; /* Autopush following commit failed if true */ + const char *zCkinLock; /* Name of check-in to lock. NULL for none */ + const char *zClientId; /* A unique identifier for this check-out */ + unsigned int mHttpFlags;/* Flags for the http_exchange() subsystem */ + const int bOutIsTty = fossil_isatty(fossil_fileno(stdout)); + + if( pnRcvd ) *pnRcvd = 0; + if( db_get_boolean("dont-push", 0) ) syncFlags &= ~SYNC_PUSH; + if( (syncFlags & (SYNC_PUSH|SYNC_PULL|SYNC_CLONE|SYNC_UNVERSIONED|SYNC_PING)) + ==0 + && configRcvMask==0 + && configSendMask==0 + ){ + return 0; /* Nothing to do */ + } + + /* Compute an appropriate project code. zPCode is the project code + ** for the local repository. zAltPCode will usually be NULL, but might + ** also be an alternative project code to expect on the server. When + ** zAltPCode is not NULL, that means we are doing a cross-project import - + ** in other words, reading content from one project into a different + ** project. + */ + if( syncFlags & SYNC_FROMPARENT ){ + const char *zPX; + configRcvMask = 0; + configSendMask = 0; + syncFlags &= ~(SYNC_PUSH); + zPX = db_get("parent-project-code", 0); + if( zPX==0 || db_get("parent-project-name",0)==0 ){ + fossil_fatal("there is no parent project: set the 'parent-project-code'" + " and 'parent-project-name' config parameters in order" + " to pull from a parent project"); + } + if( zPX ){ + zAltPCode = zPX; + } + } + if( zAltPCode!=0 && zPCode!=0 && sqlite3_stricmp(zPCode, zAltPCode)==0 ){ + zAltPCode = 0; + } + + 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); + xfer.maxTime = -1; + xfer.remoteVersion = RELEASE_VERSION_NUMBER; + if( syncFlags & SYNC_PRIVATE ){ + g.perm.Private = 1; + xfer.syncPrivate = 1; + } + + blobarray_zero(xfer.aToken, count(xfer.aToken)); + blob_zero(&send); + blob_zero(&recv); + blob_zero(&xfer.err); + blob_zero(&xfer.line); + origConfigRcvMask = 0; + nUncSent = nUncRcvd = 0; + + /* Send the send-private pragma if we are trying to sync private data */ + if( syncFlags & SYNC_PRIVATE ){ + blob_append(&send, "pragma send-private\n", -1); + } + + /* Figure out which check-in to lock */ + if( syncFlags & SYNC_CKIN_LOCK ){ + int vid = db_lget_int("checkout",0); + zCkinLock = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid); + }else{ + zCkinLock = 0; + } + zClientId = g.localOpen ? db_lget("client-id", 0) : 0; + + /* When syncing unversioned files, create a TEMP table in which to store + ** the names of files that need to be sent from client to server. + ** + ** The initial assumption is that all unversioned files need to be sent + ** to the other side. But "uvigot" cards received back from the remote + ** side will normally cause many of these entries to be removed since they + ** do not really need to be sent. + */ + if( (syncFlags & (SYNC_UNVERSIONED|SYNC_CLONE))!=0 ){ + unversioned_schema(); + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS uv_tosend(" + " name TEXT PRIMARY KEY," /* Name of file to send client->server */ + " mtimeOnly BOOLEAN" /* True to only send mtime, not content */ + ") WITHOUT ROWID;" + "REPLACE INTO uv_tosend(name,mtimeOnly)" + " SELECT name, 0 FROM unversioned WHERE hash IS NOT NULL;" + ); + } + + /* + ** The request from the client always begin with a clone, pull, + ** or push message. + */ + blob_appendf(&send, "pragma client-version %d %d %d\n", + RELEASE_VERSION_NUMBER, MANIFEST_NUMERIC_DATE, + MANIFEST_NUMERIC_TIME); + if( syncFlags & SYNC_CLONE ){ + blob_appendf(&send, "clone 3 %d\n", cloneSeqno); + syncFlags &= ~(SYNC_PUSH|SYNC_PULL); + nCardSent++; + /* TBD: Request all transferable configuration values */ + content_enable_dephantomize(0); + zOpType = "Clone"; + }else if( syncFlags & SYNC_PULL ){ + blob_appendf(&send, "pull %s %s\n", zSCode, + zAltPCode ? zAltPCode : zPCode); + nCardSent++; + zOpType = (syncFlags & SYNC_PUSH)?"Sync":"Pull"; + if( (syncFlags & SYNC_RESYNC)!=0 && nCycle<2 ){ + blob_appendf(&send, "pragma send-catalog\n"); + nCardSent++; + } + } + if( syncFlags & SYNC_PUSH ){ + blob_appendf(&send, "push %s %s\n", zSCode, zPCode); + nCardSent++; + if( (syncFlags & SYNC_PULL)==0 ) zOpType = "Push"; + if( (syncFlags & SYNC_RESYNC)!=0 ) xfer.resync = 0x7fffffff; + } + if( syncFlags & SYNC_VERBOSE ){ + fossil_print(zLabelFormat /*works-like:"%s%s%s%s%d"*/, + "", "Bytes", "Cards", "Artifacts", "Deltas"); + } + + /* Send the client-url pragma on the first cycle if the client has + ** a known public url. + */ + if( zAltPCode==0 ){ + const char *zSelfUrl = public_url(); + if( zSelfUrl ){ + blob_appendf(&send, "pragma client-url %s\n", zSelfUrl); + } + } + + /* Request URLs of alternative repositories + */ + if( zAltPCode==0 && (syncFlags & SYNC_SHARE_LINKS)!=0 ){ + blob_appendf(&send, "pragma req-links\n"); + } + + while( go ){ + int newPhantom = 0; + char *zRandomness; + db_begin_transaction(); + db_record_repository_filename(0); + db_multi_exec( + "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" + "CREATE TEMP TABLE unk(uuid TEXT PRIMARY KEY) WITHOUT ROWID;" + ); + manifest_crosslink_begin(); + + + /* Client sends the most recently received cookie back to the server. + ** 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); + } + + /* Client sends gimme cards for phantoms + */ + if( (syncFlags & SYNC_PULL)!=0 + || ((syncFlags & SYNC_CLONE)!=0 && cloneSeqno==1) + ){ + request_phantoms(&xfer, mxPhantomReq); + if( xfer.nGimmeSent>0 && nCycle==2 && (syncFlags & SYNC_PULL)!=0 ){ + blob_appendf(&send, "pragma req-clusters\n"); + } + } + if( syncFlags & SYNC_PUSH ){ + send_unsent(&xfer); + nCardSent += send_unclustered(&xfer); + if( syncFlags & SYNC_PRIVATE ) send_private(&xfer); + if( nGimmeRcvd>0 && nCycle==2 ){ + send_all_clusters(&xfer); + } + } + + /* Client sends configuration parameter requests. On a clone, delay sending + ** this until the second cycle since the login card might fail on + ** the first cycle. + */ + if( configRcvMask && ((syncFlags & SYNC_CLONE)==0 || nCycle>0) ){ + const char *zName; + if( zOpType==0 ) zOpType = "Pull"; + zName = configure_first_name(configRcvMask); + while( zName ){ + blob_appendf(&send, "reqconfig %s\n", zName); + zName = configure_next_name(configRcvMask); + nCardSent++; + } + origConfigRcvMask = configRcvMask; + configRcvMask = 0; + } + + /* Client sends a request to sync unversioned files. + ** On a clone, delay sending this until the second cycle since + ** the login card might fail on the first cycle. + */ + if( (syncFlags & SYNC_UNVERSIONED)!=0 + && ((syncFlags & SYNC_CLONE)==0 || nCycle>0) + && !uvHashSent + ){ + blob_appendf(&send, "pragma uv-hash %s\n", unversioned_content_hash(0)); + nCardSent++; + uvHashSent = 1; + } + + /* On a "fossil config push", the client send configuration parameters + ** being pushed up to the server */ + if( configSendMask ){ + if( zOpType==0 ) zOpType = "Push"; + nCardSent += configure_send_group(xfer.pOut, configSendMask, 0); + configSendMask = 0; + } + + /* Send unversioned files present here on the client but missing or + ** obsolete on the server. + ** + ** Or, if the SYNC_UV_REVERT flag is set, delete the local unversioned + ** files that do not exist on the server. + ** + ** This happens on the second exchange, since we do not know what files + ** need to be sent until after the uvigot cards from the first exchange + ** have been processed. + */ + if( uvDoPush ){ + assert( (syncFlags & SYNC_UNVERSIONED)!=0 ); + if( syncFlags & SYNC_UV_DRYRUN ){ + uvDoPush = 0; + }else if( syncFlags & SYNC_UV_REVERT ){ + db_multi_exec( + "DELETE FROM unversioned" + " WHERE name IN (SELECT name FROM uv_tosend);" + "DELETE FROM uv_tosend;" + ); + uvDoPush = 0; + }else{ + Stmt uvq; + int rc = SQLITE_OK; + db_prepare(&uvq, "SELECT name, mtimeOnly FROM uv_tosend"); + while( (rc = db_step(&uvq))==SQLITE_ROW ){ + const char *zName = db_column_text(&uvq, 0); + send_unversioned_file(&xfer, zName, db_column_int(&uvq,1)); + nCardSent++; + nArtifactSent++; + db_multi_exec("DELETE FROM uv_tosend WHERE name=%Q", zName); + if( syncFlags & SYNC_VERBOSE ){ + fossil_print("\rUnversioned-file sent: %s\n", zName); + } + if( (int)blob_size(xfer.pOut)>xfer.mxSend ) break; + } + db_finalize(&uvq); + if( rc==SQLITE_DONE ) uvDoPush = 0; + } + } + + /* Lock the current check-out */ + if( zCkinLock ){ + if( zClientId==0 ){ + zClientId = db_text(0, "SELECT lower(hex(randomblob(20)))"); + db_lset("client-id", zClientId); + } + blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); + zCkinLock = 0; + }else if( zClientId ){ + blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); + } + /* Append randomness to the end of the uplink message. This makes all + ** messages unique so that the login-card nonce will always + ** be unique. + */ + zRandomness = db_text(0, "SELECT hex(randomblob(20))"); + blob_appendf(&send, "# %s\n", zRandomness); + fossil_free(zRandomness); + + if( (syncFlags & SYNC_VERBOSE)!=0 + && (syncFlags & SYNC_XVERBOSE)==0 + ){ + fossil_print("waiting for server..."); + } + fflush(stdout); + /* Exchange messages with the server */ + if( (syncFlags & SYNC_CLONE)!=0 && nCycle==0 ){ + /* Do not send a login card on the first round-trip of a clone */ + mHttpFlags = 0; + }else{ + mHttpFlags = HTTP_USE_LOGIN; + } + if( syncFlags & SYNC_NOHTTPCOMPRESS ){ + mHttpFlags |= HTTP_NOCOMPRESS; + } + if( syncFlags & SYNC_XVERBOSE ){ + mHttpFlags |= HTTP_VERBOSE; + } + if( syncFlags & SYNC_QUIET ){ + mHttpFlags |= HTTP_QUIET; + } + + /* Do the round-trip to the server */ + if( http_exchange(&send, &recv, mHttpFlags, MAX_REDIRECTS, 0) ){ + nErr++; + go = 2; + break; + } + + /* Remember the URL of the sync target in the config file on the + ** first successful round-trip */ + if( nCycle==0 && db_is_writeable("repository") ){ + xfer_syncwith(g.url.canonical, 0); + } + + /* Output current stats */ + nRoundtrip++; + nArtifactSent += xfer.nFileSent + xfer.nDeltaSent; + if( syncFlags & SYNC_VERBOSE ){ + fossil_print(zValueFormat /*works-like:"%s%d%d%d%d"*/, "Sent:", + blob_size(&send), nCardSent+xfer.nGimmeSent+xfer.nIGotSent, + xfer.nFileSent, xfer.nDeltaSent); + }else if( syncFlags & SYNC_QUIET ){ + /* No-op */ + }else{ + if( bOutIsTty!=0 ){ + fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, + nRoundtrip, nArtifactSent, nArtifactRcvd); + } + } + nCardSent = 0; + nCardRcvd = 0; + xfer.nFileSent = 0; + xfer.nDeltaSent = 0; + xfer.nGimmeSent = 0; + xfer.nIGotSent = 0; + xfer.nPrivIGot = 0; + + lastPctDone = -1; + nUncSent += blob_size(&send); + blob_reset(&send); + blob_appendf(&send, "pragma client-version %d %d %d\n", + RELEASE_VERSION_NUMBER, MANIFEST_NUMERIC_DATE, + MANIFEST_NUMERIC_TIME); + rArrivalTime = db_double(0.0, "SELECT julianday('now')"); + + /* Send the send-private pragma if we are trying to sync private data */ + if( syncFlags & SYNC_PRIVATE ){ + blob_append(&send, "pragma send-private\n", -1); + } + + /* Begin constructing the next message (which might never be + ** sent) by beginning with the pull or push cards + */ + if( syncFlags & SYNC_PULL ){ + blob_appendf(&send, "pull %s %s\n", zSCode, + zAltPCode ? zAltPCode : zPCode); + nCardSent++; + } + if( syncFlags & SYNC_PUSH ){ + blob_appendf(&send, "push %s %s\n", zSCode, zPCode); + nCardSent++; + } + go = 0; + nUvGimmeSent = 0; + nUvFileRcvd = 0; + nGimmeRcvd = 0; + nPriorArtifact = nArtifactRcvd; + + /* Process the reply that came back from the server */ + while( blob_line(&recv, &xfer.line) ){ + if( blob_buffer(&xfer.line)[0]=='#' ){ + const char *zLine = blob_buffer(&xfer.line); + if( memcmp(zLine, "# timestamp ", 12)==0 ){ + char zTime[20]; + double rDiff; + sqlite3_snprintf(sizeof(zTime), zTime, "%.19s", &zLine[12]); + rDiff = db_double(9e99, "SELECT julianday('%q') - %.17g", + zTime, rArrivalTime); + if( rDiff>9e98 || rDiff<-9e98 ) rDiff = 0.0; + if( rDiff*24.0*3600.0 >= -(blob_size(&recv)/5000.0 + 20) ){ + rDiff = 0.0; + } + if( fossil_fabs(rDiff)>fossil_fabs(rSkew) ) rSkew = rDiff; + } + nCardRcvd++; + continue; + } + xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); + nCardRcvd++; + if( (syncFlags & SYNC_VERBOSE)!=0 && recv.nUsed>0 ){ + pctDone = (recv.iCursor*100)/recv.nUsed; + if( pctDone!=lastPctDone ){ + fossil_print("\rprocessed: %d%% ", pctDone); + lastPctDone = pctDone; + fflush(stdout); + } + } + + /* file HASH SIZE \n CONTENT + ** file HASH DELTASRC SIZE \n CONTENT + ** + ** Client receives a file transmitted from the server. + */ + if( blob_eq(&xfer.aToken[0],"file") ){ + xfer_accept_file(&xfer, (syncFlags & SYNC_CLONE)!=0, 0, 0); + nArtifactRcvd++; + }else + + /* cfile HASH USIZE CSIZE \n CONTENT + ** cfile HASH DELTASRC USIZE CSIZE \n CONTENT + ** + ** Client receives a compressed file transmitted from the server. + */ + if( blob_eq(&xfer.aToken[0],"cfile") ){ + xfer_accept_compressed_file(&xfer, 0, 0); + nArtifactRcvd++; + }else + + /* uvfile NAME MTIME HASH SIZE FLAGS \n CONTENT + ** + ** Client accepts an unversioned file from the server. + */ + if( blob_eq(&xfer.aToken[0], "uvfile") ){ + xfer_accept_unversioned_file(&xfer, 1); + nArtifactRcvd++; + nUvFileRcvd++; + if( syncFlags & SYNC_VERBOSE ){ + fossil_print("\rUnversioned-file received: %s\n", + blob_str(&xfer.aToken[1])); + } + }else + + /* gimme HASH + ** + ** Client receives an artifact request from the server. + ** If the file is a manifest, assume that the server will also want + ** to know all of the content artifacts associated with the manifest + ** and send those too. + */ + if( blob_eq(&xfer.aToken[0], "gimme") + && xfer.nToken==2 + && blob_is_hname(&xfer.aToken[1]) + ){ + remote_unk(&xfer.aToken[1]); + if( syncFlags & SYNC_PUSH ){ + int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); + if( rid ){ + send_file(&xfer, rid, &xfer.aToken[1], 0); + nGimmeRcvd++; + } + } + }else + + /* igot HASH ?PRIVATEFLAG? + ** + ** 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 the PRIVATE argument exists and is 1, then the file is + ** private. Pretend it does not exists if we are not pulling + ** private files. + */ + if( xfer.nToken>=2 + && blob_eq(&xfer.aToken[0], "igot") + && blob_is_hname(&xfer.aToken[1]) + ){ + int rid; + int isPriv = xfer.nToken>=3 && blob_eq(&xfer.aToken[2],"1"); + rid = rid_from_uuid(&xfer.aToken[1], 0, 0); + if( rid>0 ){ + if( isPriv ){ + content_make_private(rid); + }else{ + content_make_public(rid); + } + }else if( isPriv && !g.perm.Private ){ + /* ignore private files */ + }else if( (syncFlags & (SYNC_PULL|SYNC_CLONE))!=0 ){ + rid = content_new(blob_str(&xfer.aToken[1]), isPriv); + if( rid ) newPhantom = 1; + } + remote_has(rid); + }else + + /* uvigot NAME MTIME HASH SIZE + ** + ** Server announces that it has a particular unversioned file. The + ** server will only send this card if the client had previously sent + ** a "pragma uv-hash" card with a hash that does not match. + ** + ** If the identified file needs to be transferred, then setup for the + ** transfer. Generate a "uvgimme" card in the reply if the server + ** version is newer than the client. Generate a "uvfile" card if + ** the client version is newer than the server. If HASH is "-" + ** (indicating that the file has been deleted) and MTIME is newer, + ** then do the deletion. + */ + if( xfer.nToken==5 + && blob_eq(&xfer.aToken[0], "uvigot") + && blob_is_filename(&xfer.aToken[1]) + && blob_is_int64(&xfer.aToken[2], &mtime) + && blob_is_int(&xfer.aToken[4], &size) + && (blob_eq(&xfer.aToken[3],"-") || blob_is_hname(&xfer.aToken[3])) + ){ + const char *zName = blob_str(&xfer.aToken[1]); + const char *zHash = blob_str(&xfer.aToken[3]); + int iStatus; + if( mtime<0 || size<0 ){ + xfer_fatal_error("invalid uvigot"); + return ++nErr; + } + iStatus = unversioned_status(zName, mtime, zHash); + if( (syncFlags & SYNC_UV_REVERT)!=0 ){ + if( iStatus==4 ) iStatus = 2; + if( iStatus==5 ) iStatus = 1; + } + if( syncFlags & (SYNC_UV_TRACE|SYNC_UV_DRYRUN) ){ + const char *zMsg = 0; + switch( iStatus ){ + case 0: + case 1: zMsg = "UV-PULL"; break; + case 2: zMsg = "UV-PULL-MTIME-ONLY"; break; + case 4: zMsg = "UV-PUSH-MTIME-ONLY"; break; + case 5: zMsg = "UV-PUSH"; break; + } + if( zMsg ) fossil_print("\r%s: %s\n", zMsg, zName); + if( syncFlags & SYNC_UV_DRYRUN ){ + iStatus = 99; /* Prevent any changes or reply messages */ + } + } + if( iStatus<=1 ){ + if( zHash[0]!='-' ){ + blob_appendf(xfer.pOut, "uvgimme %s\n", zName); + nCardSent++; + nUvGimmeSent++; + db_multi_exec("DELETE FROM unversioned WHERE name=%Q", zName); + }else if( iStatus==1 ){ + db_multi_exec( + "UPDATE unversioned" + " SET mtime=%lld, hash=NULL, sz=0, encoding=0, content=NULL" + " WHERE name=%Q", mtime, zName + ); + db_unset("uv-hash", 0); + } + }else if( iStatus==2 ){ + db_multi_exec( + "UPDATE unversioned SET mtime=%lld WHERE name=%Q", mtime, zName + ); + db_unset("uv-hash", 0); + } + if( iStatus>=4 && uvPullOnly==1 ){ + fossil_warning( + "Warning: uv-pull-only \n" + " Unable to push unversioned content because you lack\n" + " sufficient permission on the server\n" + ); + uvPullOnly = 2; + } + if( iStatus<=3 || uvPullOnly ){ + db_multi_exec("DELETE FROM uv_tosend WHERE name=%Q", zName); + }else if( iStatus==4 ){ + db_multi_exec("UPDATE uv_tosend SET mtimeOnly=1 WHERE name=%Q",zName); + }else if( iStatus==5 ){ + db_multi_exec("REPLACE INTO uv_tosend(name,mtimeOnly) VALUES(%Q,0)", + zName); + } + }else + + /* push SERVERCODE PRODUCTCODE + ** + ** Should only happen in response to a clone. This message tells + ** the client what product code to use for the new database. + */ + if( blob_eq(&xfer.aToken[0],"push") + && xfer.nToken==3 + && (syncFlags & SYNC_CLONE)!=0 + && blob_is_hname(&xfer.aToken[2]) + ){ + if( zPCode==0 ){ + zPCode = mprintf("%b", &xfer.aToken[2]); + db_set("project-code", zPCode, 0); + } + if( cloneSeqno>0 ) blob_appendf(&send, "clone 3 %d\n", cloneSeqno); + nCardSent++; + }else + + /* config NAME SIZE \n CONTENT + ** + ** Client receive a configuration value from the server. + ** + ** The received configuration setting is silently ignored if it was + ** not requested by a prior "reqconfig" sent from client to 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; + if( size<0 ){ + xfer_fatal_error("invalid config record"); + return ++nErr; + } + blob_zero(&content); + blob_extract(xfer.pIn, size, &content); + g.perm.Admin = g.perm.RdAddr = 1; + configure_receive(zName, &content, origConfigRcvMask); + nCardRcvd++; + nArtifactRcvd++; + blob_reset(&content); + blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR); + }else + + + /* cookie TEXT + ** + ** The client reserves a cookie from the server. 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 + + + /* private + ** + ** The server tells the client that the next "file" or "cfile" will + ** contain private content. + */ + if( blob_eq(&xfer.aToken[0], "private") ){ + xfer.nextIsPrivate = 1; + }else + + + /* clone_seqno N + ** + ** When doing a clone, the server tries to send all of its artifacts + ** in sequence. This card indicates the sequence number of the next + ** blob that needs to be sent. If N<=0 that indicates that all blobs + ** have been sent. + */ + if( blob_eq(&xfer.aToken[0], "clone_seqno") && xfer.nToken==2 ){ + blob_is_int(&xfer.aToken[1], &cloneSeqno); + if( cloneSeqno<0 ){ + xfer_fatal_error("invalid clone_seqno"); + return ++nErr; + } + }else + + /* message MESSAGE + ** + ** A message is received from the server. Print it. + ** 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); + if( (syncFlags & SYNC_PUSH) && zMsg + && sqlite3_strglob("pull only *", zMsg)==0 ){ + syncFlags &= ~SYNC_PUSH; + zMsg = 0; + } + if( zMsg && zMsg[0] ){ + fossil_force_newline(); + fossil_print("Server says: %s\n", zMsg); + } + }else + + /* pragma NAME VALUE... + ** + ** The server can send pragmas to try to convey meta-information to + ** the client. These are informational only. Unknown pragmas are + ** silently ignored. + */ + if( blob_eq(&xfer.aToken[0], "pragma") && xfer.nToken>=2 ){ + /* pragma server-version VERSION ?DATE? ?TIME? + ** + ** The server announces to the server what version of Fossil it + ** is running. The DATE and TIME are a pure numeric ISO8601 time + ** for the specific check-in of the client. + */ + if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ + xfer.remoteVersion = g.syncInfo.remoteVersion = + atoi(blob_str(&xfer.aToken[2])); + if( xfer.nToken>=5 ){ + xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); + xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); + } + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, + xfer.remoteTime, 0x08 ); + } + + /* pragma uv-pull-only + ** pragma uv-push-ok + ** + ** If the server is unwilling to accept new unversioned content (because + ** this client lacks the necessary permissions) then it sends a + ** "uv-pull-only" pragma so that the client will know not to waste + ** bandwidth trying to upload unversioned content. If the server + ** does accept new unversioned content, it sends "uv-push-ok". + */ + else if( syncFlags & SYNC_UNVERSIONED ){ + if( blob_eq(&xfer.aToken[1], "uv-pull-only") ){ + uvPullOnly = 1; + if( syncFlags & SYNC_UV_REVERT ) uvDoPush = 1; + }else if( blob_eq(&xfer.aToken[1], "uv-push-ok") ){ + uvDoPush = 1; + } + } + + /* pragma ci-lock-fail USER-HOLDING-LOCK LOCK-TIME + ** + ** The server generates this message when a "pragma ci-lock" + ** is attempted on a check-in for which there is an existing + ** lock. USER-HOLDING-LOCK is the name of the user who originated + ** the lock, and LOCK-TIME is the timestamp (seconds since 1970) + ** when the lock was taken. + */ + else if( blob_eq(&xfer.aToken[1], "ci-lock-fail") && xfer.nToken==4 ){ + char *zUser = blob_terminate(&xfer.aToken[2]); + sqlite3_int64 mtime, iNow; + defossilize(zUser); + iNow = time(NULL); + if( blob_is_int64(&xfer.aToken[3], &mtime) && iNow>mtime ){ + if( mtime<0 ){ + xfer_fatal_error("invalid ci-lock-fail time"); + return ++nErr; + } + iNow = time(NULL); + fossil_print("\nParent check-in locked by %s %s ago\n", + zUser, human_readable_age((iNow+1-mtime)/86400.0)); + }else{ + fossil_print("\nParent check-in locked by %s\n", zUser); + } + g.ckinLockFail = fossil_strdup(zUser); + } + + /* pragma avoid-delta-manifests + ** + ** Discourage the use of delta manifests. The remote side sends + ** this pragma when its forbid-delta-manifests setting is true. + */ + else if( blob_eq(&xfer.aToken[1], "avoid-delta-manifests") ){ + g.bAvoidDeltaManifests = 1; + } + + /* pragma link URL ARG MTIME + ** + ** The server has sent the URL for a link to another repository. + ** Record this as a link:URL entry in the config table. + */ + else if( blob_eq(&xfer.aToken[1], "link") + && xfer.nToken==5 + && (syncFlags & SYNC_SHARE_LINKS)!=0 + ){ + UrlData x; + char *zUrl = blob_str(&xfer.aToken[2]); + char *zArg = blob_str(&xfer.aToken[3]); + i64 iTime = strtoll(blob_str(&xfer.aToken[4]),0,0); + memset(&x, 0, sizeof(x)); + defossilize(zUrl); + defossilize(zArg); + url_parse_local(zUrl, URL_OMIT_USER, &x); + if( x.protocol + && strncmp(x.protocol,"http",4)==0 + && iTime>0 + ){ + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "INSERT INTO config(name,value,mtime)\n" + " VALUES('link:%q',%Q,%lld)\n" + " ON CONFLICT DO UPDATE\n" + " SET value=excluded.value, mtime=excluded.mtime\n" + " WHERE mtime<excluded.mtime;", + zUrl, zArg, iTime + ); + db_protect_pop(); + } + url_unparse(&x); + } + + }else + + /* error MESSAGE + ** + ** The server is reporting an error. The client will abandon + ** the sync session. + ** + ** Except, when cloning we will sometimes get an error on the + ** first message exchange because the project-code is unknown + ** and so the login card on the request was invalid. The project-code + ** is returned in the reply before the error card, so second and + ** subsequent messages should be OK. Nevertheless, we need to ignore + ** the error card on the first message of a clone. + ** + ** Also ignore "not authorized to write" errors if this is an + ** autopush following a commit. + */ + if( blob_eq(&xfer.aToken[0],"error") && xfer.nToken==2 ){ + char *zMsg = blob_terminate(&xfer.aToken[1]); + defossilize(zMsg); + if( (syncFlags & SYNC_IFABLE)!=0 + && sqlite3_strlike("%not authorized to write%",zMsg,0)==0 ){ + autopushFailed = 1; + nErr++; + }else if( (syncFlags & SYNC_CLONE)==0 || nCycle>0 ){ + fossil_force_newline(); + fossil_print("Error: %s\n", zMsg); + blob_appendf(&xfer.err, "server says: %s\n", zMsg); + nErr++; + break; + } + }else + + /* Unknown message */ + if( xfer.nToken>0 ){ + if( blob_str(&xfer.aToken[0])[0]=='<' ){ + fossil_warning( + "server replies with HTML instead of fossil sync protocol:\n%b", + &recv + ); + nErr++; + break; + } + blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); + } + + if( blob_size(&xfer.err) ){ + fossil_force_newline(); + fossil_warning("%b", &xfer.err); + nErr++; + break; + } + blobarray_reset(xfer.aToken, xfer.nToken); + blob_reset(&xfer.line); + } + origConfigRcvMask = 0; + if( nCardRcvd>0 && (syncFlags & SYNC_VERBOSE) ){ + fossil_print(zValueFormat /*works-like:"%s%d%d%d%d"*/, "Received:", + blob_size(&recv), nCardRcvd, + xfer.nFileRcvd, xfer.nDeltaRcvd + xfer.nDanglingFile); + }else if( syncFlags & SYNC_QUIET ){ + /* No-op */ + }else{ + if( bOutIsTty!=0 ){ + fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, + nRoundtrip, nArtifactSent, nArtifactRcvd); + } + } + nUncRcvd += blob_size(&recv); + blob_reset(&recv); + nCycle++; + + /* Set go to 1 if we need to continue the sync/push/pull/clone for + ** another round. Set go to 0 if it is time to quit. */ + 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; + }else if( xfer.nFileSent+xfer.nDeltaSent>0 || uvDoPush ){ + /* Go another round if files are queued to send */ + go = 1; + }else if( xfer.nPrivIGot>0 && nCycle==1 ){ + go = 1; + }else if( nUvGimmeSent>0 && (nUvFileRcvd>0 || nCycle<3) ){ + /* Continue looping as long as new uvfile cards are being received + ** and uvgimme cards are being sent. */ + go = 1; + }else if( (syncFlags & SYNC_CLONE)!=0 ){ + if( nCycle==1 ){ + go = 1; /* go at least two rounds on a clone */ + }else if( nFileRecv>0 ){ + go = 1; + }else if( cloneSeqno>0 && nArtifactRcvd>nPriorArtifact ){ + /* Continue the clone until we see the clone_seqno 0" card or + ** until we stop receiving artifacts */ + go = 1; + } + } + + nCardRcvd = 0; + xfer.nFileRcvd = 0; + xfer.nDeltaRcvd = 0; + xfer.nDanglingFile = 0; + db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); + if( go ){ + manifest_crosslink_end(MC_PERMIT_HOOKS); + }else{ + manifest_crosslink_end(MC_PERMIT_HOOKS); + content_enable_dephantomize(1); + } + db_end_transaction(0); + }; /* while(go) */ + transport_stats(&nSent, &nRcvd, 1); + if( pnRcvd ) *pnRcvd = nArtifactRcvd; + if( (rSkew*24.0*3600.0) > 10.0 ){ + fossil_warning("*** time skew *** server is fast by %s", + db_timespan_name(rSkew)); + g.clockSkewSeen = 1; + }else if( rSkew*24.0*3600.0 < -10.0 ){ + fossil_warning("*** time skew *** server is slow by %s", + db_timespan_name(-rSkew)); + g.clockSkewSeen = 1; + } + if( bOutIsTty==0 && (syncFlags & SYNC_QUIET)==0 ){ + fossil_print(zBriefFormat /*works-like:"%d%d%d"*/, + nRoundtrip, nArtifactSent, nArtifactRcvd); + fossil_force_newline(); + } + fossil_force_newline(); + if( g.zHttpCmd==0 ){ + if( syncFlags & SYNC_QUIET ){ + /* no-op */ + }else if( syncFlags & SYNC_VERBOSE ){ + fossil_print( + "%s done, wire bytes sent: %lld received: %lld remote: %s%s\n", + zOpType, nSent, nRcvd, + (g.url.name && g.url.name[0]!='\0') ? g.url.name : "", + (g.zIpAddr && g.zIpAddr[0]!='\0' + && fossil_strcmp(g.zIpAddr, g.url.name)) + ? mprintf(" (%s)", g.zIpAddr) : ""); + }else{ + fossil_print( + "%s done, wire bytes sent: %lld received: %lld remote: %s\n", + zOpType, nSent, nRcvd, g.zIpAddr); + } + } + if( syncFlags & SYNC_VERBOSE ){ + fossil_print( + "Uncompressed payload sent: %lld received: %lld\n", nUncSent, nUncRcvd); + } + blob_reset(&send); + blob_reset(&recv); + transport_close(&g.url); + transport_global_shutdown(&g.url); + if( nErr && go==2 ){ + db_multi_exec("DROP TABLE onremote; DROP TABLE unk;"); + manifest_crosslink_end(MC_PERMIT_HOOKS); + content_enable_dephantomize(1); + db_end_transaction(0); + } + if( nErr && autopushFailed ){ + fossil_warning( + "Warning: The check-in was successful and is saved locally but you\n" + " are not authorized to push the changes back to the server\n" + " at %s", + g.url.canonical + ); + nErr--; + } + if( (syncFlags & SYNC_CLONE)==0 && g.rcvid && fossil_any_has_fork(g.rcvid) ){ + fossil_warning("***** WARNING: a fork has occurred *****\n" + "use \"fossil leaves -multiple\" for more details."); + } + return nErr; +} |
