1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-06-28 01:35:32 +00:00
elinks/src/protocol/ftp/ftp.c

1630 lines
42 KiB
C
Raw Permalink Normal View History

/* Internal "ftp" protocol implementation */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> /* For converting permissions to strings */
#include <sys/types.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
#endif
/* We need to have it here. Stupid BSD. */
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
2023-11-04 19:30:25 +00:00
#ifdef HAVE_WS2TCPIP_H
#include <ws2tcpip.h>
#endif
#include "elinks.h"
#include "cache/cache.h"
#include "config/options.h"
#include "intl/libintl.h"
#include "main/select.h"
#include "main/module.h"
#include "network/connection.h"
#include "network/progress.h"
#include "network/socket.h"
#include "osdep/osdep.h"
#include "osdep/stat.h"
#include "protocol/auth/auth.h"
#include "protocol/common.h"
#include "protocol/curl/ftpes.h"
#include "protocol/ftp/ftp.h"
#include "protocol/ftpparse.h"
#include "protocol/uri.h"
#include "util/conv.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/string.h"
bug 764: Initialize the right member of union option_value INIT_OPTION used to initialize union option_value at compile time by casting the default value to LIST_OF(struct option) *, which is the type of the first member. On sparc64 and other big-endian systems where sizeof(int) < sizeof(struct list_head *), this tended to leave option->value.number as zero, thus messing up OPT_INT and OPT_BOOL at least. OPT_LONG however tended to work right. This would be easy to fix with C99 designated initializers, but doc/hacking.txt says ELinks must be kept C89 compatible. Another solution would be to make register_options() read the value from option->value.tree (the first member), cast it back to the right type, and write it to the appropriate member; but that would still require somewhat dubious conversions between integers, data pointers, and function pointers. So here's a rather more invasive solution. Add struct option_init, which is somewhat similar to struct option but has non-overlapping members for different types of values, to ensure nothing is lost in compile-time conversions. Move unsigned char *path from struct option_info to struct option_init, and replace struct option_info with a union that contains struct option_init and struct option. Now, this union can be initialized with no portability problems, and register_options() then moves the values from struct option_init to their final places in struct option. In my x86 ELinks build with plenty of options configured in, this change bloated the text section by 340 bytes but compressed the data section by 2784 bytes, presumably because union option_info is a pointer smaller than struct option_info was. (cherry picked from elinks-0.12 commit e5f6592ee20780a61f70feeb1f9e17631b9c5835) Conflicts: src/protocol/fsp/fsp.c: All options had been removed in 0.13.GIT. src/protocol/smb/smb2.c: Ditto.
2009-08-15 19:39:07 +00:00
union option_info ftp_options[] = {
INIT_OPT_TREE("protocol", N_("FTP"),
"ftp", OPT_ZERO,
N_("FTP specific options.")),
INIT_OPT_TREE("protocol.ftp", N_("Proxy configuration"),
"proxy", OPT_ZERO,
N_("FTP proxy configuration.")),
INIT_OPT_STRING("protocol.ftp.proxy", N_("Host and port-number"),
"host", OPT_ZERO, "",
N_("Host and port-number (host:port) of the FTP proxy, "
"or blank. If it's blank, FTP_PROXY environment variable "
"is checked as well.")),
INIT_OPT_STRING("protocol.ftp", N_("Anonymous password"),
"anon_passwd", OPT_ZERO, "some@host.domain",
N_("FTP anonymous password to be sent.")),
#if defined(CONFIG_LIBCURL)
INIT_OPT_BOOL("protocol.ftp", N_("Use libcurl"),
"use_curl", OPT_ZERO, 0,
N_("Use libcurl implementation of ftp.")),
INIT_OPT_LONG("protocol.ftp", N_("Curl max receive speed"),
"curl_max_recv_speed", OPT_ZERO, 0, LONG_MAX, 0,
N_("Max download speed in bytes/s. 0 means unlimited.")),
INIT_OPT_LONG("protocol.ftp", N_("Curl max send speed"),
"curl_max_send_speed", OPT_ZERO, 0, LONG_MAX, 0,
N_("Max upload speed in bytes/s. 0 means unlimited.")),
INIT_OPT_STRING("protocol.ftp", N_("Curl TLS13 ciphers"),
"curl_tls13_ciphers", OPT_ZERO, "",
N_("Colon separated list of TLS13 ciphers. See https://curl.se/docs/ssl-ciphers.html")),
#endif
INIT_OPT_BOOL("protocol.ftp", N_("Use passive mode (IPv4)"),
"use_pasv", OPT_ZERO, 1,
N_("Use PASV instead of PORT (passive vs active mode, "
"IPv4 only).")),
#ifdef CONFIG_IPV6
INIT_OPT_BOOL("protocol.ftp", N_("Use passive mode (IPv6)"),
"use_epsv", OPT_ZERO, 0,
N_("Use EPSV instead of EPRT (passive vs active mode, "
"IPv6 only).")),
#endif /* CONFIG_IPV6 */
NULL_OPTION_INFO,
};
struct module ftp_protocol_module = struct_module(
/* name: */ N_("FTP"),
/* options: */ ftp_options,
/* hooks: */ NULL,
/* submodules: */ NULL,
/* data: */ NULL,
/* init: */ NULL,
/* done: */ NULL
);
/* Constants */
#define FTP_BUF_SIZE 16384
/* Types and structs */
struct ftp_connection_info {
int pending_commands; /* Num of commands queued */
int opc; /* Total num of commands queued */
int conn_state;
int buf_pos;
unsigned int dir:1; /* Directory listing in progress */
unsigned int rest_sent:1; /* Sent RESTore command */
unsigned int use_pasv:1; /* Use PASV (yes or no) */
#ifdef CONFIG_IPV6
unsigned int use_epsv:1; /* Use EPSV */
#endif
char ftp_buffer[FTP_BUF_SIZE];
char cmd_buffer[1]; /* Must be last field !! */
};
/* Prototypes */
static void ftp_login(struct socket *);
static void ftp_send_retr_req(struct connection *, struct connection_state);
static void ftp_got_info(struct socket *, struct read_buffer *);
static void ftp_got_user_info(struct socket *, struct read_buffer *);
static void ftp_pass(struct connection *);
static void ftp_pass_info(struct socket *, struct read_buffer *);
static void ftp_retr_file(struct socket *, struct read_buffer *);
static void ftp_got_final_response(struct socket *, struct read_buffer *);
static void got_something_from_data_connection(struct connection *);
static void ftp_end_request(struct connection *, struct connection_state);
static struct ftp_connection_info *add_file_cmd_to_str(struct connection *);
static void ftp_data_accept(struct connection *conn);
/* Parse EPSV or PASV response for address and/or port.
* int *n should point to a sizeof(int) * 6 space.
* It returns zero on error or count of parsed numbers.
* It returns an error if:
* - there's more than 6 or less than 1 numbers.
* - a number is strictly greater than max.
*
* On success, array of integers *n is filled with numbers starting
* from end of array (ie. if we found one number, you can access it using
* n[5]).
*
* Important:
* Negative numbers aren't handled so -123 is taken as 123.
* We don't take care about separators.
*/
static int
parse_psv_resp(char *data, int *n, int max_value)
{
char *p = data;
int i = 5;
memset(n, 0, 6 * sizeof(*n));
2021-02-27 08:51:23 +00:00
if ((unsigned char)*p < ' ') return 0;
/* Find the end. */
while (*p >= ' ') p++;
/* Ignore non-numeric ending chars. */
while (p != data && !isdigit(*p)) p--;
if (p == data) return 0;
while (i >= 0) {
int x = 1;
/* Parse one number. */
while (p != data && isdigit(*p)) {
n[i] += (*p - '0') * x;
if (n[i] > max_value) return 0;
x *= 10;
p--;
}
/* Ignore non-numeric chars. */
while (p != data && !isdigit(*p)) p--;
if (p == data) return (6 - i);
/* Get the next one. */
i--;
}
return 0;
}
/* Returns 0 if there's no numeric response, -1 if error, the positive response
* number otherwise. */
static int
get_ftp_response(struct connection *conn, struct read_buffer *rb, int part,
struct sockaddr_storage *sa, off_t *est_length)
{
char *eol;
char *num_end;
int response;
int pos;
again:
2022-01-25 18:14:40 +00:00
eol = (char *)memchr(rb->data, ASCII_LF, rb->length);
if (!eol) return 0;
pos = eol - rb->data;
errno = 0;
response = strtoul(rb->data, (char **) &num_end, 10);
if (errno || num_end != rb->data + 3 || response < 100)
return -1;
if (sa && response == 227) { /* PASV response parsing. */
struct sockaddr_in *s = (struct sockaddr_in *) sa;
int n[6];
if (parse_psv_resp(num_end, (int *) &n, 255) != 6)
return -1;
memset(s, 0, sizeof(*s));
s->sin_family = AF_INET;
s->sin_addr.s_addr = htonl((n[0] << 24) + (n[1] << 16) + (n[2] << 8) + n[3]);
s->sin_port = htons((n[4] << 8) + n[5]);
}
#ifdef CONFIG_IPV6
if (sa && response == 229) { /* EPSV response parsing. */
/* See RFC 2428 */
struct sockaddr_in6 *s = (struct sockaddr_in6 *) sa;
socklen_t sal = sizeof(*s);
int n[6];
if (parse_psv_resp(num_end, (int *) &n, 65535) != 1) {
return -1;
}
memset(s, 0, sizeof(*s));
if (getpeername(conn->socket->fd, (struct sockaddr *) sa, &sal)) {
return -1;
}
s->sin6_family = AF_INET6;
s->sin6_port = htons(n[5]);
}
#endif
if (*num_end == '-') {
int i;
for (i = 0; i < rb->length - 5; i++)
if (rb->data[i] == ASCII_LF
&& !memcmp(rb->data+i+1, rb->data, 3)
&& rb->data[i+4] == ' ') {
for (i++; i < rb->length; i++)
if (rb->data[i] == ASCII_LF)
goto ok;
return 0;
}
return 0;
ok:
pos = i;
}
if (response == 213 && est_length) {
off_t size = strtoull(num_end + 1, NULL, 10);
if (errno) {
return -1;
}
*est_length = size;
}
if (part != 2)
kill_buffer_data(rb, pos + 1);
if (!part && response >= 100 && response < 200) {
goto again;
}
return response;
}
/* Initialize or continue ftp connection. */
void
ftp_protocol_handler(struct connection *conn)
{
#if defined(CONFIG_LIBCURL)
if (get_opt_bool("protocol.ftp.use_curl", NULL)) {
ftpes_protocol_handler(conn);
return;
}
#endif
if (!has_keepalive_connection(conn)) {
make_connection(conn->socket, conn->uri, ftp_login,
conn->cache_mode >= CACHE_MODE_FORCE_RELOAD);
} else {
ftp_send_retr_req(conn, connection_state(S_SENT));
}
}
/* Send command, set connection state and free cmd string. */
static void
send_cmd(struct connection *conn, struct string *cmd, void *callback,
struct connection_state state)
{
request_from_socket(conn->socket, cmd->source, cmd->length, state,
2022-01-25 18:14:40 +00:00
SOCKET_RETRY_ONCLOSE, (socket_read_T)callback);
done_string(cmd);
}
/* Check if this auth token really belongs to this URI. */
static int
auth_user_matching_uri(struct auth_entry *auth, struct uri *uri)
{
if (!uri->userlen) /* Noone said it doesn't. */
return 1;
return !c_strlcasecmp(auth->user, -1, uri->user, uri->userlen);
}
/* Kill the current connection and ask for a username/password for the next
* try. */
static void
prompt_username_pw(struct connection *conn)
{
if (!conn->cached) {
conn->cached = get_cache_entry(conn->uri);
if (!conn->cached) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
}
mem_free_set(&conn->cached->content_type, stracpy("text/html"));
if (!conn->cached->content_type) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
add_auth_entry(conn->uri, "FTP Login", NULL, NULL, 0);
abort_connection(conn, connection_state(S_OK));
}
/* Send USER command. */
static void
ftp_login(struct socket *socket)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
struct string cmd;
struct auth_entry* auth;
auth = find_auth(conn->uri);
if (!init_string(&cmd)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
add_to_string(&cmd, "USER ");
if (conn->uri->userlen) {
struct uri *uri = conn->uri;
add_bytes_to_string(&cmd, uri->user, uri->userlen);
} else if (auth && auth->valid) {
add_to_string(&cmd, auth->user);
} else {
add_to_string(&cmd, "anonymous");
}
add_crlf_to_string(&cmd);
send_cmd(conn, &cmd, (void *) ftp_got_info, connection_state(S_SENT));
}
/* Parse connection response. */
static void
ftp_got_info(struct socket *socket, struct read_buffer *rb)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
int response = get_ftp_response(conn, rb, 0, NULL, NULL);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
read_from_socket(conn->socket, rb, conn->state, ftp_got_info);
return;
}
/* RFC959 says that possible response codes on connection are:
* 120 Service ready in nnn minutes.
* 220 Service ready for new user.
* 421 Service not available, closing control connection. */
if (response != 220) {
/* TODO? Retry in case of ... ?? */
retry_connection(conn, connection_state(S_FTP_UNAVAIL));
return;
}
ftp_got_user_info(socket, rb);
}
/* Parse USER response and send PASS command if needed. */
static void
ftp_got_user_info(struct socket *socket, struct read_buffer *rb)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
int response = get_ftp_response(conn, rb, 0, NULL, NULL);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
read_from_socket(conn->socket, rb, conn->state, ftp_got_user_info);
return;
}
/* RFC959 says that possible response codes for USER are:
* 230 User logged in, proceed.
* 331 User name okay, need password.
* 332 Need account for login.
* 421 Service not available, closing control connection.
* 500 Syntax error, command unrecognized.
* 501 Syntax error in parameters or arguments.
* 530 Not logged in. */
/* FIXME? Since ACCT command isn't implemented, login to a ftp server
* requiring it will fail (332). */
if (response == 332 || response >= 500) {
prompt_username_pw(conn);
return;
}
/* We don't require exact match here, as this is always error and some
* non-RFC compliant servers may return even something other than 421.
* --Zas */
if (response >= 400) {
abort_connection(conn, connection_state(S_FTP_UNAVAIL));
return;
}
if (response == 230) {
ftp_send_retr_req(conn, connection_state(S_GETH));
return;
}
ftp_pass(conn);
}
/* Send PASS command. */
static void
ftp_pass(struct connection *conn)
{
struct string cmd;
struct auth_entry *auth;
auth = find_auth(conn->uri);
if (!init_string(&cmd)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
add_to_string(&cmd, "PASS ");
if (conn->uri->passwordlen) {
struct uri *uri = conn->uri;
add_bytes_to_string(&cmd, uri->password, uri->passwordlen);
} else if (auth && auth->valid) {
if (!auth_user_matching_uri(auth, conn->uri)) {
prompt_username_pw(conn);
return;
}
add_to_string(&cmd, auth->password);
} else {
add_to_string(&cmd, get_opt_str("protocol.ftp.anon_passwd",
NULL));
}
add_crlf_to_string(&cmd);
send_cmd(conn, &cmd, (void *) ftp_pass_info, connection_state(S_LOGIN));
}
/* Parse PASS command response. */
static void
ftp_pass_info(struct socket *socket, struct read_buffer *rb)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
int response = get_ftp_response(conn, rb, 0, NULL, NULL);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
read_from_socket(conn->socket, rb, connection_state(S_LOGIN),
ftp_pass_info);
return;
}
/* RFC959 says that possible response codes for PASS are:
* 202 Command not implemented, superfluous at this site.
* 230 User logged in, proceed.
* 332 Need account for login.
* 421 Service not available, closing control connection.
* 500 Syntax error, command unrecognized.
* 501 Syntax error in parameters or arguments.
* 503 Bad sequence of commands.
* 530 Not logged in. */
if (response == 332 || response >= 500) {
/* If we didn't have a user, we tried anonymous. But it failed, so ask for a
* user and password */
prompt_username_pw(conn);
return;
}
if (response >= 400) {
abort_connection(conn, connection_state(S_FTP_UNAVAIL));
return;
}
ftp_send_retr_req(conn, connection_state(S_GETH));
}
/* Construct PORT command. */
static void
add_portcmd_to_string(struct string *string, char *pc)
{
/* From RFC 959: DATA PORT (PORT)
*
* The argument is a HOST-PORT specification for the data port
* to be used in data connection. There are defaults for both
* the user and server data ports, and under normal
* circumstances this command and its reply are not needed. If
* this command is used, the argument is the concatenation of a
* 32-bit internet host address and a 16-bit TCP port address.
* This address information is broken into 8-bit fields and the
* value of each field is transmitted as a decimal number (in
* character string representation). The fields are separated
* by commas. A port command would be:
*
* PORT h1,h2,h3,h4,p1,p2
*
* where h1 is the high order 8 bits of the internet host
* address. */
add_to_string(string, "PORT ");
add_long_to_string(string, pc[0]);
add_char_to_string(string, ',');
add_long_to_string(string, pc[1]);
add_char_to_string(string, ',');
add_long_to_string(string, pc[2]);
add_char_to_string(string, ',');
add_long_to_string(string, pc[3]);
add_char_to_string(string, ',');
add_long_to_string(string, pc[4]);
add_char_to_string(string, ',');
add_long_to_string(string, pc[5]);
}
#ifdef CONFIG_IPV6
/* Construct EPRT command. */
static void
add_eprtcmd_to_string(struct string *string, struct sockaddr_in6 *addr)
{
char addr_str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &addr->sin6_addr, addr_str, INET6_ADDRSTRLEN);
/* From RFC 2428: EPRT
*
* The format of EPRT is:
*
* EPRT<space><d><net-prt><d><net-addr><d><tcp-port><d>
*
* <net-prt>:
* AF Number Protocol
* --------- --------
* 1 Internet Protocol, Version 4 [Pos81a]
* 2 Internet Protocol, Version 6 [DH96] */
add_to_string(string, "EPRT |2|");
add_to_string(string, addr_str);
add_char_to_string(string, '|');
add_long_to_string(string, ntohs(addr->sin6_port));
add_char_to_string(string, '|');
}
#endif
/* Depending on options, get proper ftp data socket and command.
* It appends ftp command (either PASV,PORT,EPSV or EPRT) to @command
* string.
* When PORT or EPRT are used, related sockets are created.
* It returns 0 on error (data socket creation failure). */
static int
get_ftp_data_socket(struct connection *conn, struct string *command)
{
2022-01-25 18:14:40 +00:00
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
ftp->use_pasv = get_opt_bool("protocol.ftp.use_pasv", NULL);
#ifdef CONFIG_IPV6
ftp->use_epsv = get_opt_bool("protocol.ftp.use_epsv", NULL);
if (conn->socket->protocol_family == EL_PF_INET6) {
if (ftp->use_epsv) {
add_to_string(command, "EPSV");
} else {
struct sockaddr_storage data_addr;
int data_sock;
memset(&data_addr, 0, sizeof(data_addr));
data_sock = get_pasv_socket(conn->socket, &data_addr);
if (data_sock < 0) return 0;
conn->data_socket->fd = data_sock;
add_eprtcmd_to_string(command,
(struct sockaddr_in6 *) &data_addr);
}
} else
#endif
{
if (ftp->use_pasv) {
add_to_string(command, "PASV");
} else {
struct sockaddr_in sa;
char pc[6];
int data_sock;
memset(pc, 0, sizeof(pc));
data_sock = get_pasv_socket(conn->socket,
(struct sockaddr_storage *) &sa);
if (data_sock < 0) return 0;
memcpy(pc, &sa.sin_addr.s_addr, 4);
memcpy(pc + 4, &sa.sin_port, 2);
conn->data_socket->fd = data_sock;
add_portcmd_to_string(command, pc);
}
}
add_crlf_to_string(command);
return 1;
}
/* Check if the file or directory name @s can be safely sent to the
* FTP server. To prevent command injection attacks, this function
* must reject CR LF sequences. */
static int
is_ftp_pathname_safe(const struct string *s)
{
int i;
/* RFC 959 says the argument of CWD and RETR is a <pathname>,
* which consists of <char>s, "any of the 128 ASCII characters
* except <CR> and <LF>". So other control characters, such
* as 0x00 and 0x7F, are allowed here. Bytes 0x80...0xFF
* should not be allowed, but if we reject them, users will
* probably complain. */
for (i = 0; i < s->length; i++) {
if (s->source[i] == 0x0A || s->source[i] == 0x0D)
return 0;
}
return 1;
}
/* Create passive socket and add appropriate announcing commands to str. Then
* go and retrieve appropriate object from server.
* Returns NULL if error. */
static struct ftp_connection_info *
add_file_cmd_to_str(struct connection *conn)
{
int ok = 0;
struct ftp_connection_info *ftp = NULL;
struct string command = NULL_STRING;
struct string ftp_data_command = NULL_STRING;
struct string pathname = NULL_STRING;
if (!conn->uri->data) {
INTERNAL("conn->uri->data empty");
abort_connection(conn, connection_state(S_INTERNAL));
goto ret;
}
assert(conn->info == NULL);
assert(conn->done == NULL);
if_assert_failed {
abort_connection(conn, connection_state(S_INTERNAL));
goto ret;
}
/* This will be reallocated below when we know how long the
* command string should be. Error handling could be
* simplified a little by allocating this initial structure on
* the stack, but it's several kilobytes long so that might be
* risky. */
2022-01-16 20:08:50 +00:00
ftp = (struct ftp_connection_info *)mem_calloc(1, sizeof(*ftp));
if (!ftp) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
/* conn->info and conn->done were asserted as NULL above. */
conn->info = ftp; /* Freed when connection is destroyed. */
if (!init_string(&command)
|| !init_string(&ftp_data_command)
|| !init_string(&pathname)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
if (!get_ftp_data_socket(conn, &ftp_data_command)) {
INTERNAL("Ftp data socket failure");
abort_connection(conn, connection_state(S_INTERNAL));
goto ret;
}
if (!add_uri_to_string(&pathname, conn->uri, URI_PATH)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
decode_uri_string(&pathname);
if (!is_ftp_pathname_safe(&pathname)) {
abort_connection(conn, connection_state(S_BAD_URL));
goto ret;
}
if (!conn->uri->datalen
|| conn->uri->data[conn->uri->datalen - 1] == '/') {
/* Commands to get directory listing. */
ftp->dir = 1;
ftp->pending_commands = 4;
if (!add_to_string(&command, "TYPE A") /* ASCII */
|| !add_crlf_to_string(&command)
|| !add_string_to_string(&command, &ftp_data_command)
2007-04-26 13:02:04 +00:00
|| !add_to_string(&command, "CWD ")
|| !add_string_to_string(&command, &pathname)
|| !add_crlf_to_string(&command)
|| !add_to_string(&command, "LIST")
|| !add_crlf_to_string(&command)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
conn->from = 0;
} else {
/* Commands to get a file. */
ftp->dir = 0;
ftp->pending_commands = 4;
if (!add_to_string(&command, "TYPE I") /* BINARY */
|| !add_crlf_to_string(&command)
|| !add_string_to_string(&command, &ftp_data_command)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
if (conn->from || conn->progress->start > 0) {
const off_t offset = conn->from
? conn->from
: conn->progress->start;
if (!add_to_string(&command, "REST ")
|| !add_long_to_string(&command, offset)
|| !add_crlf_to_string(&command)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
ftp->rest_sent = 1;
ftp->pending_commands++;
}
if (!add_to_string(&command, "SIZE ")
|| !add_string_to_string(&command, &pathname)
|| !add_crlf_to_string(&command)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
if (!add_to_string(&command, "RETR ")
|| !add_string_to_string(&command, &pathname)
|| !add_crlf_to_string(&command)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
}
ftp->opc = ftp->pending_commands;
/* 1 byte is already reserved for cmd_buffer in struct ftp_connection_info. */
2022-01-16 18:38:30 +00:00
ftp = (struct ftp_connection_info *)mem_realloc(ftp, sizeof(*ftp) + command.length);
if (!ftp) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
goto ret;
}
conn->info = ftp; /* in case mem_realloc moved the buffer */
memcpy(ftp->cmd_buffer, command.source, command.length + 1);
ok = 1;
ret:
/* If @ok is false here, then abort_connection has already
* freed @ftp, which now is a dangling pointer. */
done_string(&pathname);
done_string(&ftp_data_command);
done_string(&command);
return ok ? ftp : NULL;
}
static void
send_it_line_by_line(struct connection *conn, struct string *cmd)
{
2022-01-25 18:14:40 +00:00
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
char *nl = strchr(ftp->cmd_buffer, '\n');
if (!nl) {
add_to_string(cmd, ftp->cmd_buffer);
return;
}
nl++;
add_bytes_to_string(cmd, ftp->cmd_buffer, nl - ftp->cmd_buffer);
memmove(ftp->cmd_buffer, nl, strlen(nl) + 1);
}
/* Send commands to retrieve file or directory. */
static void
ftp_send_retr_req(struct connection *conn, struct connection_state state)
{
struct string cmd;
if (!init_string(&cmd)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
/* We don't save return value from add_file_cmd_to_str(), as it's saved
* in conn->info as well. */
if (!conn->info && !add_file_cmd_to_str(conn)) {
done_string(&cmd);
return;
}
/* Send it line-by-line. */
send_it_line_by_line(conn, &cmd);
send_cmd(conn, &cmd, (void *) ftp_retr_file, state);
}
/* Parse RETR response and return file size or -1 on error. */
static off_t
get_filesize_from_RETR(char *data, int data_len, int *resume)
{
off_t file_len;
int pos;
int pos_file_len = 0;
/* Getting file size from text response.. */
/* 150 Opening BINARY mode data connection for hello-1.0-1.1.diff.gz (16452 bytes). */
*resume = 0;
for (pos = 0; pos < data_len && data[pos] != ASCII_LF; pos++)
if (data[pos] == '(')
pos_file_len = pos;
2006-03-16 21:13:40 +00:00
if (!pos_file_len || pos_file_len == data_len - 1) {
/* Getting file size from ftp.task.gda.pl */
/* 150 5676.4 kbytes to download */
unsigned char tmp = data[data_len - 1];
char *kbytes;
2006-03-16 21:13:40 +00:00
char *endptr;
double size;
data[data_len - 1] = '\0';
2022-01-18 19:55:08 +00:00
kbytes = strstr(data, "kbytes");
2006-03-16 21:13:40 +00:00
data[data_len - 1] = tmp;
if (!kbytes) return -1;
for (kbytes -= 2; kbytes >= data; kbytes--) {
if (*kbytes == ' ') break;
}
if (*kbytes != ' ') return -1;
kbytes++;
size = strtod((const char *)kbytes, &endptr);
if (endptr == (char *)kbytes) return -1;
*resume = 1;
2006-03-16 21:13:40 +00:00
return (off_t)(size * 1024.0);
}
pos_file_len++;
if (!isdigit(data[pos_file_len]))
return -1;
for (pos = pos_file_len; pos < data_len; pos++)
if (!isdigit(data[pos]))
goto next;
return -1;
next:
for (; pos < data_len; pos++)
if (data[pos] != ' ')
break;
if (pos + 4 > data_len)
return -1;
if (c_strncasecmp(&data[pos], "byte", 4))
return -1;
errno = 0;
file_len = (off_t) strtoll(&data[pos_file_len], NULL, 10);
if (errno) return -1;
return file_len;
}
/* Connect to the host and port specified by a passive FTP server. */
static int
ftp_data_connect(struct connection *conn, int pf, struct sockaddr_storage *sa,
int size_of_sockaddr)
{
int fd;
if (conn->data_socket->fd != -1) {
/* The server maliciously sent multiple 227 or 229
* responses. Do not leak the previous data_socket. */
abort_connection(conn, connection_state(S_FTP_ERROR));
return -1;
}
fd = socket(pf, SOCK_STREAM, 0);
if (fd < 0 || set_nonblocking_fd(fd) < 0) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return -1;
}
set_ip_tos_throughput(fd);
conn->data_socket->fd = fd;
/* XXX: We ignore connect() errors here. */
connect(fd, (struct sockaddr *) sa, size_of_sockaddr);
return 0;
}
static void
ftp_retr_file(struct socket *socket, struct read_buffer *rb)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
int response;
off_t size = -1;
if (ftp->pending_commands > 1) {
struct sockaddr_storage sa;
response = get_ftp_response(conn, rb, 0, &sa, &size);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
read_from_socket(conn->socket, rb,
connection_state(S_GETH),
ftp_retr_file);
return;
}
if (response == 227) {
if (ftp_data_connect(conn, PF_INET, &sa, sizeof(struct sockaddr_in)))
return;
}
#ifdef CONFIG_IPV6
if (response == 229) {
if (ftp_data_connect(conn, PF_INET6, &sa, sizeof(struct sockaddr_in6)))
return;
}
#endif
ftp->pending_commands--;
/* XXX: The case values are order numbers of commands. */
switch (ftp->opc - ftp->pending_commands) {
case 1: /* TYPE */
break;
case 2: /* PORT */
if (response >= 400) {
abort_connection(conn,
connection_state(S_FTP_PORT));
return;
}
break;
case 3: /* REST / CWD */
case 4: /* SIZE */
if (response == 213 && size != -1 && conn->est_length == -1) {
conn->est_length = size;
} else if (response >= 400) {
if (ftp->dir) {
abort_connection(conn,
connection_state(S_FTP_NO_FILE));
return;
}
conn->from = 0;
} else if (ftp->rest_sent) {
/* Following code is related to resume
* feature. */
if (response == 350)
conn->from = conn->progress->start;
/* Come on, don't be nervous ;-). */
if (conn->progress->start >= 0) {
/* Update to the real value
* which we've got from
* Content-Range. */
conn->progress->seek = conn->from;
}
conn->progress->start = conn->from;
}
break;
default:
INTERNAL("WHAT???");
}
ftp_send_retr_req(conn, connection_state(S_GETH));
return;
}
response = get_ftp_response(conn, rb, 2, NULL, NULL);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
read_from_socket(conn->socket, rb, connection_state(S_GETH),
ftp_retr_file);
return;
}
if (response >= 100 && response < 200) {
/* We only need to parse response after RETR to
* get filesize if needed. */
if (!ftp->dir && conn->est_length == -1) {
off_t file_len;
int res;
file_len = get_filesize_from_RETR(rb->data, rb->length, &res);
if (file_len > 0) {
/* FIXME: ..when downloads resuming
* implemented.. */
/* This is right for vsftpd.
* Show me urls where this is wrong. --witekfl */
conn->est_length = res ?
file_len + conn->progress->start : file_len;
}
}
}
if (conn->data_socket->fd == -1) {
/* The passive FTP server did not send a 227 or 229
* response. We check this down here, rather than
* immediately after getting the response to the PASV
* or EPSV command, to make sure that nothing can
* close the socket between the check and the
* following set_handlers call.
*
* If we were using active FTP, then
* get_ftp_data_socket would have created the
* data_socket without waiting for anything from the
* server. */
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
set_handlers(conn->data_socket->fd, (select_handler_T) ftp_data_accept,
NULL, NULL, conn);
/* read_from_socket(conn->socket, rb, ftp_got_final_response); */
ftp_got_final_response(socket, rb);
}
static void
ftp_got_final_response(struct socket *socket, struct read_buffer *rb)
{
2022-01-25 18:14:40 +00:00
struct connection *conn = (struct connection *)socket->conn;
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
int response = get_ftp_response(conn, rb, 0, NULL, NULL);
if (response == -1) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
if (!response) {
struct connection_state state = !is_in_state(conn->state, S_TRANS)
? connection_state(S_GETH) : conn->state;
read_from_socket(conn->socket, rb, state, ftp_got_final_response);
return;
}
if (response >= 550 || response == 450) {
/* Requested action not taken.
* File unavailable (e.g., file not found, no access). */
if (!conn->cached)
conn->cached = get_cache_entry(conn->uri);
if (!conn->cached
|| !redirect_cache(conn->cached, "/", 1, 0)) {
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
abort_connection(conn, connection_state(S_OK));
return;
}
if (response >= 400) {
abort_connection(conn, connection_state(S_FTP_FILE_ERROR));
return;
}
if (ftp->conn_state == 2) {
ftp_end_request(conn, connection_state(S_OK));
} else {
ftp->conn_state = 1;
if (!is_in_state(conn->state, S_TRANS))
set_connection_state(conn, connection_state(S_GETH));
}
}
/** How to format an FTP directory listing in HTML. */
struct ftp_dir_html_format {
/** Codepage used by C library functions such as strftime().
* If the FTP server sends non-ASCII bytes in file names or
* such, ELinks normally passes them straight through to the
* generated HTML, which will eventually be parsed using the
* codepage specified in the document.codepage.assume option.
* However, when ELinks itself generates strings with
* strftime(), it turns non-ASCII bytes into entity references
* based on libc_codepage, to make sure they become the right
* characters again. */
int libc_codepage;
/** Nonzero if directories should be displayed in a different
* color. From the document.browse.links.color_dirs option. */
int colorize_dir;
/** The color of directories, in "#rrggbb" format. This is
* initialized and used only if colorize_dir is nonzero. */
char dircolor[8];
};
/* Display directory entry formatted in HTML. */
static int
display_dir_entry(struct cache_entry *cached, off_t *pos, int *tries,
const struct ftp_dir_html_format *format,
struct ftp_file_info *ftp_info)
{
struct string string;
char permissions[10] = "---------";
if (!init_string(&string)) return -1;
add_char_to_string(&string, ftp_info->type);
if (ftp_info->permissions) {
2006-01-10 22:36:06 +00:00
mode_t p = ftp_info->permissions;
#define FTP_PERM(perms, buffer, flag, index, id) \
if ((perms) & (flag)) (buffer)[(index)] = (id);
FTP_PERM(p, permissions, S_IRUSR, 0, 'r');
FTP_PERM(p, permissions, S_IWUSR, 1, 'w');
FTP_PERM(p, permissions, S_IXUSR, 2, 'x');
FTP_PERM(p, permissions, S_ISUID, 2, (p & S_IXUSR ? 's' : 'S'));
FTP_PERM(p, permissions, S_IRGRP, 3, 'r');
FTP_PERM(p, permissions, S_IWGRP, 4, 'w');
FTP_PERM(p, permissions, S_IXGRP, 5, 'x');
FTP_PERM(p, permissions, S_ISGID, 5, (p & S_IXGRP ? 's' : 'S'));
FTP_PERM(p, permissions, S_IROTH, 6, 'r');
FTP_PERM(p, permissions, S_IWOTH, 7, 'w');
FTP_PERM(p, permissions, S_IXOTH, 8, 'x');
FTP_PERM(p, permissions, S_ISVTX, 8, (p & 0001 ? 't' : 'T'));
#undef FTP_PERM
}
add_to_string(&string, permissions);
add_char_to_string(&string, ' ');
add_to_string(&string, " 1 ftp ftp ");
if (ftp_info->size != FTP_SIZE_UNKNOWN) {
add_format_to_string(&string, "%12" OFF_PRINT_FORMAT " ",
(off_print_T) ftp_info->size);
} else {
add_to_string(&string, " - ");
}
#ifdef HAVE_STRFTIME
if (ftp_info->mtime > 0) {
time_t current_time = time(NULL);
time_t when = ftp_info->mtime;
struct tm *when_tm;
char *fmt;
/* LC_TIME=fi_FI.UTF_8 can generate "elo___ 31 23:59"
* where each _ denotes U+00A0 encoded as 0xC2 0xA0,
* thus needing a 19-byte buffer. */
char date[MAX_STR_LEN];
int wr;
if (ftp_info->local_time_zone)
when_tm = localtime(&when);
else
when_tm = gmtime(&when);
if (current_time > when + 6L * 30L * 24L * 60L * 60L
|| current_time < when - 60L * 60L)
fmt = gettext("%b %e %Y");
else
fmt = gettext("%b %e %H:%M");
wr = strftime(date, sizeof(date), fmt, when_tm);
add_cp_html_to_string(&string, format->libc_codepage,
date, wr);
} else
#endif
add_to_string(&string, gettext(" "));
/* TODO: Above, the number of spaces might not match the width
* of the string generated by strftime. It depends on the
* locale. So if ELinks knows the timestamps of some FTP
* files but not others, it may misalign the file names.
* Potential solutions:
* - Pad the strings to a compile-time fixed width.
* Con: If we choose a width that suffices for all known
* locales, then it will be stupidly large for most of them.
* - Generate an HTML table.
* Con: Bloats the HTML source.
* Any solution chosen here should also be applied to the
* file: protocol handler. */
if (ftp_info->type == FTP_FILE_DIRECTORY && format->colorize_dir) {
add_to_string(&string, "<font color=\"");
add_to_string(&string, format->dircolor);
add_to_string(&string, "\"><b>");
}
add_to_string(&string, "<a href=\"");
encode_uri_string(&string, ftp_info->name.source, ftp_info->name.length, 0);
if (ftp_info->type == FTP_FILE_DIRECTORY)
add_char_to_string(&string, '/');
add_to_string(&string, "\">");
add_html_to_string(&string, ftp_info->name.source, ftp_info->name.length);
add_to_string(&string, "</a>");
if (ftp_info->type == FTP_FILE_DIRECTORY && format->colorize_dir) {
add_to_string(&string, "</b></font>");
}
if (ftp_info->symlink.length) {
add_to_string(&string, " -&gt; ");
add_html_to_string(&string, ftp_info->symlink.source,
ftp_info->symlink.length);
}
add_char_to_string(&string, '\n');
if (add_fragment(cached, *pos, string.source, string.length)) *tries = 0;
*pos += string.length;
done_string(&string);
return 0;
}
/* Get the next line of input and set *@len to the length of the line.
* Return the number of newline characters at the end of the line or -1
* if we must wait for more input. */
static int
ftp_get_line(struct cache_entry *cached, char *buf, int bufl,
int last, int *len)
{
char *newline;
if (!bufl) return -1;
2022-01-25 18:14:40 +00:00
newline = (char *)memchr(buf, ASCII_LF, bufl);
if (newline) {
*len = newline - buf;
if (*len && buf[*len - 1] == ASCII_CR) {
--*len;
return 2;
} else {
return 1;
}
2006-06-09 21:43:36 +00:00
}
2006-06-09 21:43:36 +00:00
if (last || bufl >= FTP_BUF_SIZE) {
*len = bufl;
return 0;
}
2006-06-09 21:43:36 +00:00
return -1;
}
/** Generate HTML for a line that was received from the FTP server but
* could not be parsed. The caller is supposed to have added a \<pre>
* start tag. (At the time of writing, init_directory_listing() was
* used for that.)
*
* @return -1 if out of memory, or 0 if successful. */
static int
ftp_add_unparsed_line(struct cache_entry *cached, off_t *pos, int *tries,
const char *line, int line_length)
{
int our_ret;
struct string string;
int frag_ret;
our_ret = -1; /* assume out of memory if returning early */
if (!init_string(&string)) goto out;
if (!add_html_to_string(&string, line, line_length)) goto out;
if (!add_char_to_string(&string, '\n')) goto out;
frag_ret = add_fragment(cached, *pos, string.source, string.length);
if (frag_ret == -1) goto out;
*pos += string.length;
if (frag_ret == 1) *tries = 0;
our_ret = 0; /* success */
out:
done_string(&string); /* safe even if init_string failed */
return our_ret;
}
/** List a directory in html format.
*
* @return the number of bytes used from the beginning of @a buffer,
* or -1 if out of memory. */
static int
ftp_process_dirlist(struct cache_entry *cached, off_t *pos,
char *buffer, int buflen, int last,
int *tries, const struct ftp_dir_html_format *format)
{
int ret = 0;
while (1) {
struct ftp_file_info ftp_info = INIT_FTP_FILE_INFO;
char *buf = buffer + ret;
int bufl = buflen - ret;
int line_length, eol;
eol = ftp_get_line(cached, buf, bufl, last, &line_length);
if (eol == -1)
return ret;
ret += line_length + eol;
/* Process line whose end we've already found. */
if (parse_ftp_file_info(&ftp_info, buf, line_length)) {
int retv;
if (ftp_info.name.source[0] == '.'
&& (ftp_info.name.length == 1
|| (ftp_info.name.length == 2
&& ftp_info.name.source[1] == '.')))
continue;
retv = display_dir_entry(cached, pos, tries,
format, &ftp_info);
if (retv < 0) {
return ret;
}
} else {
int retv = ftp_add_unparsed_line(cached, pos, tries,
buf, line_length);
if (retv == -1) /* out of memory; propagate to caller */
return retv;
}
}
}
/* This is the initial read handler for conn->data_socket->fd,
* which may be either trying to connect to a passive FTP server or
* listening for a connection from an active FTP server. In active
* FTP, this function then accepts the connection and replaces
* conn->data_socket->fd with the resulting socket. In any case,
* this function does not read any data from the FTP server, but
* rather hands the socket over to got_something_from_data_connection,
* which then does the reads. */
static void
ftp_data_accept(struct connection *conn)
{
2022-01-25 18:14:40 +00:00
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
int newsock;
/* Because this function is called only as a read handler of
* conn->data_socket->fd, the socket must be valid if we get
* here. */
assert(conn->data_socket->fd >= 0);
if_assert_failed return;
set_connection_timeout(conn);
clear_handlers(conn->data_socket->fd);
if ((conn->socket->protocol_family != EL_PF_INET6 && ftp->use_pasv)
#ifdef CONFIG_IPV6
|| (conn->socket->protocol_family == EL_PF_INET6 && ftp->use_epsv)
#endif
) {
newsock = conn->data_socket->fd;
} else {
newsock = accept(conn->data_socket->fd, NULL, NULL);
if (newsock < 0) {
retry_connection(conn, connection_state_for_errno(errno));
return;
}
close(conn->data_socket->fd);
}
conn->data_socket->fd = newsock;
set_handlers(newsock,
(select_handler_T) got_something_from_data_connection,
NULL, NULL, conn);
}
/* A read handler for conn->data_socket->fd. This function reads
* data from the FTP server, reformats it to HTML if it's a directory
* listing, and adds the result to the cache entry. */
static void
got_something_from_data_connection(struct connection *conn)
{
2022-01-25 18:14:40 +00:00
struct ftp_connection_info *ftp = (struct ftp_connection_info *)conn->info;
struct ftp_dir_html_format format;
ssize_t len;
/* Because this function is called only as a read handler of
* conn->data_socket->fd, the socket must be valid if we get
* here. */
assert(conn->data_socket->fd >= 0);
if_assert_failed return;
/* XXX: This probably belongs rather to connect.c ? */
set_connection_timeout(conn);
if (!conn->cached) conn->cached = get_cache_entry(conn->uri);
if (!conn->cached) {
out_of_mem:
abort_connection(conn, connection_state(S_OUT_OF_MEM));
return;
}
if (ftp->dir) {
format.libc_codepage = get_cp_index("System");
format.colorize_dir = get_opt_bool("document.browse.links.color_dirs", NULL);
if (format.colorize_dir) {
color_to_string(get_opt_color("document.colors.dirs", NULL),
format.dircolor);
}
}
if (ftp->dir && !conn->from) {
struct string string;
struct connection_state state;
if (!conn->uri->data) {
abort_connection(conn, connection_state(S_FTP_ERROR));
return;
}
state = init_directory_listing(&string, conn->uri);
if (!is_in_state(state, S_OK)) {
abort_connection(conn, state);
return;
}
add_fragment(conn->cached, conn->from, string.source, string.length);
conn->from += string.length;
done_string(&string);
if (conn->uri->datalen) {
struct ftp_file_info ftp_info = INIT_FTP_FILE_INFO_ROOT;
display_dir_entry(conn->cached, &conn->from, &conn->tries,
&format, &ftp_info);
}
mem_free_set(&conn->cached->content_type, stracpy("text/html"));
}
len = safe_read(conn->data_socket->fd, ftp->ftp_buffer + ftp->buf_pos,
FTP_BUF_SIZE - ftp->buf_pos);
if (len < 0) {
retry_connection(conn, connection_state_for_errno(errno));
return;
}
if (len > 0) {
conn->received += len;
if (!ftp->dir) {
if (add_fragment(conn->cached, conn->from,
ftp->ftp_buffer, len) == 1)
conn->tries = 0;
conn->from += len;
} else {
int proceeded;
proceeded = ftp_process_dirlist(conn->cached,
&conn->from,
ftp->ftp_buffer,
len + ftp->buf_pos,
0, &conn->tries,
&format);
if (proceeded == -1) goto out_of_mem;
ftp->buf_pos += len - proceeded;
memmove(ftp->ftp_buffer, ftp->ftp_buffer + proceeded,
ftp->buf_pos);
}
set_connection_state(conn, connection_state(S_TRANS));
return;
}
if (ftp_process_dirlist(conn->cached, &conn->from,
ftp->ftp_buffer, ftp->buf_pos, 1,
&conn->tries, &format) == -1)
goto out_of_mem;
#define ADD_CONST(str) { \
add_fragment(conn->cached, conn->from, str, sizeof(str) - 1); \
conn->from += (sizeof(str) - 1); }
2006-07-31 11:24:39 +00:00
if (ftp->dir) ADD_CONST("</pre>\n<hr/>\n</body>\n</html>");
close_socket(conn->data_socket);
if (ftp->conn_state == 1) {
ftp_end_request(conn, connection_state(S_OK));
} else {
ftp->conn_state = 2;
set_connection_state(conn, connection_state(S_TRANS));
}
}
static void
ftp_end_request(struct connection *conn, struct connection_state state)
{
if (is_in_state(state, S_OK) && conn->cached) {
normalize_cache_entry(conn->cached, conn->from);
}
set_connection_state(conn, state);
add_keepalive_connection(conn, FTP_KEEPALIVE_TIMEOUT, NULL);
}