Index: src/clone.c ================================================================== --- src/clone.c +++ src/clone.c @@ -35,18 +35,21 @@ ** admin user. This can be overridden using the -A|--admin-user ** parameter. ** ** Options: ** -** --admin-user|-A USERNAME +** --admin-user|-A USERNAME Make USERNAME the administrator +** --private Also clone private branches ** */ void clone_cmd(void){ char *zPassword; const char *zDefaultUser; /* Optional name of the default user */ int nErr = 0; + int bPrivate; /* Also clone private branches */ + bPrivate = find_option("private",0,0)!=0; url_proxy_options(); if( g.argc < 4 ){ usage("?OPTIONS? FILE-OR-URL NEW-REPOSITORY"); } db_open_config(0); @@ -94,11 +97,11 @@ "REPLACE INTO config(name,value)" " VALUES('server-code', lower(hex(randomblob(20))));" ); url_enable_proxy(0); g.xlinkClusterOnly = 1; - nErr = client_sync(0,0,1,CONFIGSET_ALL,0); + nErr = client_sync(0,0,1,bPrivate,CONFIGSET_ALL,0); g.xlinkClusterOnly = 0; verify_cancel(); db_end_transaction(0); db_close(1); if( nErr ){ Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -464,13 +464,13 @@ url_parse(zServer); if( g.urlPasswd==0 && zPw ) g.urlPasswd = mprintf("%s", zPw); user_select(); url_enable_proxy("via proxy: "); if( strncmp(zMethod, "push", n)==0 ){ - client_sync(0,0,0,0,mask); + client_sync(0,0,0,0,0,mask); }else{ - client_sync(0,0,0,mask,0); + client_sync(0,0,0,0,mask,0); } }else if( strncmp(zMethod, "reset", n)==0 ){ int mask, i; char *zBackup; Index: src/http_transport.c ================================================================== --- src/http_transport.c +++ src/http_transport.c @@ -302,11 +302,11 @@ if( g.urlIsSsh ){ fprintf(sshOut, "\n\n"); }else if( g.urlIsFile ){ char *zCmd; fclose(transport.pFile); - zCmd = mprintf("\"%s\" http \"%s\" \"%s\" \"%s\" 127.0.0.1", + zCmd = mprintf("\"%s\" http \"%s\" \"%s\" \"%s\" 127.0.0.1 --localauth", fossil_nameofexe(), g.urlName, transport.zOutFile, transport.zInFile ); fossil_system(zCmd); free(zCmd); transport.pFile = fopen(transport.zInFile, "rb"); Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -374,11 +374,11 @@ && db_get_int("localauth",0)==0 && P("HTTPS")==0 ){ uid = db_int(0, "SELECT uid FROM user WHERE cap LIKE '%%s%%'"); g.zLogin = db_text("?", "SELECT login FROM user WHERE uid=%d", uid); - zCap = "s"; + zCap = "sx"; g.noPswd = 1; sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "localhost"); } /* Check the login cookie to see if it matches a known valid user. @@ -535,10 +535,11 @@ case 'w': g.okWrTkt = g.okRdTkt = g.okNewTkt = g.okApndTkt = 1; break; case 'c': g.okApndTkt = 1; break; case 't': g.okTktFmt = 1; break; case 'b': g.okAttach = 1; break; + case 'x': g.okPrivate = 1; break; /* The "u" privileges is a little different. It recursively ** inherits all privileges of the user named "reader" */ case 'u': { if( zUser==0 ){ Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -134,10 +134,11 @@ int okWrTkt; /* w: make changes to tickets via web */ int okAttach; /* b: add attachments */ int okTktFmt; /* t: create new ticket report formats */ int okRdAddr; /* e: read email addresses or other private data */ int okZip; /* z: download zipped artifact via /zip URL */ + int okPrivate; /* x: can send and receive private content */ /* For defense against Cross-site Request Forgery attacks */ char zCsrfToken[12]; /* Value of the anti-CSRF token */ int okCsrf; /* Anti-CSRF token is present and valid */ Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -540,19 +540,20 @@ } } /* ** COMMAND: scrub -** %fossil scrub [--verily] [--force] [REPOSITORY] +** %fossil scrub [--verily] [--force] [--private] [REPOSITORY] ** ** The command removes sensitive information (such as passwords) from a ** repository so that the respository can be sent to an untrusted reader. ** ** By default, only passwords are removed. However, if the --verily option ** is added, then private branches, concealed email addresses, IP ** addresses of correspondents, and similar privacy-sensitive fields -** are also purged. +** are also purged. If the --private option is used, then only private +** branches are removed and all other information is left intact. ** ** This command permanently deletes the scrubbed information. The effects ** of this command are irreversible. Use with caution. ** ** The user is prompted to confirm the scrub unless the --force option @@ -559,10 +560,11 @@ ** is used. */ void scrub_cmd(void){ int bVerily = find_option("verily",0,0)!=0; int bForce = find_option("force", "f", 0)!=0; + int privateOnly = find_option("private",0,0)!=0; int bNeedRebuild = 0; if( g.argc!=2 && g.argc!=3 ) usage("?REPOSITORY?"); if( g.argc==2 ){ db_must_be_within_tree(); }else{ @@ -569,30 +571,37 @@ db_open_repository(g.argv[2]); } if( !bForce ){ Blob ans; blob_zero(&ans); - prompt_user("Scrubbing the repository will permanently remove user\n" - "passwords and other information. Changes cannot be undone.\n" - "Continue (y/N)? ", &ans); + prompt_user("Scrubbing the repository will permanently information.\n" + "Changes cannot be undone. Continue (y/N)? ", &ans); if( blob_str(&ans)[0]!='y' ){ fossil_exit(1); } } db_begin_transaction(); - db_multi_exec( - "UPDATE user SET pw='';" - "DELETE FROM config WHERE name GLOB 'last-sync-*';" - ); - if( bVerily ){ + if( privateOnly || bVerily ){ bNeedRebuild = db_exists("SELECT 1 FROM private"); db_multi_exec( - "DELETE FROM concealed;" - "UPDATE rcvfrom SET ipaddr='unknown';" - "UPDATE user SET photo=NULL, info='';" - "INSERT INTO shun SELECT uuid FROM blob WHERE rid IN private;" + "DELETE FROM blob WHERE rid IN private;" + "DELETE FROM delta WHERE rid IN private;" + "DELETE FROM private;" + ); + } + if( !privateOnly ){ + db_multi_exec( + "UPDATE user SET pw='';" + "DELETE FROM config WHERE name GLOB 'last-sync-*';" ); + if( bVerily ){ + db_multi_exec( + "DELETE FROM concealed;" + "UPDATE rcvfrom SET ipaddr='unknown';" + "UPDATE user SET photo=NULL, info='';" + ); + } } if( !bNeedRebuild ){ db_end_transaction(0); db_multi_exec("VACUUM;"); }else{ Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -122,11 +122,10 @@ @ <th class="usetupListCon" style="text-align: left;">Contact Info</th> @ </tr> db_prepare(&s, "SELECT uid, login, cap, info FROM user ORDER BY login"); while( db_step(&s)==SQLITE_ROW ){ const char *zCap = db_column_text(&s, 2); - if( strstr(zCap, "s") ) zCap = "s"; @ <tr> @ <td class="usetupListUser" style="text-align: right;padding-right: 20px;white-space:nowrap;"> if( g.okAdmin && (zCap[0]!='s' || g.okSetup) ){ @ <a href="setup_uedit?id=%d(db_column_int(&s,0))"> } @@ -188,10 +187,12 @@ @ <tr><td valign="top"><b>v</b></td> @ <td><i>Developer:</i> Inherit privileges of @ user <tt>developer</tt></td></tr> @ <tr><td valign="top"><b>w</b></td> @ <td><i>Write-Tkt:</i> Edit tickets</td></tr> + @ <tr><td valign="top"><b>x</b></td> + @ <td><i>Private:</i> Push and/or pull private branches</td></tr> @ <tr><td valign="top"><b>z</b></td> @ <td><i>Zip download:</i> Download a baseline via the @ <tt>/zip</tt> URL even without @ check<span class="capability">o</span>ut @ and <span class="capability">h</span>istory permissions</td></tr> @@ -243,11 +244,11 @@ */ void user_edit(void){ const char *zId, *zLogin, *zInfo, *zCap, *zPw; char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap; char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae; - char *oat, *oau, *oav, *oab, *oaz; + char *oat, *oau, *oav, *oab, *oax, *oaz; const char *inherit[128]; int doWrite; int uid; int higherUser = 0; /* True if user being edited is SETUP and the */ /* user doing the editing is ADMIN. Disallow editing */ @@ -300,10 +301,11 @@ int ah = P("ah")!=0; int ag = P("ag")!=0; int at = P("at")!=0; int au = P("au")!=0; int av = P("av")!=0; + int ax = P("ax")!=0; int az = P("az")!=0; if( aa ){ zCap[i++] = 'a'; } if( ab ){ zCap[i++] = 'b'; } if( ac ){ zCap[i++] = 'c'; } if( ad ){ zCap[i++] = 'd'; } @@ -322,10 +324,11 @@ if( as ){ zCap[i++] = 's'; } if( at ){ zCap[i++] = 't'; } if( au ){ zCap[i++] = 'u'; } if( av ){ zCap[i++] = 'v'; } if( aw ){ zCap[i++] = 'w'; } + if( ax ){ zCap[i++] = 'x'; } if( az ){ zCap[i++] = 'z'; } zCap[i] = 0; zPw = P("pw"); zLogin = P("login"); @@ -360,11 +363,11 @@ zLogin = ""; zInfo = ""; zCap = ""; zPw = ""; oaa = oab = oac = oad = oae = oaf = oag = oah = oai = oaj = oak = oam = - oan = oao = oap = oar = oas = oat = oau = oav = oaw = oaz = ""; + oan = oao = oap = oar = oas = oat = oau = oav = oaw = oax = oaz = ""; if( uid ){ zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid); zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid); zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", uid); zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid); @@ -387,10 +390,11 @@ if( strchr(zCap, 's') ) oas = " checked=\"checked\""; if( strchr(zCap, 't') ) oat = " checked=\"checked\""; if( strchr(zCap, 'u') ) oau = " checked=\"checked\""; if( strchr(zCap, 'v') ) oav = " checked=\"checked\""; if( strchr(zCap, 'w') ) oaw = " checked=\"checked\""; + if( strchr(zCap, 'x') ) oax = " checked=\"checked\""; if( strchr(zCap, 'z') ) oaz = " checked=\"checked\""; } /* figure out inherited permissions */ memset(inherit, 0, sizeof(inherit)); @@ -484,10 +488,11 @@ @ <input type="checkbox" name="ar"%s(oar) />%s(B('r'))Read Ticket<br /> @ <input type="checkbox" name="an"%s(oan) />%s(B('n'))New Ticket<br /> @ <input type="checkbox" name="ac"%s(oac) />%s(B('c'))Append Ticket<br /> @ <input type="checkbox" name="aw"%s(oaw) />%s(B('w'))Write Ticket<br /> @ <input type="checkbox" name="at"%s(oat) />%s(B('t'))Ticket Report<br /> + @ <input type="checkbox" name="ax"%s(oax) />%s(B('x'))Private<br /> @ <input type="checkbox" name="az"%s(oaz) />%s(B('z'))Download Zip @ </td> @ </tr> @ <tr> @ <td align="right">Password:</td> Index: src/sync.c ================================================================== --- src/sync.c +++ src/sync.c @@ -78,11 +78,11 @@ configSync = CONFIGSET_SHUN; } #endif printf("Autosync: %s\n", g.urlCanonical); url_enable_proxy("via proxy: "); - rc = client_sync((flags & AUTOSYNC_PUSH)!=0, 1, 0, configSync, 0); + rc = client_sync((flags & AUTOSYNC_PUSH)!=0, 1, 0, 0, configSync, 0); if( rc ) fossil_warning("Autosync failed"); return rc; } /* @@ -89,16 +89,17 @@ ** This routine processes the command-line argument for push, pull, ** and sync. If a command-line argument is given, that is the URL ** of a server to sync against. If no argument is given, use the ** most recently synced URL. Remember the current URL for next time. */ -static int process_sync_args(void){ +static void process_sync_args(int *pConfigSync, int *pPrivate){ const char *zUrl = 0; const char *zPw = 0; int configSync = 0; int urlOptional = find_option("autourl",0,0)!=0; g.dontKeepUrl = find_option("once",0,0)!=0; + *pPrivate = find_option("private",0,0)!=0; url_proxy_options(); db_find_and_open_repository(0, 0); db_open_config(0); if( g.argc==2 ){ zUrl = db_get("last-sync-url", 0); @@ -126,11 +127,11 @@ user_select(); if( g.argc==2 ){ printf("Server: %s\n", g.urlCanonical); } url_enable_proxy("via proxy: "); - return configSync; + *pConfigSync = configSync; } /* ** COMMAND: pull ** @@ -145,16 +146,21 @@ ** ** The URL specified normally becomes the new "remote-url" used for ** subsequent push, pull, and sync operations. However, the "--once" ** command-line option makes the URL a one-time-use URL that is not ** saved. +** +** Use the --private option to pull private branches from the +** remote repository. ** ** See also: clone, push, sync, remote-url */ void pull_cmd(void){ - int syncFlags = process_sync_args(); - client_sync(0,1,0,syncFlags,0); + int syncFlags; + int bPrivate; + process_sync_args(&syncFlags, &bPrivate); + client_sync(0,1,0,bPrivate,syncFlags,0); } /* ** COMMAND: push ** @@ -169,16 +175,21 @@ ** ** The URL specified normally becomes the new "remote-url" used for ** subsequent push, pull, and sync operations. However, the "--once" ** command-line option makes the URL a one-time-use URL that is not ** saved. +** +** Use the --private option to push private branches to the +** remote repository. ** ** See also: clone, pull, sync, remote-url */ void push_cmd(void){ - process_sync_args(); - client_sync(1,0,0,0,0); + int syncFlags; + int bPrivate; + process_sync_args(&syncFlags, &bPrivate); + client_sync(1,0,0,bPrivate,0,0); } /* ** COMMAND: sync @@ -199,16 +210,21 @@ ** ** The URL specified normally becomes the new "remote-url" used for ** subsequent push, pull, and sync operations. However, the "--once" ** command-line option makes the URL a one-time-use URL that is not ** saved. +** +** Use the --private option to sync private branches with the +** remote repository. ** ** See also: clone, push, pull, remote-url */ void sync_cmd(void){ - int syncFlags = process_sync_args(); - client_sync(1,1,0,syncFlags,0); + int syncFlags; + int bPrivate; + process_sync_args(&syncFlags, &bPrivate); + client_sync(1,1,0,bPrivate,syncFlags,0); } /* ** COMMAND: remote-url ** Index: src/tag.c ================================================================== --- src/tag.c +++ src/tag.c @@ -204,10 +204,16 @@ break; } case TAG_USER: { zCol = "euser"; break; + } + case TAG_PRIVATE: { + db_multi_exec( + "INSERT OR IGNORE INTO private(rid) VALUES(%d);", + rid + ); } } if( zCol ){ db_multi_exec("UPDATE event SET %s=%Q WHERE objid=%d", zCol, zValue, rid); if( tagid==TAG_COMMENT ){ Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -38,10 +38,12 @@ int nDeltaSent; /* Number of deltas sent */ int nFileRcvd; /* Number of files received */ int nDeltaRcvd; /* Number of deltas received */ int nDanglingFile; /* Number of dangling deltas received */ int mxSend; /* Stop sending "file" with pOut reaches this size */ + u8 syncPrivate; /* True to enable syncing private content */ + u8 nextIsPrivate; /* If true, next "file" received is a private */ }; /* ** The input blob contains a UUID. Convert it into a record ID. @@ -49,11 +51,11 @@ ** phantomize is true. ** ** Compare to uuid_to_rid(). This routine takes a blob argument ** and does less error checking. */ -static int rid_from_uuid(Blob *pUuid, int phantomize){ +static 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); @@ -62,11 +64,11 @@ }else{ rid = 0; } db_reset(&q); if( rid==0 && phantomize ){ - rid = content_new(blob_str(pUuid), 0); + rid = content_new(blob_str(pUuid), isPrivate); } return rid; } /* @@ -106,11 +108,14 @@ static void xfer_accept_file(Xfer *pXfer, int cloneFlag){ int n; int rid; int srcid = 0; Blob content, hash; + int isPriv; + isPriv = pXfer->nextIsPrivate; + pXfer->nextIsPrivate = 0; if( pXfer->nToken<3 || pXfer->nToken>4 || !blob_is_uuid(&pXfer->aToken[1]) || !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &n) || n<0 @@ -123,32 +128,38 @@ blob_zero(&hash); blob_extract(pXfer->pIn, n, &content); if( !cloneFlag && uuid_is_shunned(blob_str(&pXfer->aToken[1])) ){ /* Ignore files that have been shunned */ return; + } + if( isPriv && !g.okPrivate ){ + /* Do not accept private files if not authorized */ + return; } if( cloneFlag ){ if( pXfer->nToken==4 ){ - srcid = rid_from_uuid(&pXfer->aToken[2], 1); + 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, 0, 0); + rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, + 0, isPriv); remote_has(rid); blob_reset(&content); return; } if( pXfer->nToken==4 ){ Blob src, next; - srcid = rid_from_uuid(&pXfer->aToken[2], 1); + srcid = rid_from_uuid(&pXfer->aToken[2], 1, isPriv); if( content_get(srcid, &src)==0 ){ - rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, 0, 0); + rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, + 0, isPriv); pXfer->nDanglingFile++; db_multi_exec("DELETE FROM phantom WHERE rid=%d", rid); - content_make_public(rid); + if( !isPriv ) content_make_public(rid); return; } pXfer->nDeltaRcvd++; blob_delta_apply(&src, &content, &next); blob_reset(&src); @@ -159,17 +170,17 @@ } sha1sum_blob(&content, &hash); if( !blob_eq_str(&pXfer->aToken[1], blob_str(&hash), -1) ){ blob_appendf(&pXfer->err, "content does not match sha1 hash"); } - rid = content_put_ex(&content, blob_str(&hash), 0, 0, 0); + rid = content_put_ex(&content, blob_str(&hash), 0, 0, isPriv); blob_reset(&hash); if( rid==0 ){ blob_appendf(&pXfer->err, "%s", g.zErrMsg); blob_reset(&content); }else{ - content_make_public(rid); + if( !isPriv ) content_make_public(rid); manifest_crosslink(rid, &content); } assert( blob_is_reset(&content) ); remote_has(rid); } @@ -201,11 +212,14 @@ 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_uuid(&pXfer->aToken[1]) || !blob_is_int(&pXfer->aToken[pXfer->nToken-2], &szU) || !blob_is_int(&pXfer->aToken[pXfer->nToken-1], &szC) @@ -212,25 +226,30 @@ || szC<0 || szU<0 || (pXfer->nToken==5 && !blob_is_uuid(&pXfer->aToken[2])) ){ blob_appendf(&pXfer->err, "malformed cfile line"); return; + } + if( isPriv && !g.okPrivate ){ + /* 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 */ return; } if( pXfer->nToken==5 ){ - srcid = rid_from_uuid(&pXfer->aToken[2], 1); + 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, 0); + rid = content_put_ex(&content, blob_str(&pXfer->aToken[1]), srcid, + szC, isPriv); remote_has(rid); blob_reset(&content); } /* @@ -242,10 +261,11 @@ ** 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 UUID of the file to send */ ){ static const char *azQuery[] = { "SELECT pid FROM plink x" @@ -266,22 +286,25 @@ int srcId = 0; for(i=0; srcId==0 && i<count(azQuery); i++){ srcId = db_int(0, azQuery[i], rid); } - if( srcId>0 && !content_is_private(srcId) && content_get(srcId, &src) ){ + 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>=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_appendf(pXfer->pOut, "\n", 1); */ } blob_reset(&delta); free(zUuid); blob_reset(&src); } @@ -297,27 +320,31 @@ ** 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 UUID of the file to send */ ){ Blob src, delta; int size = 0; int srcId; srcId = db_int(0, "SELECT srcid FROM delta WHERE rid=%d", rid); - if( srcId>0 && !content_is_private(srcId) ){ + 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); @@ -342,12 +369,13 @@ ** 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( content_is_private(rid) ) return; + if( pXfer->syncPrivate==0 && isPriv ) 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); @@ -365,38 +393,45 @@ if( uuid_is_shunned(blob_str(pUuid)) ){ blob_reset(&uuid); return; } if( pXfer->mxSend<=blob_size(pXfer->pOut) ){ - blob_appendf(pXfer->pOut, "igot %b\n", pUuid); + const char *zFormat = isPriv ? "igot %b 1\n" : "igot %b\n"; + blob_appendf(pXfer->pOut, zFormat, pUuid); pXfer->nIGotSent++; blob_reset(&uuid); return; } if( nativeDelta ){ - size = send_delta_native(pXfer, rid, pUuid); + 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, &content, pUuid); + 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++; } } remote_has(rid); blob_reset(&uuid); +#if 0 + if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ + blob_appendf(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 @@ -407,57 +442,62 @@ const char *zUuid; const char *zDelta; int szU; int szC; int rc; + int isPrivate; static Stmt q1; + isPrivate = content_is_private(rid); + if( isPrivate && pXfer->syncPrivate==0 ) return; db_static_prepare(&q1, "SELECT uuid, size, content," " (SELECT uuid FROM delta, blob" " WHERE delta.rid=:rid AND delta.srcid=blob.rid)" " FROM blob" " WHERE rid=:rid" " AND size>=0" " AND uuid NOT IN shun" - " AND rid NOT IN private", - rid ); 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); zDelta = db_column_text(&q1, 3); + if( isPrivate ) blob_append(pXfer->pOut, "private\n", -1); blob_appendf(pXfer->pOut, "cfile %s ", zUuid); - if( zDelta ){ + 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); - blob_append(pXfer->pOut, "\n", 1); + if( blob_buffer(pXfer->pOut)[blob_size(pXfer->pOut)-1]!='\n' ){ + blob_appendf(pXfer->pOut, "\n", 1); + } } db_reset(&q1); } /* ** Send a gimme message for every phantom. ** -** It should not be possible to have a private phantom. But just to be -** sure, take care not to send any "gimme" messagse on private artifacts. +** 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 JOIN blob USING(rid)" - " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" - " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)" + " WHERE 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++; @@ -641,10 +681,27 @@ content_put(&cluster); 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. */ @@ -652,12 +709,12 @@ Stmt q; int cnt = 0; db_prepare(&q, "SELECT uuid FROM unclustered JOIN blob USING(rid)" " WHERE NOT EXISTS(SELECT 1 FROM shun WHERE uuid=blob.uuid)" - " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)" " AND NOT EXISTS(SELECT 1 FROM phantom WHERE rid=blob.rid)" + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=blob.rid)" ); while( db_step(&q)==SQLITE_ROW ){ blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0)); cnt++; } @@ -704,10 +761,19 @@ blob_size(&content), blob_str(&content)); blob_reset(&content); } } + +/* +** 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 +} + /* ** If this variable is set, disable login checks. Used for debugging ** only. */ @@ -757,10 +823,11 @@ "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);" ); manifest_crosslink_begin(); while( blob_line(xfer.pIn, &xfer.line) ){ if( blob_buffer(&xfer.line)[0]=='#' ) continue; + if( blob_size(&xfer.line)==0 ) continue; xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); /* file UUID SIZE \n CONTENT ** file UUID DELTASRC SIZE \n CONTENT ** @@ -811,27 +878,34 @@ && xfer.nToken==2 && blob_is_uuid(&xfer.aToken[1]) ){ nGimme++; if( isPull ){ - int rid = rid_from_uuid(&xfer.aToken[1], 0); + int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); if( rid ){ send_file(&xfer, rid, &xfer.aToken[1], deltaFlag); } } }else - /* igot UUID + /* igot UUID ?ISPRIVATE? ** - ** Client announces that it has a particular file. + ** Client announces that it has a particular file. If the ISPRIVATE + ** argument exists and is non-zero, then the file is a private file. */ - if( xfer.nToken==2 + if( xfer.nToken>=2 && blob_eq(&xfer.aToken[0], "igot") && blob_is_uuid(&xfer.aToken[1]) ){ if( isPush ){ - rid_from_uuid(&xfer.aToken[1], 1); + if( xfer.nToken==2 || blob_eq(&xfer.aToken[2],"1")==0 ){ + rid_from_uuid(&xfer.aToken[1], 1, 0); + }else if( g.okPrivate ){ + rid_from_uuid(&xfer.aToken[1], 1, 1); + }else{ + server_private_xfer_not_authorized(); + } } }else /* pull SERVERCODE PROJECTCODE @@ -929,11 +1003,11 @@ */ if( blob_eq(&xfer.aToken[0], "login") && xfer.nToken==4 ){ if( disableLogin ){ - g.okRead = g.okWrite = 1; + g.okRead = g.okWrite = g.okPrivate = 1; }else{ if( check_tail_hash(&xfer.aToken[2], xfer.pIn) || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) ){ cgi_reset_content(); @@ -1029,10 +1103,48 @@ */ if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){ /* Process the cookie */ }else + + /* private + ** + ** This card indicates that the next "file" or "cfile" will contain + ** private content. + */ + if( blob_eq(&xfer.aToken[0], "private") ){ + if( !g.okPrivate ){ + server_private_xfer_not_authorized(); + }else{ + xfer.nextIsPrivate = 1; + } + }else + + + /* pragma NAME VALUE... + ** + ** The client issue 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 + ** + ** 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.okPrivate ){ + server_private_xfer_not_authorized(); + }else{ + xfer.syncPrivate = 1; + } + } + }else + /* Unknown message */ { cgi_reset_content(); @ error bad\scommand:\s%F(blob_str(&xfer.line)) @@ -1049,13 +1161,15 @@ ** 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); } if( recvConfig ){ configure_finalize_receive(); } manifest_crosslink_end(); @@ -1120,10 +1234,11 @@ */ int client_sync( int pushFlag, /* True to do a push (or a sync) */ int pullFlag, /* True to do a pull (or a sync) */ int cloneFlag, /* True if this is a clone */ + int privateFlag, /* True to exchange private branches */ int configRcvMask, /* Receive these configuration items */ int configSendMask /* Send these configuration items */ ){ int go = 1; /* Loop until zero */ int nCardSent = 0; /* Number of cards sent */ @@ -1155,10 +1270,14 @@ socket_global_init(); memset(&xfer, 0, sizeof(xfer)); xfer.pIn = &recv; xfer.pOut = &send; xfer.mxSend = db_get_int("max-upload", 250000); + if( privateFlag ){ + g.okPrivate = 1; + xfer.syncPrivate = 1; + } assert( pushFlag | pullFlag | cloneFlag | configRcvMask | configSendMask ); db_begin_transaction(); db_record_repository_filename(0); db_multi_exec( @@ -1169,10 +1288,14 @@ blob_zero(&recv); blob_zero(&xfer.err); blob_zero(&xfer.line); origConfigRcvMask = 0; + + /* Send the send-private pragma if we are trying to sync private data */ + if( privateFlag ) blob_append(&send, "pragma send-private\n", -1); + /* ** Always begin with a clone, pull, or push message */ if( cloneFlag ){ blob_appendf(&send, "clone 3 %d\n", cloneSeqno); @@ -1212,10 +1335,11 @@ request_phantoms(&xfer, mxPhantomReq); } if( pushFlag ){ send_unsent(&xfer); nCardSent += send_unclustered(&xfer); + if( privateFlag ) send_private(&xfer); } /* Send configuration parameter requests. On a clone, delay sending ** this until the second cycle since the login card might fail on ** the first cycle. @@ -1275,10 +1399,13 @@ break; } lastPctDone = -1; blob_reset(&send); rArrivalTime = db_double(0.0, "SELECT julianday('now')"); + + /* Send the send-private pragma if we are trying to sync private data */ + if( privateFlag ) 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( pullFlag ){ @@ -1349,32 +1476,40 @@ if( blob_eq(&xfer.aToken[0], "gimme") && xfer.nToken==2 && blob_is_uuid(&xfer.aToken[1]) ){ if( pushFlag ){ - int rid = rid_from_uuid(&xfer.aToken[1], 0); + int rid = rid_from_uuid(&xfer.aToken[1], 0, 0); if( rid ) send_file(&xfer, rid, &xfer.aToken[1], 0); } }else - /* igot UUID + /* igot UUID ?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 + if( xfer.nToken>=2 && blob_eq(&xfer.aToken[0], "igot") && blob_is_uuid(&xfer.aToken[1]) ){ - int rid = rid_from_uuid(&xfer.aToken[1], 0); + 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 ){ - content_make_public(rid); + if( !isPriv ) content_make_public(rid); + }else if( isPriv && !g.okPrivate ){ + /* ignore private files */ }else if( pullFlag || cloneFlag ){ - rid = content_new(blob_str(&xfer.aToken[1]), 0); + rid = content_new(blob_str(&xfer.aToken[1]), isPriv); if( rid ) newPhantom = 1; } remote_has(rid); }else @@ -1454,10 +1589,21 @@ ** same server. */ if( blob_eq(&xfer.aToken[0], "cookie") && xfer.nToken==2 ){ db_set("cookie", blob_str(&xfer.aToken[1]), 0); }else + + + /* private + ** + ** This card indicates 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