Fossil

Check-in [18628904c3]
Login

Check-in [18628904c3]

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

Overview
Comment:Teach the sync protocol how to work with an out-of-band login card, saving an extra server-side copy of the sync content which is required only to account for an inlined login card. i.e. it saves RAM, potentially lots of it. The new login card mechanism is instead transported via an HTTP header. This also, not coincidentally, simplifies implementation of the login card in non-fossil(1) clients which are currently learning to speak the sync protocol.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 18628904c30c9c55cee173738cf606cab5fcf92631b1b2331758243224ad4114
User & Date: stephan 2025-07-27 11:07:38.293
Original Comment: Teach the sync protocol how to work with an out-of-band login card, saving an extra server-side of the sync content which is required only to accout for an inlined login card. i.e. it saves RAM, potentially lots of it. The new login card mechanism is instead transported via an HTTP header. This also, not coincidentally, simplifies implementation of the login card in non-fossil(1) clients which are currently learning to speak the sync protocol.
Context
2025-07-27
11:58
When unversioned content is saved, add an entry to the admin log. ... (check-in: 7991defa6f user: stephan tags: trunk)
11:07
Teach the sync protocol how to work with an out-of-band login card, saving an extra server-side copy of the sync content which is required only to account for an inlined login card. i.e. it saves RAM, potentially lots of it. The new login card mechanism is instead transported via an HTTP header. This also, not coincidentally, simplifies implementation of the login card in non-fossil(1) clients which are currently learning to speak the sync protocol. ... (check-in: 18628904c3 user: stephan tags: trunk)
2025-07-25
18:47
Do not add the sync login cookie unless we know the remote supports it. It's harmless in that case but it doesn't need to be there. Rename the login cookie from the unweildy x-f-x-l (X-Fossil-Xfer-Login) to x-f-l-c (X-Fossil-Login-Card) because the former is unsightly. ... (Closed-Leaf check-in: 9789e1dce7 user: stephan tags: xfer-login-card)
2025-07-23
15:58
Minor optimization: replace calls to mprintf("%s", X) with fossil_strdup(X). ... (check-in: 4c3e1728e1 user: danield tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/cgi.c.
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
**         are ignored.
**
**      *  it is impossible for a cookie or query parameter to
**         override the value of an environment variable since
**         environment variables always have uppercase names.
**
** 2018-03-29:  Also ignore the entry if NAME that contains any characters
** other than [a-zA-Z0-9_].  There are no known exploits involving unusual
** names that contain characters outside that set, but it never hurts to
** be extra cautious when sanitizing inputs.
**
** Parameters are separated by the "terminator" character.  Whitespace
** before the NAME is ignored.
**
** The input string "z" is modified but no copies is made.  "z"







|







962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
**         are ignored.
**
**      *  it is impossible for a cookie or query parameter to
**         override the value of an environment variable since
**         environment variables always have uppercase names.
**
** 2018-03-29:  Also ignore the entry if NAME that contains any characters
** other than [-a-zA-Z0-9_].  There are no known exploits involving unusual
** names that contain characters outside that set, but it never hurts to
** be extra cautious when sanitizing inputs.
**
** Parameters are separated by the "terminator" character.  Whitespace
** before the NAME is ignored.
**
** The input string "z" is modified but no copies is made.  "z"
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287





1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310








1311
1312
1313
1314
1315
1316
1317
  fputs(z, pLog);
}

/* Forward declaration */
static NORETURN void malformed_request(const char *zMsg, ...);

/*
** Checks the QUERY_STRING environment variable, sets it up
** via add_param_list() and, if found, applies its "skin"
** setting. Returns 0 if no QUERY_STRING is set, 1 if it is,





** and 2 if it sets the skin (in which case the cookie may
** still need flushing by the page, via cookie_render()).
*/
int cgi_setup_query_string(void){
  int rc = 0;
  char * z = (char*)P("QUERY_STRING");
  if( z ){
    ++rc;
    z = fossil_strdup(z);
    add_param_list(z, '&');
    z = (char*)P("skin");
    if( z ){
      char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
      ++rc;
      if( !zErr && P("once")==0 ){
        cookie_write_parameter("skin","skin",z);
        /* Per /chat discussion, passing ?skin=... without "once"
        ** implies the "udc" argument, so we force that into the
        ** environment here. */
        cgi_set_parameter_nocopy("udc", "1", 1);
      }
      fossil_free(zErr);
    }








  }
  return rc;
}

/*
** Initialize the query parameter database.  Information is pulled from
** the QUERY_STRING environment variable (if it exists), from standard







|
|
|
>
>
>
>
>
|
|





|





|









>
>
>
>
>
>
>
>







1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
  fputs(z, pLog);
}

/* Forward declaration */
static NORETURN void malformed_request(const char *zMsg, ...);

/*
** Checks the QUERY_STRING environment variable, sets it up via
** add_param_list() and, if found, applies its "skin" setting. Returns
** 0 if no QUERY_STRING is set, else it returns a bitmask of:
**
** 0x01 = QUERY_STRING was set up
** 0x02 = "skin" URL param arg was processed
** 0x04 = "x-f-l-c" cookie arg was processed.
**
*  In the case of the skin, the cookie may still need flushing
** by the page, via cookie_render().
*/
int cgi_setup_query_string(void){
  int rc = 0;
  char * z = (char*)P("QUERY_STRING");
  if( z ){
    rc = 0x01;
    z = fossil_strdup(z);
    add_param_list(z, '&');
    z = (char*)P("skin");
    if( z ){
      char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
      rc |= 0x02;
      if( !zErr && P("once")==0 ){
        cookie_write_parameter("skin","skin",z);
        /* Per /chat discussion, passing ?skin=... without "once"
        ** implies the "udc" argument, so we force that into the
        ** environment here. */
        cgi_set_parameter_nocopy("udc", "1", 1);
      }
      fossil_free(zErr);
    }
  }
  if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){
    /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie
    ** instead of in the sync payload. */
    rc |= 0x04;
    g.syncInfo.zLoginCard = fossil_strdup(z);
    g.syncInfo.fLoginCardMode |= 0x02;
    cgi_delete_parameter("x-f-l-c");
  }
  return rc;
}

/*
** Initialize the query parameter database.  Information is pulled from
** the QUERY_STRING environment variable (if it exists), from standard
2123
2124
2125
2126
2127
2128
2129

2130
2131
2132
2133
2134
2135
2136
void cgi_handle_http_request(const char *zIpAddr){
  char *z, *zToken;
  int i;
  const char *zScheme = "http";
  char zLine[2000];     /* A single line of input. */
  g.fullHttpReply = 1;
  g.zReqType = "HTTP";

  if( cgi_fgets(zLine, sizeof(zLine))==0 ){
    malformed_request("missing header");
  }
  blob_append(&g.httpHeader, zLine, -1);
  cgi_trace(zLine);
  zToken = extract_token(zLine, &z);
  if( zToken==0 ){







>







2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
void cgi_handle_http_request(const char *zIpAddr){
  char *z, *zToken;
  int i;
  const char *zScheme = "http";
  char zLine[2000];     /* A single line of input. */
  g.fullHttpReply = 1;
  g.zReqType = "HTTP";

  if( cgi_fgets(zLine, sizeof(zLine))==0 ){
    malformed_request("missing header");
  }
  blob_append(&g.httpHeader, zLine, -1);
  cgi_trace(zLine);
  zToken = extract_token(zLine, &z);
  if( zToken==0 ){
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
  if( zIpAddr==0 ){
    zIpAddr = cgi_remote_ip(fossil_fileno(g.httpIn));
  }
  if( zIpAddr ){
    cgi_setenv("REMOTE_ADDR", zIpAddr);
    g.zIpAddr = fossil_strdup(zIpAddr);
  }


  /* Get all the optional fields that follow the first line.
  */
  while( cgi_fgets(zLine,sizeof(zLine)) ){
    char *zFieldName;
    char *zVal;








<







2172
2173
2174
2175
2176
2177
2178

2179
2180
2181
2182
2183
2184
2185
  if( zIpAddr==0 ){
    zIpAddr = cgi_remote_ip(fossil_fileno(g.httpIn));
  }
  if( zIpAddr ){
    cgi_setenv("REMOTE_ADDR", zIpAddr);
    g.zIpAddr = fossil_strdup(zIpAddr);
  }


  /* Get all the optional fields that follow the first line.
  */
  while( cgi_fgets(zLine,sizeof(zLine)) ){
    char *zFieldName;
    char *zVal;

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

/*
** 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.

**
** Write the constructed login card into pLogin.  pLogin is initialized

** by this routine.
*/
static void http_build_login_card(Blob *pPayload, Blob *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" */
  }
  if( g.url.isSsh ){
     return;  /* If 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 ){







|
|
>
|
>

|
>
|

|








|


|







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) (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. 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 * 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;  /* No login card for users "nobody" and "anonymous" */
  }
  if( g.url.isSsh ){
     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
    }
    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_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.

*/
static void http_build_header(
  Blob *pPayload,              /* the payload that will be sent */
  Blob *pHdr,                  /* construct the header here */

  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( 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");







|








|
>




>





|
|
|












>
>
>
>
>
>







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
173
174
175
    }
    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", 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 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 HTTP/1.0\r\n",
               nPayload>0 ? "POST" : "GET",
               (g.url.path && g.url.path[0]) ? g.url.path : "/");
  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( g.syncInfo.fLoginCardMode>0
      && nPayload>0 && pLogin && blob_size(pLogin) ){
    /* Add sync login card via a transient cookie. We can only do this
       if we know the remote supports it. */
    blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(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");
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
**
**   *  The ssh_needs_path_argument() function above.
**   *  The test-ssh-needs-path command that shows the settings
**      that cache whether or not a PATH= is needed for a particular
**      HOSTNAME.
*/
void ssh_add_path_argument(Blob *pCmd){
  blob_append_escaped_arg(pCmd, 
     "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
}

/*
** Return the complete text of the last HTTP reply as saved in the
** http-reply-N.txt file.  This only works if run using --httptrace.
** Without the --httptrace option, this routine returns a NULL pointer.







|







395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
**
**   *  The ssh_needs_path_argument() function above.
**   *  The test-ssh-needs-path command that shows the settings
**      that cache whether or not a PATH= is needed for a particular
**      HOSTNAME.
*/
void ssh_add_path_argument(Blob *pCmd){
  blob_append_escaped_arg(pCmd,
     "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
}

/*
** Return the complete text of the last HTTP reply as saved in the
** http-reply-N.txt file.  This only works if run using --httptrace.
** Without the --httptrace option, this routine returns a NULL pointer.
455
456
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
    g.url.flags |= URL_SSH_PATH;
  }

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

  /* Construct the login card and prepare the complete payload */

  if( blob_size(pSend)==0 ){
    blob_zero(&payload);
  }else{
    blob_zero(&login);
    if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);


    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);

    }
  }

  /* Construct the HTTP request header */
  http_build_header(&payload, &hdr, 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
  */







<

>



<

>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
|
|
|
|
>




|







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
    g.url.flags |= URL_SSH_PATH;
  }

  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{

    if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
    if( g.syncInfo.fLoginCardMode ){
      /* The login card will be sent via an HTTP header and/or URL flag. */
      if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
        /* Maintenance note: we cannot blob_swap(pSend,&payload) here
        ** because the HTTP 401 and redirect response handling below
        ** needs pSend unmodified. payload won't be modified after
        ** this point, so we can make it a proxy for pSend for
        ** zero heap memory. */
        blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
      }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, &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/http_transport.c.
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
  if( (pUrlData->flags & URL_SSH_EXE)!=0
   && !is_safe_fossil_command(pUrlData->fossil)
  ){
    fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
                 "the server.", pUrlData->fossil);
  }
  if( (pUrlData->flags & URL_SSH_EXE)==0
   && (pUrlData->flags & URL_SSH_PATH)!=0 
  ){
    ssh_add_path_argument(&zCmd);
  }
  blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
  blob_append(&zCmd, " test-http", 10);
  if( pUrlData->path && pUrlData->path[0] ){
    blob_append_escaped_arg(&zCmd, pUrlData->path, 1);







|







139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
  if( (pUrlData->flags & URL_SSH_EXE)!=0
   && !is_safe_fossil_command(pUrlData->fossil)
  ){
    fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
                 "the server.", pUrlData->fossil);
  }
  if( (pUrlData->flags & URL_SSH_EXE)==0
   && (pUrlData->flags & URL_SSH_PATH)!=0
  ){
    ssh_add_path_argument(&zCmd);
  }
  blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
  blob_append(&zCmd, " test-http", 10);
  if( pUrlData->path && pUrlData->path[0] ){
    blob_append_escaped_arg(&zCmd, pUrlData->path, 1);
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    transport.isOpen = 0;
  }
}

/*
** Send content over the wire.
*/
void transport_send(UrlData *pUrlData, Blob *toSend){
  char *z = blob_buffer(toSend);
  int n = blob_size(toSend);
  transport.nSent += n;
  if( pUrlData->isSsh ){
    fwrite(z, 1, n, sshOut);
    fflush(sshOut);
  }else if( pUrlData->isHttps ){







|







243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    transport.isOpen = 0;
  }
}

/*
** Send content over the wire.
*/
void transport_send(UrlData const *pUrlData, const Blob *toSend){
  char *z = blob_buffer(toSend);
  int n = blob_size(toSend);
  transport.nSent += n;
  if( pUrlData->isSsh ){
    fwrite(z, 1, n, sshOut);
    fflush(sshOut);
  }else if( pUrlData->isHttps ){
Changes to src/main.c.
286
287
288
289
290
291
292

















293
294
295
296
297
298
299
  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 */

















#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)







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
311
312
313
314
315
316
  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-f-l-c" Cookie header. */
    int fLoginCardMode;     /* If non-0, emit login cards in outbound
                            ** requests as a HTTP cookie instead of as
                            ** part of the payload. Gets activated
                            ** on-demand based on xfer traffic
                            ** contents. Values, for
                            ** diagnostic/debugging purposes: 0x01=CLI
                            ** --flag, 0x02=cgi_setup_query_string(),
                            ** 0x04=page_xfer(),
                            ** 0x08=client_sync(). */
    int remoteVersion;      /* Remote fossil version. Used for negotiating
                            ** how to handle the login card. */
  } 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
#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.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 ){







>
>
>
>
>
>
>







773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
#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.fLoginCardMode =
    /* The undocumented/unsupported --login-card-header provides a way
    ** to force use of the feature added by the xfer-login-card branch
    ** in 2025-07, intended for assisting in debugging any related
    ** issues. It can be removed once we reach the level of "implicit
    ** trust" in that feature. */
    find_option("login-card-header",0,0) ? 0x01 : 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 ){
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
*/
NORETURN void fossil_redirect_home(void){
  /* In order for ?skin=... to work when visiting the site from
  ** a typical external link, we have to process it here, as
  ** that parameter gets lost during the redirect. We "could"
  ** pass the whole query string along instead, but that seems
  ** unnecessary. */
  if(cgi_setup_query_string()>1){
    cookie_render();
  }
  cgi_redirectf("%R%s", db_get("index-page", "/index"));
}

/*
** If running as root, chroot to the directory containing the







|







1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
*/
NORETURN void fossil_redirect_home(void){
  /* In order for ?skin=... to work when visiting the site from
  ** a typical external link, we have to process it here, as
  ** that parameter gets lost during the redirect. We "could"
  ** pass the whole query string along instead, but that seems
  ** unnecessary. */
  if(cgi_setup_query_string() & 0x02){
    cookie_render();
  }
  cgi_redirectf("%R%s", db_get("index-page", "/index"));
}

/*
** If running as root, chroot to the directory containing the
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
        @ <!-- Looking for repository named "%h(zRepo)" -->
        fprintf(stderr, "# looking for repository named \"%s\"\n", zRepo);
      }


      /* Restrictions on the URI for security:
      **
      **    1.  Reject characters that are not ASCII alphanumerics, 
      **        "-", "_", ".", "/", or unicode (above ASCII).
      **        In other words:  No ASCII punctuation or control characters
      **        other than "-", "_", "." and "/".
      **    2.  Exception to rule 1: Allow /X:/ where X is any ASCII 
      **        alphabetic character at the beginning of the name on windows.
      **    3.  "-" may not occur immediately after "/"
      **    4.  "." may not be adjacent to another "." or to "/"
      **
      ** Any character does not satisfy these constraints a Not Found
      ** error is returned.
      */  
      szFile = 0;
      for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
        char c = zRepo[j];
        if( c>='a' && c<='z' ) continue;
        if( c>='A' && c<='Z' ) continue;
        if( c>='0' && c<='9' ) continue;
        if( (c&0x80)==0x80 ) continue;







|



|






|







1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
        @ <!-- Looking for repository named "%h(zRepo)" -->
        fprintf(stderr, "# looking for repository named \"%s\"\n", zRepo);
      }


      /* Restrictions on the URI for security:
      **
      **    1.  Reject characters that are not ASCII alphanumerics,
      **        "-", "_", ".", "/", or unicode (above ASCII).
      **        In other words:  No ASCII punctuation or control characters
      **        other than "-", "_", "." and "/".
      **    2.  Exception to rule 1: Allow /X:/ where X is any ASCII
      **        alphabetic character at the beginning of the name on windows.
      **    3.  "-" may not occur immediately after "/"
      **    4.  "." may not be adjacent to another "." or to "/"
      **
      ** Any character does not satisfy these constraints a Not Found
      ** error is returned.
      */
      szFile = 0;
      for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
        char c = zRepo[j];
        if( c>='a' && c<='z' ) continue;
        if( c>='A' && c<='Z' ) continue;
        if( c>='0' && c<='9' ) continue;
        if( (c&0x80)==0x80 ) continue;
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
  if( zRemote ){
    /* If a USER@HOST:REPO argument is supplied, then use SSH to run
    ** "fossil ui --nobrowser" on the remote system and to set up a
    ** tunnel from the local machine to the remote. */
    FILE *sshIn;
    Blob ssh;
    int bRunning = 0;    /* True when fossil starts up on the remote */
    int isRetry;         /* True if on the second attempt */        
    char zLine[1000];

    blob_init(&ssh, 0, 0);
    for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
      blob_reset(&ssh);
      transport_ssh_command(&ssh);
      blob_appendf(&ssh,







|







3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
  if( zRemote ){
    /* If a USER@HOST:REPO argument is supplied, then use SSH to run
    ** "fossil ui --nobrowser" on the remote system and to set up a
    ** tunnel from the local machine to the remote. */
    FILE *sshIn;
    Blob ssh;
    int bRunning = 0;    /* True when fossil starts up on the remote */
    int isRetry;         /* True if on the second attempt */
    char zLine[1000];

    blob_init(&ssh, 0, 0);
    for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
      blob_reset(&ssh);
      transport_ssh_command(&ssh);
      blob_appendf(&ssh,
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
      if( skin_in_use() ) blob_appendf(&ssh, " --skin %s", skin_in_use());
      if( zJsMode ) blob_appendf(&ssh, " --jsmode %s", zJsMode);
      if( fCreate ) blob_appendf(&ssh, " --create");
      blob_appendf(&ssh, " %$", g.argv[2]);
      if( isRetry ){
        fossil_print("First attempt to run \"fossil\" on %s failed\n"
                     "Retry: ", zRemote);
      } 
      fossil_print("%s\n", blob_str(&ssh));
      sshIn = popen(blob_str(&ssh), "r");
      if( sshIn==0 ){
        fossil_fatal("unable to %s", blob_str(&ssh));
      }
      while( fgets(zLine, sizeof(zLine), sshIn) ){
        fputs(zLine, stdout);







|







3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
      if( skin_in_use() ) blob_appendf(&ssh, " --skin %s", skin_in_use());
      if( zJsMode ) blob_appendf(&ssh, " --jsmode %s", zJsMode);
      if( fCreate ) blob_appendf(&ssh, " --create");
      blob_appendf(&ssh, " %$", g.argv[2]);
      if( isRetry ){
        fossil_print("First attempt to run \"fossil\" on %s failed\n"
                     "Retry: ", zRemote);
      }
      fossil_print("%s\n", blob_str(&ssh));
      sshIn = popen(blob_str(&ssh), "r");
      if( sshIn==0 ){
        fossil_fatal("unable to %s", blob_str(&ssh));
      }
      while( fgets(zLine, sizeof(zLine), sshIn) ){
        fputs(zLine, stdout);
Changes to src/url.c.
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    dehttpize(pUrlData->name);
    pUrlData->path = fossil_strdup(&zUrl[i]);
    for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
    if( pUrlData->path[i] ){
      pUrlData->path[i] = 0;
      i++;
    }
    zExe = mprintf("");
    while( pUrlData->path[i]!=0 ){
      char *zName, *zValue;
      zName = &pUrlData->path[i];
      zValue = zName;
      while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
      if( pUrlData->path[i]=='=' ){
        pUrlData->path[i] = 0;







|







231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
    dehttpize(pUrlData->name);
    pUrlData->path = fossil_strdup(&zUrl[i]);
    for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
    if( pUrlData->path[i] ){
      pUrlData->path[i] = 0;
      i++;
    }
    zExe = fossil_strdup("");
    while( pUrlData->path[i]!=0 ){
      char *zName, *zValue;
      zName = &pUrlData->path[i];
      zValue = zName;
      while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
      if( pUrlData->path[i]=='=' ){
        pUrlData->path[i] = 0;
Changes to src/user.c.
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);
    }
  }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]);







|







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();
      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.
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
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 */
  }







|







825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
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 */
  }
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
      ** 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);
      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;







|







864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
      ** 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;
1239
1240
1241
1242
1243
1244
1245















1246
1247
1248
1249
1250
1251
1252
}

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
















/*
** The CGI/HTTP preprocessor always redirects requests with a content-type
** of application/x-fossil or application/x-fossil-debug to this page,
** regardless of what path was specified in the HTTP header.  This allows
** clone clients to specify a URL that omits default pathnames, such
** as "http://fossil-scm.org/" instead of "http://fossil-scm.org/index.cgi".







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
}

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

1278
1279
1280
1281
1282
1283
1284
  const char *zScript = 0;
  char *zUuidList = 0;
  int nUuidList = 0;
  char **pzUuidList = 0;
  int *pnUuidList = 0;
  int uvCatalogSent = 0;
  int bSendLinks = 0;


  if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
     fossil_redirect_home();
  }
  g.zLogin = "anonymous";
  login_set_anon_nobody_capabilities();
  login_check_credentials();







>







1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
  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
    @ 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;














  }
  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







>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
    @ 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 */
    assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" );
    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
1548
1549
1550
1551
1552
1553
1554


1555


1556
1557
1558
1559


1560
1561





1562
1563
1564
1565
1566
1567
1568
      @ 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.


    ** The client can send multiple logins.  Permissions are cumulative.


    */
    if( blob_eq(&xfer.aToken[0], "login")
     && xfer.nToken==4
    ){


      if( disableLogin ){
        g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 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();
          @ error login\sfailed
          nErr++;







>
>
|
>
>




>
>


>
>
>
>
>







1578
1579
1580
1581
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
      @ 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++;
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
      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 ){







<







1690
1691
1692
1693
1694
1695
1696

1697
1698
1699
1700
1701
1702
1703
      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 ){
1692
1693
1694
1695
1696
1697
1698

1699
1700
1701
1702
1703
1704
1705


1706
1707
1708
1709
1710
1711
1712
      /*   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 = 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)
        }


      }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.







>
|






>
>







1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
      /*   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.
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
        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 that the login-card nonce will always
    ** be unique.
    */
    zRandomness = db_text(0, "SELECT hex(randomblob(20))");
    blob_appendf(&send, "# %s\n", zRandomness);
    free(zRandomness);

    if( (syncFlags & SYNC_VERBOSE)!=0
     && (syncFlags & SYNC_XVERBOSE)==0
    ){
      fossil_print("waiting for server...");
    }
    fflush(stdout);







<






|







2382
2383
2384
2385
2386
2387
2388

2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
        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 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);
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
        }
      }else

      /*   message MESSAGE
      **
      ** A message is received from the server.  Print it.
      ** Similar to "error" but does not stop processing.
      **
      ** If the "login failed" message is seen, clear the sync password prior
      ** to the next cycle.
      */
      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;







<
<
<







2765
2766
2767
2768
2769
2770
2771



2772
2773
2774
2775
2776
2777
2778
        }
      }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;
2755
2756
2757
2758
2759
2760
2761

2762
2763
2764
2765
2766


2767
2768
2769
2770
2771
2772
2773
        /*   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 = 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 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







>
|




>
>







2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
        /*   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
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
          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.aToken[0]);
      }

      if( blob_size(&xfer.err) ){
        fossil_force_newline();
        fossil_warning("%b", &xfer.err);
        nErr++;
        break;







|







2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
          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;
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
    if( go ){
      manifest_crosslink_end(MC_PERMIT_HOOKS);
    }else{
      manifest_crosslink_end(MC_PERMIT_HOOKS);
      content_enable_dephantomize(1);
    }
    db_end_transaction(0);
  };
  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 ){







|







3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
    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 ){
Changes to www/changes.wiki.
11
12
13
14
15
16
17


18
19
20
21
22
23
24
       so that it works with other query parameters like p=, d=, from=, and to=.
  <li> Always include nodes identify by sel1= and sel2= in the /timeline display.
  <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command.
  <li> Require at least an anonymous login to access the /blame page and similar,
       to help prevent robots from soaking up excess CPU time on such pages.
  <li> When walking the filesystem looking for Fossil repositories, avoid descending
       into directories named "/proc".


  </ol>

<h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol>
 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
     <ol type="a">
     <li> The argument to the --from option can be a directory name, causing
          Fossil to use files under that directory as the baseline for the diff.







>
>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
       so that it works with other query parameters like p=, d=, from=, and to=.
  <li> Always include nodes identify by sel1= and sel2= in the /timeline display.
  <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command.
  <li> Require at least an anonymous login to access the /blame page and similar,
       to help prevent robots from soaking up excess CPU time on such pages.
  <li> When walking the filesystem looking for Fossil repositories, avoid descending
       into directories named "/proc".
  <ll> Reduce memory requirements for sending authenticated sync protocol
       messages.
  </ol>

<h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol>
 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
     <ol type="a">
     <li> The argument to the --from option can be a directory name, causing
          Fossil to use files under that directory as the baseline for the diff.
Changes to www/sync.wiki.
218
219
220
221
222
223
224
225

226
227
228
229
230
231
232
233
234
235
236
237




238


239



240



241
242
243
244
245
246
247
<h3 id="login">3.2 Login Cards</h3>

Every message from client to server begins with one or more login
cards.  Each login card has the following format:

<pre><b>login</b>  <i>userid  nonce  signature</i></pre>

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
signature matches.  If everything
checks out, then the client is granted all privileges of the
specified user.





Privileges are cumulative.  There can be multiple successful


login cards.  The session privilege is the union of all



privileges from all login cards.




<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.







|
>
|
|
|
|

|
|
|
<
|
|

>
>
>
>
|
>
>
|
>
>
>
|
>
>
>







218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234

235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
<h3 id="login">3.2 Login Cards</h3>

Every message from client to server begins with one or more login
cards.  Each login card has the following format:

<pre><b>login</b>  <i>userid  nonce  signature</i></pre>

The userid is the name of the user that is requesting service from the
server, encoded in "fossilized" form (exactly as described for <a
href="#error">the error card</a>).  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.

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.

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
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.)

As of version 2.27, Fossil supports transfering of the login card
externally to the request payload via a Cookie HTTP header:

<verbatim>
  Cookie: x-f-x-l=...
</verbatim>

Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is
short for X-Fossil-Xfer-Login.


<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.