Files
2026-03-28 00:49:16 +03:00

6050 lines
134 KiB
C

/* Copyright (C) 2023 John Törnblom
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3, or (at your option) any
later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, see
<http://www.gnu.org/licenses/>. */
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <utime.h>
#ifdef __linux__
#include <sys/sysmacros.h>
#endif
#include <time.h>
#include <unistd.h>
#include "cmd.h"
#include "io.h"
#include "log.h"
#include "modez.h"
#include "notify.h"
#include "self.h"
#ifndef FTP_LIST_OUTBUF_SIZE
#define FTP_LIST_OUTBUF_SIZE (256 * 1024)
#endif
#define DISABLE_ASCII_MODE
#ifndef FTP_DATA_CONNECT_TIMEOUT_SEC
#define FTP_DATA_CONNECT_TIMEOUT_SEC 300
#endif
// #define IO_USE_SENDFILE // Disabled. Speed x2 down ?!
static int
ftp_wait_fd_ready(int fd, short events, int timeout_sec) {
struct pollfd pfd;
int timeout_ms;
memset(&pfd, 0, sizeof(pfd));
pfd.fd = fd;
pfd.events = events;
timeout_ms = timeout_sec > 0 ? timeout_sec * 1000 : -1;
for(;;) {
int rc = poll(&pfd, 1, timeout_ms);
if(rc > 0) {
return 0;
}
if(rc == 0) {
#ifdef ETIMEDOUT
errno = ETIMEDOUT;
#else
errno = EAGAIN;
#endif
return -1;
}
if(errno == EINTR) {
continue;
}
return -1;
}
}
static int
ftp_connect_active_data(ftp_env_t *env) {
int flags;
int so_error;
socklen_t so_error_len;
flags = fcntl(env->data_fd, F_GETFL, 0);
if(flags < 0) {
return -1;
}
if(fcntl(env->data_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
return -1;
}
if(connect(env->data_fd, (struct sockaddr*)&env->data_addr,
sizeof(env->data_addr)) != 0) {
if(errno != EINPROGRESS
#ifdef EWOULDBLOCK
&& errno != EWOULDBLOCK
#endif
) {
(void)fcntl(env->data_fd, F_SETFL, flags);
return -1;
}
if(ftp_wait_fd_ready(env->data_fd, POLLOUT, FTP_DATA_CONNECT_TIMEOUT_SEC)) {
(void)fcntl(env->data_fd, F_SETFL, flags);
return -1;
}
so_error = 0;
so_error_len = sizeof(so_error);
if(getsockopt(env->data_fd, SOL_SOCKET, SO_ERROR, &so_error,
&so_error_len) < 0) {
(void)fcntl(env->data_fd, F_SETFL, flags);
return -1;
}
if(so_error) {
(void)fcntl(env->data_fd, F_SETFL, flags);
errno = so_error;
return -1;
}
}
if(fcntl(env->data_fd, F_SETFL, flags) < 0) {
return -1;
}
return 0;
}
/**
* Create a string representation of a file mode.
**/
static void
ftp_mode_string(mode_t mode, char *buf) {
char c, d;
int i, bit;
buf[10] = 0;
for(i=0; i<9; i++) {
bit = mode & (1<<i);
c = i%3;
if(!c && (mode & (1<<((d=i/3)+9)))) {
c = "tss"[(int)d];
if (!bit) c &= ~0x20;
} else c = bit ? "xwr"[(int)c] : '-';
buf[9-i] = c;
}
if (S_ISDIR(mode)) c = 'd';
else if (S_ISBLK(mode)) c = 'b';
else if (S_ISCHR(mode)) c = 'c';
else if (S_ISLNK(mode)) c = 'l';
else if (S_ISFIFO(mode)) c = 'p';
else if (S_ISSOCK(mode)) c = 's';
else c = '-';
*buf = c;
}
/**
* Normalize a path by resolving '.', '..', and redundant slashes.
**/
static int
ftp_normpath(const char *path, char *out, size_t out_size) {
size_t stack[PATH_MAX / 2 + 2];
size_t sp = 0;
size_t len = 1;
const char *p = path;
if(!path || !out || out_size < 2) {
errno = EINVAL;
return -1;
}
out[0] = '/';
out[1] = '\0';
p += strspn(p, "/");
while(*p) {
const char *start = p;
size_t comp_len = strcspn(p, "/");
p += comp_len;
p += strspn(p, "/");
if(!comp_len || (comp_len == 1 && start[0] == '.')) {
continue;
}
if(comp_len == 2 && start[0] == '.' && start[1] == '.') {
if(sp > 0) {
len = stack[--sp];
out[len] = '\0';
} else {
len = 1;
out[1] = '\0';
}
continue;
}
size_t prelen = len;
if(len > 1) {
if(len + 1 >= out_size) {
errno = ENAMETOOLONG;
return -1;
}
out[len++] = '/';
}
if(len + comp_len >= out_size) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(out + len, start, comp_len);
len += comp_len;
out[len] = '\0';
if(sp < (sizeof(stack) / sizeof(stack[0]))) {
stack[sp++] = prelen;
}
}
return 0;
}
/**
* Open the data connection.
*/
int
ftp_data_open(ftp_env_t *env) {
struct sockaddr_in data_addr;
struct sockaddr_in ctrl_addr;
socklen_t addr_len;
socklen_t ctrl_len;
if(env->data_addr.sin_port) {
if(env->data_fd < 0) {
env->data_fd = socket(AF_INET, SOCK_STREAM, 0);
if(env->data_fd < 0) {
return -1;
}
}
if(ftp_connect_active_data(env)) {
ftp_data_close(env);
return -1;
}
} else {
if(env->passive_fd < 0) {
errno = ENOTCONN;
return -1;
}
if(ftp_wait_fd_ready(env->passive_fd, POLLIN, FTP_DATA_CONNECT_TIMEOUT_SEC)) {
return -1;
}
addr_len = sizeof(data_addr);
for(;;) {
env->data_fd = accept(env->passive_fd, (struct sockaddr*)&data_addr,
&addr_len);
if(env->data_fd >= 0) {
break;
}
if(errno == EINTR) {
continue;
}
return -1;
}
close(env->passive_fd);
env->passive_fd = -1;
memset(&ctrl_addr, 0, sizeof(ctrl_addr));
ctrl_len = sizeof(ctrl_addr);
if(getpeername(env->active_fd, (struct sockaddr *)&ctrl_addr, &ctrl_len) !=
0) {
ftp_data_close(env);
errno = EACCES;
return -1;
}
if(ctrl_addr.sin_family != AF_INET ||
ctrl_addr.sin_addr.s_addr != data_addr.sin_addr.s_addr) {
ftp_data_close(env);
errno = EACCES;
return -1;
}
}
io_set_socket_opts(env->data_fd, 1);
return 0;
}
/**
* Read data from an existing data connection.
**/
static ssize_t
ftp_data_read(ftp_env_t *env, void *buf, size_t count) {
if(env->mode_z) {
return ftp_mode_z_read(env, buf, count);
}
for(;;) {
ssize_t r = recv(env->data_fd, buf, count, 0);
if(r < 0 && errno == EINTR) {
continue;
}
return r;
}
}
/**
* Write data to an existing data connection.
**/
static int
ftp_data_write(ftp_env_t *env, const void *buf, size_t count) {
if(env->mode_z) {
return ftp_mode_z_write(env, buf, count);
}
return io_nwrite(env->data_fd, buf, count);
}
/**
* Copy file data to the data socket, converting to CRLF (ASCII mode).
**/
static int
ftp_copy_ascii_out(ftp_env_t *env, int fd_in) {
char *inbuf = env->xfer_buf;
size_t bufsize = env->xfer_buf_size;
char *outbuf = NULL;
size_t outcap = 0;
int free_in = 0;
int prev_cr = 0;
if(!inbuf || !bufsize) {
inbuf = malloc(IO_COPY_BUFSIZE);
bufsize = IO_COPY_BUFSIZE;
free_in = 1;
if(!inbuf) {
return -1;
}
}
outcap = bufsize * 2 + 2;
outbuf = malloc(outcap);
if(!outbuf) {
if(free_in) {
free(inbuf);
}
return -1;
}
for(;;) {
ssize_t r = read(fd_in, inbuf, bufsize);
size_t out_len = 0;
if(r < 0) {
if(errno == EINTR) {
continue;
}
goto error;
}
if(r == 0) {
break;
}
for(ssize_t i = 0; i < r; i++) {
unsigned char c = (unsigned char)inbuf[i];
if(prev_cr) {
if(c == '\n') {
outbuf[out_len++] = '\r';
outbuf[out_len++] = '\n';
prev_cr = 0;
continue;
}
outbuf[out_len++] = '\r';
prev_cr = 0;
}
if(c == '\r') {
prev_cr = 1;
continue;
}
if(c == '\n') {
outbuf[out_len++] = '\r';
outbuf[out_len++] = '\n';
continue;
}
outbuf[out_len++] = (char)c;
}
if(out_len && ftp_data_write(env, outbuf, out_len)) {
goto error;
}
}
if(prev_cr) {
outbuf[0] = '\r';
if(ftp_data_write(env, outbuf, 1)) {
goto error;
}
}
free(outbuf);
if(free_in) {
free(inbuf);
}
return 0;
error:
free(outbuf);
if(free_in) {
free(inbuf);
}
return -1;
}
/**
* Copy file data to the data socket.
**/
static int
ftp_copy_binary_out(ftp_env_t *env, int fd_in, size_t remaining) {
char *buf = env->xfer_buf;
size_t bufsize = env->xfer_buf_size;
int free_buf = 0;
if(!buf || !bufsize) {
buf = malloc(IO_COPY_BUFSIZE);
bufsize = IO_COPY_BUFSIZE;
free_buf = 1;
if(!buf) {
return -1;
}
}
while(remaining) {
size_t chunk = remaining;
ssize_t r;
if(chunk > bufsize) {
chunk = bufsize;
}
for(;;) {
r = read(fd_in, buf, chunk);
if(r < 0 && errno == EINTR) {
continue;
}
break;
}
if(r < 0) {
goto error;
}
if(r == 0) {
errno = EIO;
goto error;
}
if(ftp_data_write(env, buf, (size_t)r)) {
goto error;
}
remaining -= (size_t)r;
}
if(free_buf) {
free(buf);
}
return 0;
error:
if(free_buf) {
free(buf);
}
return -1;
}
/**
* Copy data from the data socket to a file, converting CRLF to LF.
**/
static int
ftp_copy_ascii_in(ftp_env_t *env, int fd_out, off_t *out_off) {
char *inbuf = env->xfer_buf;
size_t bufsize = env->xfer_buf_size;
char *outbuf = NULL;
size_t outcap = 0;
int free_in = 0;
int prev_cr = 0;
if(!out_off) {
errno = EINVAL;
return -1;
}
if(!inbuf || !bufsize) {
inbuf = malloc(IO_COPY_BUFSIZE);
bufsize = IO_COPY_BUFSIZE;
free_in = 1;
if(!inbuf) {
return -1;
}
}
outcap = bufsize + 1;
outbuf = malloc(outcap);
if(!outbuf) {
if(free_in) {
free(inbuf);
}
return -1;
}
for(;;) {
ssize_t r = ftp_data_read(env, inbuf, bufsize);
size_t out_len = 0;
if(r < 0) {
if(errno == EINTR) {
continue;
}
goto error;
}
if(r == 0) {
break;
}
for(ssize_t i = 0; i < r; i++) {
unsigned char c = (unsigned char)inbuf[i];
if(prev_cr) {
if(c == '\n') {
outbuf[out_len++] = '\n'; // \r\n -> \n
prev_cr = 0;
continue;
}
// Emit the swallowed \r
if(out_len < outcap) {
outbuf[out_len++] = '\r';
}
prev_cr = 0;
}
if(c == '\r') {
prev_cr = 1;
continue; // Swallow \r
}
if(out_len < outcap) outbuf[out_len++] = (char)c;
}
if(out_len && io_nwrite(fd_out, outbuf, out_len)) {
goto error;
}
*out_off += (off_t)out_len;
}
if(prev_cr) {
outbuf[0] = '\r';
if(io_nwrite(fd_out, outbuf, 1)) {
goto error;
}
*out_off += 1;
}
free(outbuf);
if(free_in) {
free(inbuf);
}
return 0;
error:
free(outbuf);
if(free_in) {
free(inbuf);
}
return -1;
}
/**
* Close the data connection.
**/
int
ftp_data_close(ftp_env_t *env) {
int rc = 0;
(void)ftp_mode_z_xfer_finish(env, 0);
if(env->data_fd >= 0) {
if(close(env->data_fd)) {
rc = -1;
}
env->data_fd = -1;
}
return rc;
}
/**
* Finalize a successful data transfer before closing the socket.
**/
static int
ftp_data_xfer_complete(ftp_env_t *env) {
int rc = 0;
if(ftp_mode_z_xfer_finish(env, 1)) {
rc = -1;
}
if(env->data_fd >= 0 && close(env->data_fd)) {
rc = -1;
}
env->data_fd = -1;
return rc;
}
/**
* Close data and passive sockets and reset state.
**/
static void
ftp_close_data_fds(ftp_env_t *env) {
ftp_data_close(env);
if(env->passive_fd >= 0) {
close(env->passive_fd);
env->passive_fd = -1;
}
}
/**
* Send a 550 reply and close passive socket.
**/
static int
ftp_perror_close_passive(ftp_env_t *env) {
int ret = ftp_perror(env);
if(env->passive_fd >= 0) {
close(env->passive_fd);
env->passive_fd = -1;
}
return ret;
}
/**
* Validate and configure active data connection parameters.
**/
static int
ftp_setup_active_data(ftp_env_t *env, struct in_addr in_addr,
uint16_t port, const char *illegal_msg) {
struct sockaddr_in ctrl_addr;
socklen_t ctrl_len;
memset(&ctrl_addr, 0, sizeof(ctrl_addr));
ctrl_len = sizeof(ctrl_addr);
if(getpeername(env->active_fd, (struct sockaddr *)&ctrl_addr, &ctrl_len) !=
0 || ctrl_addr.sin_family != AF_INET ||
ctrl_addr.sin_addr.s_addr != in_addr.s_addr) {
return ftp_active_printf(env, "%s\r\n", illegal_msg);
}
ftp_close_data_fds(env);
if((env->data_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return ftp_perror(env);
}
env->data_addr.sin_family = AF_INET;
env->data_addr.sin_addr = in_addr;
env->data_addr.sin_port = htons(port);
return 0;
}
/**
* Create a passive listener and return the bound port.
**/
static int
ftp_listen_passive(ftp_env_t *env, uint16_t *port_out) {
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
struct sockaddr_in sockaddr;
*port_out = 0;
if((env->passive_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return ftp_perror(env);
}
if(setsockopt(env->passive_fd, SOL_SOCKET, SO_REUSEADDR, &(int){1},
sizeof(int)) < 0) {
return ftp_perror_close_passive(env);
}
memset(&sockaddr, 0, sockaddr_len);
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
sockaddr.sin_port = htons(0);
if(bind(env->passive_fd, (struct sockaddr *)&sockaddr, sockaddr_len) != 0) {
return ftp_perror_close_passive(env);
}
if(listen(env->passive_fd, 1) != 0) {
return ftp_perror_close_passive(env);
}
if(getsockname(env->passive_fd, (struct sockaddr *)&sockaddr,
&sockaddr_len)) {
return ftp_perror_close_passive(env);
}
*port_out = ntohs(sockaddr.sin_port);
return 0;
}
/**
* Write a string to the active connection with printf semantics.
**/
int
ftp_active_printf(ftp_env_t *env, const char *fmt, ...) {
char buf[0x1000];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
if(n < 0) {
return -1;
}
size_t len = (size_t)n;
if(len >= sizeof(buf)) {
len = sizeof(buf) - 1;
}
pthread_mutex_lock(&env->ctrl_mutex);
int rc = io_nwrite(env->active_fd, buf, len);
pthread_mutex_unlock(&env->ctrl_mutex);
if(rc) {
return -1;
}
return 0;
}
/**
* Write a string to the active connection with perror semantics.
**/
int
ftp_perror(ftp_env_t *env) {
char buf[255];
if(strerror_r(errno, buf, sizeof(buf))) {
strncpy(buf, "Unknown error", sizeof(buf));
}
return ftp_active_printf(env, "550 %s\r\n", buf);
}
/**
* Check whether an errno value indicates a timeout.
**/
static int
ftp_errno_is_timeout(int e) {
if(e == EAGAIN
#ifdef EWOULDBLOCK
|| e == EWOULDBLOCK
#endif
#ifdef ETIMEDOUT
|| e == ETIMEDOUT
#endif
) {
return 1;
}
return 0;
}
/**
* Reply with a data transfer error message for common errno values.
**/
static int
ftp_data_xfer_error_reply(ftp_env_t *env) {
int e = errno;
#ifdef EPROTO
if(e == EPROTO && (env->mode == 'Z' || env->mode == 'z')) {
return ftp_active_printf(env, "426 Invalid MODE Z data stream\r\n");
}
#endif
if(e == EPIPE
#ifdef ECONNRESET
|| e == ECONNRESET
#endif
) {
return ftp_active_printf(env, "426 Data connection closed\r\n");
}
if(ftp_errno_is_timeout(e)) {
return ftp_active_printf(env, "426 Data connection timed out\r\n");
}
errno = e;
return ftp_perror(env);
}
/**
* Reply with a data open error message for common errno values.
**/
static int
ftp_data_open_error_reply(ftp_env_t *env) {
int e = errno;
if(e == ENOTCONN) {
return ftp_active_printf(env, "425 Use PORT or PASV first\r\n");
}
if(e == EACCES) {
return ftp_active_printf(env, "425 Can't open data connection\r\n");
}
if(ftp_errno_is_timeout(e)) {
return ftp_active_printf(env, "425 Data connection timed out\r\n");
}
errno = e;
return ftp_perror(env);
}
/**
* Verify that a data connection is configured before opening it.
**/
static int
ftp_data_precheck(ftp_env_t *env) {
if(!env->data_addr.sin_port && env->passive_fd < 0) {
errno = ENOTCONN;
int err = ftp_data_open_error_reply(env);
if(err < 0) {
return -1;
}
return 1;
}
return 0;
}
/**
* Send 150 and open the data connection, with optional precheck.
**/
static int
ftp_data_xfer_start(ftp_env_t *env, int prechecked, int is_send) {
if(!prechecked) {
int precheck = ftp_data_precheck(env);
if(precheck) {
return precheck;
}
}
if(ftp_mode_z_xfer_start(env, is_send)) {
int saved_errno = errno;
ftp_data_close(env);
errno = saved_errno;
if(env->mode == 'Z' || env->mode == 'z') {
int err = ftp_active_printf(env, "451 MODE Z is temporarily unavailable\r\n");
if(err < 0) {
return -1;
}
return 1;
}
return -1;
}
if(ftp_active_printf(env, "150 Opening data transfer\r\n")) {
ftp_data_close(env);
return -1;
}
if(ftp_data_open(env)) {
int saved_errno = errno;
ftp_data_close(env);
errno = saved_errno;
int err = ftp_data_open_error_reply(env);
if(err < 0) {
return -1;
}
return 1;
}
return 0;
}
/**
* Create an absolute path from the current working directory.
* Returns 0 on success, -1 on error (errno set).
**/
int
ftp_abspath(ftp_env_t *env, char *abspath, size_t abspath_size,
const char *path) {
char buf[PATH_MAX + 1];
int n;
if(!env || !abspath || !path || abspath_size < 2) {
errno = EINVAL;
return -1;
}
if(path[0] != '/') {
n = snprintf(buf, sizeof(buf), "%s/%s", env->cwd, path);
} else {
n = snprintf(buf, sizeof(buf), "%s", path);
}
if(n < 0 || (size_t)n >= sizeof(buf)) {
errno = ENAMETOOLONG;
return -1;
}
if(ftp_normpath(buf, abspath, abspath_size)) {
return -1;
}
return 0;
}
/**
* Trim leading/trailing spaces and copy a path argument.
**/
static const char *
ftp_copy_path_arg(const char *arg, char *buf, size_t bufsize) {
const char *p = arg;
if(!buf || bufsize < 2) {
return NULL;
}
if(!p) {
return NULL;
}
p += strspn(p, " ");
if(!*p) {
return NULL;
}
size_t len = strlen(p);
while(len && p[len - 1] == ' ') {
len--;
}
if(len == 0) {
return NULL;
}
if(len >= bufsize) {
len = bufsize - 1;
}
memcpy(buf, p, len);
buf[len] = '\0';
return buf;
}
/**
* Parse a LIST path argument, skipping option tokens.
**/
static const char *
ftp_list_path_arg(const char *arg, char *buf, size_t bufsize) {
const char *p = arg;
if(!buf || bufsize < 2) {
return NULL;
}
p += strspn(p, " ");
if(!*p) {
return NULL;
}
// If it doesn't start with options, treat the whole remainder as the path.
if(*p != '-') {
return ftp_copy_path_arg(p, buf, bufsize);
}
// Skip leading options (e.g., "-al") and honor "--" to end options.
while(*p == '-') {
const char *tok = p;
size_t len = strcspn(p, " ");
p += len;
p += strspn(p, " ");
if(len == 2 && tok[0] == '-' && tok[1] == '-') {
break;
}
if(len <= 1) {
p = tok;
break;
}
if(!*p) {
return NULL;
}
if(*p != '-') {
break;
}
}
p += strspn(p, " ");
if(!*p) {
return NULL;
}
return ftp_copy_path_arg(p, buf, bufsize);
}
/**
* Format an MDTM/MLSD UTC timestamp.
**/
static int
ftp_format_mdtm(time_t t, char *buf, size_t bufsize) {
struct tm tm;
if(!buf || bufsize < 15) {
return -1;
}
if(!gmtime_r(&t, &tm)) {
return -1;
}
if(snprintf(buf, bufsize, "%04d%02d%02d%02d%02d%02d", tm.tm_year + 1900,
tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec) >= (int)bufsize) {
return -1;
}
return 0;
}
/**
* Format a LIST timestamp in an ls -l compatible form.
**/
static int
ftp_format_list_time(time_t t, char *buf, size_t bufsize) {
struct tm tm;
time_t now;
// LIST output is typically server-local time (unlike MLSD's UTC "modify").
static const char *mon[] = {"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"};
if(!buf || bufsize < 14) {
return -1;
}
if(!localtime_r(&t, &tm)) {
// Fallback to a fixed epoch-ish timestamp rather than garbage.
(void)snprintf(buf, bufsize, "Jan 1 1970");
return 0;
}
now = time(NULL);
long long diff = (long long)now - (long long)t;
const long long six_months = 180LL * 24LL * 60LL * 60LL;
const char *mname = mon[(tm.tm_mon >= 0 && tm.tm_mon < 12) ? tm.tm_mon : 0];
if(diff < 0 || diff > six_months) {
// Older timestamps: show year like "ls -l".
(void)snprintf(buf, bufsize, "%s %2d %4d", mname, tm.tm_mday, tm.tm_year + 1900);
} else {
// Recent timestamps: show time.
(void)snprintf(buf, bufsize, "%s %2d %02d:%02d", mname, tm.tm_mday, tm.tm_hour, tm.tm_min);
}
return 0;
}
/**
* Enter passive mode.
**/
int
ftp_cmd_PASV(ftp_env_t *env, const char* arg) {
socklen_t sockaddr_len = sizeof(struct sockaddr_in);
struct sockaddr_in sockaddr;
uint32_t addr = 0;
uint16_t port = 0;
int ret = 0;
if(arg[0]) {
return ftp_active_printf(env, "501 Usage: PASV\r\n");
}
env->data_addr.sin_port = 0;
env->data_addr.sin_addr.s_addr = 0;
ftp_close_data_fds(env);
if(getsockname(env->active_fd, (struct sockaddr*)&sockaddr, &sockaddr_len)) {
return ftp_perror(env);
}
addr = sockaddr.sin_addr.s_addr;
ret = ftp_listen_passive(env, &port);
if(ret) {
return ret;
}
uint32_t ip = ntohl(addr);
uint16_t p = port;
return ftp_active_printf(env, "227 Entering Passive Mode (%hhu,%hhu,%hhu,%hhu,%hhu,%hhu).\r\n",
(ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, (ip >> 0) & 0xFF,
(p >> 8) & 0xFF, (p >> 0) & 0xFF);
}
/**
* Enter extended passive mode.
**/
int
ftp_cmd_EPSV(ftp_env_t *env, const char *arg) {
uint16_t port = 0;
int ret = 0;
if(arg[0]) {
char *end = NULL;
long proto = strtol(arg, &end, 10);
if(end == arg || *end) {
return ftp_active_printf(env, "501 Usage: EPSV [<NET-PRT>]\r\n");
}
if(proto != 1) {
return ftp_active_printf(env, "522 Network protocol not supported\r\n");
}
}
env->data_addr.sin_port = 0;
env->data_addr.sin_addr.s_addr = 0;
ftp_close_data_fds(env);
ret = ftp_listen_passive(env, &port);
if(ret) {
return ret;
}
return ftp_active_printf(env,
"229 Entering Extended Passive Mode (|||%hu|)\r\n",
port);
}
/**
* Change the working directory to its parent.
**/
int
ftp_cmd_CDUP(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), "..")) {
return ftp_perror(env);
}
if(stat(pathbuf, &st)) {
return ftp_perror(env);
}
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 No such directory\r\n");
}
snprintf(env->cwd, sizeof(env->cwd), "%s", pathbuf);
return ftp_active_printf(env, "250 OK\r\n");
}
/**
* Change the permission mode bits of a path.
**/
int
ftp_cmd_CHMOD(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
mode_t mode = 0;
char* ptr;
struct stat lst;
if(!arg[0] || !(ptr=strstr(arg, " "))) {
return ftp_active_printf(env, "501 Usage: CHMOD <MODE> <PATH>\r\n");
}
mode = strtol(arg, 0, 8);
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), ptr+1)) {
return ftp_perror(env);
}
if(lstat(pathbuf, &lst)) {
return ftp_perror(env);
}
if(S_ISLNK(lst.st_mode)) {
return ftp_active_printf(env, "550 Symlinks are not allowed\r\n");
}
if(chmod(pathbuf, mode)) {
return ftp_perror(env);
}
return ftp_active_printf(env, "200 OK\r\n");
}
/**
* Get or set the process umask (UMASK).
**/
int
ftp_cmd_UMASK(ftp_env_t *env, const char* arg) {
(void)env;
arg += strspn(arg, " ");
if(!*arg) {
mode_t old = umask(0);
umask(old);
return ftp_active_printf(env, "200 UMASK %03o\r\n", old & 0777);
}
char *end = NULL;
long mode = strtol(arg, &end, 8);
if(end) {
end += strspn(end, " ");
}
if(end == arg || (end && *end) || mode < 0 || mode > 0777) {
return ftp_active_printf(env, "501 Usage: UMASK <MODE>\r\n");
}
umask((mode_t)mode);
return ftp_active_printf(env, "200 UMASK set to %03o\r\n", (int)mode);
}
/**
* Create a symlink (SYMLINK <TARGET> <LINK>).
**/
int
ftp_cmd_SYMLINK(ftp_env_t *env, const char* arg) {
char target_arg[PATH_MAX + 1];
char link_arg[PATH_MAX + 1];
char target_path[PATH_MAX];
char link_path[PATH_MAX];
const char *p = arg;
p += strspn(p, " ");
if(!*p) {
return ftp_active_printf(env, "501 Usage: SYMLINK <TARGET> <LINK>\r\n");
}
const char *sep = strchr(p, ' ');
if(!sep) {
return ftp_active_printf(env, "501 Usage: SYMLINK <TARGET> <LINK>\r\n");
}
size_t target_len = (size_t)(sep - p);
if(target_len == 0 || target_len >= sizeof(target_arg)) {
errno = ENAMETOOLONG;
return ftp_perror(env);
}
memcpy(target_arg, p, target_len);
target_arg[target_len] = '\0';
p = sep;
p += strspn(p, " ");
if(!*p) {
return ftp_active_printf(env, "501 Usage: SYMLINK <TARGET> <LINK>\r\n");
}
const char *end = p + strlen(p);
while(end > p && end[-1] == ' ') {
end--;
}
size_t link_len = (size_t)(end - p);
if(link_len == 0 || link_len >= sizeof(link_arg)) {
errno = ENAMETOOLONG;
return ftp_perror(env);
}
memcpy(link_arg, p, link_len);
link_arg[link_len] = '\0';
if(ftp_abspath(env, target_path, sizeof(target_path), target_arg)) {
return ftp_perror(env);
}
if(ftp_abspath(env, link_path, sizeof(link_path), link_arg)) {
return ftp_perror(env);
}
if(symlink(target_path, link_path)) {
if(errno == 0) {
#ifdef EOPNOTSUPP
errno = EOPNOTSUPP;
#else
errno = EIO;
#endif
}
return ftp_perror(env);
}
return ftp_active_printf(env, "200 SYMLINK created\r\n");
}
/**
* Resolve an optional path and fetch filesystem stats.
**/
static int
ftp_get_vfs(ftp_env_t *env, const char *arg, struct statvfs *vfs_out) {
char pathbuf[PATH_MAX];
const char *path_arg = ftp_copy_path_arg(arg, pathbuf, sizeof(pathbuf));
const char *path = NULL;
if(!vfs_out) {
errno = EINVAL;
return -1;
}
if(path_arg) {
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), path_arg)) {
return -1;
}
path = pathbuf;
} else {
if(ftp_normpath(env->cwd, pathbuf, sizeof(pathbuf))) {
return -1;
}
path = pathbuf;
}
if(statvfs(path, vfs_out)) {
return -1;
}
return 0;
}
/**
* Return available space for a path (AVBL).
**/
int
ftp_cmd_AVBL(ftp_env_t *env, const char* arg) {
struct statvfs vfs;
if(ftp_get_vfs(env, arg, &vfs)) {
return ftp_perror(env);
}
uintmax_t unit = vfs.f_frsize ? (uintmax_t)vfs.f_frsize
: (uintmax_t)vfs.f_bsize;
uintmax_t avail = (uintmax_t)vfs.f_bavail * unit;
return ftp_active_printf(env, "213 %" PRIuMAX "\r\n", avail);
}
/**
* Return available space for a path (XQUOTA).
**/
int
ftp_cmd_XQUOTA(ftp_env_t *env, const char* arg) {
struct statvfs vfs;
if(ftp_get_vfs(env, arg, &vfs)) {
return ftp_perror(env);
}
uintmax_t block = vfs.f_frsize ? (uintmax_t)vfs.f_frsize
: (uintmax_t)vfs.f_bsize;
uintmax_t file_limit = (uintmax_t)vfs.f_files;
uintmax_t file_count = 0;
if(vfs.f_files >= vfs.f_ffree) {
file_count = (uintmax_t)(vfs.f_files - vfs.f_ffree);
}
uintmax_t disk_limit = (uintmax_t)vfs.f_blocks * block;
uintmax_t disk_usage = 0;
if(vfs.f_blocks >= vfs.f_bavail) {
disk_usage = (uintmax_t)(vfs.f_blocks - vfs.f_bavail) * block;
}
if(ftp_active_printf(env, "213-File and disk usage\r\n")) {
return -1;
}
if(ftp_active_printf(env, " File count: %" PRIuMAX "\r\n", file_count)) {
return -1;
}
if(ftp_active_printf(env, " File limit: %" PRIuMAX "\r\n", file_limit)) {
return -1;
}
if(ftp_active_printf(env, " Disk usage: %" PRIuMAX "\r\n", disk_usage)) {
return -1;
}
if(ftp_active_printf(env, " Disk limit: %" PRIuMAX "\r\n", disk_limit)) {
return -1;
}
return ftp_active_printf(env, "213 File and disk usage end\r\n");
}
/**
* Change the working directory.
**/
int
ftp_cmd_CWD(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: CWD <PATH>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(stat(pathbuf, &st)) {
return ftp_perror(env);
}
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 No such directory\r\n");
}
snprintf(env->cwd, sizeof(env->cwd), "%s", pathbuf);
return ftp_active_printf(env, "250 OK\r\n");
}
/**
* Delete a given file.
**/
int
ftp_cmd_DELE(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: DELE <FILENAME>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(lstat(pathbuf, &st)) {
return ftp_perror(env);
}
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Not a regular file\r\n");
}
if(remove(pathbuf)) {
return ftp_perror(env);
}
return ftp_active_printf(env, "250 File deleted\r\n");
}
/**
* Buffered data-transfer helpers for directory listings.
*
**/
typedef struct ftp_xfer_buf {
ftp_env_t *env;
char *buf;
size_t cap;
size_t len;
int free_buf;
int failed;
} ftp_xfer_buf_t;
/**
* Release any heap buffer used by the listing transfer buffer.
**/
static void
ftp_xfer_buf_release(ftp_xfer_buf_t *x) {
if(x->free_buf && x->buf) {
free(x->buf);
}
x->buf = NULL;
x->cap = 0;
x->len = 0;
x->free_buf = 0;
}
/**
* Write raw bytes to the data socket and mark failure on error.
**/
static int
ftp_xfer_write_raw(ftp_xfer_buf_t *x, const void *data, size_t len) {
if(x->failed) {
return -1;
}
if(ftp_data_write(x->env, data, len)) {
(void)ftp_data_xfer_error_reply(x->env);
x->failed = 1;
return -1;
}
return 0;
}
/**
* Flush buffered listing output to the data socket.
**/
static int
ftp_xfer_flush(ftp_xfer_buf_t *x) {
if(x->failed) {
return -1;
}
if(x->len) {
if(ftp_xfer_write_raw(x, x->buf, x->len)) {
return -1;
}
x->len = 0;
}
return 0;
}
/**
* Format a line into the buffer, flushing as needed.
**/
static int
ftp_xfer_vprintf(ftp_xfer_buf_t *x, const char *fmt, va_list ap) {
for(;;) {
size_t rem;
if(x->failed) {
return -1;
}
rem = x->cap - x->len;
va_list aq;
va_copy(aq, ap);
int n = vsnprintf(x->buf + x->len, rem, fmt, aq);
va_end(aq);
if(n < 0) {
// Formatting error; let caller decide whether to skip entry.
return -1;
}
if((size_t)n < rem) {
x->len += (size_t)n;
return 0;
}
// Not enough space -> flush and try again.
if(ftp_xfer_flush(x)) {
return -1;
}
// If a single line is larger than the buffer, format into a temp string and write directly.
if((size_t)n >= x->cap) {
size_t need = (size_t)n + 1;
char *tmp = malloc(need);
if(!tmp) {
x->failed = 1;
return -1;
}
va_list ar;
va_copy(ar, ap);
int m = vsnprintf(tmp, need, fmt, ar);
va_end(ar);
if(m < 0) {
free(tmp);
return -1;
}
int wr = ftp_xfer_write_raw(x, tmp, (size_t)m);
free(tmp);
return wr;
}
// else: retry with empty buffer
}
}
/**
* Convenience wrapper around ftp_xfer_vprintf.
**/
static int
ftp_xfer_printf(ftp_xfer_buf_t *x, const char *fmt, ...) {
int rc;
va_list ap;
va_start(ap, fmt);
rc = ftp_xfer_vprintf(x, fmt, ap);
va_end(ap);
return rc;
}
/**
* Shared prologue/epilogue for LIST/NLST/MLSD
**/
static int
ftp_list_xfer_start(ftp_env_t *env, DIR *dir, ftp_xfer_buf_t *x) {
memset(x, 0, sizeof(*x));
x->env = env;
x->buf = env->xfer_buf;
x->cap = env->xfer_buf_size;
if(!x->buf || !x->cap) {
x->cap = FTP_LIST_OUTBUF_SIZE;
x->buf = malloc(x->cap);
x->free_buf = 1;
if(!x->buf) {
int err = ftp_perror(env);
if(dir) {
closedir(dir);
}
if(err < 0) {
return -1;
}
return 1;
}
}
int open_err = ftp_data_xfer_start(env, 0, 1);
if(open_err) {
ftp_xfer_buf_release(x);
if(dir) {
closedir(dir);
}
return open_err;
}
return 0;
}
/**
* Flush and finalize a directory listing transfer.
**/
static int
ftp_list_xfer_finish(ftp_env_t *env, DIR *dir, ftp_xfer_buf_t *x) {
if(!x->failed) {
(void)ftp_xfer_flush(x);
}
if(ftp_data_xfer_complete(env)) {
(void)ftp_data_xfer_error_reply(env);
x->failed = 1;
}
if(dir) {
if(closedir(dir)) {
(void)ftp_perror(env);
x->failed = 1;
}
}
ftp_xfer_buf_release(x);
if(x->failed) {
return 0;
}
return ftp_active_printf(env, "226 Transfer complete\r\n");
}
/**
* Join a directory path and entry name into a single path.
**/
static int
ftp_join_path(char *dst, size_t dst_sz, const char *dir_path, const char *name) {
int n;
if(!dst || dst_sz < 2) {
errno = ENAMETOOLONG;
return -1;
}
if(dir_path[1] == '\0') {
n = snprintf(dst, dst_sz, "/%s", name);
} else {
n = snprintf(dst, dst_sz, "%s/%s", dir_path, name);
}
if(n < 0 || (size_t)n >= dst_sz) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
/**
* Recursively compute the size of a directory.
**/
static int ftp_dir_next_entry(DIR *dir, struct dirent **ent_out);
static int ftp_dir_open_child(DIR *dir, const char *child_path,
const char *name, DIR **child_out);
static int ftp_dir_size_walk(DIR *dir, const char *path, uintmax_t *size_out);
static int ftp_dir_child_lstat(DIR *dir, const char *dir_path,
const char *name, struct stat *st);
static int
ftp_dir_size(const char *path, uintmax_t *size_out) {
DIR *dir = opendir(path);
if(!dir) {
return -1;
}
if(ftp_dir_size_walk(dir, path, size_out) != 0) {
int saved_errno = errno;
closedir(dir);
errno = saved_errno;
return -1;
}
if(closedir(dir) != 0) {
return -1;
}
return 0;
}
/**
* Recursively delete a directory and its contents.
**/
#define FTP_NOTIFY_PATH_SIZE 224
#define FTP_DELETE_NOTIFY_CHECK_ITEMS 64
typedef enum {
FTP_BG_COPY,
FTP_BG_MOVE,
} ftp_bg_op_t;
static void
ftp_compact_path(const char *path, char *out, size_t out_size) {
size_t len;
size_t head;
size_t tail;
if(!out || out_size == 0) {
return;
}
out[0] = '\0';
if(!path) {
return;
}
len = strlen(path);
if(len + 1 <= out_size) {
memcpy(out, path, len + 1);
return;
}
if(out_size < 5) {
strncpy(out, path, out_size - 1);
out[out_size - 1] = '\0';
return;
}
head = (out_size - 4) / 2;
tail = out_size - 4 - head;
memcpy(out, path, head);
memcpy(out + head, "...", 3);
memcpy(out + head + 3, path + len - tail, tail);
out[out_size - 1] = '\0';
}
static uintmax_t
ftp_saturating_add(uintmax_t lhs, uintmax_t rhs) {
if(UINTMAX_MAX - lhs < rhs) {
return UINTMAX_MAX;
}
return lhs + rhs;
}
static void
ftp_now(struct timespec *ts) {
if(!ts) {
return;
}
#ifdef CLOCK_MONOTONIC
if(clock_gettime(CLOCK_MONOTONIC, ts) == 0) {
return;
}
#endif
ts->tv_sec = time(NULL);
ts->tv_nsec = 0;
}
static double
ftp_elapsed_seconds(const struct timespec *start, const struct timespec *end) {
time_t sec;
long nsec;
if(!start || !end) {
return 0.0;
}
sec = end->tv_sec - start->tv_sec;
nsec = end->tv_nsec - start->tv_nsec;
if(nsec < 0) {
sec -= 1;
nsec += 1000000000L;
}
return (double)sec + ((double)nsec / 1000000000.0);
}
typedef struct {
uintmax_t total_entries;
uintmax_t deleted_entries;
uintmax_t last_notify_entries;
uintmax_t next_check_entries;
struct timespec last_notify_ts;
int has_last_notify_ts;
} ftp_delete_progress_t;
typedef struct {
ftp_env_t *env;
char path[PATH_MAX];
char path_notify[FTP_NOTIFY_PATH_SIZE];
ftp_delete_progress_t progress;
} ftp_delete_task_t;
static void ftp_copy_thread_cleanup(ftp_env_t *env);
static void ftp_delete_thread_cleanup(ftp_env_t *env);
static void *ftp_delete_thread(void *arg);
static int ftp_split_copy_args(const char *arg, char *src, size_t src_sz,
char *dst, size_t dst_sz);
static int ftp_mkdirs_parent(const char *path);
static int ftp_dir_is_empty(const char *path);
static int ftp_dirent_is_dots(const char *name);
static int ftp_dir_next_entry(DIR *dir, struct dirent **ent_out);
#if (defined(AT_FDCWD) || defined(AT_SYMLINK_NOFOLLOW)) && !defined(__ORBIS__)
static int ftp_dir_fastpath_should_fallback(int err);
#endif
static int ftp_dir_child_lstat(DIR *dir, const char *dir_path,
const char *name, struct stat *st);
static int ftp_dir_child_unlink(DIR *dir, const char *dir_path,
const char *name);
static void ftp_store_first_error(int *err_out, int err);
static int ftp_copy_probe_destination(const char *dst_path, int target_is_dir,
int create_parent_dirs);
static int ftp_target_statvfs(const char *dst_path, int target_is_dir,
struct statvfs *vfs_out);
static int ftp_copy_check_space(const char *dst_path, int target_is_dir,
uintmax_t total_bytes);
static int ftp_copy_total_for_stat(const char *path, const struct stat *st,
uintmax_t *total_out);
static int ftp_copy_prepare_total(const char *src_path,
const struct stat *src_st,
const char *dst_path, int target_is_dir,
uintmax_t *total_bytes_out);
static int ftp_move_same_device(const char *src_path, const struct stat *src_st,
const char *dst_path, int *same_device);
static int ftp_move_start_background(ftp_env_t *env, ftp_bg_op_t op,
const char *src_path,
const char *dst_path,
const struct stat *src_st,
int create_parent_dirs);
static void
ftp_delete_format_rate(double items_per_sec, char *out, size_t out_size) {
if(!out || out_size == 0) {
return;
}
if(items_per_sec < 0.0) {
items_per_sec = 0.0;
}
if(items_per_sec >= 100.0) {
snprintf(out, out_size, "%.0f items/s", items_per_sec);
} else if(items_per_sec >= 10.0) {
snprintf(out, out_size, "%.1f items/s", items_per_sec);
} else {
snprintf(out, out_size, "%.2f items/s", items_per_sec);
}
}
static unsigned
ftp_delete_progress_percent(const ftp_delete_task_t *task) {
if(!task) {
return 0;
}
if(task->progress.total_entries == 0) {
return 100;
}
uintmax_t deleted = task->progress.deleted_entries;
if(deleted > task->progress.total_entries) {
deleted = task->progress.total_entries;
}
return (unsigned)((deleted * 100) / task->progress.total_entries);
}
static void
ftp_delete_notify_start(ftp_delete_task_t *task) {
struct timespec now;
if(!task) {
return;
}
ftp_now(&now);
task->progress.last_notify_ts = now;
task->progress.has_last_notify_ts = 1;
task->progress.last_notify_entries = task->progress.deleted_entries;
task->progress.next_check_entries =
ftp_saturating_add(task->progress.deleted_entries,
FTP_DELETE_NOTIFY_CHECK_ITEMS);
notify("Delete started: %s", task->path_notify);
}
static void
ftp_delete_notify_progress(ftp_delete_task_t *task) {
struct timespec now;
double elapsed;
double items_per_sec;
uintmax_t deleted_delta;
uintmax_t deleted;
uintmax_t total;
char rate[32];
if(!task) {
return;
}
ftp_now(&now);
if(!task->progress.has_last_notify_ts) {
task->progress.last_notify_ts = now;
task->progress.has_last_notify_ts = 1;
task->progress.last_notify_entries = task->progress.deleted_entries;
task->progress.next_check_entries =
ftp_saturating_add(task->progress.deleted_entries,
FTP_DELETE_NOTIFY_CHECK_ITEMS);
return;
}
elapsed = ftp_elapsed_seconds(&task->progress.last_notify_ts, &now);
if(elapsed < 10.0) {
task->progress.next_check_entries =
ftp_saturating_add(task->progress.deleted_entries,
FTP_DELETE_NOTIFY_CHECK_ITEMS);
return;
}
deleted_delta =
task->progress.deleted_entries - task->progress.last_notify_entries;
items_per_sec = elapsed > 0.0 ? (double)deleted_delta / elapsed : 0.0;
ftp_delete_format_rate(items_per_sec, rate, sizeof(rate));
deleted = task->progress.deleted_entries;
total = task->progress.total_entries;
if(total > 0 && deleted > total) {
deleted = total;
}
task->progress.last_notify_ts = now;
task->progress.last_notify_entries = task->progress.deleted_entries;
task->progress.next_check_entries =
ftp_saturating_add(task->progress.deleted_entries,
FTP_DELETE_NOTIFY_CHECK_ITEMS);
notify("Delete %u%% (%ju / %ju items) - %s",
ftp_delete_progress_percent(task),
(uintmax_t)deleted, (uintmax_t)total, rate);
}
static void
ftp_delete_progress_add(ftp_delete_task_t *task, uintmax_t amount) {
if(!task || amount == 0) {
return;
}
task->progress.deleted_entries =
ftp_saturating_add(task->progress.deleted_entries, amount);
if(task->progress.deleted_entries < task->progress.next_check_entries) {
return;
}
ftp_delete_notify_progress(task);
}
static void
ftp_delete_notify_result(const ftp_delete_task_t *task, int rc, int err) {
unsigned pct = rc ? ftp_delete_progress_percent(task) : 100;
if(rc) {
if(err == 0) {
err = EIO;
}
notify("Delete failed %u%%: %s (%s)",
pct, task->path_notify, strerror(err));
return;
}
notify("Delete finished %u%%: %s (OK)", pct, task->path_notify);
}
static int
ftp_dirent_is_dots(const char *name) {
if(!name || name[0] != '.') {
return 0;
}
return name[1] == '\0' ||
(name[1] == '.' && name[2] == '\0');
}
static int
ftp_dir_next_entry(DIR *dir, struct dirent **ent_out) {
for(;;) {
errno = 0;
*ent_out = readdir(dir);
if(!*ent_out) {
return errno ? -1 : 0;
}
if(!ftp_dirent_is_dots((*ent_out)->d_name)) {
return 1;
}
}
}
#if (defined(AT_FDCWD) || defined(AT_SYMLINK_NOFOLLOW)) && !defined(__ORBIS__)
static int
ftp_dir_fastpath_should_fallback(int err) {
return err == ENOSYS
#ifdef EINVAL
|| err == EINVAL
#endif
#ifdef EOPNOTSUPP
|| err == EOPNOTSUPP
#endif
;
}
#endif
typedef enum {
FTP_DIRENT_UNKNOWN,
FTP_DIRENT_REG,
FTP_DIRENT_DIR,
FTP_DIRENT_LNK,
FTP_DIRENT_OTHER,
} ftp_dirent_kind_t;
static ftp_dirent_kind_t
ftp_dirent_kind(const struct dirent *ent) {
if(!ent) {
return FTP_DIRENT_UNKNOWN;
}
#if defined(DT_DIR) && defined(DT_REG) && defined(DT_LNK) && defined(DT_UNKNOWN)
switch(ent->d_type) {
case DT_DIR:
return FTP_DIRENT_DIR;
case DT_REG:
return FTP_DIRENT_REG;
case DT_LNK:
return FTP_DIRENT_LNK;
case DT_UNKNOWN:
return FTP_DIRENT_UNKNOWN;
default:
return FTP_DIRENT_OTHER;
}
#else
(void)ent;
return FTP_DIRENT_UNKNOWN;
#endif
}
static int
ftp_dir_open_child(DIR *dir, const char *child_path,
const char *name, DIR **child_out) {
if(!child_path || !name || !child_out) {
errno = EINVAL;
return -1;
}
#if defined(AT_FDCWD) && !defined(__ORBIS__)
{
int dir_fd = dirfd(dir);
if(dir_fd >= 0) {
int flags = O_RDONLY;
int fd;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
#ifdef O_DIRECTORY
flags |= O_DIRECTORY;
#endif
fd = openat(dir_fd, name, flags, 0);
if(fd >= 0) {
*child_out = fdopendir(fd);
if(*child_out) {
return 0;
}
{
int saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
}
if(!ftp_dir_fastpath_should_fallback(errno)) {
return -1;
}
}
}
#else
(void)dir;
#endif
*child_out = opendir(child_path);
return *child_out ? 0 : -1;
}
static int
ftp_dir_child_lstat(DIR *dir, const char *dir_path,
const char *name, struct stat *st) {
#if defined(AT_SYMLINK_NOFOLLOW) && !defined(__ORBIS__)
int dir_fd = dirfd(dir);
if(dir_fd >= 0) {
if(fstatat(dir_fd, name, st, AT_SYMLINK_NOFOLLOW) == 0) {
return 0;
}
if(!ftp_dir_fastpath_should_fallback(errno)) {
return -1;
}
}
#else
(void)dir;
#endif
char child[PATH_MAX];
if(ftp_join_path(child, sizeof(child), dir_path, name) != 0) {
return -1;
}
return lstat(child, st);
}
static int
ftp_dir_size_walk(DIR *dir, const char *path, uintmax_t *size_out) {
struct dirent *ent;
if(!dir || !path || !size_out) {
errno = EINVAL;
return -1;
}
for(;;) {
int rc = ftp_dir_next_entry(dir, &ent);
if(rc <= 0) {
if(rc < 0) {
return -1;
}
break;
}
ftp_dirent_kind_t kind = ftp_dirent_kind(ent);
if(kind == FTP_DIRENT_DIR) {
char child[PATH_MAX];
DIR *child_dir;
int saved_errno;
if(ftp_join_path(child, sizeof(child), path, ent->d_name) != 0) {
return -1;
}
if(ftp_dir_open_child(dir, child, ent->d_name, &child_dir) != 0) {
return -1;
}
if(ftp_dir_size_walk(child_dir, child, size_out) != 0) {
saved_errno = errno;
closedir(child_dir);
errno = saved_errno;
return -1;
}
if(closedir(child_dir) != 0) {
return -1;
}
continue;
}
if(kind == FTP_DIRENT_OTHER) {
errno = EINVAL;
return -1;
}
struct stat st;
if(ftp_dir_child_lstat(dir, path, ent->d_name, &st) != 0) {
return -1;
}
if(S_ISDIR(st.st_mode)) {
char child[PATH_MAX];
DIR *child_dir;
int saved_errno;
if(ftp_join_path(child, sizeof(child), path, ent->d_name) != 0) {
return -1;
}
if(ftp_dir_open_child(dir, child, ent->d_name, &child_dir) != 0) {
return -1;
}
if(ftp_dir_size_walk(child_dir, child, size_out) != 0) {
saved_errno = errno;
closedir(child_dir);
errno = saved_errno;
return -1;
}
if(closedir(child_dir) != 0) {
return -1;
}
continue;
}
if(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
if(UINTMAX_MAX - *size_out < (uintmax_t)st.st_size) {
errno = EOVERFLOW;
return -1;
}
*size_out += (uintmax_t)st.st_size;
continue;
}
errno = EINVAL;
return -1;
}
return 0;
}
static int
ftp_dir_child_unlink(DIR *dir, const char *dir_path, const char *name) {
#if defined(AT_FDCWD) && !defined(__ORBIS__)
int dir_fd = dirfd(dir);
if(dir_fd >= 0) {
if(unlinkat(dir_fd, name, 0) == 0) {
return 0;
}
if(!ftp_dir_fastpath_should_fallback(errno)) {
return -1;
}
}
#else
(void)dir;
#endif
char child[PATH_MAX];
if(ftp_join_path(child, sizeof(child), dir_path, name) != 0) {
return -1;
}
return unlink(child);
}
static void
ftp_store_first_error(int *err_out, int err) {
if(err_out && !*err_out) {
*err_out = err;
}
}
static int
ftp_delete_count_dir(const char *path, uintmax_t *count_out) {
DIR *dir = opendir(path);
struct dirent *ent;
if(!dir) {
return -1;
}
for(;;) {
int rc = ftp_dir_next_entry(dir, &ent);
if(rc <= 0) {
if(rc < 0) {
int saved_errno = errno;
closedir(dir);
errno = saved_errno;
return -1;
}
break;
}
struct stat st;
if(ftp_dir_child_lstat(dir, path, ent->d_name, &st) != 0) {
int err = errno;
closedir(dir);
errno = err;
return -1;
}
if(S_ISDIR(st.st_mode)) {
char child[PATH_MAX];
if(ftp_join_path(child, sizeof(child), path, ent->d_name) != 0) {
int err = errno;
closedir(dir);
errno = err;
return -1;
}
if(ftp_delete_count_dir(child, count_out) != 0) {
int err = errno;
closedir(dir);
errno = err;
return -1;
}
} else if(!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)) {
int err = EINVAL;
closedir(dir);
errno = err;
return -1;
}
*count_out = ftp_saturating_add(*count_out, 1);
}
if(closedir(dir) != 0) {
return -1;
}
*count_out = ftp_saturating_add(*count_out, 1);
return 0;
}
static int
ftp_rmda_delete_dir(const char *path, int *err_out, ftp_delete_task_t *task) {
DIR *dir = opendir(path);
if(!dir) {
ftp_store_first_error(err_out, errno);
return -1;
}
int failed = 0;
struct dirent *ent;
for(;;) {
int rc = ftp_dir_next_entry(dir, &ent);
if(rc <= 0) {
if(rc < 0) {
ftp_store_first_error(err_out, errno);
failed = 1;
}
break;
}
struct stat st;
if(ftp_dir_child_lstat(dir, path, ent->d_name, &st) != 0) {
ftp_store_first_error(err_out, errno);
failed = 1;
continue;
}
if(S_ISDIR(st.st_mode)) {
char child[PATH_MAX];
if(ftp_join_path(child, sizeof(child), path, ent->d_name) != 0) {
ftp_store_first_error(err_out, errno);
failed = 1;
continue;
}
if(ftp_rmda_delete_dir(child, err_out, task)) {
failed = 1;
}
continue;
}
if(ftp_dir_child_unlink(dir, path, ent->d_name) != 0) {
ftp_store_first_error(err_out, errno);
failed = 1;
} else {
ftp_delete_progress_add(task, 1);
}
}
if(closedir(dir)) {
ftp_store_first_error(err_out, errno);
failed = 1;
}
if(failed) {
return -1;
}
if(rmdir(path)) {
ftp_store_first_error(err_out, errno);
return -1;
}
ftp_delete_progress_add(task, 1);
return 0;
}
static ftp_delete_task_t*
ftp_delete_create_task(ftp_env_t *env, const char *path) {
ftp_delete_task_t *task;
if(!env || !path) {
errno = EINVAL;
return NULL;
}
task = calloc(1, sizeof(*task));
if(!task) {
return NULL;
}
task->env = env;
snprintf(task->path, sizeof(task->path), "%s", path);
ftp_compact_path(path, task->path_notify, sizeof(task->path_notify));
return task;
}
static void
ftp_delete_thread_cleanup(ftp_env_t *env) {
pthread_t thread;
int should_join = 0;
pthread_mutex_lock(&env->delete_mutex);
if(env->delete_thread_valid && !env->delete_in_progress) {
thread = env->delete_thread;
env->delete_thread_valid = 0;
should_join = 1;
}
pthread_mutex_unlock(&env->delete_mutex);
if(should_join) {
pthread_join(thread, NULL);
}
}
static int
ftp_bg_op_busy(ftp_env_t *env) {
int busy;
pthread_mutex_lock(&env->copy_mutex);
busy = env->copy_in_progress;
pthread_mutex_unlock(&env->copy_mutex);
if(busy) {
return 1;
}
pthread_mutex_lock(&env->delete_mutex);
busy = env->delete_in_progress;
pthread_mutex_unlock(&env->delete_mutex);
return busy;
}
static int
ftp_delete_start_task(ftp_env_t *env, ftp_delete_task_t *task) {
int thread_rc;
pthread_mutex_lock(&env->delete_mutex);
env->delete_in_progress = 1;
env->delete_thread_valid = 1;
pthread_mutex_unlock(&env->delete_mutex);
thread_rc = pthread_create(&env->delete_thread, NULL,
ftp_delete_thread, task);
if(thread_rc != 0) {
pthread_mutex_lock(&env->delete_mutex);
env->delete_in_progress = 0;
env->delete_thread_valid = 0;
pthread_mutex_unlock(&env->delete_mutex);
free(task);
errno = thread_rc;
return ftp_perror(env);
}
return ftp_active_printf(env, "250 Delete started in background\r\n");
}
static void *
ftp_delete_thread(void *arg) {
ftp_delete_task_t *task = (ftp_delete_task_t *)arg;
ftp_env_t *env = task->env;
int err = 0;
int rc;
ftp_delete_notify_start(task);
rc = ftp_delete_count_dir(task->path, &task->progress.total_entries);
if(rc == 0) {
rc = ftp_rmda_delete_dir(task->path, &err, task);
} else {
err = errno;
}
if(rc != 0 && err == 0) {
err = errno;
}
ftp_delete_notify_result(task, rc, err);
pthread_mutex_lock(&env->delete_mutex);
env->delete_in_progress = 0;
pthread_mutex_unlock(&env->delete_mutex);
free(task);
return NULL;
}
typedef struct {
char *argbuf;
char *list_path;
char *pathbuf;
const char *dir_path;
DIR *dir;
} ftp_list_ctx_t;
/**
* Fetch the next directory entry, returning 1/0/-1.
**/
static int
ftp_list_next_dirent(DIR *dir, struct dirent **ent_out) {
errno = 0;
*ent_out = readdir(dir);
if(!*ent_out) {
return errno ? -1 : 0;
}
return 1;
}
/**
* Stat a directory entry and apply any size adjustments.
**/
static int
ftp_list_get_stat(ftp_env_t *env, int dir_fd, const char *dir_path,
char *pathbuf, size_t pathbuf_sz, struct dirent *ent,
struct stat *statbuf) {
int have_path = 0;
int stat_rc = -1;
#if defined(AT_SYMLINK_NOFOLLOW) && !defined(__ORBIS__)
if(dir_fd >= 0) {
stat_rc = fstatat(dir_fd, ent->d_name, statbuf, AT_SYMLINK_NOFOLLOW);
}
#else
(void)dir_fd;
#endif
if(stat_rc != 0) {
if(ftp_join_path(pathbuf, pathbuf_sz, dir_path, ent->d_name) != 0) {
return -1;
}
have_path = 1;
#ifdef AT_SYMLINK_NOFOLLOW
if(lstat(pathbuf, statbuf) != 0)
#else
if(stat(pathbuf, statbuf) != 0)
#endif
{
return -1;
}
}
if(env->self2elf && S_ISREG(statbuf->st_mode)) {
if(!have_path) {
if(ftp_join_path(pathbuf, pathbuf_sz, dir_path, ent->d_name) != 0) {
return -1;
}
have_path = 1;
}
size_t elf_size = self_is_valid(pathbuf);
if(elf_size) {
statbuf->st_size = elf_size;
}
}
return 0;
}
/**
* Iterate directory entries until one can be stat'd.
**/
static int
ftp_list_next_entry(ftp_env_t *env, DIR *dir, int dir_fd, const char *dir_path,
char *pathbuf, size_t pathbuf_sz, struct dirent **ent_out,
struct stat *statbuf) {
for(;;) {
struct dirent *ent = NULL;
int rc = ftp_list_next_dirent(dir, &ent);
if(rc <= 0) {
return rc;
}
if(ftp_list_get_stat(env, dir_fd, dir_path, pathbuf, pathbuf_sz,
ent, statbuf) != 0) {
continue;
}
*ent_out = ent;
return 1;
}
}
/**
* Escape a symlink target for MLST/MLSD output.
**/
static void
ftp_escape_slink(char *dst, size_t dst_sz, const char *prefix,
const char *src, size_t src_len) {
size_t pos = 0;
size_t remaining = dst_sz;
static const char hex[] = "0123456789ABCDEF";
if(!dst || !dst_sz) {
return;
}
int wrote = snprintf(dst, dst_sz, "%s", prefix);
if(wrote < 0) {
dst[0] = '\0';
return;
}
if((size_t)wrote >= dst_sz) {
dst[dst_sz - 1] = '\0';
return;
}
pos = (size_t)wrote;
remaining = dst_sz - pos;
for(size_t i = 0; i < src_len && remaining > 1; i++) {
unsigned char c = (unsigned char)src[i];
int safe = (c >= 0x20 && c < 0x7f && c != ';' && c != '%');
if(safe) {
dst[pos++] = (char)c;
remaining--;
continue;
}
if(remaining <= 3) {
break;
}
dst[pos++] = '%';
dst[pos++] = hex[(c >> 4) & 0x0f];
dst[pos++] = hex[c & 0x0f];
remaining -= 3;
}
if(remaining > 1) {
dst[pos++] = ';';
remaining--;
}
dst[pos] = '\0';
}
/**
* Build the MLST type fact string for an entry.
**/
static const char *
ftp_mlst_type_fact(char *dst, size_t dst_sz, const char *name,
const struct stat *st, const char *path) {
const char *slink_prefix = "type=OS.unix=slink:";
if(name && name[0] == '.' && name[1] == '\0') {
(void)snprintf(dst, dst_sz, "type=cdir;");
return dst;
}
if(name && name[0] == '.' && name[1] == '.' && name[2] == '\0') {
(void)snprintf(dst, dst_sz, "type=pdir;");
return dst;
}
if(S_ISDIR(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=dir;");
return dst;
}
if(S_ISREG(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=file;");
return dst;
}
if(S_ISLNK(st->st_mode)) {
char linkbuf[256];
if(path) {
ssize_t n = readlink(path, linkbuf, sizeof(linkbuf) - 1);
if(n >= 0) {
linkbuf[n] = '\0';
ftp_escape_slink(dst, dst_sz, slink_prefix, linkbuf, (size_t)n);
return dst;
}
}
(void)snprintf(dst, dst_sz, "type=OS.unix=slink;");
return dst;
}
if(S_ISCHR(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=OS.unix=chr-%u/%u;",
(unsigned)major(st->st_rdev),
(unsigned)minor(st->st_rdev));
return dst;
}
if(S_ISBLK(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=OS.unix=blk-%u/%u;",
(unsigned)major(st->st_rdev),
(unsigned)minor(st->st_rdev));
return dst;
}
if(S_ISFIFO(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=OS.unix=fifo;");
return dst;
}
if(S_ISSOCK(st->st_mode)) {
(void)snprintf(dst, dst_sz, "type=OS.unix=socket;");
return dst;
}
(void)snprintf(dst, dst_sz, "type=unknown;");
return dst;
}
/**
* Build the MLST unique fact string for an entry.
**/
static const char *
ftp_mlst_unique_fact(char *dst, size_t dst_sz, const struct stat *st) {
(void)snprintf(dst, dst_sz, "unique=%" PRIxMAX ".%" PRIxMAX ";",
(uintmax_t)st->st_dev, (uintmax_t)st->st_ino);
return dst;
}
/**
* Format a full MLST/MLSD line with facts.
**/
static int
ftp_mlst_format_line(char *dst, size_t dst_sz, int leading_space,
const char *type, const char *unique, uintmax_t size,
time_t mtime, unsigned mode_bits, uintmax_t uid,
uintmax_t gid, const char *name) {
char timebuf[32];
if(ftp_format_mdtm(mtime, timebuf, sizeof(timebuf))) {
return -1;
}
int n = snprintf(dst, dst_sz,
"%s%s%ssize=%" PRIuMAX ";modify=%s;unix.mode=%04o;"
"unix.uid=%" PRIuMAX ";unix.gid=%" PRIuMAX "; %s\r\n",
leading_space ? " " : "", type, unique, size, timebuf,
mode_bits, uid, gid, name);
if(n < 0 || (size_t)n >= dst_sz) {
return -1;
}
return 0;
}
/**
* Free buffers in the list context.
**/
static void
ftp_list_ctx_free(ftp_list_ctx_t *ctx) {
if(ctx->pathbuf) {
free(ctx->pathbuf);
}
if(ctx->list_path) {
free(ctx->list_path);
}
if(ctx->argbuf) {
free(ctx->argbuf);
}
ctx->pathbuf = NULL;
ctx->list_path = NULL;
ctx->argbuf = NULL;
ctx->dir_path = NULL;
ctx->dir = NULL;
}
/**
* Prepare list context, resolve path, and open directory if needed.
* Returns 0 on success, 1 if an FTP error reply was sent, or -1 if replying
* failed and the control connection should be closed.
**/
static int
ftp_list_open(ftp_env_t *env, const char *arg, int need_pathbuf,
int allow_file, int allow_opts, ftp_list_ctx_t *ctx,
int *dir_errno) {
memset(ctx, 0, sizeof(*ctx));
// Allocate large buffers on Heap to avoid SceLibcInternalHeap error due to stack overflow
ctx->argbuf = malloc(PATH_MAX + 1);
ctx->list_path = malloc(PATH_MAX + 1);
if(need_pathbuf) {
ctx->pathbuf = malloc(PATH_MAX * 3);
}
if(!ctx->argbuf || !ctx->list_path || (need_pathbuf && !ctx->pathbuf)) {
int err;
ftp_list_ctx_free(ctx);
err = ftp_perror(env);
return err < 0 ? -1 : 1;
}
const char *dir_path = allow_opts
? ftp_list_path_arg(arg, ctx->argbuf, PATH_MAX + 1)
: ftp_copy_path_arg(arg, ctx->argbuf, PATH_MAX + 1);
if(dir_path) {
if(ftp_abspath(env, ctx->list_path, PATH_MAX + 1, dir_path)) {
int err;
ftp_list_ctx_free(ctx);
err = ftp_perror(env);
return err < 0 ? -1 : 1;
}
} else {
if(ftp_normpath(env->cwd, ctx->list_path, PATH_MAX + 1)) {
int err;
ftp_list_ctx_free(ctx);
err = ftp_perror(env);
return err < 0 ? -1 : 1;
}
}
ctx->dir_path = ctx->list_path;
ctx->dir = opendir(ctx->dir_path);
if(!ctx->dir) {
if(dir_errno) {
*dir_errno = errno;
}
if(allow_file) {
return 0;
}
int err;
ftp_list_ctx_free(ctx);
err = ftp_perror(env);
return err < 0 ? -1 : 1;
}
if(dir_errno) {
*dir_errno = 0;
}
return 0;
}
/**
* Trasfer a list of files and folder.
**/
int
ftp_cmd_LIST(ftp_env_t *env, const char *arg) {
ftp_list_ctx_t ctx;
struct dirent *ent;
struct stat statbuf;
char timebuf[32];
char modebuf[20];
ftp_xfer_buf_t x;
int dir_errno = 0;
int err = ftp_list_open(env, arg, 1, 1, 1, &ctx, &dir_errno);
if(err) {
return err < 0 ? err : 0;
}
if(!ctx.dir) {
struct stat st;
if(lstat(ctx.dir_path, &st)) {
err = ftp_perror(env);
ftp_list_ctx_free(&ctx);
return err;
}
if(S_ISDIR(st.st_mode)) {
if(dir_errno) {
errno = dir_errno;
}
err = ftp_perror(env);
ftp_list_ctx_free(&ctx);
return err;
}
int err_xfer = ftp_list_xfer_start(env, NULL, &x);
if(err_xfer) {
ftp_list_ctx_free(&ctx);
return err_xfer < 0 ? err_xfer : 0;
}
if(env->self2elf && S_ISREG(st.st_mode)) {
size_t elf_size = self_is_valid(ctx.dir_path);
if(elf_size) {
st.st_size = elf_size;
}
}
ftp_mode_string(st.st_mode, modebuf);
if(!ftp_format_list_time(st.st_mtime, timebuf, sizeof(timebuf))) {
const char *name = strrchr(ctx.dir_path, '/');
name = name ? name + 1 : ctx.dir_path;
if(!*name) {
name = ctx.dir_path;
}
if(ftp_xfer_printf(&x,
"%s %" PRIuMAX " %" PRIuMAX " %" PRIuMAX
" %" PRIuMAX " %s %s\r\n",
modebuf, (uintmax_t)st.st_nlink,
(uintmax_t)st.st_uid,
(uintmax_t)st.st_gid,
(uintmax_t)st.st_size, timebuf, name)) {
// formatting error or transfer failure, handled in finish
}
}
err = ftp_list_xfer_finish(env, NULL, &x);
ftp_list_ctx_free(&ctx);
return err;
}
int err_xfer = ftp_list_xfer_start(env, ctx.dir, &x);
if(err_xfer) {
ftp_list_ctx_free(&ctx);
return err_xfer < 0 ? err_xfer : 0;
}
int dir_fd = -1;
#if defined(AT_SYMLINK_NOFOLLOW) && !defined(__ORBIS__)
dir_fd = dirfd(ctx.dir);
#endif
int read_errno = 0;
for(;;) {
int next = ftp_list_next_entry(env, ctx.dir, dir_fd, ctx.dir_path,
ctx.pathbuf, PATH_MAX * 3, &ent, &statbuf);
if(next < 0) {
read_errno = errno;
break;
}
if(next == 0) {
break;
}
ftp_mode_string(statbuf.st_mode, modebuf);
if(ftp_format_list_time(statbuf.st_mtime, timebuf, sizeof(timebuf))) {
continue;
}
if(ftp_xfer_printf(&x,
"%s %" PRIuMAX " %" PRIuMAX " %" PRIuMAX
" %" PRIuMAX " %s %s\r\n",
modebuf, (uintmax_t)statbuf.st_nlink,
(uintmax_t)statbuf.st_uid,
(uintmax_t)statbuf.st_gid,
(uintmax_t)statbuf.st_size, timebuf, ent->d_name)) {
if(x.failed) {
break;
}
// formatting error -> skip entry
continue;
}
if(x.failed) {
break;
}
}
if(read_errno && !x.failed) {
errno = read_errno;
(void)ftp_perror(env);
x.failed = 1;
}
err = ftp_list_xfer_finish(env, ctx.dir, &x);
ftp_list_ctx_free(&ctx);
return err;
}
/**
* Transfer a list of file names (no stat).
**/
int
ftp_cmd_NLST(ftp_env_t *env, const char *arg) {
ftp_list_ctx_t ctx;
struct dirent *ent;
ftp_xfer_buf_t x;
int err = ftp_list_open(env, arg, 0, 0, 0, &ctx, NULL);
if(err) {
return err < 0 ? err : 0;
}
int err_xfer = ftp_list_xfer_start(env, ctx.dir, &x);
if(err_xfer) {
ftp_list_ctx_free(&ctx);
return err_xfer < 0 ? err_xfer : 0;
}
int read_errno = 0;
for(;;) {
int next = ftp_list_next_dirent(ctx.dir, &ent);
if(next < 0) {
read_errno = errno;
break;
}
if(next == 0) {
break;
}
if(ftp_xfer_printf(&x, "%s\r\n", ent->d_name)) {
if(x.failed) {
break;
}
// shouldn't happen; but if it does, skip entry
continue;
}
if(x.failed) {
break;
}
}
if(read_errno && !x.failed) {
errno = read_errno;
(void)ftp_perror(env);
x.failed = 1;
}
err = ftp_list_xfer_finish(env, ctx.dir, &x);
ftp_list_ctx_free(&ctx);
return err;
}
/**
* Transfer a machine-readable list.
**/
int
ftp_cmd_MLSD(ftp_env_t *env, const char *arg) {
ftp_list_ctx_t ctx;
struct dirent *ent;
struct stat statbuf;
ftp_xfer_buf_t x;
int err = ftp_list_open(env, arg, 1, 0, 0, &ctx, NULL);
if(err) {
return err < 0 ? err : 0;
}
int err_xfer = ftp_list_xfer_start(env, ctx.dir, &x);
if(err_xfer) {
ftp_list_ctx_free(&ctx);
return err_xfer < 0 ? err_xfer : 0;
}
int dir_fd = -1;
#if defined(AT_SYMLINK_NOFOLLOW) && !defined(__ORBIS__)
dir_fd = dirfd(ctx.dir);
#endif
int read_errno = 0;
for(;;) {
const char *type;
const char *unique;
uintmax_t size;
unsigned mode_bits;
uintmax_t uid;
uintmax_t gid;
char *typebuf = ctx.argbuf;
char uniquebuf[64];
char *linebuf = ctx.pathbuf;
int next = ftp_list_next_entry(env, ctx.dir, dir_fd, ctx.dir_path,
ctx.pathbuf, PATH_MAX * 3, &ent, &statbuf);
if(next < 0) {
read_errno = errno;
break;
}
if(next == 0) {
break;
}
const char *link_path = NULL;
if(S_ISLNK(statbuf.st_mode)) {
if(ftp_join_path(ctx.pathbuf, PATH_MAX * 3, ctx.dir_path,
ent->d_name) == 0) {
link_path = ctx.pathbuf;
}
}
type = ftp_mlst_type_fact(typebuf, PATH_MAX + 1, ent->d_name, &statbuf,
link_path);
unique = ftp_mlst_unique_fact(uniquebuf, sizeof(uniquebuf), &statbuf);
size = (uintmax_t)statbuf.st_size;
mode_bits = (unsigned)(statbuf.st_mode & 07777);
uid = (uintmax_t)statbuf.st_uid;
gid = (uintmax_t)statbuf.st_gid;
if(ftp_mlst_format_line(linebuf, PATH_MAX * 3, 0, type, unique, size,
statbuf.st_mtime, mode_bits, uid, gid,
ent->d_name)) {
continue;
}
if(ftp_xfer_printf(&x, "%s", linebuf)) {
if(x.failed) {
break;
}
continue;
}
if(x.failed) {
break;
}
}
if(read_errno && !x.failed) {
errno = read_errno;
(void)ftp_perror(env);
x.failed = 1;
}
err = ftp_list_xfer_finish(env, ctx.dir, &x);
ftp_list_ctx_free(&ctx);
return err;
}
/**
* Create a new directory at a given path.
**/
int
ftp_cmd_MKD(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: MKD <DIRNAME>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(mkdir(pathbuf, 0777)) {
return ftp_perror(env);
}
return ftp_active_printf(env, "257 \"%s\"\r\n", pathbuf);
}
/**
* No operation.
**/
int
ftp_cmd_NOOP(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "200 NOOP OK\r\n");
}
/**
* Establish a data connection with client.
**/
int
ftp_cmd_PORT(ftp_env_t *env, const char* arg) {
uint8_t addr[6];
struct in_addr in_addr;
uint32_t s_addr_host;
uint16_t port_host;
int ret = 0;
if(sscanf(arg, "%hhu,%hhu,%hhu,%hhu,%hhu,%hhu",
addr, addr+1, addr+2, addr+3, addr+4, addr+5) != 6) {
return ftp_active_printf(env, "501 Usage: PORT <addr>\r\n");
}
s_addr_host = ((uint32_t)addr[0] << 24) | ((uint32_t)addr[1] << 16) |
((uint32_t)addr[2] << 8) | (uint32_t)addr[3];
in_addr.s_addr = htonl(s_addr_host);
port_host = (uint16_t)(((uint16_t)addr[4] << 8) | (uint16_t)addr[5]);
ret = ftp_setup_active_data(env, in_addr, port_host,
"500 Illegal PORT command");
if(ret) {
return ret;
}
return ftp_active_printf(env, "200 PORT command successful.\r\n");
}
/**
* Establish a data connection with client (extended).
**/
int
ftp_cmd_EPRT(ftp_env_t *env, const char *arg) {
char addrbuf[INET_ADDRSTRLEN] = {0};
char portbuf[16] = {0};
char delim;
const char *p1;
const char *p2;
const char *p3;
char *endptr = NULL;
unsigned long port_ul;
struct in_addr in_addr;
int ret = 0;
delim = arg[0];
p1 = (arg[0] ? strchr(arg + 1, delim) : NULL);
p2 = (p1 ? strchr(p1 + 1, delim) : NULL);
p3 = (p2 ? strchr(p2 + 1, delim) : NULL);
if(!arg[0] || !p1 || !p2 || !p3 || p3[1]) {
return ftp_active_printf(
env, "501 Usage: EPRT <d><af><d><addr><d><port><d>\r\n");
}
if(p1 != arg + 2 || arg[1] != '1') {
return ftp_active_printf(env, "522 Network protocol not supported\r\n");
}
size_t addr_len = (size_t)(p2 - (p1 + 1));
if(!addr_len || addr_len >= sizeof(addrbuf)) {
return ftp_active_printf(
env, "501 Usage: EPRT <d><af><d><addr><d><port><d>\r\n");
}
memcpy(addrbuf, p1 + 1, addr_len);
addrbuf[addr_len] = '\0';
size_t port_len = (size_t)(p3 - (p2 + 1));
if(!port_len || port_len >= sizeof(portbuf)) {
return ftp_active_printf(
env, "501 Usage: EPRT <d><af><d><addr><d><port><d>\r\n");
}
memcpy(portbuf, p2 + 1, port_len);
portbuf[port_len] = '\0';
if(inet_pton(AF_INET, addrbuf, &in_addr) != 1) {
return ftp_active_printf(
env, "501 Usage: EPRT <d><af><d><addr><d><port><d>\r\n");
}
port_ul = strtoul(portbuf, &endptr, 10);
if(!port_ul || port_ul > 65535 || endptr == portbuf || *endptr) {
return ftp_active_printf(
env, "501 Usage: EPRT <d><af><d><addr><d><port><d>\r\n");
}
ret = ftp_setup_active_data(env, in_addr, (uint16_t)port_ul,
"500 Illegal EPRT command");
if(ret) {
return ret;
}
return ftp_active_printf(env, "200 EPRT command successful.\r\n");
}
/**
* Print working directory.
**/
int
ftp_cmd_PWD(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "257 \"%s\"\r\n", env->cwd);
}
/**
* Disconnect client.
**/
int
ftp_cmd_QUIT(ftp_env_t *env, const char* arg) {
ftp_active_printf(env, "221 Goodbye\r\n");
return -1;
}
/**
* Mark the offset to start from in a future file transer.
**/
int
ftp_cmd_REST(ftp_env_t *env, const char* arg) {
char *end = NULL;
long long off = 0;
if(env->type == 'A') {
env->data_offset = 0;
env->data_offset_is_rest = 0;
return ftp_active_printf(env, "504 REST not supported in ASCII mode\r\n");
}
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: REST <OFFSET>\r\n");
}
errno = 0;
off = strtoll(arg, &end, 10);
if(errno || end == arg) {
return ftp_active_printf(env, "501 Usage: REST <OFFSET>\r\n");
}
if(*end || off < 0 || (off_t)off != off) {
return ftp_active_printf(env, "501 Usage: REST <OFFSET>\r\n");
}
env->data_offset = (off_t)off;
env->data_offset_is_rest = 1;
return ftp_active_printf(env, "350 REST OK\r\n");
}
/**
* Retreive data from a given file.
**/
static int
ftp_cmd_RETR_fd(ftp_env_t *env, int fd) {
off_t off = env->data_offset;
int is_rest = env->data_offset_is_rest;
env->data_offset = 0;
env->data_offset_is_rest = 0;
struct stat st;
size_t remaining;
int err = 0;
if(env->type == 'A' && off != 0 && is_rest) {
return ftp_active_printf(env, "504 REST not supported in ASCII mode\r\n");
}
if(fstat(fd, &st)) {
return ftp_perror(env);
}
if(lseek(fd, off, SEEK_SET) < 0) {
return ftp_perror(env);
}
if(off >= st.st_size) {
remaining = 0;
} else {
remaining = (size_t)(st.st_size - off);
}
int open_err = ftp_data_xfer_start(env, 1, 1);
if(open_err) {
return open_err < 0 ? open_err : 0;
}
if(env->type == 'A') {
if(ftp_copy_ascii_out(env, fd)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
return err;
}
} else if(remaining) {
if(remaining < 1460) { // Typical MSS size
#ifdef TCP_NODELAY
(void)setsockopt(env->data_fd, IPPROTO_TCP, TCP_NODELAY, &(int){1},
sizeof(int));
#endif
} else if(remaining >= 128*1024) {
#ifdef TCP_NOPUSH
(void)setsockopt(env->data_fd, IPPROTO_TCP, TCP_NOPUSH, &(int){1},
sizeof(int));
#endif
}
#ifdef IO_USE_SENDFILE
if((env->mode == 'Z' || env->mode == 'z') ?
ftp_copy_binary_out(env, fd, remaining) :
io_sendfile(fd, env->data_fd, off, remaining)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
return err;
}
#else
if(env->mode == 'Z' || env->mode == 'z') {
if(ftp_copy_binary_out(env, fd, remaining)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
return err;
}
} else if(env->xfer_buf && env->xfer_buf_size) {
if(io_ncopy_buf(fd, env->data_fd, remaining, env->xfer_buf,
env->xfer_buf_size)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
return err;
}
} else if(io_ncopy(fd, env->data_fd, remaining)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
return err;
}
#endif
}
if(ftp_data_xfer_complete(env)) {
return ftp_data_xfer_error_reply(env);
}
return ftp_active_printf(env, "226 Transfer completed\r\n");
}
/**
* Retreive an ELF file embedded within a SELF file.
**/
static int
ftp_cmd_RETR_self2elf(ftp_env_t *env, int fd) {
FILE* tmpf;
int err;
if(!(tmpf=tmpfile())) {
return ftp_perror(env);
}
if(ftp_active_printf(env, "150-Extracting ELF...\r\n")) {
fclose(tmpf);
return -1;
}
if(self_extract_elf_ex(fd, fileno(tmpf), env->self_verify)) {
if(errno != EBADMSG) {
err = ftp_perror(env);
fclose(tmpf);
return err;
}
if(ftp_active_printf(env, "150-Warning: ELF digest mismatch\r\n")) {
fclose(tmpf);
return -1;
}
}
rewind(tmpf);
err = ftp_cmd_RETR_fd(env, fileno(tmpf));
fclose(tmpf);
return err;
}
/**
* Retreive data from a given file.
**/
int
ftp_cmd_RETR(ftp_env_t *env, const char* arg) {
char path[PATH_MAX];
int err;
int fd;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: RETR <PATH>\r\n");
}
int precheck = ftp_data_precheck(env);
if(precheck) {
return precheck < 0 ? precheck : 0;
}
if(ftp_abspath(env, path, sizeof(path), arg)) {
return ftp_perror(env);
}
if((fd=open(path, O_RDONLY, 0)) < 0) {
return ftp_perror(env);
}
if(env->self2elf && self_is_valid(path)) {
err = ftp_cmd_RETR_self2elf(env, fd);
} else {
err = ftp_cmd_RETR_fd(env, fd);
}
close(fd);
return err;
}
/**
* Remove a directory.
**/
int
ftp_cmd_RMD(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: RMD <DIRNAME>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(rmdir(pathbuf)) {
return ftp_perror(env);
}
return ftp_active_printf(env, "250 Directory deleted\r\n");
}
/**
* Remove a directory and its contents.
**/
int
ftp_cmd_RMDA(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
ftp_delete_task_t *task;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: RMDA <DIRNAME>\r\n");
}
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env, "450 Background file operation in progress\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_active_printf(env, "550 %s: %s.\r\n", arg, strerror(errno));
}
if(stat(pathbuf, &st)) {
return ftp_active_printf(env, "550 %s: %s.\r\n", arg, strerror(errno));
}
if(S_ISREG(st.st_mode)) {
return ftp_active_printf(env, "550 %s: Is a file.\r\n", arg);
}
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 %s: Not a directory.\r\n", arg);
}
task = ftp_delete_create_task(env, pathbuf);
if(!task) {
return ftp_perror(env);
}
return ftp_delete_start_task(env, task);
}
/**
* Specify a path that will later be renamed by the RNTO command.
**/
int
ftp_cmd_RNFR(ftp_env_t *env, const char* arg) {
struct stat st;
env->rename_ready = 0;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: RNFR <PATH>\r\n");
}
if(ftp_abspath(env, env->rename_path, sizeof(env->rename_path), arg)) {
return ftp_perror(env);
}
if(lstat(env->rename_path, &st)) {
return ftp_perror(env);
}
env->rename_ready = 1;
return ftp_active_printf(env, "350 Awaiting new name\r\n");
}
/**
* Rename a path previously specified by the RNFR command.
**/
int
ftp_cmd_RNTO(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
int same_device = 0;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: RNTO <PATH>\r\n");
}
if(!env->rename_ready) {
return ftp_active_printf(env, "503 Bad sequence of commands\r\n");
}
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env,
"450 Background file operation in progress\r\n");
}
env->rename_ready = 0;
if(lstat(env->rename_path, &st)) {
return ftp_perror(env);
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(ftp_move_same_device(env->rename_path, &st, pathbuf, &same_device)) {
return ftp_perror(env);
}
if(same_device) {
if(rename(env->rename_path, pathbuf) == 0) {
return ftp_active_printf(env, "250 Path renamed\r\n");
}
if(errno != EXDEV) {
return ftp_perror(env);
}
}
return ftp_move_start_background(env, FTP_BG_MOVE,
env->rename_path, pathbuf, &st, 0);
}
/**
* Specify a source file to be copied by the CPTO command.
**/
static void ftp_copy_thread_cleanup(ftp_env_t *env);
static void *ftp_copy_thread(void *arg);
int
ftp_cmd_CPFR(ftp_env_t *env, const char* arg) {
struct stat st;
char argbuf[PATH_MAX + 1];
const char *path_arg = ftp_copy_path_arg(arg, argbuf, sizeof(argbuf));
ftp_copy_thread_cleanup(env);
pthread_mutex_lock(&env->copy_mutex);
int busy = env->copy_in_progress;
pthread_mutex_unlock(&env->copy_mutex);
if(busy) {
return ftp_active_printf(env, "450 Copy in progress\r\n");
}
env->copy_ready = 0;
if(!path_arg) {
return ftp_active_printf(env, "501 Usage: CPFR <PATH>\r\n");
}
if(ftp_abspath(env, env->copy_path, sizeof(env->copy_path), path_arg)) {
return ftp_perror(env);
}
if(lstat(env->copy_path, &st)) {
return ftp_perror(env);
}
if(!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode)) {
return ftp_active_printf(env, "550 Unsupported file type\r\n");
}
env->copy_ready = 1;
return ftp_active_printf(env, "350 Awaiting CPTO\r\n");
}
int
ftp_cmd_MOVE(ftp_env_t *env, const char* arg) {
char src_arg[PATH_MAX + 1];
char dst_arg[PATH_MAX + 1];
char src_path[PATH_MAX];
char dst_path[PATH_MAX];
struct stat src_st;
int same_device = 0;
if(ftp_split_copy_args(arg, src_arg, sizeof(src_arg),
dst_arg, sizeof(dst_arg))) {
return ftp_active_printf(env, "501 Usage: MOVE <FROM> <TO>\r\n");
}
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env,
"450 Background file operation in progress\r\n");
}
if(ftp_abspath(env, src_path, sizeof(src_path), src_arg)) {
return ftp_perror(env);
}
if(ftp_abspath(env, dst_path, sizeof(dst_path), dst_arg)) {
return ftp_perror(env);
}
if(strcmp(src_path, dst_path) == 0) {
return ftp_active_printf(env, "553 Source and destination are the same\r\n");
}
if(lstat(src_path, &src_st)) {
return ftp_perror(env);
}
if(S_ISDIR(src_st.st_mode)) {
size_t src_len = strlen(src_path);
if(strncmp(dst_path, src_path, src_len) == 0 &&
(dst_path[src_len] == '/' || dst_path[src_len] == '\0')) {
return ftp_active_printf(env, "553 Cannot move into itself\r\n");
}
}
if(ftp_move_same_device(src_path, &src_st, dst_path, &same_device)) {
return ftp_perror(env);
}
if(same_device) {
if(ftp_mkdirs_parent(dst_path)) {
return ftp_perror(env);
}
if(rename(src_path, dst_path) == 0) {
return ftp_active_printf(env, "250 Path moved\r\n");
}
if(errno != EXDEV) {
return ftp_perror(env);
}
}
return ftp_move_start_background(env, FTP_BG_MOVE,
src_path, dst_path, &src_st, 1);
}
/**
* Split COPY arguments into source and destination paths.
**/
static int
ftp_split_copy_args(const char *arg, char *src, size_t src_sz,
char *dst, size_t dst_sz) {
if(!arg || !src || !dst || src_sz < 2 || dst_sz < 2) {
return -1;
}
const char *p = arg;
p += strspn(p, " ");
if(!*p) {
return -1;
}
const char *sep = strchr(p, ' ');
if(!sep) {
return -1;
}
size_t src_len = (size_t)(sep - p);
if(src_len == 0 || src_len >= src_sz) {
return -1;
}
memcpy(src, p, src_len);
src[src_len] = '\0';
sep += strspn(sep, " ");
if(!*sep) {
return -1;
}
const char *end = sep + strlen(sep);
while(end > sep && end[-1] == ' ') {
end--;
}
size_t dst_len = (size_t)(end - sep);
if(dst_len == 0 || dst_len >= dst_sz) {
return -1;
}
memcpy(dst, sep, dst_len);
dst[dst_len] = '\0';
return 0;
}
static int
ftp_path_parent_dir(const char *path, char *parent, size_t parent_size) {
char *slash;
size_t len;
if(!path || !parent || parent_size == 0) {
errno = EINVAL;
return -1;
}
len = strlen(path);
if(len >= parent_size) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(parent, path, len + 1);
slash = strrchr(parent, '/');
if(!slash) {
errno = EINVAL;
return -1;
}
if(slash == parent) {
parent[1] = '\0';
} else {
*slash = '\0';
}
return 0;
}
static int
ftp_path_parent_must_exist(const char *path) {
char parent[PATH_MAX];
struct stat st;
if(ftp_path_parent_dir(path, parent, sizeof(parent)) != 0) {
return -1;
}
if(stat(parent, &st) != 0) {
return -1;
}
if(!S_ISDIR(st.st_mode)) {
errno = ENOTDIR;
return -1;
}
return 0;
}
static int
ftp_make_temp_path(const char *path, char *tmp_path, size_t tmp_path_size) {
char parent[PATH_MAX];
int n;
if(ftp_path_parent_dir(path, parent, sizeof(parent)) != 0) {
return -1;
}
n = snprintf(tmp_path, tmp_path_size, "%s/.ftpsrv-tmp-XXXXXX", parent);
if(n < 0 || (size_t)n >= tmp_path_size) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
static int
ftp_dir_make_temp_path(const char *dir_path, const char *suffix,
char *tmp_path, size_t tmp_path_size) {
int n;
if(!dir_path || !suffix || !tmp_path || tmp_path_size == 0) {
errno = EINVAL;
return -1;
}
n = snprintf(tmp_path, tmp_path_size, "%s/%s", dir_path, suffix);
if(n < 0 || (size_t)n >= tmp_path_size) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
static int
ftp_probe_write_in_dir(const char *dir_path) {
char probe_path[PATH_MAX];
int fd = -1;
int saved_errno = 0;
static const char probe_byte = '\0';
if(ftp_dir_make_temp_path(dir_path, ".ftpsrv-probe-XXXXXX",
probe_path, sizeof(probe_path)) != 0) {
return -1;
}
fd = mkstemp(probe_path);
if(fd < 0) {
return -1;
}
if(io_nwrite(fd, &probe_byte, sizeof(probe_byte)) != 0) {
saved_errno = errno;
close(fd);
unlink(probe_path);
errno = saved_errno;
return -1;
}
if(close(fd) != 0) {
saved_errno = errno;
unlink(probe_path);
errno = saved_errno;
return -1;
}
if(unlink(probe_path) != 0) {
return -1;
}
return 0;
}
static int
ftp_probe_create_dir_and_write(const char *dst_path) {
char parent[PATH_MAX];
char probe_dir[PATH_MAX];
int saved_errno = 0;
if(ftp_path_parent_dir(dst_path, parent, sizeof(parent)) != 0) {
return -1;
}
if(ftp_dir_make_temp_path(parent, ".ftpsrv-probe-dir-XXXXXX",
probe_dir, sizeof(probe_dir)) != 0) {
return -1;
}
if(!mkdtemp(probe_dir)) {
return -1;
}
if(ftp_probe_write_in_dir(probe_dir) != 0) {
saved_errno = errno;
rmdir(probe_dir);
errno = saved_errno;
return -1;
}
if(rmdir(probe_dir) != 0) {
return -1;
}
return 0;
}
static int
ftp_copy_probe_destination(const char *dst_path, int target_is_dir,
int create_parent_dirs) {
char parent[PATH_MAX];
struct stat st;
if(!dst_path) {
errno = EINVAL;
return -1;
}
if(create_parent_dirs) {
if(ftp_mkdirs_parent(dst_path) != 0) {
return -1;
}
} else if(ftp_path_parent_must_exist(dst_path) != 0) {
return -1;
}
if(target_is_dir) {
if(lstat(dst_path, &st) == 0) {
if(!S_ISDIR(st.st_mode)) {
errno = ENOTDIR;
return -1;
}
return ftp_probe_write_in_dir(dst_path);
}
if(errno != ENOENT) {
return -1;
}
return ftp_probe_create_dir_and_write(dst_path);
}
if(ftp_path_parent_dir(dst_path, parent, sizeof(parent)) != 0) {
return -1;
}
return ftp_probe_write_in_dir(parent);
}
static int
ftp_path_parent_stat(const char *path, struct stat *st) {
char parent[PATH_MAX];
if(!path || !st) {
errno = EINVAL;
return -1;
}
if(ftp_path_parent_dir(path, parent, sizeof(parent)) != 0) {
return -1;
}
while(1) {
char *slash;
if(stat(parent, st) == 0) {
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(strcmp(parent, "/") == 0) {
return -1;
}
slash = strrchr(parent, '/');
if(!slash) {
errno = ENOENT;
return -1;
}
if(slash == parent) {
parent[1] = '\0';
} else {
*slash = '\0';
}
}
}
static int
ftp_target_statvfs(const char *dst_path, int target_is_dir,
struct statvfs *vfs_out) {
char path[PATH_MAX];
if(!dst_path || !vfs_out) {
errno = EINVAL;
return -1;
}
if(target_is_dir) {
size_t len = strlen(dst_path);
if(len >= sizeof(path)) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(path, dst_path, len + 1);
} else if(ftp_path_parent_dir(dst_path, path, sizeof(path)) != 0) {
return -1;
}
while(1) {
if(statvfs(path, vfs_out) == 0) {
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(strcmp(path, "/") == 0) {
return -1;
}
if(ftp_path_parent_dir(path, path, sizeof(path)) != 0) {
return -1;
}
}
}
static int
ftp_copy_check_space(const char *dst_path, int target_is_dir,
uintmax_t total_bytes) {
struct statvfs vfs;
uintmax_t unit;
uintmax_t avail;
uintmax_t slack = 0;
uintmax_t required;
if(ftp_target_statvfs(dst_path, target_is_dir, &vfs) != 0) {
return -1;
}
unit = vfs.f_frsize ? (uintmax_t)vfs.f_frsize : (uintmax_t)vfs.f_bsize;
avail = (uintmax_t)vfs.f_bavail * unit;
if(total_bytes != 0) {
slack = (total_bytes + 99) / 100;
}
required = ftp_saturating_add(total_bytes, slack);
if(avail < required) {
errno = ENOSPC;
return -1;
}
return 0;
}
static int
ftp_copy_prepare_total(const char *src_path, const struct stat *src_st,
const char *dst_path, int target_is_dir,
uintmax_t *total_bytes_out) {
uintmax_t total_bytes = 0;
if(!src_path || !src_st || !dst_path || !total_bytes_out) {
errno = EINVAL;
return -1;
}
if(ftp_copy_total_for_stat(src_path, src_st, &total_bytes) != 0) {
return -1;
}
if(ftp_copy_check_space(dst_path, target_is_dir, total_bytes) != 0) {
return -1;
}
*total_bytes_out = total_bytes;
return 0;
}
static int
ftp_dir_is_empty(const char *path) {
DIR *dir;
int empty = 1;
int saved_errno = 0;
dir = opendir(path);
if(!dir) {
return -1;
}
struct dirent *ent;
int rc = ftp_dir_next_entry(dir, &ent);
if(rc < 0) {
saved_errno = errno;
empty = -1;
} else if(rc > 0) {
empty = 0;
}
if(closedir(dir) != 0 && saved_errno == 0 && empty >= 0) {
saved_errno = errno;
empty = -1;
}
if(saved_errno) {
errno = saved_errno;
}
return empty;
}
static int
ftp_move_same_device(const char *src_path, const struct stat *src_st,
const char *dst_path, int *same_device) {
struct stat st;
if(!src_path || !src_st || !dst_path || !same_device) {
errno = EINVAL;
return -1;
}
if(lstat(dst_path, &st) == 0) {
*same_device = st.st_dev == src_st->st_dev;
return 0;
}
if(errno != ENOENT) {
return -1;
}
if(ftp_path_parent_stat(dst_path, &st) != 0) {
return -1;
}
*same_device = st.st_dev == src_st->st_dev;
return 0;
}
/**
* Copy a symlink from src to dst.
**/
static int
ftp_copy_symlink(const char *src_path, const char *dst_path) {
char linkbuf[PATH_MAX + 1];
char tmp_path[PATH_MAX];
ssize_t len = readlink(src_path, linkbuf, sizeof(linkbuf) - 1);
int fd;
if(len < 0) {
return -1;
}
linkbuf[len] = '\0';
if(ftp_make_temp_path(dst_path, tmp_path, sizeof(tmp_path)) != 0) {
return -1;
}
fd = mkstemp(tmp_path);
if(fd < 0) {
return -1;
}
close(fd);
if(unlink(tmp_path) != 0) {
return -1;
}
if(symlink(linkbuf, tmp_path) != 0) {
return -1;
}
if(rename(tmp_path, dst_path) != 0) {
int saved_errno = errno;
unlink(tmp_path);
errno = saved_errno;
return -1;
}
return 0;
}
typedef enum {
FTP_COPY_REG,
FTP_COPY_DIR,
FTP_COPY_SYMLINK,
} ftp_copy_kind_t;
#define FTP_COPY_NOTIFY_CHECK_BYTES (4 * 1024 * 1024)
typedef struct {
uintmax_t total_bytes;
uintmax_t copied_bytes;
uintmax_t last_notify_bytes;
uintmax_t next_check_bytes;
struct timespec last_notify_ts;
int has_last_notify_ts;
} ftp_copy_progress_t;
typedef struct {
ftp_env_t *env;
ftp_bg_op_t op;
ftp_copy_kind_t kind;
char src[PATH_MAX];
char dst[PATH_MAX];
char src_notify[FTP_NOTIFY_PATH_SIZE];
char dst_notify[FTP_NOTIFY_PATH_SIZE];
ftp_copy_progress_t progress;
} ftp_copy_task_t;
static ftp_copy_task_t*
ftp_copy_create_task(ftp_env_t *env, ftp_bg_op_t op, ftp_copy_kind_t kind,
const char *src, const char *dst,
uintmax_t total_bytes) {
ftp_copy_task_t *task;
if(!env || !src || !dst) {
errno = EINVAL;
return NULL;
}
task = calloc(1, sizeof(*task));
if(!task) {
return NULL;
}
task->env = env;
task->op = op;
task->kind = kind;
task->progress.total_bytes = total_bytes;
snprintf(task->src, sizeof(task->src), "%s", src);
snprintf(task->dst, sizeof(task->dst), "%s", dst);
ftp_compact_path(src, task->src_notify, sizeof(task->src_notify));
ftp_compact_path(dst, task->dst_notify, sizeof(task->dst_notify));
return task;
}
static const char *
ftp_bg_op_name(ftp_bg_op_t op) {
return op == FTP_BG_MOVE ? "Move" : "Copy";
}
static void
ftp_copy_format_bytes(double bytes, const char *const *units, size_t unit_count,
char *out, size_t out_size) {
size_t unit = 0;
double value = bytes;
if(!out || out_size == 0) {
return;
}
if(value < 0.0) {
value = 0.0;
}
while(value >= 1024.0 && unit + 1 < unit_count) {
value /= 1024.0;
unit += 1;
}
if(unit == 0 || value >= 100.0) {
snprintf(out, out_size, "%.0f %s", value, units[unit]);
} else if(value >= 10.0) {
snprintf(out, out_size, "%.1f %s", value, units[unit]);
} else {
snprintf(out, out_size, "%.2f %s", value, units[unit]);
}
}
static void
ftp_copy_format_size(uintmax_t bytes, char *out, size_t out_size) {
static const char *units[] = {"B", "KiB", "MiB", "GiB", "TiB"};
ftp_copy_format_bytes((double)bytes, units,
sizeof(units) / sizeof(units[0]), out, out_size);
}
static void
ftp_copy_format_speed(double bytes_per_sec, char *out, size_t out_size) {
static const char *units[] = {"B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s"};
ftp_copy_format_bytes(bytes_per_sec, units,
sizeof(units) / sizeof(units[0]), out, out_size);
}
static int
ftp_copy_total_for_stat(const char *path, const struct stat *st,
uintmax_t *total_out) {
uintmax_t total = 0;
if(!path || !st || !total_out) {
errno = EINVAL;
return -1;
}
if(S_ISREG(st->st_mode) || S_ISLNK(st->st_mode)) {
total = (uintmax_t)st->st_size;
} else if(S_ISDIR(st->st_mode)) {
if(ftp_dir_size(path, &total)) {
return -1;
}
} else {
errno = EINVAL;
return -1;
}
*total_out = total;
return 0;
}
static unsigned
ftp_copy_progress_percent(const ftp_copy_task_t *task) {
if(!task) {
return 0;
}
if(task->progress.total_bytes == 0) {
return 100;
}
uintmax_t copied = task->progress.copied_bytes;
if(copied > task->progress.total_bytes) {
copied = task->progress.total_bytes;
}
return (unsigned)((copied * 100) / task->progress.total_bytes);
}
static void
ftp_copy_notify_start(ftp_copy_task_t *task) {
struct timespec now;
if(!task) {
return;
}
ftp_now(&now);
task->progress.last_notify_ts = now;
task->progress.has_last_notify_ts = 1;
task->progress.last_notify_bytes = task->progress.copied_bytes;
task->progress.next_check_bytes =
ftp_saturating_add(task->progress.copied_bytes,
FTP_COPY_NOTIFY_CHECK_BYTES);
notify("%s started: %s -> %s",
ftp_bg_op_name(task->op), task->src_notify, task->dst_notify);
}
static void
ftp_copy_notify_progress(ftp_copy_task_t *task) {
struct timespec now;
double elapsed;
double bytes_per_sec;
uintmax_t bytes_delta;
uintmax_t copied;
uintmax_t total;
char copied_str[32];
char total_str[32];
char speed[32];
if(!task) {
return;
}
ftp_now(&now);
if(!task->progress.has_last_notify_ts) {
task->progress.last_notify_ts = now;
task->progress.has_last_notify_ts = 1;
task->progress.last_notify_bytes = task->progress.copied_bytes;
task->progress.next_check_bytes =
ftp_saturating_add(task->progress.copied_bytes,
FTP_COPY_NOTIFY_CHECK_BYTES);
return;
}
elapsed = ftp_elapsed_seconds(&task->progress.last_notify_ts, &now);
if(elapsed < 10.0) {
task->progress.next_check_bytes =
ftp_saturating_add(task->progress.copied_bytes,
FTP_COPY_NOTIFY_CHECK_BYTES);
return;
}
bytes_delta = task->progress.copied_bytes - task->progress.last_notify_bytes;
bytes_per_sec = elapsed > 0.0 ? (double)bytes_delta / elapsed : 0.0;
copied = task->progress.copied_bytes;
total = task->progress.total_bytes;
if(total > 0 && copied > total) {
copied = total;
}
ftp_copy_format_size(copied, copied_str, sizeof(copied_str));
ftp_copy_format_size(total, total_str, sizeof(total_str));
ftp_copy_format_speed(bytes_per_sec, speed, sizeof(speed));
task->progress.last_notify_ts = now;
task->progress.last_notify_bytes = task->progress.copied_bytes;
task->progress.next_check_bytes =
ftp_saturating_add(task->progress.copied_bytes,
FTP_COPY_NOTIFY_CHECK_BYTES);
notify("%s %u%% (%s / %s) - %s",
ftp_bg_op_name(task->op),
ftp_copy_progress_percent(task), copied_str, total_str, speed);
}
static void
ftp_copy_progress_add(ftp_copy_task_t *task, uintmax_t amount) {
if(!task || amount == 0) {
return;
}
task->progress.copied_bytes =
ftp_saturating_add(task->progress.copied_bytes, amount);
if(task->progress.copied_bytes < task->progress.next_check_bytes) {
return;
}
ftp_copy_notify_progress(task);
}
static void
ftp_copy_notify_result(const ftp_copy_task_t *task, int rc, int err) {
unsigned pct = rc ? ftp_copy_progress_percent(task) : 100;
if(rc) {
if(err == 0) {
err = EIO;
}
notify("%s failed %u%%: %s -> %s (%s)",
ftp_bg_op_name(task->op), pct,
task->src_notify, task->dst_notify, strerror(err));
return;
}
notify("%s finished %u%%: %s -> %s (OK)",
ftp_bg_op_name(task->op), pct, task->src_notify, task->dst_notify);
}
static int
ftp_copy_start_task(ftp_env_t *env, ftp_copy_task_t *task) {
int thread_rc;
pthread_mutex_lock(&env->copy_mutex);
env->copy_in_progress = 1;
env->copy_thread_valid = 1;
pthread_mutex_unlock(&env->copy_mutex);
thread_rc = pthread_create(&env->copy_thread, NULL, ftp_copy_thread, task);
if(thread_rc != 0) {
pthread_mutex_lock(&env->copy_mutex);
env->copy_in_progress = 0;
env->copy_thread_valid = 0;
pthread_mutex_unlock(&env->copy_mutex);
free(task);
errno = thread_rc;
return ftp_perror(env);
}
return ftp_active_printf(env, "250 %s started in background\r\n",
ftp_bg_op_name(task->op));
}
static int
ftp_move_remove_source(ftp_copy_task_t *task) {
if(!task || task->op != FTP_BG_MOVE) {
return 0;
}
switch(task->kind) {
case FTP_COPY_REG:
if(unlink(task->src) != 0) {
return -1;
}
return 0;
case FTP_COPY_SYMLINK:
if(unlink(task->src) != 0) {
return -1;
}
return 0;
case FTP_COPY_DIR:
{
int err = 0;
if(ftp_rmda_delete_dir(task->src, &err, NULL) != 0) {
if(err) {
errno = err;
}
return -1;
}
}
return 0;
}
errno = EINVAL;
return -1;
}
static int
ftp_move_start_background(ftp_env_t *env, ftp_bg_op_t op,
const char *src_path, const char *dst_path,
const struct stat *src_st,
int create_parent_dirs) {
ftp_copy_task_t *task;
struct stat st;
uintmax_t total_bytes;
if(!env || !src_path || !dst_path || !src_st) {
errno = EINVAL;
return -1;
}
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env,
"450 Background file operation in progress\r\n");
}
if(S_ISREG(src_st->st_mode)) {
if(lstat(dst_path, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 0, create_parent_dirs) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, src_st, dst_path, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, op, FTP_COPY_REG,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISDIR(src_st->st_mode)) {
size_t src_len = strlen(src_path);
if(strncmp(dst_path, src_path, src_len) == 0 &&
(dst_path[src_len] == '/' || dst_path[src_len] == '\0')) {
return ftp_active_printf(env, "553 Cannot move into itself\r\n");
}
if(lstat(dst_path, &st) == 0) {
int empty;
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Not a directory\r\n");
}
empty = ftp_dir_is_empty(dst_path);
if(empty < 0) {
return ftp_perror(env);
}
if(!empty) {
return ftp_active_printf(env, "550 Target already exists\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 1, create_parent_dirs) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, src_st, dst_path, 1,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, op, FTP_COPY_DIR,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISLNK(src_st->st_mode)) {
if(lstat(dst_path, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 0, create_parent_dirs) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, src_st, dst_path, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, op, FTP_COPY_SYMLINK,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
return ftp_active_printf(env, "550 Unsupported file type\r\n");
}
/**
* Copy file metadata (mode, owner, group, timestamps) from src to dst.
**/
static int
ftp_copy_metadata(const char *src_path, const char *dst_path) {
struct stat st;
if(stat(src_path, &st)) {
return -1;
}
if(chmod(dst_path, st.st_mode & 07777)) {
if(errno != EPERM) {
return -1;
}
}
if(chown(dst_path, st.st_uid, st.st_gid)) {
if(errno != EPERM) {
return -1;
}
}
struct utimbuf times;
times.actime = st.st_atime;
times.modtime = st.st_mtime;
if(utime(dst_path, &times)) {
if(errno != EPERM) {
return -1;
}
}
return 0;
}
/**
* Copy symlink metadata (owner/group and timestamps) when supported.
**/
static void
ftp_copy_symlink_metadata(const char *src_path, const char *dst_path) {
#if defined(__linux__)
struct stat st;
if(lstat(src_path, &st) != 0) {
return;
}
if(lchown(dst_path, st.st_uid, st.st_gid) != 0 && errno != EPERM) {
return;
}
#ifdef UTIME_OMIT
struct timespec ts[2];
ts[0].tv_sec = st.st_atime;
ts[0].tv_nsec = 0;
ts[1].tv_sec = st.st_mtime;
ts[1].tv_nsec = 0;
(void)utimensat(AT_FDCWD, dst_path, ts, AT_SYMLINK_NOFOLLOW);
#endif
#else
(void)src_path;
(void)dst_path;
#endif
}
/**
* Create parent directories for a path.
**/
static int
ftp_mkdirs_parent(const char *path) {
if(!path || !*path) {
errno = EINVAL;
return -1;
}
char tmp[PATH_MAX];
size_t len = strlen(path);
if(len >= sizeof(tmp)) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(tmp, path, len + 1);
char *slash = strrchr(tmp, '/');
if(!slash || slash == tmp) {
return 0;
}
*slash = '\0';
for(char *p = tmp + 1; *p; p++) {
if(*p == '/') {
*p = '\0';
if(mkdir(tmp, 0777)) {
if(errno != EEXIST) {
return -1;
}
}
*p = '/';
}
}
if(mkdir(tmp, 0777)) {
if(errno != EEXIST) {
return -1;
}
}
return 0;
}
/**
* Copy a regular file from src to dst.
**/
static int
ftp_copy_file(const char *src_path, const char *dst_path,
void *buf, size_t bufsize, ftp_copy_task_t *task) {
char tmp_path[PATH_MAX];
#ifdef O_CLOEXEC
int src_flags = O_RDONLY | O_CLOEXEC;
#else
int src_flags = O_RDONLY;
#endif
#ifdef O_NOFOLLOW
src_flags |= O_NOFOLLOW;
#endif
int src_fd = -1;
int dst_fd = -1;
int free_buf = 0;
int saved_errno = 0;
void *tmp = buf;
size_t tmp_size = bufsize;
src_fd = open(src_path, src_flags, 0);
if(src_fd < 0) {
return -1;
}
if(ftp_make_temp_path(dst_path, tmp_path, sizeof(tmp_path)) != 0) {
saved_errno = errno;
close(src_fd);
errno = saved_errno;
return -1;
}
dst_fd = mkstemp(tmp_path);
if(dst_fd < 0) {
saved_errno = errno;
close(src_fd);
errno = saved_errno;
return -1;
}
if(!tmp || !tmp_size) {
tmp = malloc(IO_COPY_BUFSIZE);
tmp_size = IO_COPY_BUFSIZE;
free_buf = 1;
if(!tmp) {
saved_errno = errno;
close(src_fd);
close(dst_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
}
for(;;) {
ssize_t r = read(src_fd, tmp, tmp_size);
if(r < 0) {
if(errno == EINTR) {
continue;
}
saved_errno = errno;
if(free_buf) {
free(tmp);
}
close(src_fd);
close(dst_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
if(r == 0) {
break;
}
if(io_nwrite(dst_fd, tmp, (size_t)r)) {
saved_errno = errno;
if(free_buf) {
free(tmp);
}
close(src_fd);
close(dst_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
ftp_copy_progress_add(task, (uintmax_t)r);
}
if(free_buf) {
free(tmp);
}
if(close(dst_fd) != 0) {
saved_errno = errno;
close(src_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
dst_fd = -1;
if(ftp_copy_metadata(src_path, tmp_path)) {
saved_errno = errno;
close(src_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
if(rename(tmp_path, dst_path) != 0) {
saved_errno = errno;
close(src_fd);
unlink(tmp_path);
errno = saved_errno;
return -1;
}
close(src_fd);
return 0;
}
/**
* Copy a directory tree from src to dst.
**/
static int
ftp_copy_dir(const char *src_dir, const char *dst_dir,
void *buf, size_t bufsize, ftp_copy_task_t *task) {
DIR *dir = NULL;
struct dirent *ent = NULL;
struct stat st;
int res = 0;
int saved_errno = 0;
if(lstat(dst_dir, &st) == 0) {
if(!S_ISDIR(st.st_mode)) {
errno = ENOTDIR;
return -1;
}
} else if(errno == ENOENT) {
if(mkdir(dst_dir, 0777)) {
return -1;
}
} else {
return -1;
}
dir = opendir(src_dir);
if(!dir) {
return -1;
}
for(;;) {
int rc = ftp_dir_next_entry(dir, &ent);
if(rc <= 0) {
if(rc < 0) {
saved_errno = errno;
res = -1;
}
break;
}
char src_path[PATH_MAX];
char dst_path[PATH_MAX];
if(ftp_join_path(src_path, sizeof(src_path), src_dir, ent->d_name) != 0 ||
ftp_join_path(dst_path, sizeof(dst_path), dst_dir, ent->d_name) != 0) {
saved_errno = errno;
res = -1;
break;
}
if(ftp_dir_child_lstat(dir, src_dir, ent->d_name, &st) != 0) {
saved_errno = errno;
res = -1;
break;
}
if(S_ISDIR(st.st_mode)) {
if(ftp_copy_dir(src_path, dst_path, buf, bufsize, task)) {
saved_errno = errno;
res = -1;
break;
}
} else if(S_ISREG(st.st_mode)) {
if(ftp_copy_file(src_path, dst_path, buf, bufsize, task)) {
saved_errno = errno;
res = -1;
break;
}
} else if(S_ISLNK(st.st_mode)) {
if(ftp_copy_symlink(src_path, dst_path)) {
saved_errno = errno;
res = -1;
break;
}
ftp_copy_symlink_metadata(src_path, dst_path);
ftp_copy_progress_add(task, (uintmax_t)st.st_size);
} else {
errno = EINVAL;
saved_errno = errno;
res = -1;
break;
}
}
if(closedir(dir) && !saved_errno) {
saved_errno = errno;
res = -1;
}
if(saved_errno) {
errno = saved_errno;
}
if(res == 0) {
if(ftp_copy_metadata(src_dir, dst_dir)) {
return -1;
}
}
return res;
}
/**
* Join finished copy thread if needed.
**/
static void
ftp_copy_thread_cleanup(ftp_env_t *env) {
pthread_t thread;
int should_join = 0;
pthread_mutex_lock(&env->copy_mutex);
if(env->copy_thread_valid && !env->copy_in_progress) {
thread = env->copy_thread;
env->copy_thread_valid = 0;
should_join = 1;
}
pthread_mutex_unlock(&env->copy_mutex);
if(should_join) {
pthread_join(thread, NULL);
}
}
/**
* Background copy worker.
**/
static void*
ftp_copy_thread(void *arg) {
ftp_copy_task_t *task = (ftp_copy_task_t *)arg;
ftp_env_t *env = task->env;
int rc = 0;
int saved_errno = 0;
void *buf = NULL;
size_t bufsize = 0;
ftp_copy_notify_start(task);
if(task->kind == FTP_COPY_REG || task->kind == FTP_COPY_DIR) {
bufsize = IO_COPY_BUFSIZE;
buf = malloc(bufsize);
if(!buf) {
rc = -1;
errno = ENOMEM;
}
}
if(!rc) {
switch(task->kind) {
case FTP_COPY_REG:
rc = ftp_copy_file(task->src, task->dst, buf, bufsize, task);
break;
case FTP_COPY_DIR:
rc = ftp_copy_dir(task->src, task->dst, buf, bufsize, task);
break;
case FTP_COPY_SYMLINK:
rc = ftp_copy_symlink(task->src, task->dst);
if(!rc) {
struct stat st;
if(lstat(task->src, &st) == 0) {
ftp_copy_progress_add(task, (uintmax_t)st.st_size);
}
ftp_copy_symlink_metadata(task->src, task->dst);
}
break;
}
}
if(rc) {
saved_errno = errno;
}
if(!rc && ftp_move_remove_source(task) != 0) {
rc = -1;
saved_errno = errno;
}
if(buf) {
free(buf);
}
ftp_copy_notify_result(task, rc, saved_errno);
pthread_mutex_lock(&env->copy_mutex);
env->copy_in_progress = 0;
pthread_mutex_unlock(&env->copy_mutex);
free(task);
return NULL;
}
/**
* Copy a file previously specified by CPFR to a new location.
**/
int
ftp_cmd_CPTO(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
char argbuf[PATH_MAX + 1];
struct stat src_st;
struct stat st;
const char *path_arg = NULL;
uintmax_t total_bytes = 0;
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env, "450 Background file operation in progress\r\n");
}
path_arg = ftp_copy_path_arg(arg, argbuf, sizeof(argbuf));
if(!path_arg) {
return ftp_active_printf(env, "501 Usage: CPTO <PATH>\r\n");
}
if(!env->copy_ready) {
return ftp_active_printf(env, "503 Bad sequence of commands\r\n");
}
env->copy_ready = 0;
if(lstat(env->copy_path, &src_st)) {
return ftp_perror(env);
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), path_arg)) {
return ftp_perror(env);
}
if(strcmp(env->copy_path, pathbuf) == 0) {
return ftp_active_printf(env, "553 Source and destination are the same\r\n");
}
if(S_ISREG(src_st.st_mode)) {
ftp_copy_task_t *task;
if(lstat(pathbuf, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(pathbuf, 0, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(env->copy_path, &src_st, pathbuf, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_REG,
env->copy_path, pathbuf, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISDIR(src_st.st_mode)) {
ftp_copy_task_t *task;
size_t src_len = strlen(env->copy_path);
if(strncmp(pathbuf, env->copy_path, src_len) == 0 &&
(pathbuf[src_len] == '/' || pathbuf[src_len] == '\0')) {
return ftp_active_printf(env, "553 Cannot copy into itself\r\n");
}
if(lstat(pathbuf, &st) == 0) {
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Not a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(pathbuf, 1, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(env->copy_path, &src_st, pathbuf, 1,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_DIR,
env->copy_path, pathbuf, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISLNK(src_st.st_mode)) {
ftp_copy_task_t *task;
if(lstat(pathbuf, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(pathbuf, 0, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(env->copy_path, &src_st, pathbuf, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_SYMLINK,
env->copy_path, pathbuf, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
return ftp_active_printf(env, "550 Unsupported file type\r\n");
}
/**
* Copy a path on the server (COPY <FROM> <TO>).
**/
int
ftp_cmd_COPY(ftp_env_t *env, const char* arg) {
char src_arg[PATH_MAX + 1];
char dst_arg[PATH_MAX + 1];
char src_path[PATH_MAX];
char dst_path[PATH_MAX];
struct stat src_st;
struct stat st;
uintmax_t total_bytes = 0;
ftp_copy_thread_cleanup(env);
ftp_delete_thread_cleanup(env);
if(ftp_bg_op_busy(env)) {
return ftp_active_printf(env, "450 Background file operation in progress\r\n");
}
if(ftp_split_copy_args(arg, src_arg, sizeof(src_arg),
dst_arg, sizeof(dst_arg))) {
return ftp_active_printf(env, "501 Usage: COPY <FROM> <TO>\r\n");
}
if(ftp_abspath(env, src_path, sizeof(src_path), src_arg)) {
return ftp_perror(env);
}
if(ftp_abspath(env, dst_path, sizeof(dst_path), dst_arg)) {
return ftp_perror(env);
}
if(strcmp(src_path, dst_path) == 0) {
return ftp_active_printf(env, "553 Source and destination are the same\r\n");
}
if(lstat(src_path, &src_st)) {
return ftp_perror(env);
}
if(S_ISREG(src_st.st_mode)) {
ftp_copy_task_t *task;
if(lstat(dst_path, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 0, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, &src_st, dst_path, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_REG,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISDIR(src_st.st_mode)) {
ftp_copy_task_t *task;
size_t src_len = strlen(src_path);
if(strncmp(dst_path, src_path, src_len) == 0 &&
(dst_path[src_len] == '/' || dst_path[src_len] == '\0')) {
return ftp_active_printf(env, "553 Cannot copy into itself\r\n");
}
if(lstat(dst_path, &st) == 0) {
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Not a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 1, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, &src_st, dst_path, 1,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_DIR,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
if(S_ISLNK(src_st.st_mode)) {
ftp_copy_task_t *task;
if(lstat(dst_path, &st) == 0) {
if(S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 Target is a directory\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
if(ftp_copy_probe_destination(dst_path, 0, 1) != 0) {
return ftp_perror(env);
}
if(ftp_copy_prepare_total(src_path, &src_st, dst_path, 0,
&total_bytes) != 0) {
return ftp_perror(env);
}
task = ftp_copy_create_task(env, FTP_BG_COPY, FTP_COPY_SYMLINK,
src_path, dst_path, total_bytes);
if(!task) {
return ftp_perror(env);
}
return ftp_copy_start_task(env, task);
}
return ftp_active_printf(env, "550 Unsupported file type\r\n");
}
/**
* Obtain the size of a given file or directory.
**/
int
ftp_cmd_DSIZ(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st;
uintmax_t size = 0;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: DSIZ <PATH>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_active_printf(env, "550 %s: %s.\r\n", arg, strerror(errno));
}
if(stat(pathbuf, &st)) {
return ftp_active_printf(env, "550 %s: %s.\r\n", arg, strerror(errno));
}
if(S_ISREG(st.st_mode)) {
return ftp_active_printf(env, "550 %s: Is a file.\r\n", arg);
}
if(!S_ISDIR(st.st_mode)) {
return ftp_active_printf(env, "550 %s: Not a directory.\r\n", arg);
}
if(ftp_dir_size(pathbuf, &size)) {
return ftp_active_printf(env, "550 %s: %s.\r\n", arg, strerror(errno));
}
return ftp_active_printf(env, "213 %" PRIuMAX "\r\n", size);
}
/**
* Obtain the size of a given file.
**/
int
ftp_cmd_SIZE(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat st = {0};
if(env->type == 'A') {
return ftp_active_printf(env, "504 SIZE not supported in ASCII mode\r\n");
}
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: SIZE <FILENAME>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(env->self2elf) {
st.st_size = self_get_elfsize(pathbuf);
}
if(!st.st_size) {
if(stat(pathbuf, &st)) {
return ftp_perror(env);
}
}
return ftp_active_printf(env, "213 %" PRIu64 "\r\n", st.st_size);
}
/**
* Store recieved data in a given file.
**/
int
ftp_cmd_STOR(ftp_env_t *env, const char* arg) {
off_t off = env->data_offset;
int is_rest = env->data_offset_is_rest;
char pathbuf[PATH_MAX];
void *readbuf = env->xfer_buf;
size_t bufsize = env->xfer_buf_size;
int err = 0;
int free_buf = 0;
ssize_t len = 0;
struct stat st;
int flags = O_WRONLY;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int fd;
env->data_offset = 0;
env->data_offset_is_rest = 0;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: STOR <FILENAME>\r\n");
}
if(env->type == 'A' && off != 0 && is_rest) {
env->data_offset = 0;
return ftp_active_printf(env, "504 REST not supported in ASCII mode\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
// Reject symlinks and non-regular files as upload targets.
// (If you want to allow symlinks, remove the lstat() block below.)
#ifdef S_IFLNK
{
struct stat lst;
if(lstat(pathbuf, &lst) == 0) {
if(S_ISLNK(lst.st_mode)) {
return ftp_active_printf(env, "550 Symlinks are not allowed\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
}
#endif
if(stat(pathbuf, &st) == 0) {
if(!S_ISREG(st.st_mode)) {
return ftp_active_printf(env, "550 Not a regular file\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
int precheck = ftp_data_precheck(env);
if(precheck) {
return precheck < 0 ? precheck : 0;
}
if(off == 0) {
flags |= O_CREAT | O_TRUNC;
}
if((fd = open(pathbuf, flags, 0777)) < 0) {
return ftp_perror(env);
}
if(off > 0) {
if(fstat(fd, &st)) {
err = ftp_perror(env);
close(fd);
return err;
}
if(!S_ISREG(st.st_mode)) {
close(fd);
return ftp_active_printf(env, "550 Not a regular file\r\n");
}
if(off > st.st_size) {
close(fd);
return ftp_active_printf(env, "551 Restart point beyond EOF\r\n");
}
}
if(lseek(fd, off, SEEK_SET) < 0) {
err = ftp_perror(env);
close(fd);
return err;
}
int open_err = ftp_data_xfer_start(env, 1, 0);
if(open_err) {
close(fd);
return open_err < 0 ? open_err : 0;
}
if(!readbuf || !bufsize) {
readbuf = malloc(IO_COPY_BUFSIZE);
bufsize = IO_COPY_BUFSIZE;
free_buf = 1;
if(!readbuf) {
err = ftp_perror(env);
ftp_data_close(env);
close(fd);
return err;
}
}
if(env->type == 'A') {
if(ftp_copy_ascii_in(env, fd, &off)) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
if(free_buf) {
free(readbuf);
}
close(fd);
return err;
}
} else {
while((len = ftp_data_read(env, readbuf, bufsize)) > 0) {
if(io_nwrite(fd, readbuf, (size_t)len)) {
err = ftp_perror(env);
ftp_data_close(env);
if(free_buf) {
free(readbuf);
}
close(fd);
return err;
}
off += len;
}
}
if(env->type != 'A' && len < 0) {
err = ftp_data_xfer_error_reply(env);
ftp_data_close(env);
if(free_buf) {
free(readbuf);
}
close(fd);
return err;
}
if(free_buf) {
free(readbuf);
}
if(ftruncate(fd, off)) {
err = ftp_perror(env);
ftp_data_close(env);
close(fd);
return err;
}
close(fd);
if(ftp_data_xfer_complete(env)) {
return ftp_data_xfer_error_reply(env);
}
return ftp_active_printf(env, "226 Data transfer complete\r\n");
}
/**
* Append to an existing file.
**/
int
ftp_cmd_APPE(ftp_env_t *env, const char* arg) {
char pathbuf[PATH_MAX];
struct stat statbuf;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: APPE <FILENAME>\r\n");
}
env->data_offset = 0;
env->data_offset_is_rest = 0;
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
#ifdef S_IFLNK
{
struct stat lst;
if(lstat(pathbuf, &lst) == 0) {
if(S_ISLNK(lst.st_mode)) {
return ftp_active_printf(env, "550 Symlinks are not allowed\r\n");
}
} else if(errno != ENOENT) {
return ftp_perror(env);
}
}
#endif
if(stat(pathbuf, &statbuf) == 0) {
if(!S_ISREG(statbuf.st_mode)) {
return ftp_active_printf(env, "550 Not a regular file\r\n");
}
env->data_offset = statbuf.st_size;
} else {
if(errno != ENOENT) {
return ftp_perror(env);
}
env->data_offset = 0;
}
return ftp_cmd_STOR(env, arg);
}
/**
* Return system type.
**/
int
ftp_cmd_SYST(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "215 UNIX Type: L8\r\n");
}
/**
* Sets the transfer mode (ASCII or Binary).
**/
int
ftp_cmd_TYPE(ftp_env_t *env, const char* arg) {
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: TYPE <TYPE>\r\n");
}
switch(arg[0]) {
#ifdef DISABLE_ASCII_MODE
case 'A':
case 'I':
env->type = 'I';
return ftp_active_printf(env, "200 Type set to I\r\n");
#else
case 'A':
env->data_offset = 0;
env->type = 'A';
return ftp_active_printf(env, "200 Type set to %c\r\n", env->type);
case 'I':
env->type = 'I';
return ftp_active_printf(env, "200 Type set to %c\r\n", env->type);
#endif
default:
return ftp_active_printf(env, "504 Type not supported\r\n");
}
}
/**
* Authenticate user.
**/
int
ftp_cmd_USER(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "230 User logged in\r\n");
}
/**
* Specify user password.
**/
int
ftp_cmd_PASS(ftp_env_t *env, const char *arg) {
(void)arg;
return ftp_active_printf(env, "230 User logged in\r\n");
}
/**
* Reset MODE Z settings to their defaults.
**/
static void
ftp_mode_z_reset_defaults(ftp_env_t *env) {
env->mode_z_level = FTP_MODE_Z_LEVEL_DEFAULT;
env->mode_z_extra = 1;
}
/**
* Parse the next space-delimited token.
**/
static int
ftp_next_token(const char **argp, char *buf, size_t buf_size) {
const char *arg = *argp;
size_t len;
arg += strspn(arg, " ,");
if(!*arg) {
buf[0] = '\0';
*argp = arg;
return 0;
}
len = strcspn(arg, " ,");
if(len >= buf_size) {
len = buf_size - 1;
}
memcpy(buf, arg, len);
buf[len] = '\0';
arg += len;
arg += strspn(arg, " ,");
*argp = arg;
return 1;
}
/**
* Handle OPTS MODE Z requests.
**/
static int
ftp_cmd_OPTS_mode_z(ftp_env_t *env, const char *arg) {
char name[32];
char value[32];
int level;
int extra;
if(env->mode_z) {
return ftp_active_printf(env, "451 MODE Z is active during transfer\r\n");
}
level = env->mode_z_level;
extra = env->mode_z_extra;
if(!*arg) {
ftp_mode_z_reset_defaults(env);
return ftp_active_printf(env, "200 MODE Z settings reset\r\n");
}
while(ftp_next_token(&arg, name, sizeof(name))) {
if(!ftp_next_token(&arg, value, sizeof(value))) {
return ftp_active_printf(env, "501 OPTS MODE Z requires <NAME> <VALUE>\r\n");
}
if(!strcasecmp(name, "ENGINE")) {
if(strcasecmp(value, "ZLIB")) {
return ftp_active_printf(env, "501 MODE Z ENGINE %s is invalid\r\n", value);
}
continue;
}
if(!strcasecmp(name, "METHOD")) {
if(strcmp(value, "8")) {
return ftp_active_printf(env, "501 MODE Z METHOD %s is invalid\r\n", value);
}
continue;
}
if(!strcasecmp(name, "LEVEL")) {
char *end = NULL;
long tmp = strtol(value, &end, 10);
if(*value == '\0' || *end || tmp < 0 || tmp > 9) {
return ftp_active_printf(env, "501 MODE Z LEVEL %s is invalid\r\n", value);
}
level = (int)tmp;
continue;
}
if(!strcasecmp(name, "EXTRA")) {
if(!strcasecmp(value, "ON")) {
extra = 1;
continue;
}
if(!strcasecmp(value, "OFF")) {
extra = 0;
continue;
}
return ftp_active_printf(env, "501 MODE Z EXTRA %s is invalid\r\n", value);
}
if(!strcasecmp(name, "BLOCKSIZE")) {
return ftp_active_printf(env, "501 MODE Z BLOCKSIZE is not available\r\n");
}
return ftp_active_printf(env, "501 MODE Z %s is not available\r\n", name);
}
env->mode_z_level = level;
env->mode_z_extra = extra;
return ftp_active_printf(env, "200 MODE Z options updated\r\n");
}
/**
* Feature list.
**/
int
ftp_cmd_FEAT(ftp_env_t *env, const char *arg) {
(void)arg;
if(!ftp_active_printf(env,
"211-Features:\r\n"
" MLST type*;unique*;size*;modify*;unix.mode*;"
"unix.uid*;unix.gid*;\r\n"
" AVBL\r\n"
" XQUOTA\r\n"
" MLSD\r\n"
" MDTM\r\n"
" SIZE\r\n"
" DSIZ\r\n"
" RMDA\r\n"
" EPSV\r\n"
" EPRT\r\n"
" KILL\r\n"
" MTRW\r\n"
" SELF\r\n"
" SCHK\r\n"
" SITE CHMOD\r\n"
" SITE UMASK\r\n"
" SITE SYMLINK\r\n"
" SITE RMDIR\r\n"
" SITE CPFR\r\n"
" SITE CPTO\r\n"
" SITE COPY\r\n"
" SITE MOVE\r\n"
" SITE AUTHID\r\n"
" UTF8\r\n")) {
if(ftp_active_printf(env, " MODE Z\r\n")) {
return -1;
}
return ftp_active_printf(env,
" REST STREAM\r\n"
"211 End\r\n");
}
return -1;
}
/**
* Set options.
**/
int
ftp_cmd_OPTS(ftp_env_t *env, const char *arg) {
char opt[16];
char val[16];
size_t len = 0;
const char *rest = NULL;
if(!*arg) {
return ftp_active_printf(env, "501 Usage: OPTS UTF8 ON\r\n");
}
len = strcspn(arg, " ");
if(len >= sizeof(opt)) {
len = sizeof(opt) - 1;
}
memcpy(opt, arg, len);
opt[len] = '\0';
arg += len;
arg += strspn(arg, " ");
len = strcspn(arg, " ");
if(len >= sizeof(val)) {
len = sizeof(val) - 1;
}
memcpy(val, arg, len);
val[len] = '\0';
rest = arg + strcspn(arg, " ");
rest += strspn(rest, " ");
if(!strcasecmp(opt, "UTF8")) {
if(!val[0] || !strcasecmp(val, "ON")) {
return ftp_active_printf(env, "200 UTF8 enabled\r\n");
}
if(!strcasecmp(val, "OFF")) {
return ftp_active_printf(env, "200 UTF8 disabled\r\n");
}
return ftp_active_printf(env, "501 Usage: OPTS UTF8 ON\r\n");
}
if(!strcasecmp(opt, "MODE")) {
if(strcasecmp(val, "Z")) {
return ftp_active_printf(env, "504 Option not supported\r\n");
}
return ftp_cmd_OPTS_mode_z(env, rest);
}
return ftp_active_printf(env, "504 Option not supported\r\n");
}
/**
* Return modification time.
**/
int
ftp_cmd_MDTM(ftp_env_t *env, const char *arg) {
char pathbuf[PATH_MAX];
char timebuf[32];
struct stat st;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: MDTM <FILENAME>\r\n");
}
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), arg)) {
return ftp_perror(env);
}
if(stat(pathbuf, &st)) {
return ftp_perror(env);
}
if(ftp_format_mdtm(st.st_mtime, timebuf, sizeof(timebuf))) {
return ftp_active_printf(env, "550 MDTM failed\r\n");
}
return ftp_active_printf(env, "213 %s\r\n", timebuf);
}
/**
* Return machine-readable info for one path.
**/
int
ftp_cmd_MLST(ftp_env_t *env, const char *arg) {
char pathbuf[PATH_MAX];
char namebuf[PATH_MAX + 1];
char *typebuf = NULL;
char *linebuf = NULL;
struct stat st;
const char *name = NULL;
const char *type = "type=unknown;";
char uniquebuf[64];
const char *unique = "unique=;";
uintmax_t size = 0;
unsigned mode_bits = 0;
uintmax_t uid = 0;
uintmax_t gid = 0;
const char *path_arg = ftp_copy_path_arg(arg, namebuf, sizeof(namebuf));
if(path_arg) {
if(ftp_abspath(env, pathbuf, sizeof(pathbuf), path_arg)) {
return ftp_perror(env);
}
name = path_arg;
} else {
if(ftp_normpath(env->cwd, pathbuf, sizeof(pathbuf))) {
return ftp_perror(env);
}
name = pathbuf;
}
if(lstat(pathbuf, &st)) {
return ftp_perror(env);
}
typebuf = malloc(PATH_MAX + 1);
if(!typebuf) {
return ftp_perror(env);
}
linebuf = malloc(PATH_MAX * 3);
if(!linebuf) {
free(typebuf);
return ftp_perror(env);
}
type = ftp_mlst_type_fact(typebuf, PATH_MAX + 1, name, &st, pathbuf);
unique = ftp_mlst_unique_fact(uniquebuf, sizeof(uniquebuf), &st);
if(env->self2elf && S_ISREG(st.st_mode)) {
size_t elf_size = self_is_valid(pathbuf);
size = elf_size ? (uintmax_t)elf_size : (uintmax_t)st.st_size;
} else {
size = (uintmax_t)st.st_size;
}
mode_bits = (unsigned)(st.st_mode & 07777);
uid = (uintmax_t)st.st_uid;
gid = (uintmax_t)st.st_gid;
if(ftp_mlst_format_line(linebuf, PATH_MAX * 3, 1, type, unique, size,
st.st_mtime, mode_bits, uid, gid, name)) {
free(typebuf);
free(linebuf);
return ftp_active_printf(env, "550 MLST failed\r\n");
}
if(name[0] == '/' && name[1] == '\0') {
name = "/";
}
if(ftp_active_printf(env, "250-Listing\r\n")) {
free(typebuf);
free(linebuf);
return -1;
}
if(ftp_active_printf(env, "%s", linebuf)) {
free(typebuf);
free(linebuf);
return -1;
}
free(typebuf);
free(linebuf);
return ftp_active_printf(env, "250 End\r\n");
}
/**
* Status info.
**/
int
ftp_cmd_STAT(ftp_env_t *env, const char *arg) {
(void)arg;
if(ftp_active_printf(env, "211-FTP server status:\r\n")) {
return -1;
}
if(ftp_active_printf(env, " CWD %s\r\n", env->cwd)) {
return -1;
}
return ftp_active_printf(env, "211 End\r\n");
}
/**
* Help.
**/
int
ftp_cmd_HELP(ftp_env_t *env, const char *arg) {
(void)arg;
return ftp_active_printf(env,
"214-Commands:\r\n"
" USER PASS PWD CWD CDUP TYPE SIZE DSIZ MDTM AVBL\r\n"
" LIST NLST MLSD MLST RETR STOR APPE\r\n"
" DELE RMD RMDA MKD RNFR RNTO REST XQUOTA\r\n"
" PASV PORT EPSV EPRT SYST NOOP QUIT\r\n"
" SITE CHMOD UMASK SYMLINK RMDIR CPFR CPTO COPY MOVE AUTHID\r\n"
"214 End\r\n");
}
/**
* Transfer mode.
**/
int
ftp_cmd_MODE(ftp_env_t *env, const char *arg) {
if(!arg[0] || arg[1] != '\0') {
return ftp_active_printf(env, "501 Usage: MODE <S|Z>\r\n");
}
if(arg[0] == 'S' || arg[0] == 's') {
env->mode = 'S';
return ftp_active_printf(env, "200 Mode set to S\r\n");
}
if(arg[0] == 'Z' || arg[0] == 'z') {
env->mode = 'Z';
return ftp_active_printf(env, "200 Mode set to Z\r\n");
}
return ftp_active_printf(env, "504 MODE not supported\r\n");
}
/**
* File structure.
**/
int
ftp_cmd_STRU(ftp_env_t *env, const char *arg) {
(void)env;
if(!arg[0]) {
return ftp_active_printf(env, "501 Usage: STRU F\r\n");
}
if(arg[0] == 'F' || arg[0] == 'f') {
return ftp_active_printf(env, "200 Structure set to F\r\n");
}
return ftp_active_printf(env, "504 STRU not supported\r\n");
}
/**
* Allocate storage (no-op).
**/
int
ftp_cmd_ALLO(ftp_env_t *env, const char *arg) {
(void)arg;
return ftp_active_printf(env, "200 ALLO OK\r\n");
}
/**
* Abort transfer.
**/
int
ftp_cmd_ABOR(ftp_env_t *env, const char *arg) {
(void)arg;
if(env->data_fd >= 0) {
ftp_data_close(env);
env->data_offset = 0;
if(ftp_active_printf(env,
"426 Data connection closed; transfer aborted\r\n")) {
return -1;
}
return ftp_active_printf(env, "226 Abort successful\r\n");
}
env->data_offset = 0;
return ftp_active_printf(env, "225 No transfer to abort\r\n");
}
/**
* Custom command that terminates the server.
**/
int
ftp_cmd_KILL(ftp_env_t *env, const char* arg) {
FTP_LOG_PUTS("Server killed");
exit(EXIT_SUCCESS);
return -1;
}
/**
* Custom command to toggle SELF transfer mode.
**/
int
ftp_cmd_SELF(ftp_env_t *env, const char* arg) {
env->self2elf = !env->self2elf;
if(env->self2elf) {
return ftp_active_printf(env, "200 SELF transfer mode enabled\r\n");
} else {
return ftp_active_printf(env, "200 SELF transfer mode disabled\r\n");
}
}
/**
* Toggle SELF digest verification.
**/
int
ftp_cmd_SELFCHK(ftp_env_t *env, const char *arg) {
if(arg[0]) {
char *end = NULL;
long val = strtol(arg, &end, 10);
if(end == arg || (*end && *end != ' ')) {
return ftp_active_printf(env, "501 Usage: SCHK <0|1>\r\n");
}
env->self_verify = val ? 1 : 0;
} else {
env->self_verify = !env->self_verify;
}
if(env->self_verify) {
return ftp_active_printf(env, "200 SELF digest verification enabled\r\n");
} else {
return ftp_active_printf(env, "200 SELF digest verification disabled\r\n");
}
}
/**
* Unsupported command.
**/
int
ftp_cmd_unavailable(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "502 Command not implemented\r\n");
}
/**
* Unknown command.
**/
int
ftp_cmd_unknown(ftp_env_t *env, const char* arg) {
return ftp_active_printf(env, "502 Command not recognized\r\n");
}
/*
Local Variables:
c-file-style: "gnu"
End:
*/