Fossil SCM
When fossil is behaving as an HTTP client, it always uses HTTP/1.1 and it is able to understand the "chunked" transfer-encoding. Also add support for URI values of the form "file://NAME.http-test" which read the HTTP reply text out of the file "NAME.http-test" in the local filesystem, for testing purposes.
Commit
c3bd7c3a0edff89a47350dad9b98c5a50035a82eedc9f0e3f22651a9d1ed831d
Parent
052390edaf04a5f…
2 files changed
+73
-2
+64
-12
+73
-2
| --- src/http.c | ||
| +++ src/http.c | ||
| @@ -151,11 +151,11 @@ | ||
| 151 | 151 | }else if( g.url.path==0 || g.url.path[0]==0 ){ |
| 152 | 152 | zPath = "/"; |
| 153 | 153 | }else{ |
| 154 | 154 | zPath = g.url.path; |
| 155 | 155 | } |
| 156 | - blob_appendf(pHdr, "%s %s HTTP/1.0\r\n", | |
| 156 | + blob_appendf(pHdr, "%s %s HTTP/1.1\r\n", | |
| 157 | 157 | nPayload>0 ? "POST" : "GET", zPath); |
| 158 | 158 | if( g.url.proxyAuth ){ |
| 159 | 159 | blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth); |
| 160 | 160 | } |
| 161 | 161 | if( g.zHttpAuth && g.zHttpAuth[0] ){ |
| @@ -163,10 +163,11 @@ | ||
| 163 | 163 | char *zEncoded = encode64(zCredentials, -1); |
| 164 | 164 | blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded); |
| 165 | 165 | fossil_free(zEncoded); |
| 166 | 166 | } |
| 167 | 167 | blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname); |
| 168 | + blob_appendf(pHdr, "Connection: close\r\n"); | |
| 168 | 169 | blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent()); |
| 169 | 170 | if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n"); |
| 170 | 171 | if( g.syncInfo.fLoginCardMode>0 |
| 171 | 172 | && nPayload>0 && pLogin && blob_size(pLogin) ){ |
| 172 | 173 | /* Add sync login card via a transient cookie. We can only do this |
| @@ -455,10 +456,11 @@ | ||
| 455 | 456 | int iHttpVersion; /* Which version of HTTP protocol server uses */ |
| 456 | 457 | char *zLine; /* A single line of the reply header */ |
| 457 | 458 | int i; /* Loop counter */ |
| 458 | 459 | int isError = 0; /* True if the reply is an error message */ |
| 459 | 460 | int isCompressed = 1; /* True if the reply is compressed */ |
| 461 | + int isChunked = 0; /* True if Transfer-Encoding: chunked */ | |
| 460 | 462 | |
| 461 | 463 | if( g.zHttpCmd!=0 ){ |
| 462 | 464 | /* Handle the --transport-command option for "fossil sync" and similar */ |
| 463 | 465 | return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype); |
| 464 | 466 | } |
| @@ -605,10 +607,17 @@ | ||
| 605 | 607 | if( iHttpVersion<0 ) iHttpVersion = 1; |
| 606 | 608 | closeConnection = 0; |
| 607 | 609 | }else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){ |
| 608 | 610 | for(i=15; fossil_isspace(zLine[i]); i++){} |
| 609 | 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 | + size_t nx = strlen(&zLine[18]); | |
| 617 | + if( !fossil_isalnum(zLine[nx+10]) ) isChunked = 1; | |
| 618 | + } | |
| 610 | 619 | }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){ |
| 611 | 620 | if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){ |
| 612 | 621 | closeConnection = 1; |
| 613 | 622 | }else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){ |
| 614 | 623 | closeConnection = 0; |
| @@ -735,11 +744,73 @@ | ||
| 735 | 744 | |
| 736 | 745 | /* |
| 737 | 746 | ** Extract the reply payload that follows the header |
| 738 | 747 | */ |
| 739 | 748 | blob_zero(pReply); |
| 740 | - if( iLength==0 ){ | |
| 749 | + if( isChunked ){ | |
| 750 | + /* Decode an HTTP/1.1 "Transfer-Encoding: chunked" reply body. Each | |
| 751 | + ** chunk is a hex length on its own line (optionally followed by a | |
| 752 | + ** ";extension" that is ignored), then that many payload bytes, then a | |
| 753 | + ** bare CRLF. A zero-length chunk terminates the body, after which any | |
| 754 | + ** trailer header lines are read and discarded up to the blank line. */ | |
| 755 | + char *zChunk; | |
| 756 | + int sawTerminator = 0; /* True once the 0-length chunk is seen */ | |
| 757 | + while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){ | |
| 758 | + i64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */ | |
| 759 | + i64 nPrior; /* Bytes already in pReply (matches blob nUsed) */ | |
| 760 | + char *zEnd = 0; /* End of the hex digits actually parsed */ | |
| 761 | + while( fossil_isspace(zChunk[0]) ) zChunk++; | |
| 762 | + nChunk = strtoll(zChunk, &zEnd, 16); | |
| 763 | + if( zEnd==zChunk ){ | |
| 764 | + /* No hex digit consumed: a blank or malformed chunk-size line, which | |
| 765 | + ** is the symptom of a connection that closed mid-stream. Treat it as | |
| 766 | + ** a truncated (failed) */ | |
| 767 | + fossil_warning("chunked reply: missing or malformed chunk size"); | |
| 768 | + goto write_err; | |
| 769 | + } | |
| 770 | + if( nChunk<0 || nChunk>0x7fffffff ){ | |
| 771 | + /* Negative, or larger than we will ever accept in one chunk. */ | |
| 772 | + fossil_warning("chunked reply: invalid chunk size"); | |
| 773 | + goto write_err; | |
| 774 | + } | |
| 775 | + if( nChunk==0 ){ | |
| 776 | + /* Final chunk: consume trailer lines up to the terminating blank. */ | |
| 777 | + sawTerminator = 1; | |
| 778 | + while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){} | |
| 779 | + break; | |
| 780 | + } | |
| 781 | + nPrior = blob_size(pReply); | |
| 782 | + /* Reserve space without advancing nUsed, so that on error | |
| 783 | + ** the blob's reported size equals the bytes actually | |
| 784 | + ** received rather the claimed chunk length */ | |
| 785 | + blob_reserve(pReply, (u64)(nPrior+nChunk)); | |
| 786 | + { | |
| 787 | + unsigned int nRemaining = (unsigned int)nChunk; | |
| 788 | + /* transport_receive() may return short; loop until the chunk is | |
| 789 | + ** full. */ | |
| 790 | + while( nRemaining>0 ){ | |
| 791 | + int nGot; | |
| 792 | + nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining); | |
| 793 | + if( nGot<=0 ){ | |
| 794 | + fossil_warning("chunked reply truncated"); | |
| 795 | + goto write_err; | |
| 796 | + } | |
| 797 | + nPrior += nGot; | |
| 798 | + nRemaining -= nGot; | |
| 799 | + pReply->nUsed = (unsigned int)nPrior; | |
| 800 | + } | |
| 801 | + } | |
| 802 | + transport_receive_line(&g.url); /* CRLF that follows the chunk data */ | |
| 803 | + } | |
| 804 | + if( !sawTerminator ){ | |
| 805 | + /* The loop exited without ever seeing the 0-length terminator chunk, | |
| 806 | + ** meaning the peer closed before the body was complete. A truncated | |
| 807 | + ** sync must be an error, never silent success. */ | |
| 808 | + fossil_warning("chunked reply ended without terminator"); | |
| 809 | + goto write_err; | |
| 810 | + } | |
| 811 | + }else if( iLength==0 ){ | |
| 741 | 812 | /* No content to read */ |
| 742 | 813 | }else if( iLength>0 ){ |
| 743 | 814 | /* Read content of a known length */ |
| 744 | 815 | int iRecvLen; /* Received length of the reply payload */ |
| 745 | 816 | blob_resize(pReply, iLength); |
| 746 | 817 |
| --- 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,17 @@ | |
| 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 +744,73 @@ | |
| 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,17 @@ | |
| 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 | /* 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 | size_t nx = strlen(&zLine[18]); |
| 617 | if( !fossil_isalnum(zLine[nx+10]) ) isChunked = 1; |
| 618 | } |
| 619 | }else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){ |
| 620 | if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){ |
| 621 | closeConnection = 1; |
| 622 | }else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){ |
| 623 | closeConnection = 0; |
| @@ -735,11 +744,73 @@ | |
| 744 | |
| 745 | /* |
| 746 | ** Extract the reply payload that follows the header |
| 747 | */ |
| 748 | blob_zero(pReply); |
| 749 | if( isChunked ){ |
| 750 | /* Decode an HTTP/1.1 "Transfer-Encoding: chunked" reply body. Each |
| 751 | ** chunk is a hex length on its own line (optionally followed by a |
| 752 | ** ";extension" that is ignored), then that many payload bytes, then a |
| 753 | ** bare CRLF. A zero-length chunk terminates the body, after which any |
| 754 | ** trailer header lines are read and discarded up to the blank line. */ |
| 755 | char *zChunk; |
| 756 | int sawTerminator = 0; /* True once the 0-length chunk is seen */ |
| 757 | while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){ |
| 758 | i64 nChunk; /* Size of this chunk in bytes (wide, unclamped) */ |
| 759 | i64 nPrior; /* Bytes already in pReply (matches blob nUsed) */ |
| 760 | char *zEnd = 0; /* End of the hex digits actually parsed */ |
| 761 | while( fossil_isspace(zChunk[0]) ) zChunk++; |
| 762 | nChunk = strtoll(zChunk, &zEnd, 16); |
| 763 | if( zEnd==zChunk ){ |
| 764 | /* No hex digit consumed: a blank or malformed chunk-size line, which |
| 765 | ** is the symptom of a connection that closed mid-stream. Treat it as |
| 766 | ** a truncated (failed) */ |
| 767 | fossil_warning("chunked reply: missing or malformed chunk size"); |
| 768 | goto write_err; |
| 769 | } |
| 770 | if( nChunk<0 || nChunk>0x7fffffff ){ |
| 771 | /* Negative, or larger than we will ever accept in one chunk. */ |
| 772 | fossil_warning("chunked reply: invalid chunk size"); |
| 773 | goto write_err; |
| 774 | } |
| 775 | if( nChunk==0 ){ |
| 776 | /* Final chunk: consume trailer lines up to the terminating blank. */ |
| 777 | sawTerminator = 1; |
| 778 | while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){} |
| 779 | break; |
| 780 | } |
| 781 | nPrior = blob_size(pReply); |
| 782 | /* Reserve space without advancing nUsed, so that on error |
| 783 | ** the blob's reported size equals the bytes actually |
| 784 | ** received rather the claimed chunk length */ |
| 785 | blob_reserve(pReply, (u64)(nPrior+nChunk)); |
| 786 | { |
| 787 | unsigned int nRemaining = (unsigned int)nChunk; |
| 788 | /* transport_receive() may return short; loop until the chunk is |
| 789 | ** full. */ |
| 790 | while( nRemaining>0 ){ |
| 791 | int nGot; |
| 792 | nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining); |
| 793 | if( nGot<=0 ){ |
| 794 | fossil_warning("chunked reply truncated"); |
| 795 | goto write_err; |
| 796 | } |
| 797 | nPrior += nGot; |
| 798 | nRemaining -= nGot; |
| 799 | pReply->nUsed = (unsigned int)nPrior; |
| 800 | } |
| 801 | } |
| 802 | transport_receive_line(&g.url); /* CRLF that follows the chunk data */ |
| 803 | } |
| 804 | if( !sawTerminator ){ |
| 805 | /* The loop exited without ever seeing the 0-length terminator chunk, |
| 806 | ** meaning the peer closed before the body was complete. A truncated |
| 807 | ** sync must be an error, never silent success. */ |
| 808 | fossil_warning("chunked reply ended without terminator"); |
| 809 | goto write_err; |
| 810 | } |
| 811 | }else if( iLength==0 ){ |
| 812 | /* No content to read */ |
| 813 | }else if( iLength>0 ){ |
| 814 | /* Read content of a known length */ |
| 815 | int iRecvLen; /* Received length of the reply payload */ |
| 816 | blob_resize(pReply, iLength); |
| 817 |
+64
-12
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -36,12 +36,13 @@ | ||
| 36 | 36 | i64 nRcvd; /* Number of bytes received */ |
| 37 | 37 | FILE *pFile; /* File I/O for FILE: */ |
| 38 | 38 | char *zOutFile; /* Name of outbound file for FILE: */ |
| 39 | 39 | char *zInFile; /* Name of inbound file for FILE: */ |
| 40 | 40 | FILE *pLog; /* Log output here */ |
| 41 | + int bTest; /* file:// testing */ | |
| 41 | 42 | } transport = { |
| 42 | - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 43 | + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 43 | 44 | }; |
| 44 | 45 | |
| 45 | 46 | /* |
| 46 | 47 | ** Information about the connection to the SSH subprocess when |
| 47 | 48 | ** using the ssh:// sync method. |
| @@ -162,19 +163,42 @@ | ||
| 162 | 163 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 163 | 164 | } |
| 164 | 165 | blob_reset(&zCmd); |
| 165 | 166 | return sshPid==0; |
| 166 | 167 | } |
| 168 | + | |
| 169 | +/* | |
| 170 | +** Return true if the filename given matches the pattern that means | |
| 171 | +** it is a test-reply. | |
| 172 | +*/ | |
| 173 | +static int transport_is_test(const char *zFilename){ | |
| 174 | + return sqlite3_strglob("*.http-test",zFilename)==0; | |
| 175 | +} | |
| 167 | 176 | |
| 168 | 177 | /* |
| 169 | 178 | ** Open a connection to the server. The server is defined by the following |
| 170 | 179 | ** variables: |
| 171 | 180 | ** |
| 172 | 181 | ** pUrlData->name Name of the server. Ex: fossil-scm.org |
| 182 | +** | |
| 173 | 183 | ** pUrlData->port TCP/IP port. Ex: 80 |
| 184 | +** | |
| 174 | 185 | ** pUrlData->isHttps Use TLS for the connection |
| 175 | 186 | ** |
| 187 | +** pUrlData->isSsh Use an SSH connection to the server. name is | |
| 188 | +** the SSH host name. subpath is the server | |
| 189 | +** method name (ex: timeline) | |
| 190 | +** | |
| 191 | +** pUrlData->isFile The .name is a filename in the local filesystem. | |
| 192 | +** If that file is a Fossil repo, then launch | |
| 193 | +** "fossil http" as a subprocess to handle the request. | |
| 194 | +** Or if filename ends with ".test-http" then the | |
| 195 | +** content of that file becomes the HTTP reply. | |
| 196 | +** (The latter is used for testing only.) | |
| 197 | +** | |
| 198 | +** pUrlData->subpath The method name on the request for SSH and FILE | |
| 199 | +** | |
| 176 | 200 | ** Return the number of errors. |
| 177 | 201 | */ |
| 178 | 202 | int transport_open(UrlData *pUrlData){ |
| 179 | 203 | int rc = 0; |
| 180 | 204 | if( transport.isOpen==0 ){ |
| @@ -188,15 +212,30 @@ | ||
| 188 | 212 | #else |
| 189 | 213 | socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); |
| 190 | 214 | rc = 1; |
| 191 | 215 | #endif |
| 192 | 216 | }else if( pUrlData->isFile ){ |
| 193 | - if( !db_looks_like_a_repository(pUrlData->name) ){ | |
| 194 | - fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name); | |
| 217 | + if( !file_isfile(pUrlData->name, ExtFILE) ){ | |
| 218 | + fossil_fatal("no such file: \"%s\"", pUrlData->name); | |
| 195 | 219 | } |
| 196 | 220 | transport.zOutFile = fossil_temp_filename(); |
| 197 | 221 | transport.zInFile = fossil_temp_filename(); |
| 222 | + if( !db_looks_like_a_repository(pUrlData->name) ){ | |
| 223 | + if( transport_is_test(pUrlData->name) ){ | |
| 224 | + /* If the URI filename ends with ".http-test" then the content | |
| 225 | + ** of that file becomes the HTTP reply and the request is stored | |
| 226 | + ** in a file with the same name but with ".request" added to the | |
| 227 | + ** end. Used for testing only */ | |
| 228 | + transport.bTest = 1; | |
| 229 | + sqlite3_free(transport.zOutFile); | |
| 230 | + transport.zOutFile = sqlite3_mprintf("%s.request",pUrlData->name); | |
| 231 | + sqlite3_free(transport.zInFile); | |
| 232 | + transport.zInFile = sqlite3_mprintf("%s",pUrlData->name); | |
| 233 | + }else{ | |
| 234 | + fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name); | |
| 235 | + } | |
| 236 | + } | |
| 198 | 237 | transport.pFile = fossil_fopen(transport.zOutFile, "wb"); |
| 199 | 238 | if( transport.pFile==0 ){ |
| 200 | 239 | fossil_fatal("cannot output temporary file: %s", transport.zOutFile); |
| 201 | 240 | } |
| 202 | 241 | transport.isOpen = 1; |
| @@ -231,12 +270,14 @@ | ||
| 231 | 270 | }else if( pUrlData->isFile ){ |
| 232 | 271 | if( transport.pFile ){ |
| 233 | 272 | fclose(transport.pFile); |
| 234 | 273 | transport.pFile = 0; |
| 235 | 274 | } |
| 236 | - file_delete(transport.zInFile); | |
| 237 | - file_delete(transport.zOutFile); | |
| 275 | + if( !transport.bTest ){ | |
| 276 | + file_delete(transport.zInFile); | |
| 277 | + file_delete(transport.zOutFile); | |
| 278 | + } | |
| 238 | 279 | sqlite3_free(transport.zInFile); |
| 239 | 280 | sqlite3_free(transport.zOutFile); |
| 240 | 281 | }else{ |
| 241 | 282 | socket_close(); |
| 242 | 283 | } |
| @@ -278,23 +319,34 @@ | ||
| 278 | 319 | } |
| 279 | 320 | |
| 280 | 321 | /* |
| 281 | 322 | ** This routine is called when the outbound message is complete and |
| 282 | 323 | ** it is time to begin receiving a reply. |
| 324 | +** | |
| 325 | +** This is a no-op for http:, https:, and ssh:. It only does this | |
| 326 | +** for file:. If the file is a Fossil database, then an instance of | |
| 327 | +** "fossil http" is invoked in a subprocess to handle the request. | |
| 328 | +** Or if the filename ends with ".http-test" then the text of that | |
| 329 | +** file becomes the reply. The ".http-test" hack is for testing purposes. | |
| 283 | 330 | */ |
| 284 | 331 | void transport_flip(UrlData *pUrlData){ |
| 285 | 332 | if( pUrlData->isFile ){ |
| 286 | 333 | char *zCmd; |
| 287 | 334 | fclose(transport.pFile); |
| 288 | - zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1" | |
| 289 | - " %$ --localauth", | |
| 290 | - g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name | |
| 291 | - ); | |
| 292 | - if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd); | |
| 293 | - fossil_system(zCmd); | |
| 294 | - free(zCmd); | |
| 335 | + if( !transport.bTest ){ | |
| 336 | + zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1" | |
| 337 | + " %$ --localauth", | |
| 338 | + g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name | |
| 339 | + ); | |
| 340 | + if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd); | |
| 341 | + fossil_system(zCmd); | |
| 342 | + free(zCmd); | |
| 343 | + } | |
| 295 | 344 | transport.pFile = fossil_fopen(transport.zInFile, "rb"); |
| 345 | + if( transport.pFile==0 ){ | |
| 346 | + fossil_fatal("unable to open HTTP reply: \"%s\"",transport.zInFile); | |
| 347 | + } | |
| 296 | 348 | } |
| 297 | 349 | } |
| 298 | 350 | |
| 299 | 351 | /* |
| 300 | 352 | ** Log all input to a file. The transport layer will take responsibility |
| 301 | 353 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -36,12 +36,13 @@ | |
| 36 | i64 nRcvd; /* Number of bytes received */ |
| 37 | FILE *pFile; /* File I/O for FILE: */ |
| 38 | char *zOutFile; /* Name of outbound file for FILE: */ |
| 39 | char *zInFile; /* Name of inbound file for FILE: */ |
| 40 | FILE *pLog; /* Log output here */ |
| 41 | } transport = { |
| 42 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| 43 | }; |
| 44 | |
| 45 | /* |
| 46 | ** Information about the connection to the SSH subprocess when |
| 47 | ** using the ssh:// sync method. |
| @@ -162,19 +163,42 @@ | |
| 162 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 163 | } |
| 164 | blob_reset(&zCmd); |
| 165 | return sshPid==0; |
| 166 | } |
| 167 | |
| 168 | /* |
| 169 | ** Open a connection to the server. The server is defined by the following |
| 170 | ** variables: |
| 171 | ** |
| 172 | ** pUrlData->name Name of the server. Ex: fossil-scm.org |
| 173 | ** pUrlData->port TCP/IP port. Ex: 80 |
| 174 | ** pUrlData->isHttps Use TLS for the connection |
| 175 | ** |
| 176 | ** Return the number of errors. |
| 177 | */ |
| 178 | int transport_open(UrlData *pUrlData){ |
| 179 | int rc = 0; |
| 180 | if( transport.isOpen==0 ){ |
| @@ -188,15 +212,30 @@ | |
| 188 | #else |
| 189 | socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); |
| 190 | rc = 1; |
| 191 | #endif |
| 192 | }else if( pUrlData->isFile ){ |
| 193 | if( !db_looks_like_a_repository(pUrlData->name) ){ |
| 194 | fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name); |
| 195 | } |
| 196 | transport.zOutFile = fossil_temp_filename(); |
| 197 | transport.zInFile = fossil_temp_filename(); |
| 198 | transport.pFile = fossil_fopen(transport.zOutFile, "wb"); |
| 199 | if( transport.pFile==0 ){ |
| 200 | fossil_fatal("cannot output temporary file: %s", transport.zOutFile); |
| 201 | } |
| 202 | transport.isOpen = 1; |
| @@ -231,12 +270,14 @@ | |
| 231 | }else if( pUrlData->isFile ){ |
| 232 | if( transport.pFile ){ |
| 233 | fclose(transport.pFile); |
| 234 | transport.pFile = 0; |
| 235 | } |
| 236 | file_delete(transport.zInFile); |
| 237 | file_delete(transport.zOutFile); |
| 238 | sqlite3_free(transport.zInFile); |
| 239 | sqlite3_free(transport.zOutFile); |
| 240 | }else{ |
| 241 | socket_close(); |
| 242 | } |
| @@ -278,23 +319,34 @@ | |
| 278 | } |
| 279 | |
| 280 | /* |
| 281 | ** This routine is called when the outbound message is complete and |
| 282 | ** it is time to begin receiving a reply. |
| 283 | */ |
| 284 | void transport_flip(UrlData *pUrlData){ |
| 285 | if( pUrlData->isFile ){ |
| 286 | char *zCmd; |
| 287 | fclose(transport.pFile); |
| 288 | zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1" |
| 289 | " %$ --localauth", |
| 290 | g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name |
| 291 | ); |
| 292 | if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd); |
| 293 | fossil_system(zCmd); |
| 294 | free(zCmd); |
| 295 | transport.pFile = fossil_fopen(transport.zInFile, "rb"); |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | /* |
| 300 | ** Log all input to a file. The transport layer will take responsibility |
| 301 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -36,12 +36,13 @@ | |
| 36 | i64 nRcvd; /* Number of bytes received */ |
| 37 | FILE *pFile; /* File I/O for FILE: */ |
| 38 | char *zOutFile; /* Name of outbound file for FILE: */ |
| 39 | char *zInFile; /* Name of inbound file for FILE: */ |
| 40 | FILE *pLog; /* Log output here */ |
| 41 | int bTest; /* file:// testing */ |
| 42 | } transport = { |
| 43 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 |
| 44 | }; |
| 45 | |
| 46 | /* |
| 47 | ** Information about the connection to the SSH subprocess when |
| 48 | ** using the ssh:// sync method. |
| @@ -162,19 +163,42 @@ | |
| 163 | socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); |
| 164 | } |
| 165 | blob_reset(&zCmd); |
| 166 | return sshPid==0; |
| 167 | } |
| 168 | |
| 169 | /* |
| 170 | ** Return true if the filename given matches the pattern that means |
| 171 | ** it is a test-reply. |
| 172 | */ |
| 173 | static int transport_is_test(const char *zFilename){ |
| 174 | return sqlite3_strglob("*.http-test",zFilename)==0; |
| 175 | } |
| 176 | |
| 177 | /* |
| 178 | ** Open a connection to the server. The server is defined by the following |
| 179 | ** variables: |
| 180 | ** |
| 181 | ** pUrlData->name Name of the server. Ex: fossil-scm.org |
| 182 | ** |
| 183 | ** pUrlData->port TCP/IP port. Ex: 80 |
| 184 | ** |
| 185 | ** pUrlData->isHttps Use TLS for the connection |
| 186 | ** |
| 187 | ** pUrlData->isSsh Use an SSH connection to the server. name is |
| 188 | ** the SSH host name. subpath is the server |
| 189 | ** method name (ex: timeline) |
| 190 | ** |
| 191 | ** pUrlData->isFile The .name is a filename in the local filesystem. |
| 192 | ** If that file is a Fossil repo, then launch |
| 193 | ** "fossil http" as a subprocess to handle the request. |
| 194 | ** Or if filename ends with ".test-http" then the |
| 195 | ** content of that file becomes the HTTP reply. |
| 196 | ** (The latter is used for testing only.) |
| 197 | ** |
| 198 | ** pUrlData->subpath The method name on the request for SSH and FILE |
| 199 | ** |
| 200 | ** Return the number of errors. |
| 201 | */ |
| 202 | int transport_open(UrlData *pUrlData){ |
| 203 | int rc = 0; |
| 204 | if( transport.isOpen==0 ){ |
| @@ -188,15 +212,30 @@ | |
| 212 | #else |
| 213 | socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); |
| 214 | rc = 1; |
| 215 | #endif |
| 216 | }else if( pUrlData->isFile ){ |
| 217 | if( !file_isfile(pUrlData->name, ExtFILE) ){ |
| 218 | fossil_fatal("no such file: \"%s\"", pUrlData->name); |
| 219 | } |
| 220 | transport.zOutFile = fossil_temp_filename(); |
| 221 | transport.zInFile = fossil_temp_filename(); |
| 222 | if( !db_looks_like_a_repository(pUrlData->name) ){ |
| 223 | if( transport_is_test(pUrlData->name) ){ |
| 224 | /* If the URI filename ends with ".http-test" then the content |
| 225 | ** of that file becomes the HTTP reply and the request is stored |
| 226 | ** in a file with the same name but with ".request" added to the |
| 227 | ** end. Used for testing only */ |
| 228 | transport.bTest = 1; |
| 229 | sqlite3_free(transport.zOutFile); |
| 230 | transport.zOutFile = sqlite3_mprintf("%s.request",pUrlData->name); |
| 231 | sqlite3_free(transport.zInFile); |
| 232 | transport.zInFile = sqlite3_mprintf("%s",pUrlData->name); |
| 233 | }else{ |
| 234 | fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name); |
| 235 | } |
| 236 | } |
| 237 | transport.pFile = fossil_fopen(transport.zOutFile, "wb"); |
| 238 | if( transport.pFile==0 ){ |
| 239 | fossil_fatal("cannot output temporary file: %s", transport.zOutFile); |
| 240 | } |
| 241 | transport.isOpen = 1; |
| @@ -231,12 +270,14 @@ | |
| 270 | }else if( pUrlData->isFile ){ |
| 271 | if( transport.pFile ){ |
| 272 | fclose(transport.pFile); |
| 273 | transport.pFile = 0; |
| 274 | } |
| 275 | if( !transport.bTest ){ |
| 276 | file_delete(transport.zInFile); |
| 277 | file_delete(transport.zOutFile); |
| 278 | } |
| 279 | sqlite3_free(transport.zInFile); |
| 280 | sqlite3_free(transport.zOutFile); |
| 281 | }else{ |
| 282 | socket_close(); |
| 283 | } |
| @@ -278,23 +319,34 @@ | |
| 319 | } |
| 320 | |
| 321 | /* |
| 322 | ** This routine is called when the outbound message is complete and |
| 323 | ** it is time to begin receiving a reply. |
| 324 | ** |
| 325 | ** This is a no-op for http:, https:, and ssh:. It only does this |
| 326 | ** for file:. If the file is a Fossil database, then an instance of |
| 327 | ** "fossil http" is invoked in a subprocess to handle the request. |
| 328 | ** Or if the filename ends with ".http-test" then the text of that |
| 329 | ** file becomes the reply. The ".http-test" hack is for testing purposes. |
| 330 | */ |
| 331 | void transport_flip(UrlData *pUrlData){ |
| 332 | if( pUrlData->isFile ){ |
| 333 | char *zCmd; |
| 334 | fclose(transport.pFile); |
| 335 | if( !transport.bTest ){ |
| 336 | zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1" |
| 337 | " %$ --localauth", |
| 338 | g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name |
| 339 | ); |
| 340 | if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd); |
| 341 | fossil_system(zCmd); |
| 342 | free(zCmd); |
| 343 | } |
| 344 | transport.pFile = fossil_fopen(transport.zInFile, "rb"); |
| 345 | if( transport.pFile==0 ){ |
| 346 | fossil_fatal("unable to open HTTP reply: \"%s\"",transport.zInFile); |
| 347 | } |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | /* |
| 352 | ** Log all input to a file. The transport layer will take responsibility |
| 353 |