From: Stefan Eissing Date: Fri, 5 Dec 2025 13:12:47 +0000 (+0100) Subject: ftp: make EPRT connections non-blocking X-Git-Tag: rc-8_18_0-2~116 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=891566c72da47250c13047f7df0d8aa94a6ff842;p=thirdparty%2Fcurl.git ftp: make EPRT connections non-blocking On platforms where neither accept4 nor fcntl was available, an EPRT connection did not send the accepted socket as non-blocking. This became apparent when TLS was in use and the test receive on shutdown did simply hang. Reported-by: Denis Goleshchikhin Fixes #19753 Closes #19851 --- diff --git a/lib/cf-socket.c b/lib/cf-socket.c index 1881683cc8..469e9229fd 100644 --- a/lib/cf-socket.c +++ b/lib/cf-socket.c @@ -2114,15 +2114,22 @@ static CURLcode cf_tcp_accept_connect(struct Curl_cfilter *cf, curlx_strerror(SOCKERRNO, errbuf, sizeof(errbuf))); return CURLE_FTP_ACCEPT_FAILED; } -#if !defined(HAVE_ACCEPT4) && defined(HAVE_FCNTL) - if((fcntl(s_accepted, F_SETFD, FD_CLOEXEC) < 0) || - (curlx_nonblock(s_accepted, TRUE) < 0)) { - failf(data, "fcntl set CLOEXEC/NONBLOCK: %s", +#ifndef HAVE_ACCEPT4 +#ifdef HAVE_FCNTL + if(fcntl(s_accepted, F_SETFD, FD_CLOEXEC) < 0) { + failf(data, "fcntl set CLOEXEC: %s", curlx_strerror(SOCKERRNO, errbuf, sizeof(errbuf))); Curl_socket_close(data, cf->conn, s_accepted); return CURLE_FTP_ACCEPT_FAILED; } -#endif +#endif /* HAVE_FCNTL */ + if(curlx_nonblock(s_accepted, TRUE) < 0) { + failf(data, "set socket NONBLOCK: %s", + curlx_strerror(SOCKERRNO, errbuf, sizeof(errbuf))); + Curl_socket_close(data, cf->conn, s_accepted); + return CURLE_FTP_ACCEPT_FAILED; + } +#endif /* !HAVE_ACCEPT4 */ infof(data, "Connection accepted from server"); /* Replace any filter on SECONDARY with one listening on this socket */ diff --git a/tests/http/test_30_vsftpd.py b/tests/http/test_30_vsftpd.py index 7d113d5363..bcc8b76fdd 100644 --- a/tests/http/test_30_vsftpd.py +++ b/tests/http/test_30_vsftpd.py @@ -31,7 +31,7 @@ import os import shutil import pytest -from testenv import Env, CurlClient, VsFTPD +from testenv import Env, CurlClient, VsFTPD, LocalClient log = logging.getLogger(__name__) @@ -235,6 +235,17 @@ class TestVsFTPD: r.check_stats(count=1, exitcode=78) r.check_stats_timelines() + def test_30_12_upload_eprt(self, env: Env, vsftpd: VsFTPD): + docname = 'test_30_12' + client = LocalClient(name='cli_ftp_upload', env=env) + if not client.exists(): + pytest.skip(f'example client not built: {client.name}') + url = f'ftp://{env.ftp_domain}:{vsftpd.port}/{docname}' + r = client.run(args=['-r', f'{env.ftp_domain}:{vsftpd.port}:127.0.0.1', url]) + r.check_exit_code(0) + dstfile = os.path.join(vsftpd.docs_dir, docname) + assert os.path.exists(dstfile), f'{r.dump_logs()}' + def check_downloads(self, client, srcfile: str, count: int, complete: bool = True): for i in range(count): diff --git a/tests/http/test_31_vsftpds.py b/tests/http/test_31_vsftpds.py index 93fef708cc..ba4696c0cb 100644 --- a/tests/http/test_31_vsftpds.py +++ b/tests/http/test_31_vsftpds.py @@ -31,7 +31,7 @@ import os import shutil import pytest -from testenv import Env, CurlClient, VsFTPD +from testenv import Env, CurlClient, VsFTPD, LocalClient log = logging.getLogger(__name__) @@ -260,6 +260,17 @@ class TestVsFTPD: r.check_exit_code(78) r.check_stats(count=1, exitcode=78) + def test_31_12_upload_eprt(self, env: Env, vsftpds: VsFTPD): + docname = 'test_31_12' + client = LocalClient(name='cli_ftp_upload', env=env) + if not client.exists(): + pytest.skip(f'example client not built: {client.name}') + url = f'ftp://{env.ftp_domain}:{vsftpds.port}/{docname}' + r = client.run(args=['-r', f'{env.ftp_domain}:{vsftpds.port}:127.0.0.1', url]) + r.check_exit_code(0) + dstfile = os.path.join(vsftpds.docs_dir, docname) + assert os.path.exists(dstfile), f'{r.dump_logs()}' + def check_downloads(self, client, srcfile: str, count: int, complete: bool = True): for i in range(count): diff --git a/tests/http/test_32_ftps_vsftpd.py b/tests/http/test_32_ftps_vsftpd.py index 7a849d3740..bac40580ee 100644 --- a/tests/http/test_32_ftps_vsftpd.py +++ b/tests/http/test_32_ftps_vsftpd.py @@ -31,7 +31,7 @@ import os import shutil import pytest -from testenv import Env, CurlClient, VsFTPD +from testenv import Env, CurlClient, VsFTPD, LocalClient log = logging.getLogger(__name__) @@ -273,6 +273,17 @@ class TestFtpsVsFTPD: r.check_exit_code(78) r.check_stats(count=1, exitcode=78) + def test_32_12_upload_eprt(self, env: Env, vsftpds: VsFTPD): + docname = 'test_32_12' + client = LocalClient(name='cli_ftp_upload', env=env) + if not client.exists(): + pytest.skip(f'example client not built: {client.name}') + url = f'ftps://{env.ftp_domain}:{vsftpds.port}/{docname}' + r = client.run(args=['-r', f'{env.ftp_domain}:{vsftpds.port}:127.0.0.1', url]) + r.check_exit_code(0) + dstfile = os.path.join(vsftpds.docs_dir, docname) + assert os.path.exists(dstfile), f'{r.dump_logs()}' + def check_downloads(self, client, srcfile: str, count: int, complete: bool = True): for i in range(count): diff --git a/tests/libtest/Makefile.inc b/tests/libtest/Makefile.inc index 8efeba914d..a0d78f96c3 100644 --- a/tests/libtest/Makefile.inc +++ b/tests/libtest/Makefile.inc @@ -48,6 +48,7 @@ CURLX_C = \ # All libtest programs TESTS_C = \ + cli_ftp_upload.c \ cli_h2_pausing.c \ cli_h2_serverpush.c \ cli_h2_upgrade_extreme.c \ diff --git a/tests/libtest/cli_ftp_upload.c b/tests/libtest/cli_ftp_upload.c new file mode 100644 index 0000000000..7493a210e6 --- /dev/null +++ b/tests/libtest/cli_ftp_upload.c @@ -0,0 +1,182 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "first.h" + +#include "testtrace.h" + + +#ifndef CURL_DISABLE_FTP + +struct test_cli_ftp_upload_data { + const char *data; + size_t data_len; + size_t offset; + int done; +}; + +static size_t test_cli_ftp_upload_read(char *buf, + size_t nitems, size_t blen, + void *userdata) +{ + struct test_cli_ftp_upload_data *d = userdata; + size_t nread = d->data_len - d->offset; + + if(nread) { + if(nread > (nitems * blen)) + nread = (nitems * blen); + memcpy(buf, d->data + d->offset, nread); + d->offset += nread; + } + else + d->done = 1; + return nread; +} + +static void usage_ftp_upload(const char *msg) +{ + if(msg) + curl_mfprintf(stderr, "%s\n", msg); + curl_mfprintf(stderr, + "usage: [options] url\n" + " -r :: resolve information\n" + ); +} + +#endif + +static CURLcode test_cli_ftp_upload(const char *URL) +{ +#ifndef CURL_DISABLE_FTP + CURLM *multi_handle; + CURL *curl_handle; + int running_handles = 0; + int max_fd = -1; + struct timeval timeout = { 1, 0 }; + fd_set fdread; + fd_set fdwrite; + fd_set fdexcep; + struct test_cli_ftp_upload_data data; + struct curl_slist *host = NULL; + const char *resolve = NULL, *url; + int ch; + CURLcode result = CURLE_FAILED_INIT; + curl_off_t uploadsize = -1; + + (void)URL; + while((ch = cgetopt(test_argc, test_argv, "r:")) + != -1) { + switch(ch) { + case 'r': + resolve = coptarg; + break; + default: + usage_ftp_upload("unknown option"); + return (CURLcode)1; + } + } + test_argc -= coptind; + test_argv += coptind; + if(test_argc != 1) { + usage_ftp_upload("not enough arguments"); + return (CURLcode)2; + } + url = test_argv[0]; + + if(resolve) + host = curl_slist_append(NULL, resolve); + + memset(&data, 0, sizeof(data)); + data.data = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + data.data_len = strlen(data.data); + + curl_global_init(CURL_GLOBAL_ALL); + multi_handle = curl_multi_init(); + curl_handle = curl_easy_init(); + + curl_easy_setopt(curl_handle, CURLOPT_FTPPORT, "-"); + curl_easy_setopt(curl_handle, CURLOPT_FTP_USE_EPRT, 1L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl_handle, CURLOPT_USE_SSL, CURLUSESSL_TRY); + curl_easy_setopt(curl_handle, CURLOPT_URL, url); + curl_easy_setopt(curl_handle, CURLOPT_USERPWD, NULL); + curl_easy_setopt(curl_handle, CURLOPT_FTP_CREATE_MISSING_DIRS, + CURLFTP_CREATE_DIR); + curl_easy_setopt(curl_handle, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl_handle, CURLOPT_READFUNCTION, + test_cli_ftp_upload_read); + curl_easy_setopt(curl_handle, CURLOPT_READDATA, &data); + curl_easy_setopt(curl_handle, CURLOPT_INFILESIZE_LARGE, uploadsize); + + curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl_handle, CURLOPT_DEBUGFUNCTION, cli_debug_cb); + if(host) + curl_easy_setopt(curl_handle, CURLOPT_RESOLVE, host); + + curl_multi_add_handle(multi_handle, curl_handle); + curl_multi_perform(multi_handle, &running_handles); + while(running_handles && !data.done) { + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &max_fd); + select(max_fd + 1, &fdread, &fdwrite, &fdexcep, &timeout); + curl_multi_perform(multi_handle, &running_handles); + } + while(running_handles) { + curl_mfprintf(stderr, "reports to hang herel\n"); + curl_multi_perform(multi_handle, &running_handles); + } + + while(1) { + int msgq = 0; + struct CURLMsg *msg = curl_multi_info_read(multi_handle, &msgq); + if(msg && (msg->msg == CURLMSG_DONE)) { + if(msg->easy_handle == curl_handle) { + result = msg->data.result; + } + } + else + break; + } + + curl_multi_remove_handle(multi_handle, curl_handle); + + curl_easy_reset(curl_handle); + curl_easy_cleanup(curl_handle); + curl_multi_cleanup(multi_handle); + curl_global_cleanup(); + curl_slist_free_all(host); + + curl_mfprintf(stderr, "transfer result: %d\n", result); + return result; +#else /* !CURL_DISABLE_FTP */ + (void)URL; + curl_mfprintf(stderr, "FTP not enabled in libcurl\n"); + return (CURLcode)1; +#endif /* CURL_DISABLE_FTP */ +}