From: Stefan Eissing Date: Tue, 1 Oct 2024 09:59:37 +0000 (+0200) Subject: ftp: fix 0-length last write on upload from stdin X-Git-Tag: curl-8_11_0~281 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=72d2090fc25fcfd5b696101521335f351c712e26;p=thirdparty%2Fcurl.git ftp: fix 0-length last write on upload from stdin When uploading FTP with unknown length, we write a last 0-length chunk with the EOS flag set. OpenSSL's SSL_write() errors on such a write. Skip writing 0-length data to TLS backends instead. Add test in FTPS for such uploads to verify. Fixes #15101 Reported-by: Denis Goleshchikhin Closes #15102 --- diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 6c4ad33c0d..52e43fdc02 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -1743,13 +1743,17 @@ static ssize_t ssl_cf_send(struct Curl_cfilter *cf, bool eos, CURLcode *err) { struct cf_call_data save; - ssize_t nwritten; + ssize_t nwritten = 0; - (void)eos; /* unused */ - CF_DATA_SAVE(save, cf, data); + (void)eos; + /* OpenSSL and maybe other TLS libs do not like 0-length writes. Skip. */ *err = CURLE_OK; - nwritten = Curl_ssl->send_plain(cf, data, buf, len, err); - CF_DATA_RESTORE(cf, save); + if(len > 0) { + CF_DATA_SAVE(save, cf, data); + *err = CURLE_OK; + nwritten = Curl_ssl->send_plain(cf, data, buf, len, err); + CF_DATA_RESTORE(cf, save); + } return nwritten; } diff --git a/tests/http/test_31_vsftpds.py b/tests/http/test_31_vsftpds.py index 95f3957cad..c723d148d5 100644 --- a/tests/http/test_31_vsftpds.py +++ b/tests/http/test_31_vsftpds.py @@ -220,6 +220,23 @@ class TestVsFTPD: r.check_stats(count=count, http_status=226) self.check_upload(env, vsftpds, docname=docname) + @pytest.mark.parametrize("indata", [ + '1234567890', '' + ]) + def test_31_10_upload_stdin(self, env: Env, vsftpds: VsFTPD, indata): + curl = CurlClient(env=env) + docname = "upload_31_10" + dstfile = os.path.join(vsftpds.docs_dir, docname) + self._rmf(dstfile) + count = 1 + url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}' + r = curl.ftp_ssl_upload(urls=[url], updata=indata, with_stats=True) + r.check_stats(count=count, http_status=226) + assert os.path.exists(dstfile) + destdata = open(dstfile).readlines() + expdata = [indata] if len(indata) else [] + assert expdata == destdata, f'exected: {expdata}, got: {destdata}' + def check_downloads(self, client, srcfile: str, count: int, complete: bool = True): for i in range(count): diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py index 65caeb784c..c7be99d967 100644 --- a/tests/http/testenv/curl.py +++ b/tests/http/testenv/curl.py @@ -696,27 +696,39 @@ class CurlClient: with_tcpdump=with_tcpdump, extra_args=extra_args) - def ftp_upload(self, urls: List[str], fupload, + def ftp_upload(self, urls: List[str], + fupload: Optional[Any] = None, + updata: Optional[str] = None, with_stats: bool = True, with_profile: bool = False, with_tcpdump: bool = False, extra_args: List[str] = None): if extra_args is None: extra_args = [] - extra_args.extend([ - '--upload-file', fupload - ]) + if fupload is not None: + extra_args.extend([ + '--upload-file', fupload + ]) + elif updata is not None: + extra_args.extend([ + '--upload-file', '-' + ]) + else: + raise Exception('need either file or data to upload') if with_stats: extra_args.extend([ '-w', '%{json}\\n' ]) return self._raw(urls, options=extra_args, + intext=updata, with_stats=with_stats, with_headers=False, with_profile=with_profile, with_tcpdump=with_tcpdump) - def ftp_ssl_upload(self, urls: List[str], fupload, + def ftp_ssl_upload(self, urls: List[str], + fupload: Optional[Any] = None, + updata: Optional[str] = None, with_stats: bool = True, with_profile: bool = False, with_tcpdump: bool = False, @@ -726,7 +738,7 @@ class CurlClient: extra_args.extend([ '--ssl-reqd', ]) - return self.ftp_upload(urls=urls, fupload=fupload, + return self.ftp_upload(urls=urls, fupload=fupload, updata=updata, with_stats=with_stats, with_profile=with_profile, with_tcpdump=with_tcpdump, extra_args=extra_args)