Fossil SCM

Minimal client implementation of HTTP version 1.1 with chunked transfer-encoding. Some egress filters drop all HTTP version 1.0 traffic at the Web Application Firewalls (WAF). WAFs commonly provided by CDN (Content Distribution Networks) such as CloudFlare and AWS to their customers have such controls on them, and there are understandable policy reasons for disallowing 1.0. It is almost exclusively used by old software that is often exploitable (eg ancient versions of curl). Fossil users within virtual machines with such egress filters in place get confusing messages along the lines of "Upgrade required". This is inconvenient for Fossil users, and the quick solution is to seek a git mirror for the Fossil repo if one is available, because git just works. There is no need to upgrade the Fossil server to support 1.1 to solve the egress problem. This is (I believe) an RFC compliant minimal implementation and it seems to work, but none of the optional extensions are supported because I don't think Fossil needs them. It does need more careful checking on the supplied chunk length because strtol is insufficient.

danshearer 2026-06-10 11:08 UTC trunk
Commit 0f8a7d6095a971150267273c93143df56e0c597b5d4b5089d7f9c38eca963c42
1 file changed +43 -2
+43 -2
--- src/http.c
+++ src/http.c
@@ -151,11 +151,11 @@
151151
}else if( g.url.path==0 || g.url.path[0]==0 ){
152152
zPath = "/";
153153
}else{
154154
zPath = g.url.path;
155155
}
156
- blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
156
+ blob_appendf(pHdr, "%s %s HTTP/1.1\r\n",
157157
nPayload>0 ? "POST" : "GET", zPath);
158158
if( g.url.proxyAuth ){
159159
blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
160160
}
161161
if( g.zHttpAuth && g.zHttpAuth[0] ){
@@ -163,10 +163,11 @@
163163
char *zEncoded = encode64(zCredentials, -1);
164164
blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
165165
fossil_free(zEncoded);
166166
}
167167
blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
168
+ blob_appendf(pHdr, "Connection: close\r\n");
168169
blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
169170
if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
170171
if( g.syncInfo.fLoginCardMode>0
171172
&& nPayload>0 && pLogin && blob_size(pLogin) ){
172173
/* Add sync login card via a transient cookie. We can only do this
@@ -455,10 +456,11 @@
455456
int iHttpVersion; /* Which version of HTTP protocol server uses */
456457
char *zLine; /* A single line of the reply header */
457458
int i; /* Loop counter */
458459
int isError = 0; /* True if the reply is an error message */
459460
int isCompressed = 1; /* True if the reply is compressed */
461
+ int isChunked = 0; /* True if Transfer-Encoding: chunked */
460462
461463
if( g.zHttpCmd!=0 ){
462464
/* Handle the --transport-command option for "fossil sync" and similar */
463465
return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype);
464466
}
@@ -605,10 +607,14 @@
605607
if( iHttpVersion<0 ) iHttpVersion = 1;
606608
closeConnection = 0;
607609
}else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
608610
for(i=15; fossil_isspace(zLine[i]); i++){}
609611
iLength = atoi(&zLine[i]);
612
+ }else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
613
+ if( sqlite3_strlike("%chunked%", &zLine[18], 0)==0 ){
614
+ isChunked = 1;
615
+ }
610616
}else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
611617
if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
612618
closeConnection = 1;
613619
}else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){
614620
closeConnection = 0;
@@ -735,11 +741,46 @@
735741
736742
/*
737743
** Extract the reply payload that follows the header
738744
*/
739745
blob_zero(pReply);
740
- if( iLength==0 ){
746
+ if( isChunked ){
747
+ /* Decode an HTTP/1.1 "Transfer-Encoding: chunked" reply body. Each
748
+ ** chunk is a hex length on its own line (optionally followed by a
749
+ ** ";extension" that is ignored), then that many payload bytes, then a
750
+ ** bare CRLF. A zero-length chunk terminates the body, after which any
751
+ ** trailer header lines are read and discarded up to the blank line. */
752
+ char *zChunk;
753
+ while( (zChunk = transport_receive_line(&g.url))!=0 ){
754
+ int nChunk; /* Size of this chunk in bytes */
755
+ int nPrior; /* Bytes already in pReply */
756
+ while( fossil_isspace(zChunk[0]) ) zChunk++;
757
+ nChunk = (int)strtol(zChunk, 0, 16);
758
+ if( nChunk<0 ){
759
+ goto write_err;
760
+ }
761
+ if( nChunk==0 ){
762
+ /* Final chunk: consume trailer lines up to the terminating blank. */
763
+ while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
764
+ break;
765
+ }
766
+ nPrior = blob_size(pReply);
767
+ blob_resize(pReply, nPrior+nChunk);
768
+ /* transport_receive() may return short; loop until the chunk is full. */
769
+ while( nChunk>0 ){
770
+ int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nChunk);
771
+ if( nGot<=0 ){
772
+ fossil_warning("chunked reply truncated");
773
+ goto write_err;
774
+ }
775
+ nPrior += nGot;
776
+ nChunk -= nGot;
777
+ pReply->nUsed = nPrior;
778
+ }
779
+ transport_receive_line(&g.url); /* CRLF that follows the chunk data */
780
+ }
781
+ }else if( iLength==0 ){
741782
/* No content to read */
742783
}else if( iLength>0 ){
743784
/* Read content of a known length */
744785
int iRecvLen; /* Received length of the reply payload */
745786
blob_resize(pReply, iLength);
746787
--- src/http.c
+++ src/http.c
@@ -151,11 +151,11 @@
151 }else if( g.url.path==0 || g.url.path[0]==0 ){
152 zPath = "/";
153 }else{
154 zPath = g.url.path;
155 }
156 blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
157 nPayload>0 ? "POST" : "GET", zPath);
158 if( g.url.proxyAuth ){
159 blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
160 }
161 if( g.zHttpAuth && g.zHttpAuth[0] ){
@@ -163,10 +163,11 @@
163 char *zEncoded = encode64(zCredentials, -1);
164 blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
165 fossil_free(zEncoded);
166 }
167 blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
 
168 blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
169 if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
170 if( g.syncInfo.fLoginCardMode>0
171 && nPayload>0 && pLogin && blob_size(pLogin) ){
172 /* Add sync login card via a transient cookie. We can only do this
@@ -455,10 +456,11 @@
455 int iHttpVersion; /* Which version of HTTP protocol server uses */
456 char *zLine; /* A single line of the reply header */
457 int i; /* Loop counter */
458 int isError = 0; /* True if the reply is an error message */
459 int isCompressed = 1; /* True if the reply is compressed */
 
460
461 if( g.zHttpCmd!=0 ){
462 /* Handle the --transport-command option for "fossil sync" and similar */
463 return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype);
464 }
@@ -605,10 +607,14 @@
605 if( iHttpVersion<0 ) iHttpVersion = 1;
606 closeConnection = 0;
607 }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
608 for(i=15; fossil_isspace(zLine[i]); i++){}
609 iLength = atoi(&zLine[i]);
 
 
 
 
610 }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
611 if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
612 closeConnection = 1;
613 }else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){
614 closeConnection = 0;
@@ -735,11 +741,46 @@
735
736 /*
737 ** Extract the reply payload that follows the header
738 */
739 blob_zero(pReply);
740 if( iLength==0 ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741 /* No content to read */
742 }else if( iLength>0 ){
743 /* Read content of a known length */
744 int iRecvLen; /* Received length of the reply payload */
745 blob_resize(pReply, iLength);
746
--- src/http.c
+++ src/http.c
@@ -151,11 +151,11 @@
151 }else if( g.url.path==0 || g.url.path[0]==0 ){
152 zPath = "/";
153 }else{
154 zPath = g.url.path;
155 }
156 blob_appendf(pHdr, "%s %s HTTP/1.1\r\n",
157 nPayload>0 ? "POST" : "GET", zPath);
158 if( g.url.proxyAuth ){
159 blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
160 }
161 if( g.zHttpAuth && g.zHttpAuth[0] ){
@@ -163,10 +163,11 @@
163 char *zEncoded = encode64(zCredentials, -1);
164 blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
165 fossil_free(zEncoded);
166 }
167 blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
168 blob_appendf(pHdr, "Connection: close\r\n");
169 blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
170 if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
171 if( g.syncInfo.fLoginCardMode>0
172 && nPayload>0 && pLogin && blob_size(pLogin) ){
173 /* Add sync login card via a transient cookie. We can only do this
@@ -455,10 +456,11 @@
456 int iHttpVersion; /* Which version of HTTP protocol server uses */
457 char *zLine; /* A single line of the reply header */
458 int i; /* Loop counter */
459 int isError = 0; /* True if the reply is an error message */
460 int isCompressed = 1; /* True if the reply is compressed */
461 int isChunked = 0; /* True if Transfer-Encoding: chunked */
462
463 if( g.zHttpCmd!=0 ){
464 /* Handle the --transport-command option for "fossil sync" and similar */
465 return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype);
466 }
@@ -605,10 +607,14 @@
607 if( iHttpVersion<0 ) iHttpVersion = 1;
608 closeConnection = 0;
609 }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
610 for(i=15; fossil_isspace(zLine[i]); i++){}
611 iLength = atoi(&zLine[i]);
612 }else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
613 if( sqlite3_strlike("%chunked%", &zLine[18], 0)==0 ){
614 isChunked = 1;
615 }
616 }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
617 if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
618 closeConnection = 1;
619 }else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){
620 closeConnection = 0;
@@ -735,11 +741,46 @@
741
742 /*
743 ** Extract the reply payload that follows the header
744 */
745 blob_zero(pReply);
746 if( isChunked ){
747 /* Decode an HTTP/1.1 "Transfer-Encoding: chunked" reply body. Each
748 ** chunk is a hex length on its own line (optionally followed by a
749 ** ";extension" that is ignored), then that many payload bytes, then a
750 ** bare CRLF. A zero-length chunk terminates the body, after which any
751 ** trailer header lines are read and discarded up to the blank line. */
752 char *zChunk;
753 while( (zChunk = transport_receive_line(&g.url))!=0 ){
754 int nChunk; /* Size of this chunk in bytes */
755 int nPrior; /* Bytes already in pReply */
756 while( fossil_isspace(zChunk[0]) ) zChunk++;
757 nChunk = (int)strtol(zChunk, 0, 16);
758 if( nChunk<0 ){
759 goto write_err;
760 }
761 if( nChunk==0 ){
762 /* Final chunk: consume trailer lines up to the terminating blank. */
763 while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
764 break;
765 }
766 nPrior = blob_size(pReply);
767 blob_resize(pReply, nPrior+nChunk);
768 /* transport_receive() may return short; loop until the chunk is full. */
769 while( nChunk>0 ){
770 int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nChunk);
771 if( nGot<=0 ){
772 fossil_warning("chunked reply truncated");
773 goto write_err;
774 }
775 nPrior += nGot;
776 nChunk -= nGot;
777 pReply->nUsed = nPrior;
778 }
779 transport_receive_line(&g.url); /* CRLF that follows the chunk data */
780 }
781 }else if( iLength==0 ){
782 /* No content to read */
783 }else if( iLength>0 ){
784 /* Read content of a known length */
785 int iRecvLen; /* Received length of the reply payload */
786 blob_resize(pReply, iLength);
787

Keyboard Shortcuts

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