Fossil SCM

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.

stephan 2025-07-27 11:07 trunk merge
Commit 18628904c30c9c55cee173738cf606cab5fcf92631b1b2331758243224ad4114
+22 -9
--- src/cgi.c
+++ src/cgi.c
@@ -964,11 +964,11 @@
964964
** * it is impossible for a cookie or query parameter to
965965
** override the value of an environment variable since
966966
** environment variables always have uppercase names.
967967
**
968968
** 2018-03-29: Also ignore the entry if NAME that contains any characters
969
-** other than [a-zA-Z0-9_]. There are no known exploits involving unusual
969
+** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual
970970
** names that contain characters outside that set, but it never hurts to
971971
** be extra cautious when sanitizing inputs.
972972
**
973973
** Parameters are separated by the "terminator" character. Whitespace
974974
** before the NAME is ignored.
@@ -1280,36 +1280,49 @@
12801280
12811281
/* Forward declaration */
12821282
static NORETURN void malformed_request(const char *zMsg, ...);
12831283
12841284
/*
1285
-** Checks the QUERY_STRING environment variable, sets it up
1286
-** via add_param_list() and, if found, applies its "skin"
1287
-** setting. Returns 0 if no QUERY_STRING is set, 1 if it is,
1288
-** and 2 if it sets the skin (in which case the cookie may
1289
-** still need flushing by the page, via cookie_render()).
1285
+** Checks the QUERY_STRING environment variable, sets it up via
1286
+** add_param_list() and, if found, applies its "skin" setting. Returns
1287
+** 0 if no QUERY_STRING is set, else it returns a bitmask of:
1288
+**
1289
+** 0x01 = QUERY_STRING was set up
1290
+** 0x02 = "skin" URL param arg was processed
1291
+** 0x04 = "x-f-l-c" cookie arg was processed.
1292
+**
1293
+* In the case of the skin, the cookie may still need flushing
1294
+** by the page, via cookie_render().
12901295
*/
12911296
int cgi_setup_query_string(void){
12921297
int rc = 0;
12931298
char * z = (char*)P("QUERY_STRING");
12941299
if( z ){
1295
- ++rc;
1300
+ rc = 0x01;
12961301
z = fossil_strdup(z);
12971302
add_param_list(z, '&');
12981303
z = (char*)P("skin");
12991304
if( z ){
13001305
char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
1301
- ++rc;
1306
+ rc |= 0x02;
13021307
if( !zErr && P("once")==0 ){
13031308
cookie_write_parameter("skin","skin",z);
13041309
/* Per /chat discussion, passing ?skin=... without "once"
13051310
** implies the "udc" argument, so we force that into the
13061311
** environment here. */
13071312
cgi_set_parameter_nocopy("udc", "1", 1);
13081313
}
13091314
fossil_free(zErr);
13101315
}
1316
+ }
1317
+ if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){
1318
+ /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie
1319
+ ** instead of in the sync payload. */
1320
+ rc |= 0x04;
1321
+ g.syncInfo.zLoginCard = fossil_strdup(z);
1322
+ g.syncInfo.fLoginCardMode |= 0x02;
1323
+ cgi_delete_parameter("x-f-l-c");
13111324
}
13121325
return rc;
13131326
}
13141327
13151328
/*
@@ -2125,10 +2138,11 @@
21252138
int i;
21262139
const char *zScheme = "http";
21272140
char zLine[2000]; /* A single line of input. */
21282141
g.fullHttpReply = 1;
21292142
g.zReqType = "HTTP";
2143
+
21302144
if( cgi_fgets(zLine, sizeof(zLine))==0 ){
21312145
malformed_request("missing header");
21322146
}
21332147
blob_append(&g.httpHeader, zLine, -1);
21342148
cgi_trace(zLine);
@@ -2160,11 +2174,10 @@
21602174
}
21612175
if( zIpAddr ){
21622176
cgi_setenv("REMOTE_ADDR", zIpAddr);
21632177
g.zIpAddr = fossil_strdup(zIpAddr);
21642178
}
2165
-
21662179
21672180
/* Get all the optional fields that follow the first line.
21682181
*/
21692182
while( cgi_fgets(zLine,sizeof(zLine)) ){
21702183
char *zFieldName;
21712184
--- src/cgi.c
+++ src/cgi.c
@@ -964,11 +964,11 @@
964 ** * it is impossible for a cookie or query parameter to
965 ** override the value of an environment variable since
966 ** environment variables always have uppercase names.
967 **
968 ** 2018-03-29: Also ignore the entry if NAME that contains any characters
969 ** other than [a-zA-Z0-9_]. There are no known exploits involving unusual
970 ** names that contain characters outside that set, but it never hurts to
971 ** be extra cautious when sanitizing inputs.
972 **
973 ** Parameters are separated by the "terminator" character. Whitespace
974 ** before the NAME is ignored.
@@ -1280,36 +1280,49 @@
1280
1281 /* Forward declaration */
1282 static NORETURN void malformed_request(const char *zMsg, ...);
1283
1284 /*
1285 ** Checks the QUERY_STRING environment variable, sets it up
1286 ** via add_param_list() and, if found, applies its "skin"
1287 ** setting. Returns 0 if no QUERY_STRING is set, 1 if it is,
1288 ** and 2 if it sets the skin (in which case the cookie may
1289 ** still need flushing by the page, via cookie_render()).
 
 
 
 
 
1290 */
1291 int cgi_setup_query_string(void){
1292 int rc = 0;
1293 char * z = (char*)P("QUERY_STRING");
1294 if( z ){
1295 ++rc;
1296 z = fossil_strdup(z);
1297 add_param_list(z, '&');
1298 z = (char*)P("skin");
1299 if( z ){
1300 char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
1301 ++rc;
1302 if( !zErr && P("once")==0 ){
1303 cookie_write_parameter("skin","skin",z);
1304 /* Per /chat discussion, passing ?skin=... without "once"
1305 ** implies the "udc" argument, so we force that into the
1306 ** environment here. */
1307 cgi_set_parameter_nocopy("udc", "1", 1);
1308 }
1309 fossil_free(zErr);
1310 }
 
 
 
 
 
 
 
 
1311 }
1312 return rc;
1313 }
1314
1315 /*
@@ -2125,10 +2138,11 @@
2125 int i;
2126 const char *zScheme = "http";
2127 char zLine[2000]; /* A single line of input. */
2128 g.fullHttpReply = 1;
2129 g.zReqType = "HTTP";
 
2130 if( cgi_fgets(zLine, sizeof(zLine))==0 ){
2131 malformed_request("missing header");
2132 }
2133 blob_append(&g.httpHeader, zLine, -1);
2134 cgi_trace(zLine);
@@ -2160,11 +2174,10 @@
2160 }
2161 if( zIpAddr ){
2162 cgi_setenv("REMOTE_ADDR", zIpAddr);
2163 g.zIpAddr = fossil_strdup(zIpAddr);
2164 }
2165
2166
2167 /* Get all the optional fields that follow the first line.
2168 */
2169 while( cgi_fgets(zLine,sizeof(zLine)) ){
2170 char *zFieldName;
2171
--- src/cgi.c
+++ src/cgi.c
@@ -964,11 +964,11 @@
964 ** * it is impossible for a cookie or query parameter to
965 ** override the value of an environment variable since
966 ** environment variables always have uppercase names.
967 **
968 ** 2018-03-29: Also ignore the entry if NAME that contains any characters
969 ** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual
970 ** names that contain characters outside that set, but it never hurts to
971 ** be extra cautious when sanitizing inputs.
972 **
973 ** Parameters are separated by the "terminator" character. Whitespace
974 ** before the NAME is ignored.
@@ -1280,36 +1280,49 @@
1280
1281 /* Forward declaration */
1282 static NORETURN void malformed_request(const char *zMsg, ...);
1283
1284 /*
1285 ** Checks the QUERY_STRING environment variable, sets it up via
1286 ** add_param_list() and, if found, applies its "skin" setting. Returns
1287 ** 0 if no QUERY_STRING is set, else it returns a bitmask of:
1288 **
1289 ** 0x01 = QUERY_STRING was set up
1290 ** 0x02 = "skin" URL param arg was processed
1291 ** 0x04 = "x-f-l-c" cookie arg was processed.
1292 **
1293 * In the case of the skin, the cookie may still need flushing
1294 ** by the page, via cookie_render().
1295 */
1296 int cgi_setup_query_string(void){
1297 int rc = 0;
1298 char * z = (char*)P("QUERY_STRING");
1299 if( z ){
1300 rc = 0x01;
1301 z = fossil_strdup(z);
1302 add_param_list(z, '&');
1303 z = (char*)P("skin");
1304 if( z ){
1305 char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
1306 rc |= 0x02;
1307 if( !zErr && P("once")==0 ){
1308 cookie_write_parameter("skin","skin",z);
1309 /* Per /chat discussion, passing ?skin=... without "once"
1310 ** implies the "udc" argument, so we force that into the
1311 ** environment here. */
1312 cgi_set_parameter_nocopy("udc", "1", 1);
1313 }
1314 fossil_free(zErr);
1315 }
1316 }
1317 if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){
1318 /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie
1319 ** instead of in the sync payload. */
1320 rc |= 0x04;
1321 g.syncInfo.zLoginCard = fossil_strdup(z);
1322 g.syncInfo.fLoginCardMode |= 0x02;
1323 cgi_delete_parameter("x-f-l-c");
1324 }
1325 return rc;
1326 }
1327
1328 /*
@@ -2125,10 +2138,11 @@
2138 int i;
2139 const char *zScheme = "http";
2140 char zLine[2000]; /* A single line of input. */
2141 g.fullHttpReply = 1;
2142 g.zReqType = "HTTP";
2143
2144 if( cgi_fgets(zLine, sizeof(zLine))==0 ){
2145 malformed_request("missing header");
2146 }
2147 blob_append(&g.httpHeader, zLine, -1);
2148 cgi_trace(zLine);
@@ -2160,11 +2174,10 @@
2174 }
2175 if( zIpAddr ){
2176 cgi_setenv("REMOTE_ADDR", zIpAddr);
2177 g.zIpAddr = fossil_strdup(zIpAddr);
2178 }
 
2179
2180 /* Get all the optional fields that follow the first line.
2181 */
2182 while( cgi_fgets(zLine,sizeof(zLine)) ){
2183 char *zFieldName;
2184
+51 -22
--- src/http.c
+++ src/http.c
@@ -52,30 +52,33 @@
5252
** Construct the "login" card with the client credentials.
5353
**
5454
** login LOGIN NONCE SIGNATURE
5555
**
5656
** The LOGIN is the user id of the client. NONCE is the sha1 checksum
57
-** of all payload that follows the login card. Randomness for the NONCE
58
-** must be provided in the payload (in xfer.c). SIGNATURE is the sha1
59
-** checksum of the nonce followed by the user password.
57
+** of all payload that follows the login card. Randomness for the
58
+** NONCE must be provided in the payload (in xfer.c) (e.g. by
59
+** appending a timestamp or random bytes as a comment line to the
60
+** payload). SIGNATURE is the sha1 checksum of the nonce followed by
61
+** the fossil-hashed version of the user's password.
6062
**
61
-** Write the constructed login card into pLogin. pLogin is initialized
62
-** by this routine.
63
+** Write the constructed login card into pLogin. The result does not
64
+** have an EOL added to it because which type of EOL it needs has to
65
+** be determined later. pLogin is initialized by this routine.
6366
*/
64
-static void http_build_login_card(Blob *pPayload, Blob *pLogin){
67
+static void http_build_login_card(Blob * const pPayload, Blob * const pLogin){
6568
Blob nonce; /* The nonce */
6669
const char *zLogin; /* The user login name */
6770
const char *zPw; /* The user password */
6871
Blob pw; /* The nonce with user password appended */
6972
Blob sig; /* The signature field */
7073
7174
blob_zero(pLogin);
7275
if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){
73
- return; /* If no login card for users "nobody" and "anonymous" */
76
+ return; /* No login card for users "nobody" and "anonymous" */
7477
}
7578
if( g.url.isSsh ){
76
- return; /* If no login card for SSH: */
79
+ return; /* No login card for SSH: */
7780
}
7881
blob_zero(&nonce);
7982
blob_zero(&pw);
8083
sha1sum_blob(pPayload, &nonce);
8184
blob_copy(&pw, &nonce);
@@ -119,32 +122,34 @@
119122
g.url.passwd = fossil_strdup(zPw);
120123
}
121124
122125
blob_append(&pw, zPw, -1);
123126
sha1sum_blob(&pw, &sig);
124
- blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig);
127
+ blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
125128
blob_reset(&pw);
126129
blob_reset(&sig);
127130
blob_reset(&nonce);
128131
}
129132
130133
/*
131134
** Construct an appropriate HTTP request header. Write the header
132135
** into pHdr. This routine initializes the pHdr blob. pPayload is
133
-** the complete payload (including the login card) already compressed.
136
+** the complete payload (including the login card if pLogin is NULL or
137
+** empty) already compressed.
134138
*/
135139
static void http_build_header(
136140
Blob *pPayload, /* the payload that will be sent */
137141
Blob *pHdr, /* construct the header here */
142
+ Blob *pLogin, /* Login card header value or NULL */
138143
const char *zAltMimetype /* Alternative mimetype */
139144
){
140145
int nPayload = pPayload ? blob_size(pPayload) : 0;
141146
142147
blob_zero(pHdr);
143
- blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n",
144
- nPayload>0 ? "POST" : "GET", g.url.path,
145
- g.url.path[0]==0 ? "/" : "");
148
+ blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
149
+ nPayload>0 ? "POST" : "GET",
150
+ (g.url.path && g.url.path[0]) ? g.url.path : "/");
146151
if( g.url.proxyAuth ){
147152
blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
148153
}
149154
if( g.zHttpAuth && g.zHttpAuth[0] ){
150155
const char *zCredentials = g.zHttpAuth;
@@ -153,10 +158,16 @@
153158
fossil_free(zEncoded);
154159
}
155160
blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
156161
blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
157162
if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
163
+ if( g.syncInfo.fLoginCardMode>0
164
+ && nPayload>0 && pLogin && blob_size(pLogin) ){
165
+ /* Add sync login card via a transient cookie. We can only do this
166
+ if we know the remote supports it. */
167
+ blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin));
168
+ }
158169
if( nPayload ){
159170
if( zAltMimetype ){
160171
blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
161172
}else if( g.fHttpTrace ){
162173
blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
@@ -386,11 +397,11 @@
386397
** * The test-ssh-needs-path command that shows the settings
387398
** that cache whether or not a PATH= is needed for a particular
388399
** HOSTNAME.
389400
*/
390401
void ssh_add_path_argument(Blob *pCmd){
391
- blob_append_escaped_arg(pCmd,
402
+ blob_append_escaped_arg(pCmd,
392403
"PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
393404
}
394405
395406
/*
396407
** Return the complete text of the last HTTP reply as saved in the
@@ -457,28 +468,46 @@
457468
458469
if( transport_open(&g.url) ){
459470
fossil_warning("%s", transport_errmsg(&g.url));
460471
return 1;
461472
}
462
-
463473
/* Construct the login card and prepare the complete payload */
474
+ blob_zero(&login);
464475
if( blob_size(pSend)==0 ){
465476
blob_zero(&payload);
466477
}else{
467
- blob_zero(&login);
468478
if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
469
- if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
470
- payload = login;
471
- blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
479
+ if( g.syncInfo.fLoginCardMode ){
480
+ /* The login card will be sent via an HTTP header and/or URL flag. */
481
+ if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
482
+ /* Maintenance note: we cannot blob_swap(pSend,&payload) here
483
+ ** because the HTTP 401 and redirect response handling below
484
+ ** needs pSend unmodified. payload won't be modified after
485
+ ** this point, so we can make it a proxy for pSend for
486
+ ** zero heap memory. */
487
+ blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
488
+ }else{
489
+ blob_compress(pSend, &payload);
490
+ }
472491
}else{
473
- blob_compress2(&login, pSend, &payload);
474
- blob_reset(&login);
492
+ /* Prepend the login card (if set) to the payload */
493
+ if( blob_size(&login) ){
494
+ blob_append_char(&login, '\n');
495
+ }
496
+ if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
497
+ payload = login;
498
+ login = empty_blob/*transfer ownership*/;
499
+ blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
500
+ }else{
501
+ blob_compress2(&login, pSend, &payload);
502
+ blob_reset(&login);
503
+ }
475504
}
476505
}
477506
478507
/* Construct the HTTP request header */
479
- http_build_header(&payload, &hdr, zAltMimetype);
508
+ http_build_header(&payload, &hdr, &login, zAltMimetype);
480509
481510
/* When tracing, write the transmitted HTTP message both to standard
482511
** output and into a file. The file can then be used to drive the
483512
** server-side like this:
484513
**
485514
--- src/http.c
+++ src/http.c
@@ -52,30 +52,33 @@
52 ** Construct the "login" card with the client credentials.
53 **
54 ** login LOGIN NONCE SIGNATURE
55 **
56 ** The LOGIN is the user id of the client. NONCE is the sha1 checksum
57 ** of all payload that follows the login card. Randomness for the NONCE
58 ** must be provided in the payload (in xfer.c). SIGNATURE is the sha1
59 ** checksum of the nonce followed by the user password.
 
 
60 **
61 ** Write the constructed login card into pLogin. pLogin is initialized
62 ** by this routine.
 
63 */
64 static void http_build_login_card(Blob *pPayload, Blob *pLogin){
65 Blob nonce; /* The nonce */
66 const char *zLogin; /* The user login name */
67 const char *zPw; /* The user password */
68 Blob pw; /* The nonce with user password appended */
69 Blob sig; /* The signature field */
70
71 blob_zero(pLogin);
72 if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){
73 return; /* If no login card for users "nobody" and "anonymous" */
74 }
75 if( g.url.isSsh ){
76 return; /* If no login card for SSH: */
77 }
78 blob_zero(&nonce);
79 blob_zero(&pw);
80 sha1sum_blob(pPayload, &nonce);
81 blob_copy(&pw, &nonce);
@@ -119,32 +122,34 @@
119 g.url.passwd = fossil_strdup(zPw);
120 }
121
122 blob_append(&pw, zPw, -1);
123 sha1sum_blob(&pw, &sig);
124 blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig);
125 blob_reset(&pw);
126 blob_reset(&sig);
127 blob_reset(&nonce);
128 }
129
130 /*
131 ** Construct an appropriate HTTP request header. Write the header
132 ** into pHdr. This routine initializes the pHdr blob. pPayload is
133 ** the complete payload (including the login card) already compressed.
 
134 */
135 static void http_build_header(
136 Blob *pPayload, /* the payload that will be sent */
137 Blob *pHdr, /* construct the header here */
 
138 const char *zAltMimetype /* Alternative mimetype */
139 ){
140 int nPayload = pPayload ? blob_size(pPayload) : 0;
141
142 blob_zero(pHdr);
143 blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n",
144 nPayload>0 ? "POST" : "GET", g.url.path,
145 g.url.path[0]==0 ? "/" : "");
146 if( g.url.proxyAuth ){
147 blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
148 }
149 if( g.zHttpAuth && g.zHttpAuth[0] ){
150 const char *zCredentials = g.zHttpAuth;
@@ -153,10 +158,16 @@
153 fossil_free(zEncoded);
154 }
155 blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
156 blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
157 if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
 
 
 
 
 
 
158 if( nPayload ){
159 if( zAltMimetype ){
160 blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
161 }else if( g.fHttpTrace ){
162 blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
@@ -386,11 +397,11 @@
386 ** * The test-ssh-needs-path command that shows the settings
387 ** that cache whether or not a PATH= is needed for a particular
388 ** HOSTNAME.
389 */
390 void ssh_add_path_argument(Blob *pCmd){
391 blob_append_escaped_arg(pCmd,
392 "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
393 }
394
395 /*
396 ** Return the complete text of the last HTTP reply as saved in the
@@ -457,28 +468,46 @@
457
458 if( transport_open(&g.url) ){
459 fossil_warning("%s", transport_errmsg(&g.url));
460 return 1;
461 }
462
463 /* Construct the login card and prepare the complete payload */
 
464 if( blob_size(pSend)==0 ){
465 blob_zero(&payload);
466 }else{
467 blob_zero(&login);
468 if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
469 if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
470 payload = login;
471 blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
 
 
 
 
 
 
 
 
 
472 }else{
473 blob_compress2(&login, pSend, &payload);
474 blob_reset(&login);
 
 
 
 
 
 
 
 
 
 
475 }
476 }
477
478 /* Construct the HTTP request header */
479 http_build_header(&payload, &hdr, zAltMimetype);
480
481 /* When tracing, write the transmitted HTTP message both to standard
482 ** output and into a file. The file can then be used to drive the
483 ** server-side like this:
484 **
485
--- src/http.c
+++ src/http.c
@@ -52,30 +52,33 @@
52 ** Construct the "login" card with the client credentials.
53 **
54 ** login LOGIN NONCE SIGNATURE
55 **
56 ** The LOGIN is the user id of the client. NONCE is the sha1 checksum
57 ** of all payload that follows the login card. Randomness for the
58 ** NONCE must be provided in the payload (in xfer.c) (e.g. by
59 ** appending a timestamp or random bytes as a comment line to the
60 ** payload). SIGNATURE is the sha1 checksum of the nonce followed by
61 ** the fossil-hashed version of the user's password.
62 **
63 ** Write the constructed login card into pLogin. The result does not
64 ** have an EOL added to it because which type of EOL it needs has to
65 ** be determined later. pLogin is initialized by this routine.
66 */
67 static void http_build_login_card(Blob * const pPayload, Blob * const pLogin){
68 Blob nonce; /* The nonce */
69 const char *zLogin; /* The user login name */
70 const char *zPw; /* The user password */
71 Blob pw; /* The nonce with user password appended */
72 Blob sig; /* The signature field */
73
74 blob_zero(pLogin);
75 if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){
76 return; /* No login card for users "nobody" and "anonymous" */
77 }
78 if( g.url.isSsh ){
79 return; /* No login card for SSH: */
80 }
81 blob_zero(&nonce);
82 blob_zero(&pw);
83 sha1sum_blob(pPayload, &nonce);
84 blob_copy(&pw, &nonce);
@@ -119,32 +122,34 @@
122 g.url.passwd = fossil_strdup(zPw);
123 }
124
125 blob_append(&pw, zPw, -1);
126 sha1sum_blob(&pw, &sig);
127 blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
128 blob_reset(&pw);
129 blob_reset(&sig);
130 blob_reset(&nonce);
131 }
132
133 /*
134 ** Construct an appropriate HTTP request header. Write the header
135 ** into pHdr. This routine initializes the pHdr blob. pPayload is
136 ** the complete payload (including the login card if pLogin is NULL or
137 ** empty) already compressed.
138 */
139 static void http_build_header(
140 Blob *pPayload, /* the payload that will be sent */
141 Blob *pHdr, /* construct the header here */
142 Blob *pLogin, /* Login card header value or NULL */
143 const char *zAltMimetype /* Alternative mimetype */
144 ){
145 int nPayload = pPayload ? blob_size(pPayload) : 0;
146
147 blob_zero(pHdr);
148 blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
149 nPayload>0 ? "POST" : "GET",
150 (g.url.path && g.url.path[0]) ? g.url.path : "/");
151 if( g.url.proxyAuth ){
152 blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
153 }
154 if( g.zHttpAuth && g.zHttpAuth[0] ){
155 const char *zCredentials = g.zHttpAuth;
@@ -153,10 +158,16 @@
158 fossil_free(zEncoded);
159 }
160 blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
161 blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
162 if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
163 if( g.syncInfo.fLoginCardMode>0
164 && nPayload>0 && pLogin && blob_size(pLogin) ){
165 /* Add sync login card via a transient cookie. We can only do this
166 if we know the remote supports it. */
167 blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin));
168 }
169 if( nPayload ){
170 if( zAltMimetype ){
171 blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
172 }else if( g.fHttpTrace ){
173 blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
@@ -386,11 +397,11 @@
397 ** * The test-ssh-needs-path command that shows the settings
398 ** that cache whether or not a PATH= is needed for a particular
399 ** HOSTNAME.
400 */
401 void ssh_add_path_argument(Blob *pCmd){
402 blob_append_escaped_arg(pCmd,
403 "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
404 }
405
406 /*
407 ** Return the complete text of the last HTTP reply as saved in the
@@ -457,28 +468,46 @@
468
469 if( transport_open(&g.url) ){
470 fossil_warning("%s", transport_errmsg(&g.url));
471 return 1;
472 }
 
473 /* Construct the login card and prepare the complete payload */
474 blob_zero(&login);
475 if( blob_size(pSend)==0 ){
476 blob_zero(&payload);
477 }else{
 
478 if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
479 if( g.syncInfo.fLoginCardMode ){
480 /* The login card will be sent via an HTTP header and/or URL flag. */
481 if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
482 /* Maintenance note: we cannot blob_swap(pSend,&payload) here
483 ** because the HTTP 401 and redirect response handling below
484 ** needs pSend unmodified. payload won't be modified after
485 ** this point, so we can make it a proxy for pSend for
486 ** zero heap memory. */
487 blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
488 }else{
489 blob_compress(pSend, &payload);
490 }
491 }else{
492 /* Prepend the login card (if set) to the payload */
493 if( blob_size(&login) ){
494 blob_append_char(&login, '\n');
495 }
496 if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
497 payload = login;
498 login = empty_blob/*transfer ownership*/;
499 blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
500 }else{
501 blob_compress2(&login, pSend, &payload);
502 blob_reset(&login);
503 }
504 }
505 }
506
507 /* Construct the HTTP request header */
508 http_build_header(&payload, &hdr, &login, zAltMimetype);
509
510 /* When tracing, write the transmitted HTTP message both to standard
511 ** output and into a file. The file can then be used to drive the
512 ** server-side like this:
513 **
514
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141141
){
142142
fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143143
"the server.", pUrlData->fossil);
144144
}
145145
if( (pUrlData->flags & URL_SSH_EXE)==0
146
- && (pUrlData->flags & URL_SSH_PATH)!=0
146
+ && (pUrlData->flags & URL_SSH_PATH)!=0
147147
){
148148
ssh_add_path_argument(&zCmd);
149149
}
150150
blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151151
blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245245
}
246246
247247
/*
248248
** Send content over the wire.
249249
*/
250
-void transport_send(UrlData *pUrlData, Blob *toSend){
250
+void transport_send(UrlData const *pUrlData, const Blob *toSend){
251251
char *z = blob_buffer(toSend);
252252
int n = blob_size(toSend);
253253
transport.nSent += n;
254254
if( pUrlData->isSsh ){
255255
fwrite(z, 1, n, sshOut);
256256
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141 ){
142 fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143 "the server.", pUrlData->fossil);
144 }
145 if( (pUrlData->flags & URL_SSH_EXE)==0
146 && (pUrlData->flags & URL_SSH_PATH)!=0
147 ){
148 ssh_add_path_argument(&zCmd);
149 }
150 blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151 blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245 }
246
247 /*
248 ** Send content over the wire.
249 */
250 void transport_send(UrlData *pUrlData, Blob *toSend){
251 char *z = blob_buffer(toSend);
252 int n = blob_size(toSend);
253 transport.nSent += n;
254 if( pUrlData->isSsh ){
255 fwrite(z, 1, n, sshOut);
256
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141 ){
142 fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143 "the server.", pUrlData->fossil);
144 }
145 if( (pUrlData->flags & URL_SSH_EXE)==0
146 && (pUrlData->flags & URL_SSH_PATH)!=0
147 ){
148 ssh_add_path_argument(&zCmd);
149 }
150 blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151 blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245 }
246
247 /*
248 ** Send content over the wire.
249 */
250 void transport_send(UrlData const *pUrlData, const Blob *toSend){
251 char *z = blob_buffer(toSend);
252 int n = blob_size(toSend);
253 transport.nSent += n;
254 if( pUrlData->isSsh ){
255 fwrite(z, 1, n, sshOut);
256
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141141
){
142142
fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143143
"the server.", pUrlData->fossil);
144144
}
145145
if( (pUrlData->flags & URL_SSH_EXE)==0
146
- && (pUrlData->flags & URL_SSH_PATH)!=0
146
+ && (pUrlData->flags & URL_SSH_PATH)!=0
147147
){
148148
ssh_add_path_argument(&zCmd);
149149
}
150150
blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151151
blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245245
}
246246
247247
/*
248248
** Send content over the wire.
249249
*/
250
-void transport_send(UrlData *pUrlData, Blob *toSend){
250
+void transport_send(UrlData const *pUrlData, const Blob *toSend){
251251
char *z = blob_buffer(toSend);
252252
int n = blob_size(toSend);
253253
transport.nSent += n;
254254
if( pUrlData->isSsh ){
255255
fwrite(z, 1, n, sshOut);
256256
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141 ){
142 fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143 "the server.", pUrlData->fossil);
144 }
145 if( (pUrlData->flags & URL_SSH_EXE)==0
146 && (pUrlData->flags & URL_SSH_PATH)!=0
147 ){
148 ssh_add_path_argument(&zCmd);
149 }
150 blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151 blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245 }
246
247 /*
248 ** Send content over the wire.
249 */
250 void transport_send(UrlData *pUrlData, Blob *toSend){
251 char *z = blob_buffer(toSend);
252 int n = blob_size(toSend);
253 transport.nSent += n;
254 if( pUrlData->isSsh ){
255 fwrite(z, 1, n, sshOut);
256
--- src/http_transport.c
+++ src/http_transport.c
@@ -141,11 +141,11 @@
141 ){
142 fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
143 "the server.", pUrlData->fossil);
144 }
145 if( (pUrlData->flags & URL_SSH_EXE)==0
146 && (pUrlData->flags & URL_SSH_PATH)!=0
147 ){
148 ssh_add_path_argument(&zCmd);
149 }
150 blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
151 blob_append(&zCmd, " test-http", 10);
@@ -245,11 +245,11 @@
245 }
246
247 /*
248 ** Send content over the wire.
249 */
250 void transport_send(UrlData const *pUrlData, const Blob *toSend){
251 char *z = blob_buffer(toSend);
252 int n = blob_size(toSend);
253 transport.nSent += n;
254 if( pUrlData->isSsh ){
255 fwrite(z, 1, n, sshOut);
256
+30 -6
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288288
int allowSymlinks; /* Cached "allow-symlinks" option */
289289
int mainTimerId; /* Set to fossil_timer_start() */
290290
int nPendingRequest; /* # of HTTP requests in "fossil server" */
291291
int nRequest; /* Total # of HTTP request */
292292
int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
293
+
294
+ /* State for communicating specific details between the inbound HTTP
295
+ ** header parser (cgi.c), xfer.c, and http.c. */
296
+ struct {
297
+ char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */
298
+ int fLoginCardMode; /* If non-0, emit login cards in outbound
299
+ ** requests as a HTTP cookie instead of as
300
+ ** part of the payload. Gets activated
301
+ ** on-demand based on xfer traffic
302
+ ** contents. Values, for
303
+ ** diagnostic/debugging purposes: 0x01=CLI
304
+ ** --flag, 0x02=cgi_setup_query_string(),
305
+ ** 0x04=page_xfer(),
306
+ ** 0x08=client_sync(). */
307
+ int remoteVersion; /* Remote fossil version. Used for negotiating
308
+ ** how to handle the login card. */
309
+ } syncInfo;
293310
#ifdef FOSSIL_ENABLE_JSON
294311
struct FossilJsonBits {
295312
int isJsonMode; /* True if running in JSON mode, else
296313
false. This changes how errors are
297314
reported. In JSON mode we try to
@@ -758,10 +775,17 @@
758775
g.tcl.argc = g.argc;
759776
g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
760777
#endif
761778
g.mainTimerId = fossil_timer_start();
762779
capture_case_sensitive_option();
780
+ g.syncInfo.fLoginCardMode =
781
+ /* The undocumented/unsupported --login-card-header provides a way
782
+ ** to force use of the feature added by the xfer-login-card branch
783
+ ** in 2025-07, intended for assisting in debugging any related
784
+ ** issues. It can be removed once we reach the level of "implicit
785
+ ** trust" in that feature. */
786
+ find_option("login-card-header",0,0) ? 0x01 : 0;
763787
g.zVfsName = find_option("vfs",0,1);
764788
if( g.zVfsName==0 ){
765789
g.zVfsName = fossil_getenv("FOSSIL_VFS");
766790
}
767791
if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
14901514
/* In order for ?skin=... to work when visiting the site from
14911515
** a typical external link, we have to process it here, as
14921516
** that parameter gets lost during the redirect. We "could"
14931517
** pass the whole query string along instead, but that seems
14941518
** unnecessary. */
1495
- if(cgi_setup_query_string()>1){
1519
+ if(cgi_setup_query_string() & 0x02){
14961520
cookie_render();
14971521
}
14981522
cgi_redirectf("%R%s", db_get("index-page", "/index"));
14991523
}
15001524
@@ -1794,22 +1818,22 @@
17941818
}
17951819
17961820
17971821
/* Restrictions on the URI for security:
17981822
**
1799
- ** 1. Reject characters that are not ASCII alphanumerics,
1823
+ ** 1. Reject characters that are not ASCII alphanumerics,
18001824
** "-", "_", ".", "/", or unicode (above ASCII).
18011825
** In other words: No ASCII punctuation or control characters
18021826
** other than "-", "_", "." and "/".
1803
- ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1827
+ ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
18041828
** alphabetic character at the beginning of the name on windows.
18051829
** 3. "-" may not occur immediately after "/"
18061830
** 4. "." may not be adjacent to another "." or to "/"
18071831
**
18081832
** Any character does not satisfy these constraints a Not Found
18091833
** error is returned.
1810
- */
1834
+ */
18111835
szFile = 0;
18121836
for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
18131837
char c = zRepo[j];
18141838
if( c>='a' && c<='z' ) continue;
18151839
if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
35103534
** "fossil ui --nobrowser" on the remote system and to set up a
35113535
** tunnel from the local machine to the remote. */
35123536
FILE *sshIn;
35133537
Blob ssh;
35143538
int bRunning = 0; /* True when fossil starts up on the remote */
3515
- int isRetry; /* True if on the second attempt */
3539
+ int isRetry; /* True if on the second attempt */
35163540
char zLine[1000];
35173541
35183542
blob_init(&ssh, 0, 0);
35193543
for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
35203544
blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
35493573
if( fCreate ) blob_appendf(&ssh, " --create");
35503574
blob_appendf(&ssh, " %$", g.argv[2]);
35513575
if( isRetry ){
35523576
fossil_print("First attempt to run \"fossil\" on %s failed\n"
35533577
"Retry: ", zRemote);
3554
- }
3578
+ }
35553579
fossil_print("%s\n", blob_str(&ssh));
35563580
sshIn = popen(blob_str(&ssh), "r");
35573581
if( sshIn==0 ){
35583582
fossil_fatal("unable to %s", blob_str(&ssh));
35593583
}
35603584
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288 int allowSymlinks; /* Cached "allow-symlinks" option */
289 int mainTimerId; /* Set to fossil_timer_start() */
290 int nPendingRequest; /* # of HTTP requests in "fossil server" */
291 int nRequest; /* Total # of HTTP request */
292 int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293 #ifdef FOSSIL_ENABLE_JSON
294 struct FossilJsonBits {
295 int isJsonMode; /* True if running in JSON mode, else
296 false. This changes how errors are
297 reported. In JSON mode we try to
@@ -758,10 +775,17 @@
758 g.tcl.argc = g.argc;
759 g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
760 #endif
761 g.mainTimerId = fossil_timer_start();
762 capture_case_sensitive_option();
 
 
 
 
 
 
 
763 g.zVfsName = find_option("vfs",0,1);
764 if( g.zVfsName==0 ){
765 g.zVfsName = fossil_getenv("FOSSIL_VFS");
766 }
767 if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
1490 /* In order for ?skin=... to work when visiting the site from
1491 ** a typical external link, we have to process it here, as
1492 ** that parameter gets lost during the redirect. We "could"
1493 ** pass the whole query string along instead, but that seems
1494 ** unnecessary. */
1495 if(cgi_setup_query_string()>1){
1496 cookie_render();
1497 }
1498 cgi_redirectf("%R%s", db_get("index-page", "/index"));
1499 }
1500
@@ -1794,22 +1818,22 @@
1794 }
1795
1796
1797 /* Restrictions on the URI for security:
1798 **
1799 ** 1. Reject characters that are not ASCII alphanumerics,
1800 ** "-", "_", ".", "/", or unicode (above ASCII).
1801 ** In other words: No ASCII punctuation or control characters
1802 ** other than "-", "_", "." and "/".
1803 ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1804 ** alphabetic character at the beginning of the name on windows.
1805 ** 3. "-" may not occur immediately after "/"
1806 ** 4. "." may not be adjacent to another "." or to "/"
1807 **
1808 ** Any character does not satisfy these constraints a Not Found
1809 ** error is returned.
1810 */
1811 szFile = 0;
1812 for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
1813 char c = zRepo[j];
1814 if( c>='a' && c<='z' ) continue;
1815 if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
3510 ** "fossil ui --nobrowser" on the remote system and to set up a
3511 ** tunnel from the local machine to the remote. */
3512 FILE *sshIn;
3513 Blob ssh;
3514 int bRunning = 0; /* True when fossil starts up on the remote */
3515 int isRetry; /* True if on the second attempt */
3516 char zLine[1000];
3517
3518 blob_init(&ssh, 0, 0);
3519 for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
3520 blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
3549 if( fCreate ) blob_appendf(&ssh, " --create");
3550 blob_appendf(&ssh, " %$", g.argv[2]);
3551 if( isRetry ){
3552 fossil_print("First attempt to run \"fossil\" on %s failed\n"
3553 "Retry: ", zRemote);
3554 }
3555 fossil_print("%s\n", blob_str(&ssh));
3556 sshIn = popen(blob_str(&ssh), "r");
3557 if( sshIn==0 ){
3558 fossil_fatal("unable to %s", blob_str(&ssh));
3559 }
3560
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288 int allowSymlinks; /* Cached "allow-symlinks" option */
289 int mainTimerId; /* Set to fossil_timer_start() */
290 int nPendingRequest; /* # of HTTP requests in "fossil server" */
291 int nRequest; /* Total # of HTTP request */
292 int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
293
294 /* State for communicating specific details between the inbound HTTP
295 ** header parser (cgi.c), xfer.c, and http.c. */
296 struct {
297 char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */
298 int fLoginCardMode; /* If non-0, emit login cards in outbound
299 ** requests as a HTTP cookie instead of as
300 ** part of the payload. Gets activated
301 ** on-demand based on xfer traffic
302 ** contents. Values, for
303 ** diagnostic/debugging purposes: 0x01=CLI
304 ** --flag, 0x02=cgi_setup_query_string(),
305 ** 0x04=page_xfer(),
306 ** 0x08=client_sync(). */
307 int remoteVersion; /* Remote fossil version. Used for negotiating
308 ** how to handle the login card. */
309 } syncInfo;
310 #ifdef FOSSIL_ENABLE_JSON
311 struct FossilJsonBits {
312 int isJsonMode; /* True if running in JSON mode, else
313 false. This changes how errors are
314 reported. In JSON mode we try to
@@ -758,10 +775,17 @@
775 g.tcl.argc = g.argc;
776 g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
777 #endif
778 g.mainTimerId = fossil_timer_start();
779 capture_case_sensitive_option();
780 g.syncInfo.fLoginCardMode =
781 /* The undocumented/unsupported --login-card-header provides a way
782 ** to force use of the feature added by the xfer-login-card branch
783 ** in 2025-07, intended for assisting in debugging any related
784 ** issues. It can be removed once we reach the level of "implicit
785 ** trust" in that feature. */
786 find_option("login-card-header",0,0) ? 0x01 : 0;
787 g.zVfsName = find_option("vfs",0,1);
788 if( g.zVfsName==0 ){
789 g.zVfsName = fossil_getenv("FOSSIL_VFS");
790 }
791 if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
1514 /* In order for ?skin=... to work when visiting the site from
1515 ** a typical external link, we have to process it here, as
1516 ** that parameter gets lost during the redirect. We "could"
1517 ** pass the whole query string along instead, but that seems
1518 ** unnecessary. */
1519 if(cgi_setup_query_string() & 0x02){
1520 cookie_render();
1521 }
1522 cgi_redirectf("%R%s", db_get("index-page", "/index"));
1523 }
1524
@@ -1794,22 +1818,22 @@
1818 }
1819
1820
1821 /* Restrictions on the URI for security:
1822 **
1823 ** 1. Reject characters that are not ASCII alphanumerics,
1824 ** "-", "_", ".", "/", or unicode (above ASCII).
1825 ** In other words: No ASCII punctuation or control characters
1826 ** other than "-", "_", "." and "/".
1827 ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1828 ** alphabetic character at the beginning of the name on windows.
1829 ** 3. "-" may not occur immediately after "/"
1830 ** 4. "." may not be adjacent to another "." or to "/"
1831 **
1832 ** Any character does not satisfy these constraints a Not Found
1833 ** error is returned.
1834 */
1835 szFile = 0;
1836 for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
1837 char c = zRepo[j];
1838 if( c>='a' && c<='z' ) continue;
1839 if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
3534 ** "fossil ui --nobrowser" on the remote system and to set up a
3535 ** tunnel from the local machine to the remote. */
3536 FILE *sshIn;
3537 Blob ssh;
3538 int bRunning = 0; /* True when fossil starts up on the remote */
3539 int isRetry; /* True if on the second attempt */
3540 char zLine[1000];
3541
3542 blob_init(&ssh, 0, 0);
3543 for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
3544 blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
3573 if( fCreate ) blob_appendf(&ssh, " --create");
3574 blob_appendf(&ssh, " %$", g.argv[2]);
3575 if( isRetry ){
3576 fossil_print("First attempt to run \"fossil\" on %s failed\n"
3577 "Retry: ", zRemote);
3578 }
3579 fossil_print("%s\n", blob_str(&ssh));
3580 sshIn = popen(blob_str(&ssh), "r");
3581 if( sshIn==0 ){
3582 fossil_fatal("unable to %s", blob_str(&ssh));
3583 }
3584
+30 -6
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288288
int allowSymlinks; /* Cached "allow-symlinks" option */
289289
int mainTimerId; /* Set to fossil_timer_start() */
290290
int nPendingRequest; /* # of HTTP requests in "fossil server" */
291291
int nRequest; /* Total # of HTTP request */
292292
int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
293
+
294
+ /* State for communicating specific details between the inbound HTTP
295
+ ** header parser (cgi.c), xfer.c, and http.c. */
296
+ struct {
297
+ char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */
298
+ int fLoginCardMode; /* If non-0, emit login cards in outbound
299
+ ** requests as a HTTP cookie instead of as
300
+ ** part of the payload. Gets activated
301
+ ** on-demand based on xfer traffic
302
+ ** contents. Values, for
303
+ ** diagnostic/debugging purposes: 0x01=CLI
304
+ ** --flag, 0x02=cgi_setup_query_string(),
305
+ ** 0x04=page_xfer(),
306
+ ** 0x08=client_sync(). */
307
+ int remoteVersion; /* Remote fossil version. Used for negotiating
308
+ ** how to handle the login card. */
309
+ } syncInfo;
293310
#ifdef FOSSIL_ENABLE_JSON
294311
struct FossilJsonBits {
295312
int isJsonMode; /* True if running in JSON mode, else
296313
false. This changes how errors are
297314
reported. In JSON mode we try to
@@ -758,10 +775,17 @@
758775
g.tcl.argc = g.argc;
759776
g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
760777
#endif
761778
g.mainTimerId = fossil_timer_start();
762779
capture_case_sensitive_option();
780
+ g.syncInfo.fLoginCardMode =
781
+ /* The undocumented/unsupported --login-card-header provides a way
782
+ ** to force use of the feature added by the xfer-login-card branch
783
+ ** in 2025-07, intended for assisting in debugging any related
784
+ ** issues. It can be removed once we reach the level of "implicit
785
+ ** trust" in that feature. */
786
+ find_option("login-card-header",0,0) ? 0x01 : 0;
763787
g.zVfsName = find_option("vfs",0,1);
764788
if( g.zVfsName==0 ){
765789
g.zVfsName = fossil_getenv("FOSSIL_VFS");
766790
}
767791
if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
14901514
/* In order for ?skin=... to work when visiting the site from
14911515
** a typical external link, we have to process it here, as
14921516
** that parameter gets lost during the redirect. We "could"
14931517
** pass the whole query string along instead, but that seems
14941518
** unnecessary. */
1495
- if(cgi_setup_query_string()>1){
1519
+ if(cgi_setup_query_string() & 0x02){
14961520
cookie_render();
14971521
}
14981522
cgi_redirectf("%R%s", db_get("index-page", "/index"));
14991523
}
15001524
@@ -1794,22 +1818,22 @@
17941818
}
17951819
17961820
17971821
/* Restrictions on the URI for security:
17981822
**
1799
- ** 1. Reject characters that are not ASCII alphanumerics,
1823
+ ** 1. Reject characters that are not ASCII alphanumerics,
18001824
** "-", "_", ".", "/", or unicode (above ASCII).
18011825
** In other words: No ASCII punctuation or control characters
18021826
** other than "-", "_", "." and "/".
1803
- ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1827
+ ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
18041828
** alphabetic character at the beginning of the name on windows.
18051829
** 3. "-" may not occur immediately after "/"
18061830
** 4. "." may not be adjacent to another "." or to "/"
18071831
**
18081832
** Any character does not satisfy these constraints a Not Found
18091833
** error is returned.
1810
- */
1834
+ */
18111835
szFile = 0;
18121836
for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
18131837
char c = zRepo[j];
18141838
if( c>='a' && c<='z' ) continue;
18151839
if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
35103534
** "fossil ui --nobrowser" on the remote system and to set up a
35113535
** tunnel from the local machine to the remote. */
35123536
FILE *sshIn;
35133537
Blob ssh;
35143538
int bRunning = 0; /* True when fossil starts up on the remote */
3515
- int isRetry; /* True if on the second attempt */
3539
+ int isRetry; /* True if on the second attempt */
35163540
char zLine[1000];
35173541
35183542
blob_init(&ssh, 0, 0);
35193543
for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
35203544
blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
35493573
if( fCreate ) blob_appendf(&ssh, " --create");
35503574
blob_appendf(&ssh, " %$", g.argv[2]);
35513575
if( isRetry ){
35523576
fossil_print("First attempt to run \"fossil\" on %s failed\n"
35533577
"Retry: ", zRemote);
3554
- }
3578
+ }
35553579
fossil_print("%s\n", blob_str(&ssh));
35563580
sshIn = popen(blob_str(&ssh), "r");
35573581
if( sshIn==0 ){
35583582
fossil_fatal("unable to %s", blob_str(&ssh));
35593583
}
35603584
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288 int allowSymlinks; /* Cached "allow-symlinks" option */
289 int mainTimerId; /* Set to fossil_timer_start() */
290 int nPendingRequest; /* # of HTTP requests in "fossil server" */
291 int nRequest; /* Total # of HTTP request */
292 int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293 #ifdef FOSSIL_ENABLE_JSON
294 struct FossilJsonBits {
295 int isJsonMode; /* True if running in JSON mode, else
296 false. This changes how errors are
297 reported. In JSON mode we try to
@@ -758,10 +775,17 @@
758 g.tcl.argc = g.argc;
759 g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
760 #endif
761 g.mainTimerId = fossil_timer_start();
762 capture_case_sensitive_option();
 
 
 
 
 
 
 
763 g.zVfsName = find_option("vfs",0,1);
764 if( g.zVfsName==0 ){
765 g.zVfsName = fossil_getenv("FOSSIL_VFS");
766 }
767 if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
1490 /* In order for ?skin=... to work when visiting the site from
1491 ** a typical external link, we have to process it here, as
1492 ** that parameter gets lost during the redirect. We "could"
1493 ** pass the whole query string along instead, but that seems
1494 ** unnecessary. */
1495 if(cgi_setup_query_string()>1){
1496 cookie_render();
1497 }
1498 cgi_redirectf("%R%s", db_get("index-page", "/index"));
1499 }
1500
@@ -1794,22 +1818,22 @@
1794 }
1795
1796
1797 /* Restrictions on the URI for security:
1798 **
1799 ** 1. Reject characters that are not ASCII alphanumerics,
1800 ** "-", "_", ".", "/", or unicode (above ASCII).
1801 ** In other words: No ASCII punctuation or control characters
1802 ** other than "-", "_", "." and "/".
1803 ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1804 ** alphabetic character at the beginning of the name on windows.
1805 ** 3. "-" may not occur immediately after "/"
1806 ** 4. "." may not be adjacent to another "." or to "/"
1807 **
1808 ** Any character does not satisfy these constraints a Not Found
1809 ** error is returned.
1810 */
1811 szFile = 0;
1812 for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
1813 char c = zRepo[j];
1814 if( c>='a' && c<='z' ) continue;
1815 if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
3510 ** "fossil ui --nobrowser" on the remote system and to set up a
3511 ** tunnel from the local machine to the remote. */
3512 FILE *sshIn;
3513 Blob ssh;
3514 int bRunning = 0; /* True when fossil starts up on the remote */
3515 int isRetry; /* True if on the second attempt */
3516 char zLine[1000];
3517
3518 blob_init(&ssh, 0, 0);
3519 for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
3520 blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
3549 if( fCreate ) blob_appendf(&ssh, " --create");
3550 blob_appendf(&ssh, " %$", g.argv[2]);
3551 if( isRetry ){
3552 fossil_print("First attempt to run \"fossil\" on %s failed\n"
3553 "Retry: ", zRemote);
3554 }
3555 fossil_print("%s\n", blob_str(&ssh));
3556 sshIn = popen(blob_str(&ssh), "r");
3557 if( sshIn==0 ){
3558 fossil_fatal("unable to %s", blob_str(&ssh));
3559 }
3560
--- src/main.c
+++ src/main.c
@@ -288,10 +288,27 @@
288 int allowSymlinks; /* Cached "allow-symlinks" option */
289 int mainTimerId; /* Set to fossil_timer_start() */
290 int nPendingRequest; /* # of HTTP requests in "fossil server" */
291 int nRequest; /* Total # of HTTP request */
292 int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
293
294 /* State for communicating specific details between the inbound HTTP
295 ** header parser (cgi.c), xfer.c, and http.c. */
296 struct {
297 char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */
298 int fLoginCardMode; /* If non-0, emit login cards in outbound
299 ** requests as a HTTP cookie instead of as
300 ** part of the payload. Gets activated
301 ** on-demand based on xfer traffic
302 ** contents. Values, for
303 ** diagnostic/debugging purposes: 0x01=CLI
304 ** --flag, 0x02=cgi_setup_query_string(),
305 ** 0x04=page_xfer(),
306 ** 0x08=client_sync(). */
307 int remoteVersion; /* Remote fossil version. Used for negotiating
308 ** how to handle the login card. */
309 } syncInfo;
310 #ifdef FOSSIL_ENABLE_JSON
311 struct FossilJsonBits {
312 int isJsonMode; /* True if running in JSON mode, else
313 false. This changes how errors are
314 reported. In JSON mode we try to
@@ -758,10 +775,17 @@
775 g.tcl.argc = g.argc;
776 g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */
777 #endif
778 g.mainTimerId = fossil_timer_start();
779 capture_case_sensitive_option();
780 g.syncInfo.fLoginCardMode =
781 /* The undocumented/unsupported --login-card-header provides a way
782 ** to force use of the feature added by the xfer-login-card branch
783 ** in 2025-07, intended for assisting in debugging any related
784 ** issues. It can be removed once we reach the level of "implicit
785 ** trust" in that feature. */
786 find_option("login-card-header",0,0) ? 0x01 : 0;
787 g.zVfsName = find_option("vfs",0,1);
788 if( g.zVfsName==0 ){
789 g.zVfsName = fossil_getenv("FOSSIL_VFS");
790 }
791 if( g.zVfsName ){
@@ -1490,11 +1514,11 @@
1514 /* In order for ?skin=... to work when visiting the site from
1515 ** a typical external link, we have to process it here, as
1516 ** that parameter gets lost during the redirect. We "could"
1517 ** pass the whole query string along instead, but that seems
1518 ** unnecessary. */
1519 if(cgi_setup_query_string() & 0x02){
1520 cookie_render();
1521 }
1522 cgi_redirectf("%R%s", db_get("index-page", "/index"));
1523 }
1524
@@ -1794,22 +1818,22 @@
1818 }
1819
1820
1821 /* Restrictions on the URI for security:
1822 **
1823 ** 1. Reject characters that are not ASCII alphanumerics,
1824 ** "-", "_", ".", "/", or unicode (above ASCII).
1825 ** In other words: No ASCII punctuation or control characters
1826 ** other than "-", "_", "." and "/".
1827 ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII
1828 ** alphabetic character at the beginning of the name on windows.
1829 ** 3. "-" may not occur immediately after "/"
1830 ** 4. "." may not be adjacent to another "." or to "/"
1831 **
1832 ** Any character does not satisfy these constraints a Not Found
1833 ** error is returned.
1834 */
1835 szFile = 0;
1836 for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){
1837 char c = zRepo[j];
1838 if( c>='a' && c<='z' ) continue;
1839 if( c>='A' && c<='Z' ) continue;
@@ -3510,11 +3534,11 @@
3534 ** "fossil ui --nobrowser" on the remote system and to set up a
3535 ** tunnel from the local machine to the remote. */
3536 FILE *sshIn;
3537 Blob ssh;
3538 int bRunning = 0; /* True when fossil starts up on the remote */
3539 int isRetry; /* True if on the second attempt */
3540 char zLine[1000];
3541
3542 blob_init(&ssh, 0, 0);
3543 for(isRetry=0; isRetry<2 && !bRunning; isRetry++){
3544 blob_reset(&ssh);
@@ -3549,11 +3573,11 @@
3573 if( fCreate ) blob_appendf(&ssh, " --create");
3574 blob_appendf(&ssh, " %$", g.argv[2]);
3575 if( isRetry ){
3576 fossil_print("First attempt to run \"fossil\" on %s failed\n"
3577 "Retry: ", zRemote);
3578 }
3579 fossil_print("%s\n", blob_str(&ssh));
3580 sshIn = popen(blob_str(&ssh), "r");
3581 if( sshIn==0 ){
3582 fossil_fatal("unable to %s", blob_str(&ssh));
3583 }
3584
+1 -1
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233233
for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234234
if( pUrlData->path[i] ){
235235
pUrlData->path[i] = 0;
236236
i++;
237237
}
238
- zExe = mprintf("");
238
+ zExe = fossil_strdup("");
239239
while( pUrlData->path[i]!=0 ){
240240
char *zName, *zValue;
241241
zName = &pUrlData->path[i];
242242
zValue = zName;
243243
while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244244
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233 for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234 if( pUrlData->path[i] ){
235 pUrlData->path[i] = 0;
236 i++;
237 }
238 zExe = mprintf("");
239 while( pUrlData->path[i]!=0 ){
240 char *zName, *zValue;
241 zName = &pUrlData->path[i];
242 zValue = zName;
243 while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233 for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234 if( pUrlData->path[i] ){
235 pUrlData->path[i] = 0;
236 i++;
237 }
238 zExe = fossil_strdup("");
239 while( pUrlData->path[i]!=0 ){
240 char *zName, *zValue;
241 zName = &pUrlData->path[i];
242 zValue = zName;
243 while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244
+1 -1
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233233
for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234234
if( pUrlData->path[i] ){
235235
pUrlData->path[i] = 0;
236236
i++;
237237
}
238
- zExe = mprintf("");
238
+ zExe = fossil_strdup("");
239239
while( pUrlData->path[i]!=0 ){
240240
char *zName, *zValue;
241241
zName = &pUrlData->path[i];
242242
zValue = zName;
243243
while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244244
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233 for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234 if( pUrlData->path[i] ){
235 pUrlData->path[i] = 0;
236 i++;
237 }
238 zExe = mprintf("");
239 while( pUrlData->path[i]!=0 ){
240 char *zName, *zValue;
241 zName = &pUrlData->path[i];
242 zValue = zName;
243 while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244
--- src/url.c
+++ src/url.c
@@ -233,11 +233,11 @@
233 for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
234 if( pUrlData->path[i] ){
235 pUrlData->path[i] = 0;
236 i++;
237 }
238 zExe = fossil_strdup("");
239 while( pUrlData->path[i]!=0 ){
240 char *zName, *zValue;
241 zName = &pUrlData->path[i];
242 zValue = zName;
243 while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
244
+1 -1
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465465
char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466466
db_unprotect(PROTECT_USER);
467467
db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468468
zSecret, uid);
469469
db_protect_pop();
470
- free(zSecret);
470
+ fossil_free(zSecret);
471471
}
472472
}else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473473
int uid;
474474
if( g.argc!=4 && g.argc!=5 ){
475475
usage("capabilities USERNAME ?PERMISSIONS?");
476476
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466 db_unprotect(PROTECT_USER);
467 db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468 zSecret, uid);
469 db_protect_pop();
470 free(zSecret);
471 }
472 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473 int uid;
474 if( g.argc!=4 && g.argc!=5 ){
475 usage("capabilities USERNAME ?PERMISSIONS?");
476
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466 db_unprotect(PROTECT_USER);
467 db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468 zSecret, uid);
469 db_protect_pop();
470 fossil_free(zSecret);
471 }
472 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473 int uid;
474 if( g.argc!=4 && g.argc!=5 ){
475 usage("capabilities USERNAME ?PERMISSIONS?");
476
+1 -1
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465465
char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466466
db_unprotect(PROTECT_USER);
467467
db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468468
zSecret, uid);
469469
db_protect_pop();
470
- free(zSecret);
470
+ fossil_free(zSecret);
471471
}
472472
}else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473473
int uid;
474474
if( g.argc!=4 && g.argc!=5 ){
475475
usage("capabilities USERNAME ?PERMISSIONS?");
476476
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466 db_unprotect(PROTECT_USER);
467 db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468 zSecret, uid);
469 db_protect_pop();
470 free(zSecret);
471 }
472 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473 int uid;
474 if( g.argc!=4 && g.argc!=5 ){
475 usage("capabilities USERNAME ?PERMISSIONS?");
476
--- src/user.c
+++ src/user.c
@@ -465,11 +465,11 @@
465 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
466 db_unprotect(PROTECT_USER);
467 db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
468 zSecret, uid);
469 db_protect_pop();
470 fossil_free(zSecret);
471 }
472 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
473 int uid;
474 if( g.argc!=4 && g.argc!=5 ){
475 usage("capabilities USERNAME ?PERMISSIONS?");
476
+55 -13
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827827
int rc = -1;
828828
char *zLogin = blob_terminate(pLogin);
829829
defossilize(zLogin);
830830
831831
if( fossil_strcmp(zLogin, "nobody")==0
832
- || fossil_strcmp(zLogin,"anonymous")==0
832
+ || fossil_strcmp(zLogin, "anonymous")==0
833833
){
834834
return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835835
}
836836
if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837837
&& db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866866
const char *zPw = db_column_text(&q, 0);
867867
char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868868
blob_zero(&combined);
869869
blob_copy(&combined, pNonce);
870870
blob_append(&combined, zSecret, -1);
871
- free(zSecret);
871
+ fossil_free(zSecret);
872872
sha1sum_blob(&combined, &hash);
873873
rc = blob_constant_time_cmp(&hash, pSig);
874874
blob_reset(&hash);
875875
blob_reset(&combined);
876876
}
@@ -1241,10 +1241,25 @@
12411241
/*
12421242
** If this variable is set, disable login checks. Used for debugging
12431243
** only.
12441244
*/
12451245
static int disableLogin = 0;
1246
+
1247
+/*
1248
+** Must be passed the version info from pragmas
1249
+** client-version/server-version cards. If the version info is "new
1250
+** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login
1251
+** card flag, else this is a no-op.
1252
+*/
1253
+static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime,
1254
+ int fLoginCardMode){
1255
+ if( iRemoteVersion>=22700
1256
+ && (iDate > 20250727
1257
+ || (iDate == 20250727 && iTime >= 110500)) ){
1258
+ g.syncInfo.fLoginCardMode |= fLoginCardMode;
1259
+ }
1260
+}
12461261
12471262
/*
12481263
** The CGI/HTTP preprocessor always redirects requests with a content-type
12491264
** of application/x-fossil or application/x-fossil-debug to this page,
12501265
** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
12731288
int nUuidList = 0;
12741289
char **pzUuidList = 0;
12751290
int *pnUuidList = 0;
12761291
int uvCatalogSent = 0;
12771292
int bSendLinks = 0;
1293
+ int nLogin = 0;
12781294
12791295
if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
12801296
fossil_redirect_home();
12811297
}
12821298
g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
13141330
}
13151331
zScript = xfer_push_code();
13161332
if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
13171333
pzUuidList = &zUuidList;
13181334
pnUuidList = &nUuidList;
1335
+ }
1336
+ if( g.syncInfo.zLoginCard ){
1337
+ /* Login card received via HTTP Cookie header */
1338
+ assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" );
1339
+ blob_zero(&xfer.line);
1340
+ blob_append(&xfer.line, g.syncInfo.zLoginCard, -1);
1341
+ xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken,
1342
+ count(xfer.aToken));
1343
+ fossil_free( g.syncInfo.zLoginCard );
1344
+ g.syncInfo.zLoginCard = 0;
1345
+ if( xfer.nToken==4
1346
+ && blob_eq(&xfer.aToken[0], "login") ){
1347
+ goto handle_login_card;
1348
+ }
13191349
}
13201350
while( blob_line(xfer.pIn, &xfer.line) ){
13211351
if( blob_buffer(&xfer.line)[0]=='#' ) continue;
13221352
if( blob_size(&xfer.line)==0 ) continue;
13231353
xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
15501580
15511581
/* login USER NONCE SIGNATURE
15521582
**
15531583
** The client has sent login credentials to the server.
15541584
** Validate the login. This has to happen before anything else.
1555
- ** The client can send multiple logins. Permissions are cumulative.
1585
+ **
1586
+ ** For many years, Fossil would accept multiple login cards with
1587
+ ** cumulative permissions. But that feature was never used. Hence
1588
+ ** it is now prohibited. Any login card after the first generates
1589
+ ** a fatal error.
15561590
*/
15571591
if( blob_eq(&xfer.aToken[0], "login")
15581592
&& xfer.nToken==4
15591593
){
1594
+ handle_login_card:
1595
+ nLogin++;
15601596
if( disableLogin ){
15611597
g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
1598
+ }else if( nLogin > 1 ){
1599
+ cgi_reset_content();
1600
+ @ error multiple\slogin\cards
1601
+ nErr++;
1602
+ break;
15621603
}else{
15631604
if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
15641605
|| check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
15651606
){
15661607
cgi_reset_content();
@@ -1651,11 +1692,10 @@
16511692
}else{
16521693
xfer.nextIsPrivate = 1;
16531694
}
16541695
}else
16551696
1656
-
16571697
/* pragma NAME VALUE...
16581698
**
16591699
** The client issues pragmas to try to influence the behavior of the
16601700
** server. These are requests only. Unknown pragmas are silently
16611701
** ignored.
@@ -1694,17 +1734,20 @@
16941734
** The client announces to the server what version of Fossil it
16951735
** is running. The DATE and TIME are a pure numeric ISO8601 time
16961736
** for the specific check-in of the client.
16971737
*/
16981738
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1699
- xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
1739
+ xfer.remoteVersion = g.syncInfo.remoteVersion =
1740
+ atoi(blob_str(&xfer.aToken[2]));
17001741
if( xfer.nToken>=5 ){
17011742
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
17021743
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
17031744
@ pragma server-version %d(RELEASE_VERSION_NUMBER) \
17041745
@ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
17051746
}
1747
+ xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
1748
+ xfer.remoteTime, 0x04 );
17061749
}else
17071750
17081751
/* pragma uv-hash HASH
17091752
**
17101753
** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
23412384
blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
23422385
zCkinLock = 0;
23432386
}else if( zClientId ){
23442387
blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
23452388
}
2346
-
23472389
/* Append randomness to the end of the uplink message. This makes all
23482390
** messages unique so that that the login-card nonce will always
23492391
** be unique.
23502392
*/
23512393
zRandomness = db_text(0, "SELECT hex(randomblob(20))");
23522394
blob_appendf(&send, "# %s\n", zRandomness);
2353
- free(zRandomness);
2395
+ fossil_free(zRandomness);
23542396
23552397
if( (syncFlags & SYNC_VERBOSE)!=0
23562398
&& (syncFlags & SYNC_XVERBOSE)==0
23572399
){
23582400
fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
27252767
27262768
/* message MESSAGE
27272769
**
27282770
** A message is received from the server. Print it.
27292771
** Similar to "error" but does not stop processing.
2730
- **
2731
- ** If the "login failed" message is seen, clear the sync password prior
2732
- ** to the next cycle.
27332772
*/
27342773
if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
27352774
char *zMsg = blob_terminate(&xfer.aToken[1]);
27362775
defossilize(zMsg);
27372776
if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
27572796
** The server announces to the server what version of Fossil it
27582797
** is running. The DATE and TIME are a pure numeric ISO8601 time
27592798
** for the specific check-in of the client.
27602799
*/
27612800
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2762
- xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
2801
+ xfer.remoteVersion = g.syncInfo.remoteVersion =
2802
+ atoi(blob_str(&xfer.aToken[2]));
27632803
if( xfer.nToken>=5 ){
27642804
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
27652805
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
27662806
}
2807
+ xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
2808
+ xfer.remoteTime, 0x08 );
27672809
}
27682810
27692811
/* pragma uv-pull-only
27702812
** pragma uv-push-ok
27712813
**
@@ -2896,11 +2938,11 @@
28962938
&recv
28972939
);
28982940
nErr++;
28992941
break;
29002942
}
2901
- blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]);
2943
+ blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line);
29022944
}
29032945
29042946
if( blob_size(&xfer.err) ){
29052947
fossil_force_newline();
29062948
fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
29633005
}else{
29643006
manifest_crosslink_end(MC_PERMIT_HOOKS);
29653007
content_enable_dephantomize(1);
29663008
}
29673009
db_end_transaction(0);
2968
- };
3010
+ }; /* while(go) */
29693011
transport_stats(&nSent, &nRcvd, 1);
29703012
if( pnRcvd ) *pnRcvd = nArtifactRcvd;
29713013
if( (rSkew*24.0*3600.0) > 10.0 ){
29723014
fossil_warning("*** time skew *** server is fast by %s",
29733015
db_timespan_name(rSkew));
29743016
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827 int rc = -1;
828 char *zLogin = blob_terminate(pLogin);
829 defossilize(zLogin);
830
831 if( fossil_strcmp(zLogin, "nobody")==0
832 || fossil_strcmp(zLogin,"anonymous")==0
833 ){
834 return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835 }
836 if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837 && db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866 const char *zPw = db_column_text(&q, 0);
867 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868 blob_zero(&combined);
869 blob_copy(&combined, pNonce);
870 blob_append(&combined, zSecret, -1);
871 free(zSecret);
872 sha1sum_blob(&combined, &hash);
873 rc = blob_constant_time_cmp(&hash, pSig);
874 blob_reset(&hash);
875 blob_reset(&combined);
876 }
@@ -1241,10 +1241,25 @@
1241 /*
1242 ** If this variable is set, disable login checks. Used for debugging
1243 ** only.
1244 */
1245 static int disableLogin = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1246
1247 /*
1248 ** The CGI/HTTP preprocessor always redirects requests with a content-type
1249 ** of application/x-fossil or application/x-fossil-debug to this page,
1250 ** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
1273 int nUuidList = 0;
1274 char **pzUuidList = 0;
1275 int *pnUuidList = 0;
1276 int uvCatalogSent = 0;
1277 int bSendLinks = 0;
 
1278
1279 if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
1280 fossil_redirect_home();
1281 }
1282 g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
1314 }
1315 zScript = xfer_push_code();
1316 if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
1317 pzUuidList = &zUuidList;
1318 pnUuidList = &nUuidList;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1319 }
1320 while( blob_line(xfer.pIn, &xfer.line) ){
1321 if( blob_buffer(&xfer.line)[0]=='#' ) continue;
1322 if( blob_size(&xfer.line)==0 ) continue;
1323 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
1550
1551 /* login USER NONCE SIGNATURE
1552 **
1553 ** The client has sent login credentials to the server.
1554 ** Validate the login. This has to happen before anything else.
1555 ** The client can send multiple logins. Permissions are cumulative.
 
 
 
 
1556 */
1557 if( blob_eq(&xfer.aToken[0], "login")
1558 && xfer.nToken==4
1559 ){
 
 
1560 if( disableLogin ){
1561 g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
 
 
 
 
 
1562 }else{
1563 if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
1564 || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
1565 ){
1566 cgi_reset_content();
@@ -1651,11 +1692,10 @@
1651 }else{
1652 xfer.nextIsPrivate = 1;
1653 }
1654 }else
1655
1656
1657 /* pragma NAME VALUE...
1658 **
1659 ** The client issues pragmas to try to influence the behavior of the
1660 ** server. These are requests only. Unknown pragmas are silently
1661 ** ignored.
@@ -1694,17 +1734,20 @@
1694 ** The client announces to the server what version of Fossil it
1695 ** is running. The DATE and TIME are a pure numeric ISO8601 time
1696 ** for the specific check-in of the client.
1697 */
1698 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1699 xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
 
1700 if( xfer.nToken>=5 ){
1701 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
1702 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
1703 @ pragma server-version %d(RELEASE_VERSION_NUMBER) \
1704 @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
1705 }
 
 
1706 }else
1707
1708 /* pragma uv-hash HASH
1709 **
1710 ** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
2341 blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
2342 zCkinLock = 0;
2343 }else if( zClientId ){
2344 blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
2345 }
2346
2347 /* Append randomness to the end of the uplink message. This makes all
2348 ** messages unique so that that the login-card nonce will always
2349 ** be unique.
2350 */
2351 zRandomness = db_text(0, "SELECT hex(randomblob(20))");
2352 blob_appendf(&send, "# %s\n", zRandomness);
2353 free(zRandomness);
2354
2355 if( (syncFlags & SYNC_VERBOSE)!=0
2356 && (syncFlags & SYNC_XVERBOSE)==0
2357 ){
2358 fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
2725
2726 /* message MESSAGE
2727 **
2728 ** A message is received from the server. Print it.
2729 ** Similar to "error" but does not stop processing.
2730 **
2731 ** If the "login failed" message is seen, clear the sync password prior
2732 ** to the next cycle.
2733 */
2734 if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
2735 char *zMsg = blob_terminate(&xfer.aToken[1]);
2736 defossilize(zMsg);
2737 if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
2757 ** The server announces to the server what version of Fossil it
2758 ** is running. The DATE and TIME are a pure numeric ISO8601 time
2759 ** for the specific check-in of the client.
2760 */
2761 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2762 xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
 
2763 if( xfer.nToken>=5 ){
2764 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
2765 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
2766 }
 
 
2767 }
2768
2769 /* pragma uv-pull-only
2770 ** pragma uv-push-ok
2771 **
@@ -2896,11 +2938,11 @@
2896 &recv
2897 );
2898 nErr++;
2899 break;
2900 }
2901 blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]);
2902 }
2903
2904 if( blob_size(&xfer.err) ){
2905 fossil_force_newline();
2906 fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
2963 }else{
2964 manifest_crosslink_end(MC_PERMIT_HOOKS);
2965 content_enable_dephantomize(1);
2966 }
2967 db_end_transaction(0);
2968 };
2969 transport_stats(&nSent, &nRcvd, 1);
2970 if( pnRcvd ) *pnRcvd = nArtifactRcvd;
2971 if( (rSkew*24.0*3600.0) > 10.0 ){
2972 fossil_warning("*** time skew *** server is fast by %s",
2973 db_timespan_name(rSkew));
2974
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827 int rc = -1;
828 char *zLogin = blob_terminate(pLogin);
829 defossilize(zLogin);
830
831 if( fossil_strcmp(zLogin, "nobody")==0
832 || fossil_strcmp(zLogin, "anonymous")==0
833 ){
834 return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835 }
836 if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837 && db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866 const char *zPw = db_column_text(&q, 0);
867 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868 blob_zero(&combined);
869 blob_copy(&combined, pNonce);
870 blob_append(&combined, zSecret, -1);
871 fossil_free(zSecret);
872 sha1sum_blob(&combined, &hash);
873 rc = blob_constant_time_cmp(&hash, pSig);
874 blob_reset(&hash);
875 blob_reset(&combined);
876 }
@@ -1241,10 +1241,25 @@
1241 /*
1242 ** If this variable is set, disable login checks. Used for debugging
1243 ** only.
1244 */
1245 static int disableLogin = 0;
1246
1247 /*
1248 ** Must be passed the version info from pragmas
1249 ** client-version/server-version cards. If the version info is "new
1250 ** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login
1251 ** card flag, else this is a no-op.
1252 */
1253 static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime,
1254 int fLoginCardMode){
1255 if( iRemoteVersion>=22700
1256 && (iDate > 20250727
1257 || (iDate == 20250727 && iTime >= 110500)) ){
1258 g.syncInfo.fLoginCardMode |= fLoginCardMode;
1259 }
1260 }
1261
1262 /*
1263 ** The CGI/HTTP preprocessor always redirects requests with a content-type
1264 ** of application/x-fossil or application/x-fossil-debug to this page,
1265 ** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
1288 int nUuidList = 0;
1289 char **pzUuidList = 0;
1290 int *pnUuidList = 0;
1291 int uvCatalogSent = 0;
1292 int bSendLinks = 0;
1293 int nLogin = 0;
1294
1295 if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
1296 fossil_redirect_home();
1297 }
1298 g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
1330 }
1331 zScript = xfer_push_code();
1332 if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
1333 pzUuidList = &zUuidList;
1334 pnUuidList = &nUuidList;
1335 }
1336 if( g.syncInfo.zLoginCard ){
1337 /* Login card received via HTTP Cookie header */
1338 assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" );
1339 blob_zero(&xfer.line);
1340 blob_append(&xfer.line, g.syncInfo.zLoginCard, -1);
1341 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken,
1342 count(xfer.aToken));
1343 fossil_free( g.syncInfo.zLoginCard );
1344 g.syncInfo.zLoginCard = 0;
1345 if( xfer.nToken==4
1346 && blob_eq(&xfer.aToken[0], "login") ){
1347 goto handle_login_card;
1348 }
1349 }
1350 while( blob_line(xfer.pIn, &xfer.line) ){
1351 if( blob_buffer(&xfer.line)[0]=='#' ) continue;
1352 if( blob_size(&xfer.line)==0 ) continue;
1353 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
1580
1581 /* login USER NONCE SIGNATURE
1582 **
1583 ** The client has sent login credentials to the server.
1584 ** Validate the login. This has to happen before anything else.
1585 **
1586 ** For many years, Fossil would accept multiple login cards with
1587 ** cumulative permissions. But that feature was never used. Hence
1588 ** it is now prohibited. Any login card after the first generates
1589 ** a fatal error.
1590 */
1591 if( blob_eq(&xfer.aToken[0], "login")
1592 && xfer.nToken==4
1593 ){
1594 handle_login_card:
1595 nLogin++;
1596 if( disableLogin ){
1597 g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
1598 }else if( nLogin > 1 ){
1599 cgi_reset_content();
1600 @ error multiple\slogin\cards
1601 nErr++;
1602 break;
1603 }else{
1604 if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
1605 || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
1606 ){
1607 cgi_reset_content();
@@ -1651,11 +1692,10 @@
1692 }else{
1693 xfer.nextIsPrivate = 1;
1694 }
1695 }else
1696
 
1697 /* pragma NAME VALUE...
1698 **
1699 ** The client issues pragmas to try to influence the behavior of the
1700 ** server. These are requests only. Unknown pragmas are silently
1701 ** ignored.
@@ -1694,17 +1734,20 @@
1734 ** The client announces to the server what version of Fossil it
1735 ** is running. The DATE and TIME are a pure numeric ISO8601 time
1736 ** for the specific check-in of the client.
1737 */
1738 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1739 xfer.remoteVersion = g.syncInfo.remoteVersion =
1740 atoi(blob_str(&xfer.aToken[2]));
1741 if( xfer.nToken>=5 ){
1742 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
1743 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
1744 @ pragma server-version %d(RELEASE_VERSION_NUMBER) \
1745 @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
1746 }
1747 xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
1748 xfer.remoteTime, 0x04 );
1749 }else
1750
1751 /* pragma uv-hash HASH
1752 **
1753 ** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
2384 blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
2385 zCkinLock = 0;
2386 }else if( zClientId ){
2387 blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
2388 }
 
2389 /* Append randomness to the end of the uplink message. This makes all
2390 ** messages unique so that that the login-card nonce will always
2391 ** be unique.
2392 */
2393 zRandomness = db_text(0, "SELECT hex(randomblob(20))");
2394 blob_appendf(&send, "# %s\n", zRandomness);
2395 fossil_free(zRandomness);
2396
2397 if( (syncFlags & SYNC_VERBOSE)!=0
2398 && (syncFlags & SYNC_XVERBOSE)==0
2399 ){
2400 fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
2767
2768 /* message MESSAGE
2769 **
2770 ** A message is received from the server. Print it.
2771 ** Similar to "error" but does not stop processing.
 
 
 
2772 */
2773 if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
2774 char *zMsg = blob_terminate(&xfer.aToken[1]);
2775 defossilize(zMsg);
2776 if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
2796 ** The server announces to the server what version of Fossil it
2797 ** is running. The DATE and TIME are a pure numeric ISO8601 time
2798 ** for the specific check-in of the client.
2799 */
2800 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2801 xfer.remoteVersion = g.syncInfo.remoteVersion =
2802 atoi(blob_str(&xfer.aToken[2]));
2803 if( xfer.nToken>=5 ){
2804 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
2805 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
2806 }
2807 xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
2808 xfer.remoteTime, 0x08 );
2809 }
2810
2811 /* pragma uv-pull-only
2812 ** pragma uv-push-ok
2813 **
@@ -2896,11 +2938,11 @@
2938 &recv
2939 );
2940 nErr++;
2941 break;
2942 }
2943 blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line);
2944 }
2945
2946 if( blob_size(&xfer.err) ){
2947 fossil_force_newline();
2948 fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
3005 }else{
3006 manifest_crosslink_end(MC_PERMIT_HOOKS);
3007 content_enable_dephantomize(1);
3008 }
3009 db_end_transaction(0);
3010 }; /* while(go) */
3011 transport_stats(&nSent, &nRcvd, 1);
3012 if( pnRcvd ) *pnRcvd = nArtifactRcvd;
3013 if( (rSkew*24.0*3600.0) > 10.0 ){
3014 fossil_warning("*** time skew *** server is fast by %s",
3015 db_timespan_name(rSkew));
3016
+55 -13
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827827
int rc = -1;
828828
char *zLogin = blob_terminate(pLogin);
829829
defossilize(zLogin);
830830
831831
if( fossil_strcmp(zLogin, "nobody")==0
832
- || fossil_strcmp(zLogin,"anonymous")==0
832
+ || fossil_strcmp(zLogin, "anonymous")==0
833833
){
834834
return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835835
}
836836
if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837837
&& db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866866
const char *zPw = db_column_text(&q, 0);
867867
char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868868
blob_zero(&combined);
869869
blob_copy(&combined, pNonce);
870870
blob_append(&combined, zSecret, -1);
871
- free(zSecret);
871
+ fossil_free(zSecret);
872872
sha1sum_blob(&combined, &hash);
873873
rc = blob_constant_time_cmp(&hash, pSig);
874874
blob_reset(&hash);
875875
blob_reset(&combined);
876876
}
@@ -1241,10 +1241,25 @@
12411241
/*
12421242
** If this variable is set, disable login checks. Used for debugging
12431243
** only.
12441244
*/
12451245
static int disableLogin = 0;
1246
+
1247
+/*
1248
+** Must be passed the version info from pragmas
1249
+** client-version/server-version cards. If the version info is "new
1250
+** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login
1251
+** card flag, else this is a no-op.
1252
+*/
1253
+static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime,
1254
+ int fLoginCardMode){
1255
+ if( iRemoteVersion>=22700
1256
+ && (iDate > 20250727
1257
+ || (iDate == 20250727 && iTime >= 110500)) ){
1258
+ g.syncInfo.fLoginCardMode |= fLoginCardMode;
1259
+ }
1260
+}
12461261
12471262
/*
12481263
** The CGI/HTTP preprocessor always redirects requests with a content-type
12491264
** of application/x-fossil or application/x-fossil-debug to this page,
12501265
** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
12731288
int nUuidList = 0;
12741289
char **pzUuidList = 0;
12751290
int *pnUuidList = 0;
12761291
int uvCatalogSent = 0;
12771292
int bSendLinks = 0;
1293
+ int nLogin = 0;
12781294
12791295
if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
12801296
fossil_redirect_home();
12811297
}
12821298
g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
13141330
}
13151331
zScript = xfer_push_code();
13161332
if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
13171333
pzUuidList = &zUuidList;
13181334
pnUuidList = &nUuidList;
1335
+ }
1336
+ if( g.syncInfo.zLoginCard ){
1337
+ /* Login card received via HTTP Cookie header */
1338
+ assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" );
1339
+ blob_zero(&xfer.line);
1340
+ blob_append(&xfer.line, g.syncInfo.zLoginCard, -1);
1341
+ xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken,
1342
+ count(xfer.aToken));
1343
+ fossil_free( g.syncInfo.zLoginCard );
1344
+ g.syncInfo.zLoginCard = 0;
1345
+ if( xfer.nToken==4
1346
+ && blob_eq(&xfer.aToken[0], "login") ){
1347
+ goto handle_login_card;
1348
+ }
13191349
}
13201350
while( blob_line(xfer.pIn, &xfer.line) ){
13211351
if( blob_buffer(&xfer.line)[0]=='#' ) continue;
13221352
if( blob_size(&xfer.line)==0 ) continue;
13231353
xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
15501580
15511581
/* login USER NONCE SIGNATURE
15521582
**
15531583
** The client has sent login credentials to the server.
15541584
** Validate the login. This has to happen before anything else.
1555
- ** The client can send multiple logins. Permissions are cumulative.
1585
+ **
1586
+ ** For many years, Fossil would accept multiple login cards with
1587
+ ** cumulative permissions. But that feature was never used. Hence
1588
+ ** it is now prohibited. Any login card after the first generates
1589
+ ** a fatal error.
15561590
*/
15571591
if( blob_eq(&xfer.aToken[0], "login")
15581592
&& xfer.nToken==4
15591593
){
1594
+ handle_login_card:
1595
+ nLogin++;
15601596
if( disableLogin ){
15611597
g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
1598
+ }else if( nLogin > 1 ){
1599
+ cgi_reset_content();
1600
+ @ error multiple\slogin\cards
1601
+ nErr++;
1602
+ break;
15621603
}else{
15631604
if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
15641605
|| check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
15651606
){
15661607
cgi_reset_content();
@@ -1651,11 +1692,10 @@
16511692
}else{
16521693
xfer.nextIsPrivate = 1;
16531694
}
16541695
}else
16551696
1656
-
16571697
/* pragma NAME VALUE...
16581698
**
16591699
** The client issues pragmas to try to influence the behavior of the
16601700
** server. These are requests only. Unknown pragmas are silently
16611701
** ignored.
@@ -1694,17 +1734,20 @@
16941734
** The client announces to the server what version of Fossil it
16951735
** is running. The DATE and TIME are a pure numeric ISO8601 time
16961736
** for the specific check-in of the client.
16971737
*/
16981738
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1699
- xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
1739
+ xfer.remoteVersion = g.syncInfo.remoteVersion =
1740
+ atoi(blob_str(&xfer.aToken[2]));
17001741
if( xfer.nToken>=5 ){
17011742
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
17021743
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
17031744
@ pragma server-version %d(RELEASE_VERSION_NUMBER) \
17041745
@ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
17051746
}
1747
+ xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
1748
+ xfer.remoteTime, 0x04 );
17061749
}else
17071750
17081751
/* pragma uv-hash HASH
17091752
**
17101753
** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
23412384
blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
23422385
zCkinLock = 0;
23432386
}else if( zClientId ){
23442387
blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
23452388
}
2346
-
23472389
/* Append randomness to the end of the uplink message. This makes all
23482390
** messages unique so that that the login-card nonce will always
23492391
** be unique.
23502392
*/
23512393
zRandomness = db_text(0, "SELECT hex(randomblob(20))");
23522394
blob_appendf(&send, "# %s\n", zRandomness);
2353
- free(zRandomness);
2395
+ fossil_free(zRandomness);
23542396
23552397
if( (syncFlags & SYNC_VERBOSE)!=0
23562398
&& (syncFlags & SYNC_XVERBOSE)==0
23572399
){
23582400
fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
27252767
27262768
/* message MESSAGE
27272769
**
27282770
** A message is received from the server. Print it.
27292771
** Similar to "error" but does not stop processing.
2730
- **
2731
- ** If the "login failed" message is seen, clear the sync password prior
2732
- ** to the next cycle.
27332772
*/
27342773
if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
27352774
char *zMsg = blob_terminate(&xfer.aToken[1]);
27362775
defossilize(zMsg);
27372776
if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
27572796
** The server announces to the server what version of Fossil it
27582797
** is running. The DATE and TIME are a pure numeric ISO8601 time
27592798
** for the specific check-in of the client.
27602799
*/
27612800
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2762
- xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
2801
+ xfer.remoteVersion = g.syncInfo.remoteVersion =
2802
+ atoi(blob_str(&xfer.aToken[2]));
27632803
if( xfer.nToken>=5 ){
27642804
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
27652805
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
27662806
}
2807
+ xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
2808
+ xfer.remoteTime, 0x08 );
27672809
}
27682810
27692811
/* pragma uv-pull-only
27702812
** pragma uv-push-ok
27712813
**
@@ -2896,11 +2938,11 @@
28962938
&recv
28972939
);
28982940
nErr++;
28992941
break;
29002942
}
2901
- blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]);
2943
+ blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line);
29022944
}
29032945
29042946
if( blob_size(&xfer.err) ){
29052947
fossil_force_newline();
29062948
fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
29633005
}else{
29643006
manifest_crosslink_end(MC_PERMIT_HOOKS);
29653007
content_enable_dephantomize(1);
29663008
}
29673009
db_end_transaction(0);
2968
- };
3010
+ }; /* while(go) */
29693011
transport_stats(&nSent, &nRcvd, 1);
29703012
if( pnRcvd ) *pnRcvd = nArtifactRcvd;
29713013
if( (rSkew*24.0*3600.0) > 10.0 ){
29723014
fossil_warning("*** time skew *** server is fast by %s",
29733015
db_timespan_name(rSkew));
29743016
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827 int rc = -1;
828 char *zLogin = blob_terminate(pLogin);
829 defossilize(zLogin);
830
831 if( fossil_strcmp(zLogin, "nobody")==0
832 || fossil_strcmp(zLogin,"anonymous")==0
833 ){
834 return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835 }
836 if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837 && db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866 const char *zPw = db_column_text(&q, 0);
867 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868 blob_zero(&combined);
869 blob_copy(&combined, pNonce);
870 blob_append(&combined, zSecret, -1);
871 free(zSecret);
872 sha1sum_blob(&combined, &hash);
873 rc = blob_constant_time_cmp(&hash, pSig);
874 blob_reset(&hash);
875 blob_reset(&combined);
876 }
@@ -1241,10 +1241,25 @@
1241 /*
1242 ** If this variable is set, disable login checks. Used for debugging
1243 ** only.
1244 */
1245 static int disableLogin = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1246
1247 /*
1248 ** The CGI/HTTP preprocessor always redirects requests with a content-type
1249 ** of application/x-fossil or application/x-fossil-debug to this page,
1250 ** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
1273 int nUuidList = 0;
1274 char **pzUuidList = 0;
1275 int *pnUuidList = 0;
1276 int uvCatalogSent = 0;
1277 int bSendLinks = 0;
 
1278
1279 if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
1280 fossil_redirect_home();
1281 }
1282 g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
1314 }
1315 zScript = xfer_push_code();
1316 if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
1317 pzUuidList = &zUuidList;
1318 pnUuidList = &nUuidList;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1319 }
1320 while( blob_line(xfer.pIn, &xfer.line) ){
1321 if( blob_buffer(&xfer.line)[0]=='#' ) continue;
1322 if( blob_size(&xfer.line)==0 ) continue;
1323 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
1550
1551 /* login USER NONCE SIGNATURE
1552 **
1553 ** The client has sent login credentials to the server.
1554 ** Validate the login. This has to happen before anything else.
1555 ** The client can send multiple logins. Permissions are cumulative.
 
 
 
 
1556 */
1557 if( blob_eq(&xfer.aToken[0], "login")
1558 && xfer.nToken==4
1559 ){
 
 
1560 if( disableLogin ){
1561 g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
 
 
 
 
 
1562 }else{
1563 if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
1564 || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
1565 ){
1566 cgi_reset_content();
@@ -1651,11 +1692,10 @@
1651 }else{
1652 xfer.nextIsPrivate = 1;
1653 }
1654 }else
1655
1656
1657 /* pragma NAME VALUE...
1658 **
1659 ** The client issues pragmas to try to influence the behavior of the
1660 ** server. These are requests only. Unknown pragmas are silently
1661 ** ignored.
@@ -1694,17 +1734,20 @@
1694 ** The client announces to the server what version of Fossil it
1695 ** is running. The DATE and TIME are a pure numeric ISO8601 time
1696 ** for the specific check-in of the client.
1697 */
1698 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1699 xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
 
1700 if( xfer.nToken>=5 ){
1701 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
1702 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
1703 @ pragma server-version %d(RELEASE_VERSION_NUMBER) \
1704 @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
1705 }
 
 
1706 }else
1707
1708 /* pragma uv-hash HASH
1709 **
1710 ** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
2341 blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
2342 zCkinLock = 0;
2343 }else if( zClientId ){
2344 blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
2345 }
2346
2347 /* Append randomness to the end of the uplink message. This makes all
2348 ** messages unique so that that the login-card nonce will always
2349 ** be unique.
2350 */
2351 zRandomness = db_text(0, "SELECT hex(randomblob(20))");
2352 blob_appendf(&send, "# %s\n", zRandomness);
2353 free(zRandomness);
2354
2355 if( (syncFlags & SYNC_VERBOSE)!=0
2356 && (syncFlags & SYNC_XVERBOSE)==0
2357 ){
2358 fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
2725
2726 /* message MESSAGE
2727 **
2728 ** A message is received from the server. Print it.
2729 ** Similar to "error" but does not stop processing.
2730 **
2731 ** If the "login failed" message is seen, clear the sync password prior
2732 ** to the next cycle.
2733 */
2734 if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
2735 char *zMsg = blob_terminate(&xfer.aToken[1]);
2736 defossilize(zMsg);
2737 if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
2757 ** The server announces to the server what version of Fossil it
2758 ** is running. The DATE and TIME are a pure numeric ISO8601 time
2759 ** for the specific check-in of the client.
2760 */
2761 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2762 xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2]));
 
2763 if( xfer.nToken>=5 ){
2764 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
2765 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
2766 }
 
 
2767 }
2768
2769 /* pragma uv-pull-only
2770 ** pragma uv-push-ok
2771 **
@@ -2896,11 +2938,11 @@
2896 &recv
2897 );
2898 nErr++;
2899 break;
2900 }
2901 blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]);
2902 }
2903
2904 if( blob_size(&xfer.err) ){
2905 fossil_force_newline();
2906 fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
2963 }else{
2964 manifest_crosslink_end(MC_PERMIT_HOOKS);
2965 content_enable_dephantomize(1);
2966 }
2967 db_end_transaction(0);
2968 };
2969 transport_stats(&nSent, &nRcvd, 1);
2970 if( pnRcvd ) *pnRcvd = nArtifactRcvd;
2971 if( (rSkew*24.0*3600.0) > 10.0 ){
2972 fossil_warning("*** time skew *** server is fast by %s",
2973 db_timespan_name(rSkew));
2974
--- src/xfer.c
+++ src/xfer.c
@@ -827,11 +827,11 @@
827 int rc = -1;
828 char *zLogin = blob_terminate(pLogin);
829 defossilize(zLogin);
830
831 if( fossil_strcmp(zLogin, "nobody")==0
832 || fossil_strcmp(zLogin, "anonymous")==0
833 ){
834 return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */
835 }
836 if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0
837 && db_get_boolean("remote_user_ok",0) ){
@@ -866,11 +866,11 @@
866 const char *zPw = db_column_text(&q, 0);
867 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
868 blob_zero(&combined);
869 blob_copy(&combined, pNonce);
870 blob_append(&combined, zSecret, -1);
871 fossil_free(zSecret);
872 sha1sum_blob(&combined, &hash);
873 rc = blob_constant_time_cmp(&hash, pSig);
874 blob_reset(&hash);
875 blob_reset(&combined);
876 }
@@ -1241,10 +1241,25 @@
1241 /*
1242 ** If this variable is set, disable login checks. Used for debugging
1243 ** only.
1244 */
1245 static int disableLogin = 0;
1246
1247 /*
1248 ** Must be passed the version info from pragmas
1249 ** client-version/server-version cards. If the version info is "new
1250 ** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login
1251 ** card flag, else this is a no-op.
1252 */
1253 static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime,
1254 int fLoginCardMode){
1255 if( iRemoteVersion>=22700
1256 && (iDate > 20250727
1257 || (iDate == 20250727 && iTime >= 110500)) ){
1258 g.syncInfo.fLoginCardMode |= fLoginCardMode;
1259 }
1260 }
1261
1262 /*
1263 ** The CGI/HTTP preprocessor always redirects requests with a content-type
1264 ** of application/x-fossil or application/x-fossil-debug to this page,
1265 ** regardless of what path was specified in the HTTP header. This allows
@@ -1273,10 +1288,11 @@
1288 int nUuidList = 0;
1289 char **pzUuidList = 0;
1290 int *pnUuidList = 0;
1291 int uvCatalogSent = 0;
1292 int bSendLinks = 0;
1293 int nLogin = 0;
1294
1295 if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){
1296 fossil_redirect_home();
1297 }
1298 g.zLogin = "anonymous";
@@ -1314,10 +1330,24 @@
1330 }
1331 zScript = xfer_push_code();
1332 if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */
1333 pzUuidList = &zUuidList;
1334 pnUuidList = &nUuidList;
1335 }
1336 if( g.syncInfo.zLoginCard ){
1337 /* Login card received via HTTP Cookie header */
1338 assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" );
1339 blob_zero(&xfer.line);
1340 blob_append(&xfer.line, g.syncInfo.zLoginCard, -1);
1341 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken,
1342 count(xfer.aToken));
1343 fossil_free( g.syncInfo.zLoginCard );
1344 g.syncInfo.zLoginCard = 0;
1345 if( xfer.nToken==4
1346 && blob_eq(&xfer.aToken[0], "login") ){
1347 goto handle_login_card;
1348 }
1349 }
1350 while( blob_line(xfer.pIn, &xfer.line) ){
1351 if( blob_buffer(&xfer.line)[0]=='#' ) continue;
1352 if( blob_size(&xfer.line)==0 ) continue;
1353 xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken));
@@ -1550,17 +1580,28 @@
1580
1581 /* login USER NONCE SIGNATURE
1582 **
1583 ** The client has sent login credentials to the server.
1584 ** Validate the login. This has to happen before anything else.
1585 **
1586 ** For many years, Fossil would accept multiple login cards with
1587 ** cumulative permissions. But that feature was never used. Hence
1588 ** it is now prohibited. Any login card after the first generates
1589 ** a fatal error.
1590 */
1591 if( blob_eq(&xfer.aToken[0], "login")
1592 && xfer.nToken==4
1593 ){
1594 handle_login_card:
1595 nLogin++;
1596 if( disableLogin ){
1597 g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1;
1598 }else if( nLogin > 1 ){
1599 cgi_reset_content();
1600 @ error multiple\slogin\cards
1601 nErr++;
1602 break;
1603 }else{
1604 if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
1605 || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
1606 ){
1607 cgi_reset_content();
@@ -1651,11 +1692,10 @@
1692 }else{
1693 xfer.nextIsPrivate = 1;
1694 }
1695 }else
1696
 
1697 /* pragma NAME VALUE...
1698 **
1699 ** The client issues pragmas to try to influence the behavior of the
1700 ** server. These are requests only. Unknown pragmas are silently
1701 ** ignored.
@@ -1694,17 +1734,20 @@
1734 ** The client announces to the server what version of Fossil it
1735 ** is running. The DATE and TIME are a pure numeric ISO8601 time
1736 ** for the specific check-in of the client.
1737 */
1738 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
1739 xfer.remoteVersion = g.syncInfo.remoteVersion =
1740 atoi(blob_str(&xfer.aToken[2]));
1741 if( xfer.nToken>=5 ){
1742 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
1743 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
1744 @ pragma server-version %d(RELEASE_VERSION_NUMBER) \
1745 @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
1746 }
1747 xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
1748 xfer.remoteTime, 0x04 );
1749 }else
1750
1751 /* pragma uv-hash HASH
1752 **
1753 ** The client wants to make sure that unversioned files are all synced.
@@ -2341,18 +2384,17 @@
2384 blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId);
2385 zCkinLock = 0;
2386 }else if( zClientId ){
2387 blob_appendf(&send, "pragma ci-unlock %s\n", zClientId);
2388 }
 
2389 /* Append randomness to the end of the uplink message. This makes all
2390 ** messages unique so that that the login-card nonce will always
2391 ** be unique.
2392 */
2393 zRandomness = db_text(0, "SELECT hex(randomblob(20))");
2394 blob_appendf(&send, "# %s\n", zRandomness);
2395 fossil_free(zRandomness);
2396
2397 if( (syncFlags & SYNC_VERBOSE)!=0
2398 && (syncFlags & SYNC_XVERBOSE)==0
2399 ){
2400 fossil_print("waiting for server...");
@@ -2725,13 +2767,10 @@
2767
2768 /* message MESSAGE
2769 **
2770 ** A message is received from the server. Print it.
2771 ** Similar to "error" but does not stop processing.
 
 
 
2772 */
2773 if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){
2774 char *zMsg = blob_terminate(&xfer.aToken[1]);
2775 defossilize(zMsg);
2776 if( (syncFlags & SYNC_PUSH) && zMsg
@@ -2757,15 +2796,18 @@
2796 ** The server announces to the server what version of Fossil it
2797 ** is running. The DATE and TIME are a pure numeric ISO8601 time
2798 ** for the specific check-in of the client.
2799 */
2800 if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
2801 xfer.remoteVersion = g.syncInfo.remoteVersion =
2802 atoi(blob_str(&xfer.aToken[2]));
2803 if( xfer.nToken>=5 ){
2804 xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
2805 xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
2806 }
2807 xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate,
2808 xfer.remoteTime, 0x08 );
2809 }
2810
2811 /* pragma uv-pull-only
2812 ** pragma uv-push-ok
2813 **
@@ -2896,11 +2938,11 @@
2938 &recv
2939 );
2940 nErr++;
2941 break;
2942 }
2943 blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line);
2944 }
2945
2946 if( blob_size(&xfer.err) ){
2947 fossil_force_newline();
2948 fossil_warning("%b", &xfer.err);
@@ -2963,11 +3005,11 @@
3005 }else{
3006 manifest_crosslink_end(MC_PERMIT_HOOKS);
3007 content_enable_dephantomize(1);
3008 }
3009 db_end_transaction(0);
3010 }; /* while(go) */
3011 transport_stats(&nSent, &nRcvd, 1);
3012 if( pnRcvd ) *pnRcvd = nArtifactRcvd;
3013 if( (rSkew*24.0*3600.0) > 10.0 ){
3014 fossil_warning("*** time skew *** server is fast by %s",
3015 db_timespan_name(rSkew));
3016
--- www/changes.wiki
+++ www/changes.wiki
@@ -13,10 +13,12 @@
1313
<li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command.
1414
<li> Require at least an anonymous login to access the /blame page and similar,
1515
to help prevent robots from soaking up excess CPU time on such pages.
1616
<li> When walking the filesystem looking for Fossil repositories, avoid descending
1717
into directories named "/proc".
18
+ <ll> Reduce memory requirements for sending authenticated sync protocol
19
+ messages.
1820
</ol>
1921
2022
<h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol>
2123
<li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
2224
<ol type="a">
2325
--- www/changes.wiki
+++ www/changes.wiki
@@ -13,10 +13,12 @@
13 <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command.
14 <li> Require at least an anonymous login to access the /blame page and similar,
15 to help prevent robots from soaking up excess CPU time on such pages.
16 <li> When walking the filesystem looking for Fossil repositories, avoid descending
17 into directories named "/proc".
 
 
18 </ol>
19
20 <h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol>
21 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
22 <ol type="a">
23
--- www/changes.wiki
+++ www/changes.wiki
@@ -13,10 +13,12 @@
13 <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command.
14 <li> Require at least an anonymous login to access the /blame page and similar,
15 to help prevent robots from soaking up excess CPU time on such pages.
16 <li> When walking the filesystem looking for Fossil repositories, avoid descending
17 into directories named "/proc".
18 <ll> Reduce memory requirements for sending authenticated sync protocol
19 messages.
20 </ol>
21
22 <h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol>
23 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
24 <ol type="a">
25
+28 -16
--- www/sync.wiki
+++ www/sync.wiki
@@ -220,26 +220,38 @@
220220
Every message from client to server begins with one or more login
221221
cards. Each login card has the following format:
222222
223223
<pre><b>login</b> <i>userid nonce signature</i></pre>
224224
225
-The userid is the name of the user that is requesting service
226
-from the server. The nonce is the SHA1 hash of the remainder of
227
-the message - all text that follows the newline character that
228
-terminates the login card. The signature is the SHA1 hash of
229
-the concatenation of the nonce and the users password.
230
-
231
-For each login card, the server looks up the user and verifies
232
-that the nonce matches the SHA1 hash of the remainder of the
233
-message. It then checks the signature hash to make sure the
234
-signature matches. If everything
235
-checks out, then the client is granted all privileges of the
236
-specified user.
237
-
238
-Privileges are cumulative. There can be multiple successful
239
-login cards. The session privilege is the union of all
240
-privileges from all login cards.
225
+The userid is the name of the user that is requesting service from the
226
+server, encoded in "fossilized" form (exactly as described for <a
227
+href="#error">the error card</a>). The nonce is the SHA1 hash of the
228
+remainder of the message - all text that follows the newline character
229
+that terminates the login card. The signature is the SHA1 hash of the
230
+concatenation of the nonce and the users password.
231
+
232
+When receving a login card, the server looks up the user and verifies
233
+that the nonce matches the SHA1 hash of the remainder of the message.
234
+It then checks the signature hash to make sure the signature matches.
235
+If everything checks out, then the client is granted all privileges of
236
+the specified user.
237
+
238
+Only one login card is permitted. A second login card will trigger
239
+a sync error. (Prior to 2025-07-21, the protocol permitted multiple
240
+logins, treating the login as the union of all privileges from all
241
+login cards. That capability was never used and has been removed.)
242
+
243
+As of version 2.27, Fossil supports transfering of the login card
244
+externally to the request payload via a Cookie HTTP header:
245
+
246
+<verbatim>
247
+ Cookie: x-f-x-l=...
248
+</verbatim>
249
+
250
+Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is
251
+short for X-Fossil-Xfer-Login.
252
+
241253
242254
<h3 id="file">3.3 File Cards</h3>
243255
244256
Artifacts are transferred using either "file" cards, or "cfile"
245257
or "uvfile" cards.
246258
--- www/sync.wiki
+++ www/sync.wiki
@@ -220,26 +220,38 @@
220 Every message from client to server begins with one or more login
221 cards. Each login card has the following format:
222
223 <pre><b>login</b> <i>userid nonce signature</i></pre>
224
225 The userid is the name of the user that is requesting service
226 from the server. The nonce is the SHA1 hash of the remainder of
227 the message - all text that follows the newline character that
228 terminates the login card. The signature is the SHA1 hash of
229 the concatenation of the nonce and the users password.
230
231 For each login card, the server looks up the user and verifies
232 that the nonce matches the SHA1 hash of the remainder of the
233 message. It then checks the signature hash to make sure the
234 signature matches. If everything
235 checks out, then the client is granted all privileges of the
236 specified user.
237
238 Privileges are cumulative. There can be multiple successful
239 login cards. The session privilege is the union of all
240 privileges from all login cards.
 
 
 
 
 
 
 
 
 
 
 
 
241
242 <h3 id="file">3.3 File Cards</h3>
243
244 Artifacts are transferred using either "file" cards, or "cfile"
245 or "uvfile" cards.
246
--- www/sync.wiki
+++ www/sync.wiki
@@ -220,26 +220,38 @@
220 Every message from client to server begins with one or more login
221 cards. Each login card has the following format:
222
223 <pre><b>login</b> <i>userid nonce signature</i></pre>
224
225 The userid is the name of the user that is requesting service from the
226 server, encoded in "fossilized" form (exactly as described for <a
227 href="#error">the error card</a>). The nonce is the SHA1 hash of the
228 remainder of the message - all text that follows the newline character
229 that terminates the login card. The signature is the SHA1 hash of the
230 concatenation of the nonce and the users password.
231
232 When receving a login card, the server looks up the user and verifies
233 that the nonce matches the SHA1 hash of the remainder of the message.
234 It then checks the signature hash to make sure the signature matches.
235 If everything checks out, then the client is granted all privileges of
236 the specified user.
237
238 Only one login card is permitted. A second login card will trigger
239 a sync error. (Prior to 2025-07-21, the protocol permitted multiple
240 logins, treating the login as the union of all privileges from all
241 login cards. That capability was never used and has been removed.)
242
243 As of version 2.27, Fossil supports transfering of the login card
244 externally to the request payload via a Cookie HTTP header:
245
246 <verbatim>
247 Cookie: x-f-x-l=...
248 </verbatim>
249
250 Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is
251 short for X-Fossil-Xfer-Login.
252
253
254 <h3 id="file">3.3 File Cards</h3>
255
256 Artifacts are transferred using either "file" cards, or "cfile"
257 or "uvfile" cards.
258

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button