Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Account for CGI-hosted fossil instances by sending the xfer login card as a URL argument. This is somewhat inelegant but works around their inability to read HTTP headers. This version is still more verbose than it needs to be, and requires more testing for compatibility with trunk fossil versions. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | xfer-login-card |
| Files: | files | file ages | folders |
| SHA3-256: |
439af9348b5c1d05b8f338a991ce0699 |
| User & Date: | stephan 2025-07-23 20:56:04.049 |
Context
|
2025-07-23
| ||
| 23:31 | Remove some xfer login process debug output. ... (check-in: 815a84cbcc user: stephan tags: xfer-login-card) | |
| 20:56 | Account for CGI-hosted fossil instances by sending the xfer login card as a URL argument. This is somewhat inelegant but works around their inability to read HTTP headers. This version is still more verbose than it needs to be, and requires more testing for compatibility with trunk fossil versions. ... (check-in: 439af9348b user: stephan tags: xfer-login-card) | |
| 17:39 | Add the x-fossil-xfer-login header check in one additional place. With the help of the included debug output, the login problem seems to be caused by CGI (only) instances not reading the inbound HTTP headers. My attempts to make it do have, so far, only triggered HTTP 500 responses. (Edit: i'd forgotten that CGIs don't get headers. The headers are necessarily consumed by the web server to find the CGI script and populate its environment.) ... (check-in: 6c900645ea user: stephan tags: xfer-login-card) | |
Changes
Changes to src/cgi.c.
| ︙ | ︙ | |||
962 963 964 965 966 967 968 | ** are ignored. ** ** * it is impossible for a cookie or query parameter to ** override the value of an environment variable since ** environment variables always have uppercase names. ** ** 2018-03-29: Also ignore the entry if NAME that contains any characters | | | 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 | ** are ignored. ** ** * it is impossible for a cookie or query parameter to ** override the value of an environment variable since ** environment variables always have uppercase names. ** ** 2018-03-29: Also ignore the entry if NAME that contains any characters ** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual ** names that contain characters outside that set, but it never hurts to ** be extra cautious when sanitizing inputs. ** ** Parameters are separated by the "terminator" character. Whitespace ** before the NAME is ignored. ** ** The input string "z" is modified but no copies is made. "z" |
| ︙ | ︙ | |||
1278 1279 1280 1281 1282 1283 1284 | fputs(z, pLog); } /* Forward declaration */ static NORETURN void malformed_request(const char *zMsg, ...); /* | | | | > > > > > | | | | > > > > > > > > > > > > | 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 |
fputs(z, pLog);
}
/* Forward declaration */
static NORETURN void malformed_request(const char *zMsg, ...);
/*
** Checks the QUERY_STRING environment variable, sets it up via
** add_param_list() and, if found, applies its "skin" setting. Returns
** 0 if no QUERY_STRING is set, else it returns a bitmask of:
**
** 0x01 = QUERY_STRING was set.
** 0x02 = "skin" argument was set and processed
** 0x04 = "x-f-x-l" arg was processed.
**
* In the case of the skin, the cookie may still need flushing
** by the page, via cookie_render().
*/
int cgi_setup_query_string(void){
int rc = 0;
char * z = (char*)P("QUERY_STRING");
if( z ){
rc = 0x01;
z = fossil_strdup(z);
add_param_list(z, '&');
z = (char*)P("skin");
if( z ){
char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM);
rc |= 0x02;
if( !zErr && P("once")==0 ){
cookie_write_parameter("skin","skin",z);
/* Per /chat discussion, passing ?skin=... without "once"
** implies the "udc" argument, so we force that into the
** environment here. */
cgi_set_parameter_nocopy("udc", "1", 1);
}
fossil_free(zErr);
}
if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-x-l")) ){
/* CGI fossil instances do not read the HTTP headers, so
** they cannot see the X-Fossil-Xfer-Login card. As a consolation
** to them, we'll accept that via this query argument. */
rc |= 0x04;
fossil_free( g.syncInfo.zLoginCard );
g.syncInfo.zLoginCard = fossil_strdup(z);
g.syncInfo.bLoginCardHeader = 3;
/*cgi_delete_parameter("x-f-x-l");*/
/*fprintf(stderr, "query string setup: x-f-x-l=%s\n",
g.syncInfo.zLoginCard);*/
}
}
return rc;
}
/*
** Initialize the query parameter database. Information is pulled from
|
| ︙ | ︙ | |||
2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 |
int x2 = 0;
if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){
rangeStart = x1;
rangeEnd = x2+1;
}
}else if( fossil_strcmp(zFieldName, "x-fossil-xfer-login:")==0 ){
/*cgi_setenv("FOSSIL_LCH_cgi_handle_http_request", zVal);*/
g.syncInfo.zLoginCard = fossil_strdup(zVal);
g.syncInfo.bLoginCardHeader = 1;
}
}
cgi_setenv("REQUEST_SCHEME",zScheme);
cgi_init();
cgi_trace(0);
| > | 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 |
int x2 = 0;
if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){
rangeStart = x1;
rangeEnd = x2+1;
}
}else if( fossil_strcmp(zFieldName, "x-fossil-xfer-login:")==0 ){
/*cgi_setenv("FOSSIL_LCH_cgi_handle_http_request", zVal);*/
fossil_free( g.syncInfo.zLoginCard );
g.syncInfo.zLoginCard = fossil_strdup(zVal);
g.syncInfo.bLoginCardHeader = 1;
}
}
cgi_setenv("REQUEST_SCHEME",zScheme);
cgi_init();
cgi_trace(0);
|
| ︙ | ︙ |
Changes to src/http.c.
| ︙ | ︙ | |||
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
blob_append(&pw, zPw, -1);
sha1sum_blob(&pw, &sig);
blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
blob_reset(&pw);
blob_reset(&sig);
blob_reset(&nonce);
}
/*
** Construct an appropriate HTTP request header. Write the header
** into pHdr. This routine initializes the pHdr blob. pPayload is
** the complete payload (including the login card if pLogin is NULL or
** empty) already compressed.
*/
static void http_build_header(
Blob *pPayload, /* the payload that will be sent */
Blob *pHdr, /* construct the header here */
Blob *pLogin, /* Login card header value or NULL */
const char *zAltMimetype /* Alternative mimetype */
){
int nPayload = pPayload ? blob_size(pPayload) : 0;
blob_zero(pHdr);
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | > | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
blob_append(&pw, zPw, -1);
sha1sum_blob(&pw, &sig);
blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
blob_reset(&pw);
blob_reset(&sig);
blob_reset(&nonce);
}
/*
** If we're in "login card header" mode, append ?x-f-x-l=ABC to
** g.url.path, replacing any "?..." part of g.url.path. ABC = the
** %T-encoded contents of pLogin. This is workaround for feeding the
** login card to CGI-hosted fossil instances, as those do not read the
** HTTP headers so cannot see the X-Fossil-Xfer-Login (x-f-x-l)
** header.
*/
static void url_append_login_card(Blob * const pLogin){
if( g.syncInfo.bLoginCardHeader ||
g.syncInfo.remoteVersion >= RELEASE_VERSION_NUMBER ){
char * x;
char * z = g.url.path;
while( z && *z && '?'!=*z ) ++z;
if( z && *z ) *z = 0;
x = mprintf("%s?x-f-x-l=%T", g.url.path ? g.url.path : "/",
blob_str(pLogin));
fossil_free(g.url.path);
g.url.path = x;
if( !g.syncInfo.bLoginCardHeader ){
g.syncInfo.bLoginCardHeader = 4;
}
}
}
/*
** Construct an appropriate HTTP request header. Write the header
** into pHdr. This routine initializes the pHdr blob. pPayload is
** the complete payload (including the login card if pLogin is NULL or
** empty) already compressed.
*/
static void http_build_header(
Blob *pPayload, /* the payload that will be sent */
Blob *pHdr, /* construct the header here */
Blob *pLogin, /* Login card header value or NULL */
const char *zAltMimetype /* Alternative mimetype */
){
int nPayload = pPayload ? blob_size(pPayload) : 0;
blob_zero(pHdr);
if( nPayload>0 && pLogin && blob_size(pLogin)>0 ){
/* Add login card URL arg for POST requests */
url_append_login_card(pLogin);
}
blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
nPayload>0 ? "POST" : "GET",
(g.url.path && g.url.path[0]) ? g.url.path : "/");
if( g.url.proxyAuth ){
blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
}
if( g.zHttpAuth && g.zHttpAuth[0] ){
const char *zCredentials = g.zHttpAuth;
char *zEncoded = encode64(zCredentials, -1);
blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
fossil_free(zEncoded);
}
blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
if( pLogin && blob_size(pLogin) ){
blob_appendf(pHdr, "X-Fossil-Xfer-Login: %b\r\n", pLogin)
/* Noting that CGIs can't read headers */;
}
if( nPayload ){
if( zAltMimetype ){
blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
}else if( g.fHttpTrace ){
blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
}else{
|
| ︙ | ︙ | |||
392 393 394 395 396 397 398 |
**
** * The ssh_needs_path_argument() function above.
** * The test-ssh-needs-path command that shows the settings
** that cache whether or not a PATH= is needed for a particular
** HOSTNAME.
*/
void ssh_add_path_argument(Blob *pCmd){
| | | 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 |
**
** * The ssh_needs_path_argument() function above.
** * The test-ssh-needs-path command that shows the settings
** that cache whether or not a PATH= is needed for a particular
** HOSTNAME.
*/
void ssh_add_path_argument(Blob *pCmd){
blob_append_escaped_arg(pCmd,
"PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
}
/*
** Return the complete text of the last HTTP reply as saved in the
** http-reply-N.txt file. This only works if run using --httptrace.
** Without the --httptrace option, this routine returns a NULL pointer.
|
| ︙ | ︙ | |||
469 470 471 472 473 474 475 |
}
/* Construct the login card and prepare the complete payload */
blob_zero(&login);
if( blob_size(pSend)==0 ){
blob_zero(&payload);
}else{
if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
| | | | > | 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 |
}
/* Construct the login card and prepare the complete payload */
blob_zero(&login);
if( blob_size(pSend)==0 ){
blob_zero(&payload);
}else{
if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
if( g.syncInfo.bLoginCardHeader>0 ){
/* The login card will be sent via an HTTP header and/or URL flag. */
if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
/* Maintenance note: we cannot blob_swap(pSend,&payload) here
** because the HTTP 401 and redirect response handling below
** needs pSend unmodified. payload won't be modified after
** this point, so we can make it a proxy for pSend for
** zero heap memory. */
blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
}else{
blob_compress(pSend, &payload);
}
}else{
/* Prepend the login card (if set) to the payload */
if( blob_size(&login) ){
|
| ︙ | ︙ | |||
528 529 530 531 532 533 534 535 536 537 538 539 540 541 |
}
/*
** Send the request to the server.
*/
if( mHttpFlags & HTTP_VERBOSE ){
fossil_print("URL: %s\n", g.url.canonical);
fossil_print("Sending %d byte header and %d byte payload\n",
blob_size(&hdr), blob_size(&payload));
}
transport_send(&g.url, &hdr);
transport_send(&g.url, &payload);
blob_reset(&hdr);
blob_reset(&payload);
| > | 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 |
}
/*
** Send the request to the server.
*/
if( mHttpFlags & HTTP_VERBOSE ){
fossil_print("URL: %s\n", g.url.canonical);
fossil_print("URL path: %s\n", g.url.path);
fossil_print("Sending %d byte header and %d byte payload\n",
blob_size(&hdr), blob_size(&payload));
}
transport_send(&g.url, &hdr);
transport_send(&g.url, &payload);
blob_reset(&hdr);
blob_reset(&payload);
|
| ︙ | ︙ | |||
659 660 661 662 663 664 665 |
if( mHttpFlags & HTTP_GENERIC ){
if( mHttpFlags & HTTP_NOCOMPRESS ) isCompressed = 0;
}else if( fossil_strnicmp(&zLine[14], "application/x-fossil", -1)!=0 ){
isError = 1;
}
}
}else if( fossil_strnicmp(zLine, "x-fossil-xfer-login: ", 21)==0 ){
| | | 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 |
if( mHttpFlags & HTTP_GENERIC ){
if( mHttpFlags & HTTP_NOCOMPRESS ) isCompressed = 0;
}else if( fossil_strnicmp(&zLine[14], "application/x-fossil", -1)!=0 ){
isError = 1;
}
}
}else if( fossil_strnicmp(zLine, "x-fossil-xfer-login: ", 21)==0 ){
fossil_free( g.syncInfo.zLoginCard );
g.syncInfo.zLoginCard = fossil_strdup(&zLine[21]);
g.syncInfo.bLoginCardHeader = 1;
}
}
if( iHttpVersion<0 ){
/* We got nothing back from the server. If using the ssh: protocol,
** this might mean we need to add or remove the PATH=... argument
|
| ︙ | ︙ |
Changes to src/http_transport.c.
| ︙ | ︙ | |||
139 140 141 142 143 144 145 |
if( (pUrlData->flags & URL_SSH_EXE)!=0
&& !is_safe_fossil_command(pUrlData->fossil)
){
fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
"the server.", pUrlData->fossil);
}
if( (pUrlData->flags & URL_SSH_EXE)==0
| | | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
if( (pUrlData->flags & URL_SSH_EXE)!=0
&& !is_safe_fossil_command(pUrlData->fossil)
){
fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
"the server.", pUrlData->fossil);
}
if( (pUrlData->flags & URL_SSH_EXE)==0
&& (pUrlData->flags & URL_SSH_PATH)!=0
){
ssh_add_path_argument(&zCmd);
}
blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
blob_append(&zCmd, " test-http", 10);
if( pUrlData->path && pUrlData->path[0] ){
blob_append_escaped_arg(&zCmd, pUrlData->path, 1);
|
| ︙ | ︙ |
Changes to src/main.c.
| ︙ | ︙ | |||
290 291 292 293 294 295 296 |
int nPendingRequest; /* # of HTTP requests in "fossil server" */
int nRequest; /* Total # of HTTP request */
int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
/* State for communicating specific details between the inbound HTTP
** header parser (cgi.c), xfer.c, and http.c. */
struct {
| | > | | > > > > > > | 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
int nPendingRequest; /* # of HTTP requests in "fossil server" */
int nRequest; /* Total # of HTTP request */
int bAvoidDeltaManifests; /* Avoid using delta manifests if true */
/* State for communicating specific details between the inbound HTTP
** header parser (cgi.c), xfer.c, and http.c. */
struct {
char *zLoginCard; /* Inbound X-Fossil-Xfer-Login request header
** or x-f-x-l URL parameter. */
int bLoginCardHeader; /* If non-0, emit login cards in outbound
** requests as HTTP headers instead of as
** part of the payload. Gets activated
** on-demand based on xfer traffic
** contents. Values, for
** diagnostic/debuggin purposes: 1=set via
** CLI --flag. 2=set via inbound HTTP
** header. 3=set via query string
** arg. 4=set via http_build_header(). */
int remoteVersion; /* Remote fossil version. Used for negotiating
** how to handle the login card. */
} syncInfo;
#ifdef FOSSIL_ENABLE_JSON
struct FossilJsonBits {
int isJsonMode; /* True if running in JSON mode, else
false. This changes how errors are
reported. In JSON mode we try to
always output JSON-form error
|
| ︙ | ︙ | |||
1504 1505 1506 1507 1508 1509 1510 |
*/
NORETURN void fossil_redirect_home(void){
/* In order for ?skin=... to work when visiting the site from
** a typical external link, we have to process it here, as
** that parameter gets lost during the redirect. We "could"
** pass the whole query string along instead, but that seems
** unnecessary. */
| | | 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 |
*/
NORETURN void fossil_redirect_home(void){
/* In order for ?skin=... to work when visiting the site from
** a typical external link, we have to process it here, as
** that parameter gets lost during the redirect. We "could"
** pass the whole query string along instead, but that seems
** unnecessary. */
if(cgi_setup_query_string() & 0x02){
cookie_render();
}
cgi_redirectf("%R%s", db_get("index-page", "/index"));
}
/*
** If running as root, chroot to the directory containing the
|
| ︙ | ︙ |
Changes to src/url.c.
| ︙ | ︙ | |||
225 226 227 228 229 230 231 |
if( c!=0 && c!='/' ) fossil_fatal("url missing '/' after port number");
pUrlData->hostname = mprintf("%s:%d", pUrlData->name, pUrlData->port);
}else{
pUrlData->port = pUrlData->dfltPort;
pUrlData->hostname = pUrlData->name;
}
dehttpize(pUrlData->name);
| | | | 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
if( c!=0 && c!='/' ) fossil_fatal("url missing '/' after port number");
pUrlData->hostname = mprintf("%s:%d", pUrlData->name, pUrlData->port);
}else{
pUrlData->port = pUrlData->dfltPort;
pUrlData->hostname = pUrlData->name;
}
dehttpize(pUrlData->name);
pUrlData->path = fossil_strdup(&zUrl[i]);
for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){}
if( pUrlData->path[i] ){
pUrlData->path[i] = 0;
i++;
}
zExe = fossil_strdup("");
while( pUrlData->path[i]!=0 ){
char *zName, *zValue;
zName = &pUrlData->path[i];
zValue = zName;
while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; }
if( pUrlData->path[i]=='=' ){
pUrlData->path[i] = 0;
|
| ︙ | ︙ |
Changes to src/xfer.c.
| ︙ | ︙ | |||
1723 1724 1725 1726 1727 1728 1729 |
/* pragma client-version VERSION ?DATE? ?TIME?
**
** The client announces to the server what version of Fossil it
** is running. The DATE and TIME are a pure numeric ISO8601 time
** for the specific check-in of the client.
*/
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
| > | | 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 |
/* pragma client-version VERSION ?DATE? ?TIME?
**
** The client announces to the server what version of Fossil it
** is running. The DATE and TIME are a pure numeric ISO8601 time
** for the specific check-in of the client.
*/
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){
xfer.remoteVersion = g.syncInfo.remoteVersion =
atoi(blob_str(&xfer.aToken[2]));
g.syncInfo.bLoginCardHeader =
xfer.remoteVersion>=RELEASE_VERSION_NUMBER;
if( xfer.nToken>=5 ){
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
@ pragma server-version %d(RELEASE_VERSION_NUMBER) \
@ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME)
|
| ︙ | ︙ | |||
2784 2785 2786 2787 2788 2789 2790 |
/* pragma server-version VERSION ?DATE? ?TIME?
**
** The server announces to the server what version of Fossil it
** is running. The DATE and TIME are a pure numeric ISO8601 time
** for the specific check-in of the client.
*/
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
| > | | 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 |
/* pragma server-version VERSION ?DATE? ?TIME?
**
** The server announces to the server what version of Fossil it
** is running. The DATE and TIME are a pure numeric ISO8601 time
** for the specific check-in of the client.
*/
if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){
xfer.remoteVersion = g.syncInfo.remoteVersion =
atoi(blob_str(&xfer.aToken[2]));
g.syncInfo.bLoginCardHeader =
xfer.remoteVersion>=RELEASE_VERSION_NUMBER;
if( xfer.nToken>=5 ){
xfer.remoteDate = atoi(blob_str(&xfer.aToken[3]));
xfer.remoteTime = atoi(blob_str(&xfer.aToken[4]));
}
}
|
| ︙ | ︙ |