| /**************************************************************************** |
| * apps/netutils/ftpc/ftpc_transfer.c |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. The |
| * ASF licenses this file to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the |
| * License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| * |
| ****************************************************************************/ |
| |
| /**************************************************************************** |
| * Included Files |
| ****************************************************************************/ |
| |
| #include <sys/stat.h> |
| #include <sys/time.h> |
| |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <signal.h> |
| #include <poll.h> |
| #include <ctype.h> |
| #include <errno.h> |
| #include <assert.h> |
| #include <debug.h> |
| #include <arpa/inet.h> |
| |
| #include "netutils/ftpc.h" |
| #include "netutils/netlib.h" |
| |
| #include "ftpc_internal.h" |
| |
| /**************************************************************************** |
| * Private Functions |
| ****************************************************************************/ |
| |
| /**************************************************************************** |
| * Name: ftp_cmd_epsv |
| * |
| * Description: |
| * Enter passive mode using EPSV command. |
| * |
| * In active mode FTP the client connects from a random port (N>1023) to |
| * the FTP server's command port, port 21. Then the client starts listening |
| * to port N+1 and sends the FTP command PORT N+1 to the FTP server. The |
| * server will then connect back to the client's specified data port from |
| * its local data port, which is port 20. In passive mode FTP the client |
| * initiates both connections to the server, solving the problem of |
| * firewalls filtering the incoming data port connection to the client from |
| * the server. When opening an FTP connection, the client opens two random |
| * ports locally (N>1023 and N+1). The first port contacts the server on |
| * port 21, but instead of then issuing a PORT command and allowing the |
| * server to connect back to its data port, the client will issue the PASV |
| * command. The result of this is that the server then opens a random |
| * unprivileged port (P > 1023) and sends the PORT P command back to the |
| * client. The client then initiates the connection from port N+1 to port |
| * P on the server to transfer data. |
| * |
| ****************************************************************************/ |
| |
| #ifndef CONFIG_FTPC_DISABLE_EPSV |
| static int ftp_cmd_epsv(FAR struct ftpc_session_s *session, |
| FAR union ftpc_sockaddr_u *addr) |
| { |
| char *ptr; |
| int nscan; |
| int ret; |
| uint16_t tmp; |
| |
| /* Request passive mode. The server normally accepts EPSV with code 227. |
| * Its response is a single line showing the IP address of the server and |
| * the TCP port number where the server is accepting connections. |
| */ |
| |
| ret = ftpc_cmd(session, "EPSV"); |
| if (ret < 0 || !ftpc_connected(session)) |
| { |
| return ERROR; |
| } |
| |
| /* Skip over any leading stuff before important data begins */ |
| |
| ptr = session->reply + 4; |
| while (*ptr != '(') |
| { |
| ptr++; |
| |
| if (ptr > (session->reply + sizeof(session->reply) - 1)) |
| { |
| nwarn("WARNING: Error parsing EPSV reply: '%s'\n", session->reply); |
| return ERROR; |
| } |
| } |
| |
| ptr++; |
| |
| /* The response is then just the port number. None of the other fields |
| * are supplied. |
| */ |
| |
| nscan = sscanf(ptr, "|||%hu|", &tmp); |
| if (nscan != 1) |
| { |
| nwarn("WARNING: Error parsing EPSV reply: '%s'\n", session->reply); |
| return ERROR; |
| } |
| |
| #ifdef CONFIG_NET_IPv4 |
| if (addr->sa.sa_family == AF_INET) |
| { |
| addr->in4.sin_port = HTONS(tmp); |
| } |
| #endif |
| |
| #ifdef CONFIG_NET_IPv6 |
| if (addr->sa.sa_family == AF_INET6) |
| { |
| addr->in6.sin6_port = HTONS(tmp); |
| } |
| #endif |
| |
| return OK; |
| } |
| #endif |
| |
| /**************************************************************************** |
| * Name: ftp_cmd_pasv |
| * |
| * Description: |
| * Enter passive mode using PASV command. |
| * |
| * In active mode FTP the client connects from a random port (N>1023) to |
| * the FTP server's command port, port 21. Then the client starts listening |
| * to port N+1 and sends the FTP command PORT N+1 to the FTP server. The |
| * server will then connect back to the client's specified data port from |
| * its local data port, which is port 20. In passive mode FTP the client |
| * initiates both connections to the server, solving the problem of |
| * firewalls filtering the incoming data port connection to the client from |
| * the server. When opening an FTP connection, the client opens two random |
| * ports locally (N>1023 and N+1). The first port contacts the server on |
| * port 21, but instead of then issuing a PORT command and allowing the |
| * server to connect back to its data port, the client will issue the PASV |
| * of this is that the server then opens a random unprivileged port (P > |
| * command. The result 1023) and sends the PORT P command back to the |
| * client. The client then initiates the connection from port N+1 to port P |
| * on the server to transfer data. |
| * |
| ****************************************************************************/ |
| |
| #ifdef CONFIG_FTPC_DISABLE_EPSV |
| static int ftp_cmd_pasv(FAR struct ftpc_session_s *session, |
| FAR union ftpc_sockaddr_u *addr) |
| { |
| int tmpap[6]; |
| char *ptr; |
| int nscan; |
| int ret; |
| |
| /* Request passive mode. The server normally accepts PASV with code 227. |
| * Its response is a single line showing the IP address of the server and |
| * the TCP port number where the server is accepting connections. |
| */ |
| |
| ret = ftpc_cmd(session, "PASV"); |
| if (ret < 0 || !ftpc_connected(session)) |
| { |
| return ERROR; |
| } |
| |
| /* Skip over any leading stuff before important data begins */ |
| |
| ptr = session->reply + 4; |
| while (*ptr != '\0' && !isdigit((int)*ptr)) |
| { |
| ptr++; |
| } |
| |
| /* The response is then 6 integer values: four representing the |
| * IP address and two representing the port number. |
| */ |
| |
| nscan = sscanf(ptr, "%d,%d,%d,%d,%d,%d", |
| &tmpap[0], &tmpap[1], &tmpap[2], |
| &tmpap[3], &tmpap[4], &tmpap[5]); |
| if (nscan != 6) |
| { |
| nwarn("WARNING: Error parsing PASV reply: '%s'\n", session->reply); |
| return ERROR; |
| } |
| |
| /* Then copy the sscanf'ed values as bytes */ |
| |
| memcpy(&addr->in4.sin_addr, tmpap, sizeof(addr->in4.sin_addr)); |
| memcpy(&addr->in4.sin_port, &tmpap[4], sizeof(addr->in4.sin_port)); |
| |
| return OK; |
| } |
| #endif |
| |
| /**************************************************************************** |
| * Name: ftpc_abspath |
| * |
| * Description: |
| * Get the absolute path to a file, handling tilde expansion. |
| * |
| ****************************************************************************/ |
| |
| static FAR char *ftpc_abspath(FAR struct ftpc_session_s *session, |
| FAR const char *relpath, |
| FAR const char *homedir, |
| FAR const char *curdir) |
| { |
| FAR char *ptr = NULL; |
| |
| /* If no relative path was provide, |
| * then use the current working directory |
| */ |
| |
| if (!relpath) |
| { |
| return strdup(curdir); |
| } |
| |
| /* Handle tilde expansion */ |
| |
| if (relpath[0] == '~') |
| { |
| /* Is the relative path only '~' */ |
| |
| if (relpath[1] == '\0') |
| { |
| return strdup(homedir); |
| } |
| |
| /* No... then a '/' better follow the tilde */ |
| |
| else if (relpath[1] == '/') |
| { |
| if (asprintf(&ptr, "%s%s", homedir, &relpath[1]) < 0) |
| { |
| ptr = NULL; |
| } |
| } |
| |
| /* Hmmm... this pretty much guaranteed to fail */ |
| |
| else |
| { |
| ptr = strdup(relpath); |
| } |
| } |
| |
| /* No tilde expansion. Check for a path relative to the current |
| * directory. |
| */ |
| |
| else if (strncmp(relpath, "./", 2) == 0) |
| { |
| if (asprintf(&ptr, "%s%s", curdir, relpath + 1) < 0) |
| { |
| ptr = NULL; |
| } |
| } |
| |
| /* Check for an absolute path */ |
| |
| else if (relpath[0] == '/') |
| { |
| ptr = strdup(relpath); |
| } |
| |
| /* Assume it a relative path */ |
| |
| else |
| { |
| if (asprintf(&ptr, "%s/%s", curdir, relpath) < 0) |
| { |
| ptr = NULL; |
| } |
| } |
| |
| return ptr; |
| } |
| |
| /**************************************************************************** |
| * Public Functions |
| ****************************************************************************/ |
| |
| /**************************************************************************** |
| * Name: ftpc_xfrinit |
| * |
| * Description: |
| * Perform common transfer setup logic. |
| * |
| ****************************************************************************/ |
| |
| int ftpc_xfrinit(FAR struct ftpc_session_s *session) |
| { |
| union ftpc_sockaddr_u addr; |
| int ret; |
| #ifdef CONFIG_FTPC_DISABLE_EPRT |
| uint8_t *paddr; |
| uint8_t *pport; |
| #else |
| char ipstr[48]; |
| #endif |
| |
| /* We must be connected to initiate a transfer */ |
| |
| if (!ftpc_connected(session)) |
| { |
| nerr("ERROR: Not connected\n"); |
| goto errout; |
| } |
| |
| /* Should we enter passive mode? */ |
| |
| if (FTPC_IS_PASSIVE(session)) |
| { |
| /* Initialize the data channel */ |
| |
| ret = ftpc_sockinit(&session->data, session->server.sa.sa_family); |
| if (ret != OK) |
| { |
| nerr("ERROR: ftpc_sockinit() failed: %d\n", errno); |
| goto errout; |
| } |
| |
| /* Does this host support the PASV command */ |
| |
| if (!FTPC_HAS_PASV(session)) |
| { |
| nerr("ERROR: Host doesn't support passive mode\n"); |
| goto errout_with_data; |
| } |
| |
| /* Configure the address to be the server address. If EPSV is used, the |
| * port will be populated by parsing the reply of the EPSV command. If |
| * the PASV command is used, the address and port will be overwritten. |
| */ |
| |
| memcpy(&addr, &session->server, sizeof(union ftpc_sockaddr_u)); |
| |
| /* Yes.. going passive. */ |
| |
| #ifdef CONFIG_FTPC_DISABLE_EPSV |
| ret = ftp_cmd_pasv(session, &addr); |
| if (ret < 0) |
| { |
| nerr("ERROR: ftp_cmd_pasv() failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| #else |
| ret = ftp_cmd_epsv(session, &addr); |
| if (ret < 0) |
| { |
| nerr("ERROR: ftp_cmd_epsv() failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| #endif |
| |
| /* Connect the data socket */ |
| |
| ret = ftpc_sockconnect(&session->data, (FAR struct sockaddr *)&addr); |
| if (ret < 0) |
| { |
| nerr("ERROR: ftpc_sockconnect() failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| } |
| else |
| { |
| /* Initialize the data listener socket that allows us to accept new |
| * data connections from the server |
| */ |
| |
| ret = ftpc_sockinit(&session->dacceptor, session->server.sa.sa_family); |
| if (ret != OK) |
| { |
| nerr("ERROR: ftpc_sockinit() failed: %d\n", errno); |
| goto errout; |
| } |
| |
| /* Use the server IP address to find the network interface, and |
| * subsequent local IP address used to establish the active |
| * connection. We must send the IP and port to the server so that |
| * it knows how to connect. |
| */ |
| |
| #ifdef CONFIG_NET_IPv6 |
| if (session->server.sa.sa_family == AF_INET6) |
| { |
| ret = netlib_ipv6adaptor(&session->server.in6.sin6_addr, |
| &session->dacceptor.laddr.in6.sin6_addr); |
| if (ret < 0) |
| { |
| nerr("ERROR: netlib_ipv6adaptor() failed: %d\n", ret); |
| goto errout_with_data; |
| } |
| } |
| else |
| #endif |
| #ifdef CONFIG_NET_IPv4 |
| if (session->server.sa.sa_family == AF_INET) |
| { |
| ret = netlib_ipv4adaptor(session->server.in4.sin_addr.s_addr, |
| &session->dacceptor.laddr.in4.sin_addr.s_addr); |
| if (ret < 0) |
| { |
| nerr("ERROR: netlib_ipv4adaptor() failed: %d\n", ret); |
| goto errout_with_data; |
| } |
| } |
| else |
| #endif |
| { |
| nerr("ERROR: unsupported address family\n"); |
| goto errout_with_data; |
| } |
| |
| /* Wait for the connection to be established */ |
| |
| ftpc_socklisten(&session->dacceptor); |
| |
| #ifdef CONFIG_FTPC_DISABLE_EPRT |
| /* Then send our local data channel address to the server */ |
| |
| paddr = (uint8_t *)&session->dacceptor.laddr.in4.sin_addr; |
| pport = (uint8_t *)&session->dacceptor.laddr.in4.sin_port; |
| |
| ret = ftpc_cmd(session, "PORT %d,%d,%d,%d,%d,%d", |
| paddr[0], paddr[1], paddr[2], |
| paddr[3], pport[0], pport[1]); |
| #else |
| #ifdef CONFIG_NET_IPv6 |
| if (session->dacceptor.laddr.sa.sa_family == AF_INET6) |
| { |
| if (!inet_ntop(AF_INET6, &session->dacceptor.laddr.in6.sin6_addr, |
| ipstr, 48)) |
| { |
| nerr("ERROR: inet_ntop failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| |
| ret = ftpc_cmd(session, "EPRT |2|%s|%d|", ipstr, |
| session->dacceptor.laddr.in6.sin6_port); |
| } |
| else |
| #endif /* CONFIG_NET_IPv6 */ |
| #ifdef CONFIG_NET_IPv4 |
| if (session->dacceptor.laddr.sa.sa_family == AF_INET) |
| { |
| if (!inet_ntop(AF_INET, &session->dacceptor.laddr.in4.sin_addr, |
| ipstr, 48)) |
| { |
| nerr("ERROR: inet_ntop failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| |
| ret = ftpc_cmd(session, "EPRT |1|%s|%d|", ipstr, |
| session->dacceptor.laddr.in4.sin_port); |
| } |
| else |
| #endif /* CONFIG_NET_IPv4 */ |
| #endif /* CONFIG_FTPC_DISABLE_EPRT */ |
| |
| if (ret < 0) |
| { |
| nerr("ERROR: ftpc_cmd() failed: %d\n", errno); |
| goto errout_with_data; |
| } |
| } |
| |
| return OK; |
| |
| errout_with_data: |
| ftpc_sockclose(&session->data); |
| ftpc_sockclose(&session->dacceptor); |
| errout: |
| return ERROR; |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_xfrreset |
| * |
| * Description: |
| * Reset transfer variables |
| * |
| ****************************************************************************/ |
| |
| void ftpc_xfrreset(struct ftpc_session_s *session) |
| { |
| session->size = 0; |
| session->flags &= ~FTPC_XFER_FLAGS; |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_xfrmode |
| * |
| * Description: |
| * Select ASCII or Binary transfer mode |
| * |
| ****************************************************************************/ |
| |
| int ftpc_xfrmode(struct ftpc_session_s *session, uint8_t xfrmode) |
| { |
| int ret; |
| |
| /* Check if we have already selected the requested mode */ |
| |
| DEBUGASSERT(xfrmode != FTPC_XFRMODE_UNKNOWN); |
| if (session->xfrmode != xfrmode) |
| { |
| /* Send the TYPE request to control the binary flag. |
| * Parameters for the TYPE request include: |
| * |
| * A: Turn the binary flag off. |
| * A N: Turn the binary flag off. |
| * I: Turn the binary flag on. |
| * L 8: Turn the binary flag on. |
| * |
| * The server accepts the TYPE request with code 200. |
| */ |
| |
| ret = ftpc_cmd(session, "TYPE %c", |
| xfrmode == FTPC_XFRMODE_ASCII ? 'A' : 'I'); |
| if (ret < 0) |
| { |
| return ERROR; |
| } |
| |
| session->xfrmode = xfrmode; |
| } |
| |
| return OK; |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_xfrabort |
| * |
| * Description: |
| * Abort a transfer in progress |
| * |
| ****************************************************************************/ |
| |
| int ftpc_xfrabort(FAR struct ftpc_session_s *session, FAR FILE *stream) |
| { |
| FAR struct pollfd fds; |
| int ret; |
| |
| /* Make sure that we are still connected */ |
| |
| if (!ftpc_connected(session)) |
| { |
| return ERROR; |
| } |
| |
| /* Check if there is data waiting to be read from the cmd channel */ |
| |
| fds.fd = session->cmd.sd; |
| fds.events = POLLIN; |
| ret = poll(&fds, 1, 0); |
| if (ret > 0) |
| { |
| /* Read data from command channel */ |
| |
| ninfo("Flush cmd channel data\n"); |
| while (stream && |
| fread(session->buffer, 1, CONFIG_FTP_BUFSIZE, stream) > 0); |
| return OK; |
| } |
| |
| FTPC_SET_INTERRUPT(session); |
| |
| /* Send the Telnet interrupt sequence to abort the transfer: |
| * <IAC IP><IAC DM>ABORT<CR><LF> |
| */ |
| |
| ninfo("Telnet ABORt sequence\n"); |
| ftpc_sockprintf(&session->cmd, "%c%c", TELNET_IAC, TELNET_IP); /* Interrupt process */ |
| ftpc_sockprintf(&session->cmd, "%c%c", TELNET_IAC, TELNET_DM); /* Telnet synch signal */ |
| ftpc_sockprintf(&session->cmd, "ABOR\r\n"); /* Abort */ |
| ftpc_sockflush(&session->cmd); |
| |
| /* Read remaining bytes from connection */ |
| |
| while (stream && |
| fread(session->buffer, 1, CONFIG_FTP_BUFSIZE, stream) > 0); |
| |
| /* Get the ABORt reply */ |
| |
| fptc_getreply(session); |
| |
| /* Expected replies are: "226 Closing data connection" or |
| * "426 Connection closed; transfer aborted" |
| */ |
| |
| if (session->code != 226 && session->code != 426) |
| { |
| ninfo("Expected 226 or 426 reply\n"); |
| } |
| else |
| { |
| /* Get the next reply */ |
| |
| fptc_getreply(session); |
| |
| /* Expected replies are: "226 Closing data connection" or |
| * "225 Data connection open; no transfer in progress" |
| */ |
| |
| if (session->code != 226 && session->code != 225) |
| { |
| ninfo("Expected 225 or 226 reply\n"); |
| } |
| } |
| |
| return ERROR; |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_timeout |
| * |
| * Description: |
| * A timeout occurred -- either on a specific command or while waiting |
| * for a reply. |
| * |
| * NOTE: |
| * This function executes in the context of a timer interrupt handler. |
| * |
| ****************************************************************************/ |
| |
| void ftpc_timeout(wdparm_t arg) |
| { |
| FAR struct ftpc_session_s *session = (FAR struct ftpc_session_s *)arg; |
| |
| nerr("ERROR: Timeout!\n"); |
| DEBUGASSERT(session); |
| kill(session->pid, CONFIG_FTP_SIGNAL); |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_absrpath |
| * |
| * Description: |
| * Get the absolute path to a remote file, handling tilde expansion. |
| * |
| ****************************************************************************/ |
| |
| FAR char *ftpc_absrpath(FAR struct ftpc_session_s *session, |
| FAR const char *relpath) |
| { |
| FAR char *absrpath = ftpc_abspath(session, relpath, |
| session->homerdir, session->currdir); |
| ninfo("%s -> %s\n", relpath, absrpath); |
| return absrpath; |
| } |
| |
| /**************************************************************************** |
| * Name: ftpc_abslpath |
| * |
| * Description: |
| * Get the absolute path to a local file, handling tilde expansion. |
| * |
| ****************************************************************************/ |
| |
| FAR char *ftpc_abslpath(FAR struct ftpc_session_s *session, |
| FAR const char *relpath) |
| { |
| FAR char *abslpath = ftpc_abspath(session, relpath, |
| session->homeldir, session->curldir); |
| ninfo("%s -> %s\n", relpath, abslpath); |
| return abslpath; |
| } |