ftp: split ftp_state_use_port into sub functions

For readability and reduced complexity.

Fixed a bug when FTPPORT specifies an IPv6-address only, without brackets.

Closes #20685
This commit is contained in:
Daniel Stenberg 2026-02-22 16:55:55 +01:00
parent f1cee1f18a
commit 29bca12978
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2

572
lib/ftp.c
View File

@ -873,199 +873,228 @@ typedef enum {
DONE
} ftpport;
static CURLcode ftp_state_use_port(struct Curl_easy *data,
struct ftp_conn *ftpc,
ftpport fcmd) /* start with this */
/*
* Parse the CURLOPT_FTPPORT string
* "(ipv4|ipv6|domain|interface)?(:port(-range)?)?"
* and extract addr/addrlen and port_min/port_max.
*/
static CURLcode ftp_port_parse_string(struct Curl_easy *data,
struct connectdata *conn,
const char *string_ftpport,
struct Curl_sockaddr_storage *ss,
unsigned short *port_minp,
unsigned short *port_maxp,
const char **hostp,
char *hbuf, size_t hbuflen)
{
CURLcode result = CURLE_FTP_PORT_FAILED;
struct connectdata *conn = data->conn;
curl_socket_t portsock = CURL_SOCKET_BAD;
char myhost[MAX_IPADR_LEN + 1] = "";
const char *ip_end = NULL;
const char *addr = NULL;
size_t addrlen = 0;
unsigned short port_min = 0;
unsigned short port_max = 0;
char ipstr[50];
#ifndef USE_IPV6
(void)conn;
(void)ss;
#endif
struct Curl_sockaddr_storage ss;
const struct Curl_addrinfo *res, *ai;
curl_socklen_t sslen;
char hbuf[NI_MAXHOST];
struct sockaddr *sa = (struct sockaddr *)&ss;
/* default to nothing */
*hostp = NULL;
*port_minp = *port_maxp = 0;
if(!string_ftpport || (strlen(string_ftpport) <= 1))
goto done;
#ifdef USE_IPV6
if(*string_ftpport == '[') {
/* [ipv6]:port(-range) */
const char *ip_start = string_ftpport + 1;
ip_end = strchr(ip_start, ']');
if(ip_end) {
addrlen = ip_end - ip_start;
addr = ip_start;
}
}
else
#endif
if(*string_ftpport == ':') {
/* :port */
ip_end = string_ftpport;
}
else {
ip_end = strchr(string_ftpport, ':');
addr = string_ftpport;
if(ip_end) {
#ifdef USE_IPV6
struct sockaddr_in6 * const sa6 = (void *)ss;
#endif
/* either ipv6 or (ipv4|domain|interface):port(-range) */
addrlen = ip_end - string_ftpport;
#ifdef USE_IPV6
if(curlx_inet_pton(AF_INET6, string_ftpport, &sa6->sin6_addr) == 1) {
/* ipv6 */
port_min = port_max = 0;
addrlen = strlen(string_ftpport);
ip_end = NULL; /* this got no port ! */
}
#endif
}
else
/* ipv4|interface */
addrlen = strlen(string_ftpport);
}
/* parse the port */
if(ip_end) {
const char *portp = strchr(ip_end, ':');
if(portp) {
curl_off_t start;
curl_off_t end;
portp++;
if(!curlx_str_number(&portp, &start, 0xffff)) {
port_min = (unsigned short)start;
if(!curlx_str_single(&portp, '-') &&
!curlx_str_number(&portp, &end, 0xffff))
port_max = (unsigned short)end;
else
port_max = port_min;
}
else
port_max = port_min;
}
}
/* correct errors like :1234-1230 or :-4711 */
if(port_min > port_max)
port_min = port_max = 0;
if(addrlen) {
const struct Curl_sockaddr_ex *remote_addr =
Curl_conn_get_remote_addr(data, FIRSTSOCKET);
DEBUGASSERT(remote_addr);
DEBUGASSERT(addr);
if(!remote_addr || (addrlen >= sizeof(ipstr)) || (addrlen >= hbuflen))
return CURLE_FTP_PORT_FAILED;
memcpy(ipstr, addr, addrlen);
ipstr[addrlen] = 0;
/* attempt to get the address of the given interface name */
switch(Curl_if2ip(remote_addr->family,
#ifdef USE_IPV6
Curl_ipv6_scope(&remote_addr->curl_sa_addr),
conn->scope_id,
#endif
ipstr, hbuf, hbuflen)) {
case IF2IP_NOT_FOUND:
/* not an interface, use the string as hostname instead */
memcpy(hbuf, addr, addrlen);
hbuf[addrlen] = 0;
*hostp = hbuf;
break;
case IF2IP_AF_NOT_SUPPORTED:
return CURLE_FTP_PORT_FAILED;
case IF2IP_FOUND:
*hostp = hbuf; /* use the hbuf for hostname */
break;
}
}
/* else: only a port(-range) given, leave host as NULL */
done:
*port_minp = port_min;
*port_maxp = port_max;
return CURLE_OK;
}
/*
* If no host was derived from the FTPPORT string, fall back to the IP address
* of the control connection's local socket.
*/
static CURLcode ftp_port_default_host(struct Curl_easy *data,
struct connectdata *conn,
struct Curl_sockaddr_storage *ss,
curl_socklen_t *sslenp,
const char **hostp,
char *hbuf, size_t hbuflen,
bool *non_localp)
{
struct sockaddr *sa = (struct sockaddr *)ss;
struct sockaddr_in * const sa4 = (void *)sa;
#ifdef USE_IPV6
struct sockaddr_in6 * const sa6 = (void *)sa;
#endif
static const char mode[][5] = { "EPRT", "PORT" };
int error;
const char *host = NULL;
const char *string_ftpport = data->set.str[STRING_FTPPORT];
struct Curl_dns_entry *dns_entry = NULL;
unsigned short port_min = 0;
unsigned short port_max = 0;
unsigned short port;
bool possibly_non_local = TRUE;
char buffer[STRERROR_LEN];
const char *addr = NULL;
size_t addrlen = 0;
char ipstr[50];
const char *r;
/* Step 1, figure out what is requested,
* accepted format :
* (ipv4|ipv6|domain|interface)?(:port(-range)?)?
*/
if(data->set.str[STRING_FTPPORT] &&
(strlen(data->set.str[STRING_FTPPORT]) > 1)) {
const char *ip_end = NULL;
#ifdef USE_IPV6
if(*string_ftpport == '[') {
/* [ipv6]:port(-range) */
const char *ip_start = string_ftpport + 1;
ip_end = strchr(ip_start, ']');
if(ip_end) {
addrlen = ip_end - ip_start;
addr = ip_start;
}
}
else
#endif
if(*string_ftpport == ':') {
/* :port */
ip_end = string_ftpport;
}
else {
ip_end = strchr(string_ftpport, ':');
addr = string_ftpport;
if(ip_end) {
/* either ipv6 or (ipv4|domain|interface):port(-range) */
addrlen = ip_end - string_ftpport;
#ifdef USE_IPV6
if(curlx_inet_pton(AF_INET6, string_ftpport, &sa6->sin6_addr) == 1) {
/* ipv6 */
port_min = port_max = 0;
ip_end = NULL; /* this got no port ! */
}
#endif
}
else
/* ipv4|interface */
addrlen = strlen(string_ftpport);
}
/* parse the port */
if(ip_end) {
const char *portp = strchr(ip_end, ':');
if(portp) {
curl_off_t start;
curl_off_t end;
portp++;
if(!curlx_str_number(&portp, &start, 0xffff)) {
/* got the first number */
port_min = (unsigned short)start;
if(!curlx_str_single(&portp, '-')) {
/* got the dash */
if(!curlx_str_number(&portp, &end, 0xffff))
/* got the second number */
port_max = (unsigned short)end;
}
}
else
port_max = port_min;
}
}
/* correct errors like:
* :1234-1230
* :-4711, in this case port_min is (unsigned)-1,
* therefore port_min > port_max for all cases
* but port_max = (unsigned)-1
*/
if(port_min > port_max)
port_min = port_max = 0;
if(addrlen) {
const struct Curl_sockaddr_ex *remote_addr =
Curl_conn_get_remote_addr(data, FIRSTSOCKET);
DEBUGASSERT(remote_addr);
if(!remote_addr)
goto out;
DEBUGASSERT(addr);
if(addrlen >= sizeof(ipstr))
goto out;
memcpy(ipstr, addr, addrlen);
ipstr[addrlen] = 0;
/* attempt to get the address of the given interface name */
switch(Curl_if2ip(remote_addr->family,
#ifdef USE_IPV6
Curl_ipv6_scope(&remote_addr->curl_sa_addr),
conn->scope_id,
#endif
ipstr, hbuf, sizeof(hbuf))) {
case IF2IP_NOT_FOUND:
/* not an interface, use the given string as hostname instead */
host = ipstr;
break;
case IF2IP_AF_NOT_SUPPORTED:
goto out;
case IF2IP_FOUND:
host = hbuf; /* use the hbuf for hostname */
break;
}
}
else
/* there was only a port(-range) given, default the host */
host = NULL;
} /* data->set.ftpport */
if(!host) {
const char *r;
/* not an interface and not a hostname, get default by extracting
the IP from the control connection */
sslen = sizeof(ss);
if(getsockname(conn->sock[FIRSTSOCKET], sa, &sslen)) {
failf(data, "getsockname() failed: %s",
curlx_strerror(SOCKERRNO, buffer, sizeof(buffer)));
goto out;
}
switch(sa->sa_family) {
#ifdef USE_IPV6
case AF_INET6:
r = curlx_inet_ntop(sa->sa_family, &sa6->sin6_addr, hbuf, sizeof(hbuf));
break;
#endif
default:
r = curlx_inet_ntop(sa->sa_family, &sa4->sin_addr, hbuf, sizeof(hbuf));
break;
}
if(!r) {
goto out;
}
host = hbuf; /* use this hostname */
possibly_non_local = FALSE; /* we know it is local now */
*sslenp = sizeof(*ss);
if(getsockname(conn->sock[FIRSTSOCKET], sa, sslenp)) {
failf(data, "getsockname() failed: %s",
curlx_strerror(SOCKERRNO, buffer, sizeof(buffer)));
return CURLE_FTP_PORT_FAILED;
}
/* resolv ip/host to ip */
res = NULL;
result = Curl_resolv_blocking(data, host, 0, conn->ip_version, &dns_entry);
if(!result) {
DEBUGASSERT(dns_entry);
res = dns_entry->addr;
switch(sa->sa_family) {
#ifdef USE_IPV6
case AF_INET6:
r = curlx_inet_ntop(sa->sa_family, &sa6->sin6_addr, hbuf, hbuflen);
break;
#endif
default:
r = curlx_inet_ntop(sa->sa_family, &sa4->sin_addr, hbuf, hbuflen);
break;
}
if(!r)
return CURLE_FTP_PORT_FAILED;
if(!res) {
*hostp = hbuf;
*non_localp = FALSE; /* we know it is local now */
return CURLE_OK;
}
/*
* Resolve the host string to a list of addresses.
*/
static CURLcode ftp_port_resolve_host(struct Curl_easy *data,
struct connectdata *conn,
const char *host,
struct Curl_dns_entry **dns_entryp,
const struct Curl_addrinfo **resp)
{
CURLcode result;
*resp = NULL;
result = Curl_resolv_blocking(data, host, 0, conn->ip_version,
dns_entryp);
if(result)
failf(data, "failed to resolve the address provided to PORT: %s", host);
goto out;
else {
DEBUGASSERT(*dns_entryp);
*resp = (*dns_entryp)->addr;
}
return result;
}
host = NULL;
/*
* Open a TCP socket for the resolved address family.
*/
static CURLcode ftp_port_open_socket(struct Curl_easy *data,
struct connectdata *conn,
const struct Curl_addrinfo *res,
const struct Curl_addrinfo **aip,
curl_socket_t *portsockp)
{
char buffer[STRERROR_LEN];
int error = 0;
const struct Curl_addrinfo *ai;
CURLcode result = CURLE_FTP_PORT_FAILED;
/* step 2, create a socket for the requested address */
error = 0;
for(ai = res; ai; ai = ai->ai_next) {
result = Curl_socket_open(data, ai, NULL,
Curl_conn_get_transport(data, conn), &portsock);
result =
Curl_socket_open(data, ai, NULL,
Curl_conn_get_transport(data, conn), portsockp);
if(result) {
if(result == CURLE_OUT_OF_MEMORY)
goto out;
return result;
result = CURLE_FTP_PORT_FAILED;
error = SOCKERRNO;
continue;
@ -1075,15 +1104,38 @@ static CURLcode ftp_state_use_port(struct Curl_easy *data,
if(!ai) {
failf(data, "socket failure: %s",
curlx_strerror(error, buffer, sizeof(buffer)));
goto out;
return CURLE_FTP_PORT_FAILED;
}
CURL_TRC_FTP(data, "[%s] ftp_state_use_port(), opened socket",
FTP_CSTATE(ftpc));
*aip = ai;
return result;
}
/* step 3, bind to a suitable local address */
/*
* Bind the socket to a local address and port within the requested range.
* Falls back to the control-connection address if the user-requested address
* is non-local.
*/
static CURLcode ftp_port_bind_socket(struct Curl_easy *data,
struct connectdata *conn,
curl_socket_t portsock,
const struct Curl_addrinfo *ai,
struct Curl_sockaddr_storage *ss,
curl_socklen_t *sslen_io,
unsigned short port_min,
unsigned short port_max,
bool non_local)
{
struct sockaddr *sa = (struct sockaddr *)ss;
struct sockaddr_in * const sa4 = (void *)sa;
#ifdef USE_IPV6
struct sockaddr_in6 * const sa6 = (void *)sa;
#endif
char buffer[STRERROR_LEN];
unsigned short port;
int error;
memcpy(sa, ai->ai_addr, ai->ai_addrlen);
sslen = ai->ai_addrlen;
*sslen_io = ai->ai_addrlen;
for(port = port_min; port <= port_max;) {
if(sa->sa_family == AF_INET)
@ -1092,71 +1144,97 @@ static CURLcode ftp_state_use_port(struct Curl_easy *data,
else
sa6->sin6_port = htons(port);
#endif
/* Try binding the given address. */
if(bind(portsock, sa, sslen)) {
/* It failed. */
if(bind(portsock, sa, *sslen_io)) {
error = SOCKERRNO;
if(possibly_non_local && (error == SOCKEADDRNOTAVAIL)) {
if(non_local && (error == SOCKEADDRNOTAVAIL)) {
/* The requested bind address is not local. Use the address used for
* the control connection instead and restart the port loop
* the control connection instead and restart the port loop.
*/
infof(data, "bind(port=%hu) on non-local address failed: %s", port,
curlx_strerror(error, buffer, sizeof(buffer)));
sslen = sizeof(ss);
if(getsockname(conn->sock[FIRSTSOCKET], sa, &sslen)) {
*sslen_io = sizeof(*ss);
if(getsockname(conn->sock[FIRSTSOCKET], sa, sslen_io)) {
failf(data, "getsockname() failed: %s",
curlx_strerror(SOCKERRNO, buffer, sizeof(buffer)));
goto out;
return CURLE_FTP_PORT_FAILED;
}
port = port_min;
possibly_non_local = FALSE; /* do not try this again */
non_local = FALSE; /* do not try this again */
continue;
}
if(error != SOCKEADDRINUSE && error != SOCKEACCES) {
failf(data, "bind(port=%hu) failed: %s", port,
curlx_strerror(error, buffer, sizeof(buffer)));
goto out;
return CURLE_FTP_PORT_FAILED;
}
}
else
break;
/* check if port is the maximum value here, because it might be 0xffff and
then the increment below will wrap the 16-bit counter */
/* check if port is the maximum value here, because it might be 0xffff
and then the increment below will wrap the 16-bit counter */
if(port == port_max) {
/* maybe all ports were in use already */
failf(data, "bind() failed, ran out of ports");
goto out;
return CURLE_FTP_PORT_FAILED;
}
port++;
}
/* get the name again after the bind() so that we can extract the
port number it uses now */
sslen = sizeof(ss);
if(getsockname(portsock, sa, &sslen)) {
/* re-read the name so we can extract the actual port chosen */
*sslen_io = sizeof(*ss);
if(getsockname(portsock, sa, sslen_io)) {
failf(data, "getsockname() failed: %s",
curlx_strerror(SOCKERRNO, buffer, sizeof(buffer)));
goto out;
return CURLE_FTP_PORT_FAILED;
}
CURL_TRC_FTP(data, "[%s] ftp_state_use_port(), socket bound to port %d",
FTP_CSTATE(ftpc), port);
CURL_TRC_FTP(data, "ftp_port_bind_socket(), socket bound to port %d",
port);
return CURLE_OK;
}
/* step 4, listen on the socket */
/*
* Start listening on the data socket.
*/
static CURLcode ftp_port_listen(struct Curl_easy *data, curl_socket_t portsock)
{
char buffer[STRERROR_LEN];
if(listen(portsock, 1)) {
failf(data, "socket failure: %s",
curlx_strerror(SOCKERRNO, buffer, sizeof(buffer)));
goto out;
return CURLE_FTP_PORT_FAILED;
}
CURL_TRC_FTP(data, "[%s] ftp_state_use_port(), listening on %d",
FTP_CSTATE(ftpc), port);
CURL_TRC_FTP(data, "ftp_port_listen(), listening on port");
return CURLE_OK;
}
/* step 5, send the proper FTP command */
/*
* Send the EPRT or PORT command to the server.
*/
static CURLcode ftp_port_send_command(struct Curl_easy *data,
struct ftp_conn *ftpc,
struct connectdata *conn,
struct Curl_sockaddr_storage *ss,
const struct Curl_addrinfo *ai,
ftpport fcmd)
{
static const char mode[][5] = { "EPRT", "PORT" };
struct sockaddr *sa = (struct sockaddr *)ss;
struct sockaddr_in * const sa4 = (void *)sa;
#ifdef USE_IPV6
struct sockaddr_in6 * const sa6 = (void *)sa;
#endif
char myhost[MAX_IPADR_LEN + 1] = "";
unsigned short port;
CURLcode result;
/* get a plain printable version of the numerical address to work with
below */
/* Get a plain printable version of the numerical address to work with. This
logic uses the address provided by the FTPPORT option, which at times
might differ from the address in 'ss' used to bind to: when a user asks
the server to connect to a specific address knowing that it works, but
curl instead selects to listen to the local address because it cannot use
the provided address. FTP is strange. */
Curl_printable_address(ai, myhost, sizeof(myhost));
#ifdef USE_IPV6
@ -1197,14 +1275,13 @@ static CURLcode ftp_state_use_port(struct Curl_easy *data,
*
* EPRT |2|1080::8:800:200C:417A|5282|
*/
result = Curl_pp_sendf(data, &ftpc->pp, "%s |%d|%s|%hu|", mode[fcmd],
sa->sa_family == AF_INET ? 1 : 2,
myhost, port);
if(result) {
failf(data, "Failure sending EPRT command: %s",
curl_easy_strerror(result));
goto out;
return result;
}
break;
}
@ -1230,7 +1307,7 @@ static CURLcode ftp_state_use_port(struct Curl_easy *data,
if(result) {
failf(data, "Failure sending PORT command: %s",
curl_easy_strerror(result));
goto out;
return result;
}
break;
}
@ -1239,21 +1316,84 @@ static CURLcode ftp_state_use_port(struct Curl_easy *data,
/* store which command was sent */
ftpc->count1 = fcmd;
ftp_state(data, ftpc, FTP_PORT);
return CURLE_OK;
}
/*
* ftp_state_use_port()
*
* Set up an active-mode FTP data connection (using PORT or EPRT) and start
* listening for the server's incoming connection on SECONDARYSOCKET.
*/
static CURLcode ftp_state_use_port(struct Curl_easy *data,
struct ftp_conn *ftpc,
ftpport fcmd) /* start with this */
{
CURLcode result = CURLE_FTP_PORT_FAILED;
struct connectdata *conn = data->conn;
curl_socket_t portsock = CURL_SOCKET_BAD;
struct Curl_sockaddr_storage ss;
curl_socklen_t sslen;
char hbuf[NI_MAXHOST];
const char *host = NULL;
const char *string_ftpport = data->set.str[STRING_FTPPORT];
struct Curl_dns_entry *dns_entry = NULL;
const struct Curl_addrinfo *res = NULL;
const struct Curl_addrinfo *ai = NULL;
unsigned short port_min = 0;
unsigned short port_max = 0;
bool non_local = TRUE;
/* parse the FTPPORT string for address and port range */
result = ftp_port_parse_string(data, conn, string_ftpport,
&ss, &port_min, &port_max,
&host, hbuf, sizeof(hbuf));
if(!result && !host)
/* if no host was specified, use the control connection's local IP */
result = ftp_port_default_host(data, conn, &ss, &sslen, &host,
hbuf, sizeof(hbuf), &non_local);
/* resolve host string to address list */
if(!result)
result = ftp_port_resolve_host(data, conn, host, &dns_entry, &res);
/* Open a TCP socket for the data connection */
if(!result)
result = ftp_port_open_socket(data, conn, res, &ai, &portsock);
if(!result) {
CURL_TRC_FTP(data, "[%s] ftp_state_use_port(), opened socket",
FTP_CSTATE(ftpc));
/* bind to a suitable local address / port */
result = ftp_port_bind_socket(data, conn, portsock, ai, &ss, &sslen,
port_min, port_max, non_local);
}
/* listen */
if(!result)
result = ftp_port_listen(data, portsock);
/* send the PORT / EPRT command */
if(!result)
result = ftp_port_send_command(data, ftpc, conn, &ss, ai, fcmd);
/* replace any filter on SECONDARY with one listening on this socket */
if(!result)
result = Curl_conn_tcp_listen_set(data, conn, SECONDARYSOCKET, &portsock);
/* Replace any filter on SECONDARY with one listening on this socket */
result = Curl_conn_tcp_listen_set(data, conn, SECONDARYSOCKET, &portsock);
if(!result)
portsock = CURL_SOCKET_BAD; /* now held in filter */
out:
/* If we looked up a dns_entry, now is the time to safely release it */
/* cleanup */
if(dns_entry)
Curl_resolv_unlink(data, &dns_entry);
if(result) {
ftp_state(data, ftpc, FTP_STOP);
}
else {
/* successfully setup the list socket filter. Do we need more? */
/* successfully set up the listen socket filter. SSL needed? */
if(conn->bits.ftp_use_data_ssl && data->set.ftp_use_port &&
!Curl_conn_is_ssl(conn, SECONDARYSOCKET)) {
result = Curl_ssl_cfilter_add(data, conn, SECONDARYSOCKET);