curl: implement non-blocking STDIN read on Windows

Implements a seperate read thread for STDIN on Windows when curl is run
with -T/--upload-file .

This uses a similar technique to the nmap/ncat project, spawning a
seperate thread which creates a loop-back bound socket, sending STDIN
into this socket, and reading from the other end of said TCP socket in a
non-blocking way in the rest of curl.

Fixes #17451
Closes #17572
This commit is contained in:
DoI 2025-06-10 23:13:35 +12:00 committed by Daniel Stenberg
parent 84b62696d9
commit 9a2663322c
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
4 changed files with 286 additions and 10 deletions

View File

@ -87,15 +87,39 @@ size_t tool_read_cb(char *buffer, size_t sz, size_t nmemb, void *userdata)
#endif
}
rc = read(per->infd, buffer, sz*nmemb);
if(rc < 0) {
if(errno == EAGAIN) {
CURL_SETERRNO(0);
config->readbusy = TRUE;
return CURL_READFUNC_PAUSE;
/* If we are on Windows, and using `-T .`, then per->infd points to a socket
connected to stdin via a reader thread, and needs to be read with recv()
Make sure we are in non-blocking mode and infd is not regular stdin
On Linux per->infd should be stdin (0) and the block below should not
execute */
if(!strcmp(per->uploadfile, ".") && per->infd > 0) {
#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
rc = recv(per->infd, buffer, curlx_uztosi(sz * nmemb), 0);
if(rc < 0) {
if(SOCKERRNO == SOCKEWOULDBLOCK) {
CURL_SETERRNO(0);
config->readbusy = TRUE;
return CURL_READFUNC_PAUSE;
}
rc = 0;
}
#else
warnf(per->config->global, "per->infd != 0: FD == %d. This behavior"
" is only supported on desktop Windows", per->infd);
#endif
}
else {
rc = read(per->infd, buffer, sz*nmemb);
if(rc < 0) {
if(errno == EAGAIN) {
CURL_SETERRNO(0);
config->readbusy = TRUE;
return CURL_READFUNC_PAUSE;
}
/* since size_t is unsigned we cannot return negative values fine */
rc = 0;
}
/* since size_t is unsigned we cannot return negative values fine */
rc = 0;
}
if((per->uploadfilesize != -1) &&
(per->uploadedsofar + rc > per->uploadfilesize)) {

View File

@ -38,6 +38,7 @@
#include "tool_bname.h"
#include "tool_doswin.h"
#include "tool_msgs.h"
#include <curlx.h>
#include <memdebug.h> /* keep this as LAST include */
@ -741,6 +742,219 @@ CURLcode win32_init(void)
return CURLE_OK;
}
#if !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
/* The following STDIN non - blocking read techniques are heavily inspired
by nmap and ncat (https://nmap.org/ncat/) */
struct win_thread_data {
/* This is a copy of the true stdin file handle before any redirection. It is
read by the thread. */
HANDLE stdin_handle;
/* This is the listen socket for the thread. It is closed after the first
connection. */
curl_socket_t socket_l;
/* This is the global config - used for printing errors and so forth */
struct GlobalConfig *global;
};
static DWORD WINAPI win_stdin_thread_func(void *thread_data)
{
struct win_thread_data *tdata = (struct win_thread_data *)thread_data;
DWORD n;
int nwritten;
char buffer[BUFSIZ];
BOOL r;
SOCKADDR_IN clientAddr;
int clientAddrLen = sizeof(clientAddr);
curl_socket_t socket_w = accept(tdata->socket_l, (SOCKADDR*)&clientAddr,
&clientAddrLen);
if(socket_w == CURL_SOCKET_BAD) {
errorf(tdata->global, "accept error: %08lx\n", GetLastError());
goto ThreadCleanup;
}
closesocket(tdata->socket_l); /* sclose here fails test 1498 */
tdata->socket_l = CURL_SOCKET_BAD;
if(shutdown(socket_w, SD_RECEIVE) == SOCKET_ERROR) {
errorf(tdata->global, "shutdown error: %08lx\n", GetLastError());
goto ThreadCleanup;
}
for(;;) {
r = ReadFile(tdata->stdin_handle, buffer, sizeof(buffer), &n, NULL);
if(r == 0)
break;
if(n == 0)
break;
nwritten = send(socket_w, buffer, n, 0);
if(nwritten == SOCKET_ERROR)
break;
if((DWORD)nwritten != n)
break;
}
ThreadCleanup:
CloseHandle(tdata->stdin_handle);
tdata->stdin_handle = NULL;
if(tdata->socket_l != CURL_SOCKET_BAD) {
sclose(tdata->socket_l);
tdata->socket_l = CURL_SOCKET_BAD;
}
if(socket_w != CURL_SOCKET_BAD)
sclose(socket_w);
if(tdata) {
free(tdata);
}
return 0;
}
/* The background thread that reads and buffers the true stdin. */
static HANDLE stdin_thread = NULL;
static curl_socket_t socket_r = CURL_SOCKET_BAD;
curl_socket_t win32_stdin_read_thread(struct GlobalConfig *global)
{
int result;
bool r;
int rc = 0, socksize = 0;
struct win_thread_data *tdata = NULL;
SOCKADDR_IN selfaddr;
if(socket_r != CURL_SOCKET_BAD) {
assert(stdin_thread != NULL);
return socket_r;
}
assert(stdin_thread == NULL);
do {
/* Prepare handles for thread */
tdata = (struct win_thread_data*)calloc(1, sizeof(struct win_thread_data));
if(!tdata) {
errorf(global, "calloc() error");
break;
}
/* Create the listening socket for the thread. When it starts, it will
* accept our connection and begin writing STDIN data to the connection. */
tdata->socket_l = WSASocketW(AF_INET, SOCK_STREAM,
IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if(tdata->socket_l == CURL_SOCKET_BAD) {
errorf(global, "WSASocketW error: %08lx", GetLastError());
break;
}
socksize = sizeof(selfaddr);
memset(&selfaddr, 0, socksize);
selfaddr.sin_family = AF_INET;
selfaddr.sin_addr.S_un.S_addr = htonl(INADDR_LOOPBACK);
/* Bind to any available loopback port */
result = bind(tdata->socket_l, (SOCKADDR*)&selfaddr, socksize);
if(result == SOCKET_ERROR) {
errorf(global, "bind error: %08lx", GetLastError());
break;
}
/* Bind to any available loopback port */
result = getsockname(tdata->socket_l, (SOCKADDR*)&selfaddr, &socksize);
if(result == SOCKET_ERROR) {
errorf(global, "getsockname error: %08lx", GetLastError());
break;
}
result = listen(tdata->socket_l, 1);
if(result == SOCKET_ERROR) {
errorf(global, "listen error: %08lx\n", GetLastError());
break;
}
/* Make a copy of the stdin handle to be used by win_stdin_thread_func */
r = DuplicateHandle(GetCurrentProcess(), GetStdHandle(STD_INPUT_HANDLE),
GetCurrentProcess(), &tdata->stdin_handle,
0, FALSE, DUPLICATE_SAME_ACCESS);
if(!r) {
errorf(global, "DuplicateHandle error: %08lx", GetLastError());
break;
}
/* Start up the thread. We don't bother keeping a reference to it
because it runs until program termination. From here on out all reads
from the stdin handle or file descriptor 0 will be reading from the
socket that is fed by the thread. */
stdin_thread = CreateThread(NULL, 0, win_stdin_thread_func,
tdata, 0, NULL);
if(!stdin_thread) {
errorf(global, "CreateThread error: %08lx", GetLastError());
break;
}
/* Connect to the thread and rearrange our own STDIN handles */
socket_r = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(socket_r == CURL_SOCKET_BAD) {
errorf(global, "socket error: %08lx", GetLastError());
break;
}
/* Hard close the socket on closesocket() */
setsockopt(socket_r, SOL_SOCKET, SO_DONTLINGER, 0, 0);
if(connect(socket_r, (SOCKADDR*)&selfaddr, socksize) == SOCKET_ERROR) {
errorf(global, "connect error: %08lx", GetLastError());
break;
}
if(shutdown(socket_r, SD_SEND) == SOCKET_ERROR) {
errorf(global, "shutdown error: %08lx", GetLastError());
break;
}
/* Set the stdin handle to read from the socket. */
if(SetStdHandle(STD_INPUT_HANDLE, (HANDLE)socket_r) == 0) {
errorf(global, "SetStdHandle error: %08lx", GetLastError());
break;
}
rc = 1;
} while(0);
if(rc != 1) {
if(socket_r != CURL_SOCKET_BAD && tdata) {
if(GetStdHandle(STD_INPUT_HANDLE) == (HANDLE)socket_r &&
tdata->stdin_handle) {
/* restore STDIN */
SetStdHandle(STD_INPUT_HANDLE, tdata->stdin_handle);
tdata->stdin_handle = NULL;
}
sclose(socket_r);
socket_r = CURL_SOCKET_BAD;
}
if(stdin_thread) {
TerminateThread(stdin_thread, 1);
stdin_thread = NULL;
}
if(tdata) {
if(tdata->stdin_handle)
CloseHandle(tdata->stdin_handle);
if(tdata->socket_l != CURL_SOCKET_BAD)
sclose(tdata->socket_l);
free(tdata);
}
return CURL_SOCKET_BAD;
}
assert(socket_r != CURL_SOCKET_BAD);
return socket_r;
}
#endif /* !CURL_WINDOWS_UWP && !UNDER_CE */
#endif /* _WIN32 */
#endif /* _WIN32 || MSDOS */

View File

@ -55,6 +55,10 @@ CURLcode FindWin32CACert(struct OperationConfig *config,
struct curl_slist *GetLoadedModulePaths(void);
CURLcode win32_init(void);
#if !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
curl_socket_t win32_stdin_read_thread(struct GlobalConfig *global);
#endif /* !CURL_WINDOWS_UWP && !UNDER_CE */
#endif /* _WIN32 */
#endif /* _WIN32 || MSDOS */

View File

@ -572,8 +572,22 @@ static CURLcode post_per_transfer(struct GlobalConfig *global,
if(!curl || !config)
return result;
if(per->infdopen)
close(per->infd);
if(per->uploadfile) {
if(!strcmp(per->uploadfile, ".") && per->infd > 0) {
#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
sclose(per->infd);
#else
warnf(per->config->global, "Closing per->infd != 0: FD == "
"%d. This behavior is only supported on desktop "
" Windows", per->infd);
#endif
}
}
else {
if(per->infdopen) {
close(per->infd);
}
}
if(per->skip)
goto skip;
@ -1066,6 +1080,26 @@ static void check_stdin_upload(struct GlobalConfig *global,
CURLX_SET_BINMODE(stdin);
if(!strcmp(per->uploadfile, ".")) {
#if defined(_WIN32) && !defined(CURL_WINDOWS_UWP) && !defined(UNDER_CE)
/* non-blocking stdin behavior on Windows is challenging
Spawn a new thread that will read from stdin and write
out to a socket */
curl_socket_t f = win32_stdin_read_thread(global);
if(f == CURL_SOCKET_BAD)
warnf(global, "win32_stdin_read_thread returned INVALID_SOCKET "
"falling back to blocking mode");
else if(f > INT_MAX) {
warnf(global, "win32_stdin_read_thread returned identifier "
"larger than INT_MAX. This should not happen unless "
"the upper 32 bits of a Windows socket have started "
"being used for something... falling back to blocking "
"mode");
sclose(f);
}
else
per->infd = (int)f;
#endif
if(curlx_nonblock((curl_socket_t)per->infd, TRUE) < 0)
warnf(global,
"fcntl failed on fd=%d: %s", per->infd, strerror(errno));