Changes On Branch 2250a684cc1f5c68
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch xfer-login-card Through [2250a684cc] Excluding Merge-Ins

This is equivalent to a diff from c6f0d7aecd to 2250a684cc

2025-07-23
15:58
Minor optimization: replace calls to mprintf("%s", X) with fossil_strdup(X). check-in: 4c3e1728e1 user: danield tags: trunk
2025-07-22
17:52
Remove lots of debug output. Replace a couple of mprintf() with fossil_strdup() and a couple free() with fossil_free(). Milestone: libfossil has successfully logged in to this version of fossil. check-in: 1078a123c1 user: stephan tags: xfer-login-card
15:53
Doc typo fixes. check-in: 2250a684cc user: stephan tags: xfer-login-card
15:51
Set g.syncInfo.bLoginCardHeader=1 if that inbound header is detected, rather than delaying it until the /xfer handling. Internal doc additions. check-in: 4fc13c5c88 user: stephan tags: xfer-login-card
2025-07-21
18:38
Enable an /xfer login card to be delivered via the X-Fossil-Xfer-Login HTTP header, which is expected to be in the same format as the sync protocol's login card. The purpose of this is to simplify generation of the login card from non-fossil(1) clients, namely libfossil. This is untested until libfossil can generate such cards (it's just missing a ... check-in: cfddded40e user: stephan tags: xfer-login-card
17:16
Account for [638b7e094b899a] when building with --json, as reported in [forum:9acc3d0022407bfe | forum post 9acc3d0022]. check-in: c6f0d7aecd user: stephan tags: trunk
13:20
Remove FossilUserPerms::Query, as it's unused and its designated capabilities letter 'q' collides with ModTkt. It's been there since 2011-09-14 but went unnoticed until today when that struct was emacs-macro-reformatted into libfossil and triggered a duplicate case value for the letter 'q'. check-in: 638b7e094b user: stephan tags: trunk

Changes to VERSION.
1


1
-
+
2.27
2.27.1
Changes to src/cgi.c.
2219
2220
2221
2222
2223
2224
2225




2226
2227
2228
2229
2230
2231
2232
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236







+
+
+
+







    }else if( fossil_strcmp(zFieldName,"range:")==0 ){
      int x1 = 0;
      int x2 = 0;
      if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){
        rangeStart = x1;
        rangeEnd = x2+1;
      }
    }else if( fossil_strcmp(zFieldName, "x-fossil-xfer-login:")==0 ){
      g.syncInfo.zLoginCard = fossil_strdup(zVal);
      g.syncInfo.bLoginCardHeader = 1;
      /*fprintf(stderr, "X-Fossil-Xfer-Login: %s\n", g.syncInfo.zLoginCard);*/
    }
  }
  cgi_setenv("REQUEST_SCHEME",zScheme);
  cgi_init();
  cgi_trace(0);
}

Changes to src/http.c.
50
51
52
53
54
55
56
57
58
59





60
61
62



63
64

65
66
67
68
69
70
71
72
73

74
75
76

77
78
79
80
81
82
83
50
51
52
53
54
55
56



57
58
59
60
61
62


63
64
65
66

67
68
69
70
71
72
73
74
75

76
77
78

79
80
81
82
83
84
85
86







-
-
-
+
+
+
+
+

-
-
+
+
+

-
+








-
+


-
+








/*
** Construct the "login" card with the client credentials.
**
**       login LOGIN NONCE SIGNATURE
**
** The LOGIN is the user id of the client.  NONCE is the sha1 checksum
** of all payload that follows the login card.  Randomness for the NONCE 
** must be provided in the payload (in xfer.c).  SIGNATURE is the sha1
** checksum of the nonce followed by the user password.
** of all payload that follows the login card.  Randomness for the
** NONCE must be provided in the payload (in xfer.c) (e.g. by
** appending a timestamp or random bytes as a comment line to the
** payload).  SIGNATURE is the sha1 checksum of the nonce followed by
** the fossil-hashed version of the user's password.
**
** Write the constructed login card into pLogin.  pLogin is initialized
** by this routine.
** Write the constructed login card into pLogin. The result does not
** have an EOL added to it because which type of EOL it needs has to
** be determined later.  pLogin is initialized by this routine.
*/
static void http_build_login_card(Blob *pPayload, Blob *pLogin){
static void http_build_login_card(Blob * const pPayload, Blob * const pLogin){
  Blob nonce;          /* The nonce */
  const char *zLogin;  /* The user login name */
  const char *zPw;     /* The user password */
  Blob pw;             /* The nonce with user password appended */
  Blob sig;            /* The signature field */

  blob_zero(pLogin);
  if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){
     return;  /* If no login card for users "nobody" and "anonymous" */
     return;  /* No login card for users "nobody" and "anonymous" */
  }
  if( g.url.isSsh ){
     return;  /* If no login card for SSH: */
     return;  /* No login card for SSH: */
  }
  blob_zero(&nonce);
  blob_zero(&pw);
  sha1sum_blob(pPayload, &nonce);
  blob_copy(&pw, &nonce);
  zLogin = g.url.user;
  if( g.url.passwd ){
117
118
119
120
121
122
123
124

125
126
127
128
129
130
131
132
133


134
135
136
137

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157



158
159
160
161
162
163
164
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172







-
+








-
+
+




+




















+
+
+







    }
    fossil_free(g.url.passwd);
    g.url.passwd = fossil_strdup(zPw);
  }

  blob_append(&pw, zPw, -1);
  sha1sum_blob(&pw, &sig);
  blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig);
  blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
  blob_reset(&pw);
  blob_reset(&sig);
  blob_reset(&nonce);
}

/*
** Construct an appropriate HTTP request header.  Write the header
** into pHdr.  This routine initializes the pHdr blob.  pPayload is
** the complete payload (including the login card) already compressed.
** the complete payload (including the login card if pLogin is NULL or
** empty) already compressed.
*/
static void http_build_header(
  Blob *pPayload,              /* the payload that will be sent */
  Blob *pHdr,                  /* construct the header here */
  Blob *pLogin,                /* Login card header value or NULL */
  const char *zAltMimetype     /* Alternative mimetype */
){
  int nPayload = pPayload ? blob_size(pPayload) : 0;

  blob_zero(pHdr);
  blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n",
               nPayload>0 ? "POST" : "GET", g.url.path,
               g.url.path[0]==0 ? "/" : "");
  if( g.url.proxyAuth ){
    blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
  }
  if( g.zHttpAuth && g.zHttpAuth[0] ){
    const char *zCredentials = g.zHttpAuth;
    char *zEncoded = encode64(zCredentials, -1);
    blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
    fossil_free(zEncoded);
  }
  blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
  blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
  if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
  if( pLogin && blob_size(pLogin) ){
    blob_appendf(pHdr, "X-Fossil-Xfer-Login: %b\r\n", pLogin);
  }
  if( nPayload ){
    if( zAltMimetype ){
      blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
    }else if( g.fHttpTrace ){
      blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
    }else{
      blob_appendf(pHdr, "Content-Type: application/x-fossil\r\n");
457
458
459
460
461
462
463

464
465
466
467
468







469
470
471
472
473
474



























475
476
477
478
479

480
481
482
483
484
485
486
465
466
467
468
469
470
471
472
473
474
475

476
477
478
479
480
481
482
483






484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514

515
516
517
518
519
520
521
522







+



-

+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+








  if( transport_open(&g.url) ){
    fossil_warning("%s", transport_errmsg(&g.url));
    return 1;
  }

  /* Construct the login card and prepare the complete payload */
  blob_zero(&login);
  if( blob_size(pSend)==0 ){
    blob_zero(&payload);
  }else{
    blob_zero(&login);
    if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
#if 0
    fprintf(stderr, "# g.syncInfo.bLoginCardHeader=%d login card=%s\n",
            g.syncInfo.bLoginCardHeader,
            blob_size(&login) ? blob_str(&login) : "<empty>");
#endif
    if( g.syncInfo.bLoginCardHeader ) {
      /* Send the login card as an HTTP header. */
    if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
      payload = login;
      blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
    }else{
      blob_compress2(&login, pSend, &payload);
      blob_reset(&login);
      if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
#if 1
        /*blob_append(&payload, blob_buffer(pSend), blob_size(pSend));*/
        blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
#else
        /* This could save memory but looks like it would break in a
        ** couple of cases in the loop below where pSend is referenced
        ** for HTTP 401 and redirects. */
        blob_zero(&payload);
        blob_swap(pSend, &payload);
#endif
      }else{
        blob_compress(pSend, &payload);
      }
    }else{
      /* Prepend the login card (if set) to the payload */
      if( blob_size(&login) ){
        blob_append_char(&login, '\n');
      }
      if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
        payload = login;
        login = empty_blob/*transfer ownership*/;
        blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
      }else{
        blob_compress2(&login, pSend, &payload);
        blob_reset(&login);
      }
    }
  }

  /* Construct the HTTP request header */
  http_build_header(&payload, &hdr, zAltMimetype);
  http_build_header(&payload, &hdr, &login, zAltMimetype);

  /* When tracing, write the transmitted HTTP message both to standard
  ** output and into a file.  The file can then be used to drive the
  ** server-side like this:
  **
  **      ./fossil test-http <http-request-1.txt
  */
Changes to src/main.c.
286
287
288
289
290
291
292











293
294
295
296
297
298
299
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310







+
+
+
+
+
+
+
+
+
+
+







  const char **azAuxOpt[MX_AUX]; /* Options of each option() value */
  int anAuxCols[MX_AUX];         /* Number of columns for option() values */
  int allowSymlinks;             /* Cached "allow-symlinks" option */
  int mainTimerId;               /* Set to fossil_timer_start() */
  int nPendingRequest;           /* # of HTTP requests in "fossil server" */
  int nRequest;                  /* Total # of HTTP request */
  int bAvoidDeltaManifests;      /* Avoid using delta manifests if true */

  /* State for communicating specific details between the inbound HTTP
  ** header parser (cgi.c), xfer.c, and http.c. */
  struct {
    char *zLoginCard;       /* Inbound X-Fossil-Xfer-Login request header */
    int bLoginCardHeader;   /* If true, emit login cards in outbound
                            ** requests as HTTP headers instead of as
                            ** part of the payload. Gets activated
                            ** on-demand based on xfer traffic
                            ** contents. */
  } syncInfo;
#ifdef FOSSIL_ENABLE_JSON
  struct FossilJsonBits {
    int isJsonMode;            /* True if running in JSON mode, else
                                  false. This changes how errors are
                                  reported. In JSON mode we try to
                                  always output JSON-form error
                                  responses and always (in CGI mode)
756
757
758
759
760
761
762





763
764
765
766
767
768
769
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785







+
+
+
+
+







#ifdef FOSSIL_ENABLE_TCL
  memset(&g.tcl, 0, sizeof(TclContext));
  g.tcl.argc = g.argc;
  g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
#endif
  g.mainTimerId = fossil_timer_start();
  capture_case_sensitive_option();
  g.syncInfo.bLoginCardHeader =
    /* This is only for facilitating development of the
    ** xfer-login-card branch. It will be removed or re-imagined at
    ** some point. */
    !!find_option("login-card-header","lch", 0);
  g.zVfsName = find_option("vfs",0,1);
  if( g.zVfsName==0 ){
    g.zVfsName = fossil_getenv("FOSSIL_VFS");
  }
  if( g.zVfsName ){
    sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName);
    if( pVfs ){
Changes to src/user.c.
463
464
465
466
467
468
469
470

471
472
473
474
475
476
477
463
464
465
466
467
468
469

470
471
472
473
474
475
476
477







-
+







      fossil_print("password unchanged\n");
    }else{
      char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
      db_unprotect(PROTECT_USER);
      db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
                    zSecret, uid);
      db_protect_pop();
      free(zSecret);
      fossil_free(zSecret);
    }
  }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
    int uid;
    if( g.argc!=4 && g.argc!=5 ){
      usage("capabilities USERNAME ?PERMISSIONS?");
    }
    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
Changes to src/xfer.c.
788
789
790
791
792
793
794





795
796
797
798
799
800
801
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806







+
+
+
+
+







** 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));
#if 0
  fprintf(stderr, "check tail=%d hash=[%.*s]\ntail=<<%.*s>>\n", rc,
          blob_size(pHash), blob_str(pHash),
          blob_size(&tail), blob_str(&tail));
#endif
  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:
825
826
827
828
829
830
831
832

833
834
835
836
837
838
839
830
831
832
833
834
835
836

837
838
839
840
841
842
843
844







-
+







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
   || 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 */
  }
852
853
854
855
856
857
858









859
860
861
862
863
864
865
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879







+
+
+
+
+
+
+
+
+







    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);
#if 0
  fprintf(stderr,
          "check login rc=%d nonce=[%.*s] pSig=[%.*s] .hash=[%.*s]\n",
          rc,
          blob_size(pNonce), blob_str(pNonce),
          blob_size(pSig), blob_str(pSig),
          blob_size(&hash), blob_str(&hash));

#endif
    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.
      */
1271
1272
1273
1274
1275
1276
1277

1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299







+







  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();
1312
1313
1314
1315
1316
1317
1318



















1319
1320
1321
1322
1323
1324
1325
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







    @ 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 header X-Fossil-Xfer-Login */
    blob_zero(&xfer.line);
    blob_append(&xfer.line, g.syncInfo.zLoginCard, -1);
    xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken,
                                count(xfer.aToken));
#if 0
    fprintf(stderr,"%s:%d: g.syncInfo.zLoginCard=[%s]\nnToken=%d tok[0]=%s line=%s\n",
            __FILE__, __LINE__, g.syncInfo.zLoginCard,
            xfer.nToken, xfer.nToken ? blob_str(&xfer.aToken[0]) : "<NULL>",
            blob_str(&xfer.line));
#endif
    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
1548
1549
1550
1551
1552
1553
1554


1555



1556
1557
1558
1559


1560
1561





1562





1563
1564
1565
1566
1567
1568
1569
1570





1571
1572
1573
1574
1575
1576
1577
1582
1583
1584
1585
1586
1587
1588
1589
1590

1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632







+
+
-
+
+
+




+
+


+
+
+
+
+

+
+
+
+
+








+
+
+
+
+







      @ 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
    ** The client can send multiple logins.  Permissions are cumulative.
    ** 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 0
        fprintf(stderr, "# handle_login_card: aToken[2]=[%.*s]\n",
                blob_size(&xfer.aToken[2]),
                blob_str(&xfer.aToken[2]));
#endif
        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;
        }
#if 0
        fprintf(stderr, "# logged in as [%.*s]\n",
                blob_size(&xfer.aToken[1]),
                blob_str(&xfer.aToken[1]));
#endif
      }
    }else

    /*    reqconfig  NAME
    **
    ** Client is requesting a configuration value from the server
    */
1693
1694
1695
1696
1697
1698
1699


1700
1701
1702
1703
1704
1705
1706
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763







+
+







      **
      ** 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 = atoi(blob_str(&xfer.aToken[2]));
        g.syncInfo.bLoginCardHeader =
          xfer.remoteVersion>=RELEASE_VERSION_NUMBER;
        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)
        }
      }else
2756
2757
2758
2759
2760
2761
2762


2763
2764
2765
2766
2767
2768
2769
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828







+
+







        **
        ** 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 = atoi(blob_str(&xfer.aToken[2]));
          g.syncInfo.bLoginCardHeader =
            xfer.remoteVersion>=RELEASE_VERSION_NUMBER;
          if( xfer.nToken>=5 ){
            xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
            xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
          }
        }

        /*   pragma uv-pull-only
Changes to www/sync.wiki.
224
225
226
227
228
229
230
231
232
233



234
235
236


237

238
239
240



241
242
243
244
245
246
247
224
225
226
227
228
229
230



231
232
233



234
235
236
237



238
239
240
241
242
243
244
245
246
247







-
-
-
+
+
+
-
-
-
+
+

+
-
-
-
+
+
+








The userid is the name of the user that is requesting service
from the server.  The nonce is the SHA1 hash of the remainder of
the message - all text that follows the newline character that
terminates the login card.  The signature is the SHA1 hash of
the concatenation of the nonce and the users password.

For each login card, the server looks up the user and verifies
that the nonce matches the SHA1 hash of the remainder of the
message.  It then checks the signature hash to make sure the
When receving a login card, the server looks up the user and verifies
that the nonce matches the SHA1 hash of the remainder of the message.
It then checks the signature hash to make sure the signature matches.
signature matches.  If everything
checks out, then the client is granted all privileges of the
specified user.
If everything checks out, then the client is granted all privileges of
the specified user.

Only one login card is permitted. A second login card will trigger
Privileges are cumulative.  There can be multiple successful
login cards.  The session privilege is the union of all
privileges from all login cards.
a sync error. (Prior to 2025-07-21, the protocol permitted multiple
logins, treating the login as the union of all privileges from all
login cards. That capability was never used and has been removed.)

<h3 id="file">3.3 File Cards</h3>

Artifacts are transferred using either "file" cards, or "cfile"
or "uvfile" cards.
The name "file" card comes from the fact that most artifacts correspond to
files that are under version control.