Merge #306633: curl: backport unreleased curl 8.7 compression bugfix

...into staging

+174
+168
pkgs/tools/networking/curl/8.7.1-compression-fix.patch
··· 1 + From b30d694a027eb771c02a3db0dee0ca03ccab7377 Mon Sep 17 00:00:00 2001 2 + From: Stefan Eissing <stefan@eissing.org> 3 + Date: Thu, 28 Mar 2024 11:08:15 +0100 4 + Subject: [PATCH] content_encoding: brotli and others, pass through 0-length 5 + writes 6 + 7 + - curl's transfer handling may write 0-length chunks at the end of the 8 + download with an EOS flag. (HTTP/2 does this commonly) 9 + 10 + - content encoders need to pass-through such a write and not count this 11 + as error in case they are finished decoding 12 + 13 + Fixes #13209 14 + Fixes #13212 15 + Closes #13219 16 + --- 17 + lib/content_encoding.c | 10 +++++----- 18 + tests/http/test_02_download.py | 13 +++++++++++++ 19 + tests/http/testenv/env.py | 7 ++++++- 20 + tests/http/testenv/httpd.py | 20 ++++++++++++++++++++ 21 + 4 files changed, 44 insertions(+), 6 deletions(-) 22 + 23 + diff --git a/lib/content_encoding.c b/lib/content_encoding.c 24 + index c1abf24e8c027c..8e926dd2ecd5ad 100644 25 + --- a/lib/content_encoding.c 26 + +++ b/lib/content_encoding.c 27 + @@ -300,7 +300,7 @@ static CURLcode deflate_do_write(struct Curl_easy *data, 28 + struct zlib_writer *zp = (struct zlib_writer *) writer; 29 + z_stream *z = &zp->z; /* zlib state structure */ 30 + 31 + - if(!(type & CLIENTWRITE_BODY)) 32 + + if(!(type & CLIENTWRITE_BODY) || !nbytes) 33 + return Curl_cwriter_write(data, writer->next, type, buf, nbytes); 34 + 35 + /* Set the compressed input when this function is called */ 36 + @@ -457,7 +457,7 @@ static CURLcode gzip_do_write(struct Curl_easy *data, 37 + struct zlib_writer *zp = (struct zlib_writer *) writer; 38 + z_stream *z = &zp->z; /* zlib state structure */ 39 + 40 + - if(!(type & CLIENTWRITE_BODY)) 41 + + if(!(type & CLIENTWRITE_BODY) || !nbytes) 42 + return Curl_cwriter_write(data, writer->next, type, buf, nbytes); 43 + 44 + if(zp->zlib_init == ZLIB_INIT_GZIP) { 45 + @@ -669,7 +669,7 @@ static CURLcode brotli_do_write(struct Curl_easy *data, 46 + CURLcode result = CURLE_OK; 47 + BrotliDecoderResult r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; 48 + 49 + - if(!(type & CLIENTWRITE_BODY)) 50 + + if(!(type & CLIENTWRITE_BODY) || !nbytes) 51 + return Curl_cwriter_write(data, writer->next, type, buf, nbytes); 52 + 53 + if(!bp->br) 54 + @@ -762,7 +762,7 @@ static CURLcode zstd_do_write(struct Curl_easy *data, 55 + ZSTD_outBuffer out; 56 + size_t errorCode; 57 + 58 + - if(!(type & CLIENTWRITE_BODY)) 59 + + if(!(type & CLIENTWRITE_BODY) || !nbytes) 60 + return Curl_cwriter_write(data, writer->next, type, buf, nbytes); 61 + 62 + if(!zp->decomp) { 63 + @@ -916,7 +916,7 @@ static CURLcode error_do_write(struct Curl_easy *data, 64 + (void) buf; 65 + (void) nbytes; 66 + 67 + - if(!(type & CLIENTWRITE_BODY)) 68 + + if(!(type & CLIENTWRITE_BODY) || !nbytes) 69 + return Curl_cwriter_write(data, writer->next, type, buf, nbytes); 70 + 71 + failf(data, "Unrecognized content encoding type. " 72 + diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py 73 + index 4db9c9d36e9ed5..395fc862f2f839 100644 74 + --- a/tests/http/test_02_download.py 75 + +++ b/tests/http/test_02_download.py 76 + @@ -394,6 +394,19 @@ def test_02_27_paused_no_cl(self, env: Env, httpd, nghttpx, repeat): 77 + r = client.run(args=[url]) 78 + r.check_exit_code(0) 79 + 80 + + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) 81 + + def test_02_28_get_compressed(self, env: Env, httpd, nghttpx, repeat, proto): 82 + + if proto == 'h3' and not env.have_h3(): 83 + + pytest.skip("h3 not supported") 84 + + count = 1 85 + + urln = f'https://{env.authority_for(env.domain1brotli, proto)}/data-100k?[0-{count-1}]' 86 + + curl = CurlClient(env=env) 87 + + r = curl.http_download(urls=[urln], alpn_proto=proto, extra_args=[ 88 + + '--compressed' 89 + + ]) 90 + + r.check_exit_code(code=0) 91 + + r.check_response(count=count, http_status=200) 92 + + 93 + def check_downloads(self, client, srcfile: str, count: int, 94 + complete: bool = True): 95 + for i in range(count): 96 + diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py 97 + index a207059dcd57c5..13c5d6bd46ee57 100644 98 + --- a/tests/http/testenv/env.py 99 + +++ b/tests/http/testenv/env.py 100 + @@ -129,10 +129,11 @@ def __init__(self): 101 + self.htdocs_dir = os.path.join(self.gen_dir, 'htdocs') 102 + self.tld = 'http.curl.se' 103 + self.domain1 = f"one.{self.tld}" 104 + + self.domain1brotli = f"brotli.one.{self.tld}" 105 + self.domain2 = f"two.{self.tld}" 106 + self.proxy_domain = f"proxy.{self.tld}" 107 + self.cert_specs = [ 108 + - CertificateSpec(domains=[self.domain1, 'localhost'], key_type='rsa2048'), 109 + + CertificateSpec(domains=[self.domain1, self.domain1brotli, 'localhost'], key_type='rsa2048'), 110 + CertificateSpec(domains=[self.domain2], key_type='rsa2048'), 111 + CertificateSpec(domains=[self.proxy_domain, '127.0.0.1'], key_type='rsa2048'), 112 + CertificateSpec(name="clientsX", sub_specs=[ 113 + @@ -376,6 +377,10 @@ def htdocs_dir(self) -> str: 114 + def domain1(self) -> str: 115 + return self.CONFIG.domain1 116 + 117 + + @property 118 + + def domain1brotli(self) -> str: 119 + + return self.CONFIG.domain1brotli 120 + + 121 + @property 122 + def domain2(self) -> str: 123 + return self.CONFIG.domain2 124 + diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py 125 + index c04c22699a62c4..b8615875a9a558 100644 126 + --- a/tests/http/testenv/httpd.py 127 + +++ b/tests/http/testenv/httpd.py 128 + @@ -50,6 +50,7 @@ class Httpd: 129 + 'alias', 'env', 'filter', 'headers', 'mime', 'setenvif', 130 + 'socache_shmcb', 131 + 'rewrite', 'http2', 'ssl', 'proxy', 'proxy_http', 'proxy_connect', 132 + + 'brotli', 133 + 'mpm_event', 134 + ] 135 + COMMON_MODULES_DIRS = [ 136 + @@ -203,6 +204,7 @@ def _mkpath(self, path): 137 + 138 + def _write_config(self): 139 + domain1 = self.env.domain1 140 + + domain1brotli = self.env.domain1brotli 141 + creds1 = self.env.get_credentials(domain1) 142 + domain2 = self.env.domain2 143 + creds2 = self.env.get_credentials(domain2) 144 + @@ -285,6 +287,24 @@ def _write_config(self): 145 + f'</VirtualHost>', 146 + f'', 147 + ]) 148 + + # Alternate to domain1 with BROTLI compression 149 + + conf.extend([ # https host for domain1, h1 + h2 150 + + f'<VirtualHost *:{self.env.https_port}>', 151 + + f' ServerName {domain1brotli}', 152 + + f' Protocols h2 http/1.1', 153 + + f' SSLEngine on', 154 + + f' SSLCertificateFile {creds1.cert_file}', 155 + + f' SSLCertificateKeyFile {creds1.pkey_file}', 156 + + f' DocumentRoot "{self._docs_dir}"', 157 + + f' SetOutputFilter BROTLI_COMPRESS', 158 + + ]) 159 + + conf.extend(self._curltest_conf(domain1)) 160 + + if domain1 in self._extra_configs: 161 + + conf.extend(self._extra_configs[domain1]) 162 + + conf.extend([ 163 + + f'</VirtualHost>', 164 + + f'', 165 + + ]) 166 + conf.extend([ # https host for domain2, no h2 167 + f'<VirtualHost *:{self.env.https_port}>', 168 + f' ServerName {domain2}',
+6
pkgs/tools/networking/curl/default.nix
··· 59 59 hash = "sha256-b+oqrGpGEPvQQAr7C83b5yWKZMY/H2jlhV68DGWXEM0="; 60 60 }; 61 61 62 + patches = lib.optionals (lib.versionOlder finalAttrs.version "8.7.2") [ 63 + # https://github.com/curl/curl/pull/13219 64 + # https://github.com/newsboat/newsboat/issues/2728 65 + ./8.7.1-compression-fix.patch 66 + ]; 67 + 62 68 postPatch = '' 63 69 patchShebangs scripts 64 70 '';