Fossil SCM
http.c: Complete the HTTP 1.1 chunked reply decoder Fix error conditions and exceptions when Transfer-Encoding chunked, partly from drh's Claude: * Premature EOF was accepted as a valid EOF. * strtol --> strtoll with checking, to avoid casting errors * Per RFC 7230, match "chunked" at the end of the Transfer-Encoding value rather than a substring, so the word "chunked" appearing isn't acceped as a token * make nPrior unsigned int to match blob_size() to avoid a negative offset on large replies * Report only the chunk bytes actually received, not chunk bytes from the header, so the number is correct if the connection terminates part-way.
Commit
441a35ce2a1f63c0a85521cd709da43a35294a7555a0ba598180280cb7fc27f1
Parent
0f8a7d6095a9711…
1 file changed
+44
-17
+44
-17
| --- src/http.c | ||
| +++ src/http.c | ||
| @@ -608,11 +608,13 @@ | ||
| 608 | 608 | closeConnection = 0; |
| 609 | 609 | }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){ |
| 610 | 610 | for(i=15; fossil_isspace(zLine[i]); i++){} |
| 611 | 611 | iLength = atoi(&zLine[i]); |
| 612 | 612 | }else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){ |
| 613 | - if( sqlite3_strlike("%chunked%", &zLine[18], 0)==0 ){ | |
| 613 | + /* RFC 7230: "chunked" must be the final transfer-coding so only | |
| 614 | + ** match when it appears at the end of the line. */ | |
| 615 | + if( sqlite3_strlike("%chunked", &zLine[18], 0)==0 ){ | |
| 614 | 616 | isChunked = 1; |
| 615 | 617 | } |
| 616 | 618 | }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){ |
| 617 | 619 | if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){ |
| 618 | 620 | closeConnection = 1; |
| @@ -748,37 +750,62 @@ | ||
| 748 | 750 | ** chunk is a hex length on its own line (optionally followed by a |
| 749 | 751 | ** ";extension" that is ignored), then that many payload bytes, then a |
| 750 | 752 | ** bare CRLF. A zero-length chunk terminates the body, after which any |
| 751 | 753 | ** trailer header lines are read and discarded up to the blank line. */ |
| 752 | 754 | 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 */ | |
| 755 | + int sawTerminator = 0; /* True once the 0-length chunk is seen */ | |
| 756 | + while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){ | |
| 757 | + sqlite3_int64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */ | |
| 758 | + unsigned int nPrior; /* Bytes already in pReply (matches blob nUsed) */ | |
| 759 | + char *zEnd = 0; /* End of the hex digits actually parsed */ | |
| 756 | 760 | while( fossil_isspace(zChunk[0]) ) zChunk++; |
| 757 | - nChunk = (int)strtol(zChunk, 0, 16); | |
| 758 | - if( nChunk<0 ){ | |
| 761 | + nChunk = strtoll(zChunk, &zEnd, 16); | |
| 762 | + if( zEnd==zChunk ){ | |
| 763 | + /* No hex digit consumed: a blank or malformed chunk-size line, which | |
| 764 | + ** is the symptom of a connection that closed mid-stream. Treat it as | |
| 765 | + ** a truncated (failed) */ | |
| 766 | + fossil_warning("chunked reply: missing or malformed chunk size"); | |
| 767 | + goto write_err; | |
| 768 | + } | |
| 769 | + if( nChunk<0 || nChunk>0x7fffffff ){ | |
| 770 | + /* Negative, or larger than we will ever accept in one chunk. */ | |
| 771 | + fossil_warning("chunked reply: invalid chunk size"); | |
| 759 | 772 | goto write_err; |
| 760 | 773 | } |
| 761 | 774 | if( nChunk==0 ){ |
| 762 | 775 | /* Final chunk: consume trailer lines up to the terminating blank. */ |
| 776 | + sawTerminator = 1; | |
| 763 | 777 | while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){} |
| 764 | 778 | break; |
| 765 | 779 | } |
| 766 | 780 | 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; | |
| 781 | + /* Reserve space without advancing nUsed, so that on error | |
| 782 | + ** the blob's reported size equals the bytes actually | |
| 783 | + ** received rather the claimed chunk length */ | |
| 784 | + blob_reserve(pReply, nPrior+(unsigned int)nChunk); | |
| 785 | + { | |
| 786 | + unsigned int nRemaining = (unsigned int)nChunk; | |
| 787 | + /* transport_receive() may return short; loop until the chunk is full. */ | |
| 788 | + while( nRemaining>0 ){ | |
| 789 | + int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining); | |
| 790 | + if( nGot<=0 ){ | |
| 791 | + fossil_warning("chunked reply truncated"); | |
| 792 | + goto write_err; | |
| 793 | + } | |
| 794 | + nPrior += (unsigned int)nGot; | |
| 795 | + nRemaining -= (unsigned int)nGot; | |
| 796 | + pReply->nUsed = nPrior; | |
| 797 | + } | |
| 778 | 798 | } |
| 779 | 799 | transport_receive_line(&g.url); /* CRLF that follows the chunk data */ |
| 800 | + } | |
| 801 | + if( !sawTerminator ){ | |
| 802 | + /* The loop exited without ever seeing the 0-length terminator chunk, | |
| 803 | + ** meaning the peer closed before the body was complete. A truncated | |
| 804 | + ** sync must be an error, never silent success. */ | |
| 805 | + fossil_warning("chunked reply ended without terminator"); | |
| 806 | + goto write_err; | |
| 780 | 807 | } |
| 781 | 808 | }else if( iLength==0 ){ |
| 782 | 809 | /* No content to read */ |
| 783 | 810 | }else if( iLength>0 ){ |
| 784 | 811 | /* Read content of a known length */ |
| 785 | 812 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -608,11 +608,13 @@ | |
| 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; |
| @@ -748,37 +750,62 @@ | |
| 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 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -608,11 +608,13 @@ | |
| 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 | /* RFC 7230: "chunked" must be the final transfer-coding so only |
| 614 | ** match when it appears at the end of the line. */ |
| 615 | if( sqlite3_strlike("%chunked", &zLine[18], 0)==0 ){ |
| 616 | isChunked = 1; |
| 617 | } |
| 618 | }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){ |
| 619 | if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){ |
| 620 | closeConnection = 1; |
| @@ -748,37 +750,62 @@ | |
| 750 | ** chunk is a hex length on its own line (optionally followed by a |
| 751 | ** ";extension" that is ignored), then that many payload bytes, then a |
| 752 | ** bare CRLF. A zero-length chunk terminates the body, after which any |
| 753 | ** trailer header lines are read and discarded up to the blank line. */ |
| 754 | char *zChunk; |
| 755 | int sawTerminator = 0; /* True once the 0-length chunk is seen */ |
| 756 | while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){ |
| 757 | sqlite3_int64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */ |
| 758 | unsigned int nPrior; /* Bytes already in pReply (matches blob nUsed) */ |
| 759 | char *zEnd = 0; /* End of the hex digits actually parsed */ |
| 760 | while( fossil_isspace(zChunk[0]) ) zChunk++; |
| 761 | nChunk = strtoll(zChunk, &zEnd, 16); |
| 762 | if( zEnd==zChunk ){ |
| 763 | /* No hex digit consumed: a blank or malformed chunk-size line, which |
| 764 | ** is the symptom of a connection that closed mid-stream. Treat it as |
| 765 | ** a truncated (failed) */ |
| 766 | fossil_warning("chunked reply: missing or malformed chunk size"); |
| 767 | goto write_err; |
| 768 | } |
| 769 | if( nChunk<0 || nChunk>0x7fffffff ){ |
| 770 | /* Negative, or larger than we will ever accept in one chunk. */ |
| 771 | fossil_warning("chunked reply: invalid chunk size"); |
| 772 | goto write_err; |
| 773 | } |
| 774 | if( nChunk==0 ){ |
| 775 | /* Final chunk: consume trailer lines up to the terminating blank. */ |
| 776 | sawTerminator = 1; |
| 777 | while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){} |
| 778 | break; |
| 779 | } |
| 780 | nPrior = blob_size(pReply); |
| 781 | /* Reserve space without advancing nUsed, so that on error |
| 782 | ** the blob's reported size equals the bytes actually |
| 783 | ** received rather the claimed chunk length */ |
| 784 | blob_reserve(pReply, nPrior+(unsigned int)nChunk); |
| 785 | { |
| 786 | unsigned int nRemaining = (unsigned int)nChunk; |
| 787 | /* transport_receive() may return short; loop until the chunk is full. */ |
| 788 | while( nRemaining>0 ){ |
| 789 | int nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining); |
| 790 | if( nGot<=0 ){ |
| 791 | fossil_warning("chunked reply truncated"); |
| 792 | goto write_err; |
| 793 | } |
| 794 | nPrior += (unsigned int)nGot; |
| 795 | nRemaining -= (unsigned int)nGot; |
| 796 | pReply->nUsed = nPrior; |
| 797 | } |
| 798 | } |
| 799 | transport_receive_line(&g.url); /* CRLF that follows the chunk data */ |
| 800 | } |
| 801 | if( !sawTerminator ){ |
| 802 | /* The loop exited without ever seeing the 0-length terminator chunk, |
| 803 | ** meaning the peer closed before the body was complete. A truncated |
| 804 | ** sync must be an error, never silent success. */ |
| 805 | fossil_warning("chunked reply ended without terminator"); |
| 806 | goto write_err; |
| 807 | } |
| 808 | }else if( iLength==0 ){ |
| 809 | /* No content to read */ |
| 810 | }else if( iLength>0 ){ |
| 811 | /* Read content of a known length */ |
| 812 |