920 lines
31 KiB
C
920 lines
31 KiB
C
/*
|
|
|
|
ftpii -- an FTP server for the Wii
|
|
|
|
Copyright (C) 2008 Joseph Jordan <joe.ftpii@psychlaw.com.au>
|
|
|
|
This software is provided 'as-is', without any express or implied warranty.
|
|
In no event will the authors be held liable for any damages arising from
|
|
the use of this software.
|
|
|
|
Permission is granted to anyone to use this software for any purpose,
|
|
including commercial applications, and to alter it and redistribute it
|
|
freely, subject to the following restrictions:
|
|
|
|
1.The origin of this software must not be misrepresented; you must not
|
|
claim that you wrote the original software. If you use this software in a
|
|
product, an acknowledgment in the product documentation would be
|
|
appreciated but is not required.
|
|
|
|
2.Altered source versions must be plainly marked as such, and must not be
|
|
misrepresented as being the original software.
|
|
|
|
3.This notice may not be removed or altered from any source distribution.
|
|
|
|
*/
|
|
#include <malloc.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/dir.h>
|
|
#include <coreinit/thread.h>
|
|
#include <unistd.h>
|
|
#include <nsysnet/socket.h>
|
|
#include "main.h"
|
|
|
|
//! TODO: fix those function
|
|
#define gettime() OSGetTick()
|
|
#define errno wiiu_geterrno()
|
|
|
|
#include "ftp.h"
|
|
#include "virtualpath.h"
|
|
#include "net.h"
|
|
#include "vrt.h"
|
|
|
|
#define UNUSED __attribute__((unused))
|
|
|
|
#define FTP_BUFFER_SIZE 1024
|
|
#define MAX_CLIENTS 5
|
|
|
|
extern void console_printf(const char *format, ...);
|
|
|
|
static const uint16_t SRC_PORT = 20;
|
|
static const int32_t EQUIT = 696969;
|
|
static const char *CRLF = "\r\n";
|
|
static const uint32_t CRLF_LENGTH = 2;
|
|
|
|
static uint8_t num_clients = 0;
|
|
static uint16_t passive_port = 1024;
|
|
static char *password = NULL;
|
|
|
|
typedef int32_t (*data_connection_callback)(int32_t data_socket, void *arg);
|
|
|
|
struct client_struct {
|
|
int32_t socket;
|
|
char representation_type;
|
|
int32_t passive_socket;
|
|
int32_t data_socket;
|
|
char cwd[MAXPATHLEN];
|
|
char pending_rename[MAXPATHLEN];
|
|
off_t restart_marker;
|
|
struct sockaddr_in address;
|
|
bool authenticated;
|
|
char buf[FTP_BUFFER_SIZE];
|
|
int32_t offset;
|
|
bool data_connection_connected;
|
|
data_connection_callback data_callback;
|
|
void *data_connection_callback_arg;
|
|
void (*data_connection_cleanup)(void *arg);
|
|
uint64_t data_connection_timer;
|
|
};
|
|
|
|
typedef struct client_struct client_t;
|
|
|
|
static client_t *clients[MAX_CLIENTS] = { NULL };
|
|
|
|
void set_ftp_password(char *new_password) {
|
|
if (password)
|
|
free(password);
|
|
if (new_password) {
|
|
password = malloc(strlen(new_password) + 1);
|
|
if (!password)
|
|
return;
|
|
|
|
strcpy((char *)password, new_password);
|
|
} else {
|
|
password = NULL;
|
|
}
|
|
}
|
|
|
|
static bool compare_ftp_password(char *password_attempt) {
|
|
return !password || !strcmp((char *)password, password_attempt);
|
|
}
|
|
|
|
/*
|
|
TODO: support multi-line reply
|
|
*/
|
|
static int32_t write_reply(client_t *client, uint16_t code, char *msg) {
|
|
uint32_t msglen = 4 + strlen(msg) + CRLF_LENGTH;
|
|
char * msgbuf = (char *) malloc(msglen + 1);
|
|
if (msgbuf == NULL)
|
|
return -ENOMEM;
|
|
sprintf(msgbuf, "%u %s\r\n", code, msg);
|
|
console_printf("Wrote reply: %s", msgbuf);
|
|
int32_t ret = send_exact(client->socket, msgbuf, msglen);
|
|
free(msgbuf);
|
|
return ret;
|
|
}
|
|
|
|
static void close_passive_socket(client_t *client) {
|
|
if (client->passive_socket >= 0) {
|
|
network_close_blocking(client->passive_socket);
|
|
client->passive_socket = -1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
result must be able to hold up to maxsplit+1 null-terminated strings of length strlen(s)
|
|
returns the number of strings stored in the result array (up to maxsplit+1)
|
|
*/
|
|
static uint32_t split(char *s, char sep, uint32_t maxsplit, char *result[]) {
|
|
uint32_t num_results = 0;
|
|
uint32_t result_pos = 0;
|
|
uint32_t trim_pos = 0;
|
|
bool in_word = false;
|
|
for (; *s; s++) {
|
|
if (*s == sep) {
|
|
if (num_results <= maxsplit) {
|
|
in_word = false;
|
|
continue;
|
|
} else if (!trim_pos) {
|
|
trim_pos = result_pos;
|
|
}
|
|
} else if (trim_pos) {
|
|
trim_pos = 0;
|
|
}
|
|
if (!in_word) {
|
|
in_word = true;
|
|
if (num_results <= maxsplit) {
|
|
num_results++;
|
|
result_pos = 0;
|
|
}
|
|
}
|
|
result[num_results - 1][result_pos++] = *s;
|
|
result[num_results - 1][result_pos] = '\0';
|
|
}
|
|
if (trim_pos) {
|
|
result[num_results - 1][trim_pos] = '\0';
|
|
}
|
|
uint32_t i = num_results;
|
|
for (i = num_results; i <= maxsplit; i++) {
|
|
result[i][0] = '\0';
|
|
}
|
|
return num_results;
|
|
}
|
|
|
|
static int32_t ftp_USER(client_t *client, char *username UNUSED) {
|
|
return write_reply(client, 331, "User name okay, need password.");
|
|
}
|
|
|
|
static int32_t ftp_PASS(client_t *client, char *password_attempt) {
|
|
if (compare_ftp_password(password_attempt)) {
|
|
client->authenticated = true;
|
|
return write_reply(client, 230, "User logged in, proceed.");
|
|
} else {
|
|
return write_reply(client, 530, "Login incorrect.");
|
|
}
|
|
}
|
|
|
|
static int32_t ftp_REIN(client_t *client, char *rest UNUSED) {
|
|
close_passive_socket(client);
|
|
strcpy(client->cwd, "/");
|
|
client->representation_type = 'A';
|
|
client->authenticated = false;
|
|
return write_reply(client, 220, "Service ready for new user.");
|
|
}
|
|
|
|
static int32_t ftp_QUIT(client_t *client, char *rest UNUSED) {
|
|
// TODO: dont quit if xfer in progress
|
|
int32_t result = write_reply(client, 221, "Service closing control connection.");
|
|
return result < 0 ? result : -EQUIT;
|
|
}
|
|
|
|
static int32_t ftp_SYST(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 215, "UNIX Type: L8 Version: ftpii");
|
|
}
|
|
|
|
static int32_t ftp_TYPE(client_t *client, char *rest) {
|
|
char representation_type[FTP_BUFFER_SIZE], param[FTP_BUFFER_SIZE];
|
|
char *args[] = { representation_type, param };
|
|
uint32_t num_args = split(rest, ' ', 1, args);
|
|
if (num_args == 0) {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
} else if ((!strcasecmp("A", representation_type) && (!*param || !strcasecmp("N", param))) ||
|
|
(!strcasecmp("I", representation_type) && num_args == 1)) {
|
|
client->representation_type = *representation_type;
|
|
} else {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
char msg[15];
|
|
sprintf(msg, "Type set to %s.", representation_type);
|
|
return write_reply(client, 200, msg);
|
|
}
|
|
|
|
static int32_t ftp_MODE(client_t *client, char *rest) {
|
|
if (!strcasecmp("S", rest)) {
|
|
return write_reply(client, 200, "Mode S ok.");
|
|
} else {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
}
|
|
|
|
static int32_t ftp_PWD(client_t *client, char *rest UNUSED) {
|
|
char msg[MAXPATHLEN + 24];
|
|
// TODO: escape double-quotes
|
|
sprintf(msg, "\"%s\" is current directory.", client->cwd);
|
|
return write_reply(client, 257, msg);
|
|
}
|
|
|
|
static int32_t ftp_CWD(client_t *client, char *path) {
|
|
int32_t result;
|
|
if (!vrt_chdir(client->cwd, path)) {
|
|
result = write_reply(client, 250, "CWD command successful.");
|
|
} else {
|
|
result = write_reply(client, 550, strerror(errno));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_CDUP(client_t *client, char *rest UNUSED) {
|
|
int32_t result;
|
|
if (!vrt_chdir(client->cwd, "..")) {
|
|
result = write_reply(client, 250, "CDUP command successful.");
|
|
} else {
|
|
result = write_reply(client, 550, strerror(errno));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_DELE(client_t *client, char *path) {
|
|
if (!vrt_unlink(client->cwd, path)) {
|
|
return write_reply(client, 250, "File or directory removed.");
|
|
} else {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
}
|
|
|
|
static int32_t ftp_MKD(client_t *client, char *path) {
|
|
if (!*path) {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
if (!vrt_mkdir(client->cwd, path, 0777)) {
|
|
char msg[MAXPATHLEN + 21];
|
|
char abspath[MAXPATHLEN];
|
|
strcpy(abspath, client->cwd);
|
|
vrt_chdir(abspath, path); // TODO: error checking
|
|
// TODO: escape double-quotes
|
|
sprintf(msg, "\"%s\" directory created.", abspath);
|
|
return write_reply(client, 257, msg);
|
|
} else {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
}
|
|
|
|
static int32_t ftp_RNFR(client_t *client, char *path) {
|
|
strcpy(client->pending_rename, path);
|
|
return write_reply(client, 350, "Ready for RNTO.");
|
|
}
|
|
|
|
static int32_t ftp_RNTO(client_t *client, char *path) {
|
|
if (!*client->pending_rename) {
|
|
return write_reply(client, 503, "RNFR required first.");
|
|
}
|
|
int32_t result;
|
|
if (!vrt_rename(client->cwd, client->pending_rename, path)) {
|
|
result = write_reply(client, 250, "Rename successful.");
|
|
} else {
|
|
result = write_reply(client, 550, strerror(
|
|
|
|
errno));
|
|
}
|
|
*client->pending_rename = '\0';
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_SIZE(client_t *client, char *path) {
|
|
struct stat st;
|
|
if (!vrt_stat(client->cwd, path, &st)) {
|
|
char size_buf[12];
|
|
sprintf(size_buf, "%llu", st.st_size);
|
|
return write_reply(client, 213, size_buf);
|
|
} else {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
}
|
|
|
|
static int32_t ftp_PASV(client_t *client, char *rest UNUSED) {
|
|
close_passive_socket(client);
|
|
client->passive_socket = network_socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
if (client->passive_socket < 0) {
|
|
return write_reply(client, 520, "Unable to create listening socket.");
|
|
}
|
|
set_blocking(client->passive_socket, false);
|
|
struct sockaddr_in bindAddress;
|
|
memset(&bindAddress, 0, sizeof(bindAddress));
|
|
bindAddress.sin_family = AF_INET;
|
|
bindAddress.sin_port = htons(passive_port++); // XXX: BUG: This will overflow eventually, with interesting results...
|
|
bindAddress.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
int32_t result;
|
|
if ((result = network_bind(client->passive_socket, (struct sockaddr *)&bindAddress, sizeof(bindAddress))) < 0) {
|
|
close_passive_socket(client);
|
|
return write_reply(client, 520, "Unable to bind listening socket.");
|
|
}
|
|
if ((result = network_listen(client->passive_socket, 1)) < 0) {
|
|
close_passive_socket(client);
|
|
return write_reply(client, 520, "Unable to listen on socket.");
|
|
}
|
|
char reply[49];
|
|
uint16_t port = bindAddress.sin_port;
|
|
uint32_t ip = network_gethostip();
|
|
struct in_addr addr;
|
|
addr.s_addr = ip;
|
|
console_printf("Listening for data connections at %s:%u...\n", inet_ntoa(addr), port);
|
|
sprintf(reply, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).", (ip >> 24) & 0xff, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff, (port >> 8) & 0xff, port & 0xff);
|
|
return write_reply(client, 227, reply);
|
|
}
|
|
|
|
static int32_t ftp_PORT(client_t *client, char *portspec) {
|
|
uint32_t h1, h2, h3, h4, p1, p2;
|
|
if (sscanf(portspec, "%3u,%3u,%3u,%3u,%3u,%3u", &h1, &h2, &h3, &h4, &p1, &p2) < 6) {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
char addr_str[44];
|
|
sprintf(addr_str, "%u.%u.%u.%u", h1, h2, h3, h4);
|
|
struct in_addr sin_addr;
|
|
if (!inet_aton(addr_str, &sin_addr)) {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
close_passive_socket(client);
|
|
uint16_t port = ((p1 &0xff) << 8) | (p2 & 0xff);
|
|
client->address.sin_addr = sin_addr;
|
|
client->address.sin_port = htons(port);
|
|
console_printf("Set client address to %s:%u\n", addr_str, port);
|
|
return write_reply(client, 200, "PORT command successful.");
|
|
}
|
|
|
|
typedef int32_t (*data_connection_handler)(client_t *client, data_connection_callback callback, void *arg);
|
|
|
|
static int32_t prepare_data_connection_active(client_t *client, data_connection_callback callback UNUSED, void *arg UNUSED) {
|
|
int32_t data_socket = network_socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
|
|
if (data_socket < 0)
|
|
return data_socket;
|
|
set_blocking(data_socket, false);
|
|
struct sockaddr_in bindAddress;
|
|
memset(&bindAddress, 0, sizeof(bindAddress));
|
|
bindAddress.sin_family = AF_INET;
|
|
bindAddress.sin_port = htons(SRC_PORT);
|
|
bindAddress.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
int32_t result;
|
|
if ((result = network_bind(data_socket, (struct sockaddr *)&bindAddress, sizeof(bindAddress))) < 0) {
|
|
network_close(data_socket);
|
|
return result;
|
|
}
|
|
|
|
client->data_socket = data_socket;
|
|
console_printf("Attempting to connect to client at %s:%u\n", inet_ntoa(client->address.sin_addr), client->address.sin_port);
|
|
return 0;
|
|
}
|
|
|
|
static int32_t prepare_data_connection_passive(client_t *client, data_connection_callback callback UNUSED, void *arg UNUSED) {
|
|
client->data_socket = client->passive_socket;
|
|
console_printf("Waiting for data connections...\n");
|
|
return 0;
|
|
}
|
|
|
|
static int32_t prepare_data_connection(client_t *client, void *callback, void *arg, void *cleanup) {
|
|
int32_t result = write_reply(client, 150, "Transferring data.");
|
|
if (result >= 0) {
|
|
data_connection_handler handler = prepare_data_connection_active;
|
|
if (client->passive_socket >= 0)
|
|
handler = prepare_data_connection_passive;
|
|
result = handler(client, (data_connection_callback)callback, arg);
|
|
if (result < 0) {
|
|
result = write_reply(client, 520, "Closing data connection, error occurred during transfer.");
|
|
} else {
|
|
client->data_connection_connected = false;
|
|
client->data_callback = callback;
|
|
client->data_connection_callback_arg = arg;
|
|
client->data_connection_cleanup = cleanup;
|
|
client->data_connection_timer = gettime() + OSSecondsToTicks(10);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static int32_t send_nlst(int32_t data_socket, DIR_P *iter) {
|
|
int32_t result = 0;
|
|
char filename[MAXPATHLEN];
|
|
struct dirent *dirent = NULL;
|
|
while ((dirent = vrt_readdir(iter)) != 0) {
|
|
size_t end_index = strlen(dirent->d_name);
|
|
if(end_index + 2 >= MAXPATHLEN)
|
|
continue;
|
|
strcpy(filename, dirent->d_name);
|
|
filename[end_index] = CRLF[0];
|
|
filename[end_index + 1] = CRLF[1];
|
|
filename[end_index + 2] = '\0';
|
|
if ((result = send_exact(data_socket, filename, strlen(filename))) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
return result < 0 ? result : 0;
|
|
}
|
|
|
|
static int32_t send_list(int32_t data_socket, DIR_P *iter) {
|
|
struct stat st;
|
|
int32_t result = 0;
|
|
time_t mtime = 0;
|
|
uint64_t size = 0;
|
|
char filename[MAXPATHLEN];
|
|
char line[MAXPATHLEN + 56 + CRLF_LENGTH + 1];
|
|
struct dirent *dirent = NULL;
|
|
while ((dirent = vrt_readdir(iter)) != 0) {
|
|
|
|
snprintf(filename, sizeof(filename), "%s/%s", iter->path, dirent->d_name);
|
|
if(stat(filename, &st) == 0) {
|
|
mtime = st.st_mtime;
|
|
size = st.st_size;
|
|
} else {
|
|
mtime = time(0);
|
|
size = 0;
|
|
}
|
|
|
|
char timestamp[13];
|
|
strftime(timestamp, sizeof(timestamp), "%b %d %Y", localtime(&mtime));
|
|
snprintf(line, sizeof(line), "%crwxr-xr-x 1 0 0 %10llu %s %s\r\n", (dirent->d_type & DT_DIR) ? 'd' : '-', size, timestamp, dirent->d_name);
|
|
if ((result = send_exact(data_socket, line, strlen(line))) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
return result < 0 ? result : 0;
|
|
}
|
|
|
|
static int32_t ftp_NLST(client_t *client, char *path) {
|
|
if (!*path) {
|
|
path = ".";
|
|
}
|
|
|
|
DIR_P *dir = vrt_opendir(client->cwd, path);
|
|
if (dir == NULL) {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
|
|
int32_t result = prepare_data_connection(client, send_nlst, dir, vrt_closedir);
|
|
if (result < 0)
|
|
vrt_closedir(dir);
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_LIST(client_t *client, char *path) {
|
|
if (*path == '-') {
|
|
// handle buggy clients that use "LIST -aL" or similar, at the expense of breaking paths that begin with '-'
|
|
char flags[FTP_BUFFER_SIZE];
|
|
char rest[FTP_BUFFER_SIZE];
|
|
char *args[] = { flags, rest };
|
|
split(path, ' ', 1, args);
|
|
path = rest;
|
|
}
|
|
if (!*path) {
|
|
path = ".";
|
|
}
|
|
|
|
if(path && client->cwd) {
|
|
if(strcmp(path, ".") == 0 && strcmp(client->cwd, "/") == 0) {
|
|
UnmountVirtualPaths();
|
|
MountVirtualDevices();
|
|
}
|
|
}
|
|
|
|
DIR_P *dir = vrt_opendir(client->cwd, path);
|
|
if (dir == NULL) {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
|
|
int32_t result = prepare_data_connection(client, send_list, dir, vrt_closedir);
|
|
if (result < 0)
|
|
vrt_closedir(dir);
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_RETR(client_t *client, char *path) {
|
|
FILE *f = vrt_fopen(client->cwd, path, "rb");
|
|
if (!f) {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
|
|
int fd = fileno(f);
|
|
if (client->restart_marker && lseek(fd, client->restart_marker, SEEK_SET) != client->restart_marker) {
|
|
int32_t lseek_error = errno;
|
|
fclose(f);
|
|
client->restart_marker = 0;
|
|
return write_reply(client, 550, strerror(lseek_error));
|
|
}
|
|
client->restart_marker = 0;
|
|
|
|
int32_t result = prepare_data_connection(client, send_from_file, f, fclose);
|
|
if (result < 0)
|
|
fclose(f);
|
|
return result;
|
|
}
|
|
|
|
static int32_t stor_or_append(client_t *client, FILE *f) {
|
|
if (!f) {
|
|
return write_reply(client, 550, strerror(errno));
|
|
}
|
|
int32_t result = prepare_data_connection(client, recv_to_file, f, fclose);
|
|
if (result < 0)
|
|
fclose(f);
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_STOR(client_t *client, char *path) {
|
|
FILE *f = vrt_fopen(client->cwd, path, "wb");
|
|
int fd;
|
|
if (f)
|
|
fd = fileno(f);
|
|
if (f && client->restart_marker && lseek(fd, client->restart_marker, SEEK_SET) != client->restart_marker) {
|
|
int32_t lseek_error = errno;
|
|
fclose(f);
|
|
client->restart_marker = 0;
|
|
return write_reply(client, 550, strerror(lseek_error));
|
|
}
|
|
client->restart_marker = 0;
|
|
|
|
return stor_or_append(client, f);
|
|
}
|
|
|
|
static int32_t ftp_APPE(client_t *client, char *path) {
|
|
return stor_or_append(client, vrt_fopen(client->cwd, path, "ab"));
|
|
}
|
|
|
|
static int32_t ftp_REST(client_t *client, char *offset_str) {
|
|
off_t offset;
|
|
if (sscanf(offset_str, "%lli", &offset) < 1 || offset < 0) {
|
|
return write_reply(client, 501, "Syntax error in parameters.");
|
|
}
|
|
client->restart_marker = offset;
|
|
char msg[FTP_BUFFER_SIZE];
|
|
sprintf(msg, "Restart position accepted (%lli).", offset);
|
|
return write_reply(client, 350, msg);
|
|
}
|
|
|
|
static int32_t ftp_SITE_LOADER(client_t *client, char *rest UNUSED) {
|
|
int32_t result = write_reply(client, 200, "Exiting to loader.");
|
|
//set_reset_flag();
|
|
return result;
|
|
}
|
|
|
|
static int32_t ftp_SITE_CLEAR(client_t *client, char *rest UNUSED) {
|
|
int32_t result = write_reply(client, 200, "Cleared.");
|
|
uint32_t i;
|
|
for (i = 0; i < 18; i++)
|
|
console_printf("\n");
|
|
//console_printf("\x1b[2;0H");
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
This is implemented as a no-op to prevent some FTP clients
|
|
from displaying skip/abort/retry type prompts.
|
|
*/
|
|
static int32_t ftp_SITE_CHMOD(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 250, "SITE CHMOD command ok.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_PASSWD(client_t *client, char *new_password) {
|
|
set_ftp_password(new_password);
|
|
return write_reply(client, 200, "Password changed.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_NOPASSWD(client_t *client, char *rest UNUSED) {
|
|
set_ftp_password(NULL);
|
|
return write_reply(client, 200, "Authentication disabled.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_EJECT(client_t *client, char *rest UNUSED) {
|
|
//if (dvd_eject()) return write_reply(client, 550, "Unable to eject DVD.");
|
|
return write_reply(client, 200, "DVD ejected.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_MOUNT(client_t *client, char *path UNUSED) {
|
|
//if (!mount_virtual(path)) return write_reply(client, 550, "Unable to mount.");
|
|
return write_reply(client, 250, "Mounted.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_UNMOUNT(client_t *client, char *path UNUSED) {
|
|
//if (!unmount_virtual(path)) return write_reply(client, 550, "Unable to unmount.");
|
|
return write_reply(client, 250, "Unmounted.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_UNKNOWN(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 501, "Unknown SITE command.");
|
|
}
|
|
|
|
static int32_t ftp_SITE_LOAD(client_t *client, char *path UNUSED) {
|
|
// FILE *f = vrt_fopen(client->cwd, path, "rb");
|
|
// if (!f) return write_reply(client, 550, strerror(errno));
|
|
// char *real_path = to_real_path(client->cwd, path);
|
|
// if (!real_path) goto end;
|
|
// load_from_file(f, real_path);
|
|
// free(real_path);
|
|
// end:
|
|
// fclose(f);
|
|
return write_reply(client, 500, "Unable to load.");
|
|
}
|
|
|
|
typedef int32_t (*ftp_command_handler)(client_t *client, char *args);
|
|
|
|
static int32_t dispatch_to_handler(client_t *client, char *cmd_line, const char **commands, const ftp_command_handler *handlers) {
|
|
char cmd[FTP_BUFFER_SIZE], rest[FTP_BUFFER_SIZE];
|
|
char *args[] = { cmd, rest };
|
|
split(cmd_line, ' ', 1, args);
|
|
int32_t i;
|
|
for (i = 0; commands[i]; i++) {
|
|
if (!strcasecmp(commands[i], cmd))
|
|
break;
|
|
}
|
|
return handlers[i](client, rest);
|
|
}
|
|
|
|
static const char *site_commands[] = { "LOADER", "CLEAR", "CHMOD", "PASSWD", "NOPASSWD", "EJECT", "MOUNT", "UNMOUNT", "LOAD", NULL };
|
|
static const ftp_command_handler site_handlers[] = { ftp_SITE_LOADER, ftp_SITE_CLEAR, ftp_SITE_CHMOD, ftp_SITE_PASSWD, ftp_SITE_NOPASSWD, ftp_SITE_EJECT, ftp_SITE_MOUNT, ftp_SITE_UNMOUNT, ftp_SITE_LOAD, ftp_SITE_UNKNOWN };
|
|
|
|
static int32_t ftp_SITE(client_t *client, char *cmd_line) {
|
|
return dispatch_to_handler(client, cmd_line, site_commands, site_handlers);
|
|
}
|
|
|
|
static int32_t ftp_NOOP(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 200, "NOOP command successful.");
|
|
}
|
|
|
|
static int32_t ftp_SUPERFLUOUS(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 202, "Command not implemented, superfluous at this site.");
|
|
}
|
|
|
|
static int32_t ftp_NEEDAUTH(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 530, "Please login with USER and PASS.");
|
|
}
|
|
|
|
static int32_t ftp_UNKNOWN(client_t *client, char *rest UNUSED) {
|
|
return write_reply(client, 502, "Command not implemented.");
|
|
}
|
|
|
|
static const char *unauthenticated_commands[] = { "USER", "PASS", "QUIT", "REIN", "NOOP", NULL };
|
|
static const ftp_command_handler unauthenticated_handlers[] = { ftp_USER, ftp_PASS, ftp_QUIT, ftp_REIN, ftp_NOOP, ftp_NEEDAUTH };
|
|
|
|
static const char *authenticated_commands[] = {
|
|
"USER", "PASS", "LIST", "PWD", "CWD", "CDUP",
|
|
"SIZE", "PASV", "PORT", "TYPE", "SYST", "MODE",
|
|
"RETR", "STOR", "APPE", "REST", "DELE", "MKD",
|
|
"RMD", "RNFR", "RNTO", "NLST", "QUIT", "REIN",
|
|
"SITE", "NOOP", "ALLO", NULL
|
|
};
|
|
static const ftp_command_handler authenticated_handlers[] = {
|
|
ftp_USER, ftp_PASS, ftp_LIST, ftp_PWD, ftp_CWD, ftp_CDUP,
|
|
ftp_SIZE, ftp_PASV, ftp_PORT, ftp_TYPE, ftp_SYST, ftp_MODE,
|
|
ftp_RETR, ftp_STOR, ftp_APPE, ftp_REST, ftp_DELE, ftp_MKD,
|
|
ftp_DELE, ftp_RNFR, ftp_RNTO, ftp_NLST, ftp_QUIT, ftp_REIN,
|
|
ftp_SITE, ftp_NOOP, ftp_SUPERFLUOUS, ftp_UNKNOWN
|
|
};
|
|
|
|
/*
|
|
returns negative to signal an error that requires closing the connection
|
|
*/
|
|
static int32_t process_command(client_t *client, char *cmd_line) {
|
|
if (strlen(cmd_line) == 0) {
|
|
return 0;
|
|
}
|
|
|
|
console_printf("Got command: %s\n", cmd_line);
|
|
|
|
const char **commands = unauthenticated_commands;
|
|
const ftp_command_handler *handlers = unauthenticated_handlers;
|
|
|
|
if (client->authenticated) {
|
|
commands = authenticated_commands;
|
|
handlers = authenticated_handlers;
|
|
}
|
|
|
|
return dispatch_to_handler(client, cmd_line, commands, handlers);
|
|
}
|
|
|
|
static void cleanup_data_resources(client_t *client) {
|
|
if (client->data_socket >= 0 && client->data_socket != client->passive_socket) {
|
|
network_close_blocking(client->data_socket);
|
|
}
|
|
client->data_socket = -1;
|
|
client->data_connection_connected = false;
|
|
client->data_callback = NULL;
|
|
if (client->data_connection_cleanup) {
|
|
client->data_connection_cleanup(client->data_connection_callback_arg);
|
|
}
|
|
client->data_connection_callback_arg = NULL;
|
|
client->data_connection_cleanup = NULL;
|
|
client->data_connection_timer = 0;
|
|
}
|
|
|
|
static void cleanup_client(client_t *client) {
|
|
network_close_blocking(client->socket);
|
|
cleanup_data_resources(client);
|
|
close_passive_socket(client);
|
|
int client_index;
|
|
for (client_index = 0; client_index < MAX_CLIENTS; client_index++) {
|
|
if (clients[client_index] == client) {
|
|
clients[client_index] = NULL;
|
|
break;
|
|
}
|
|
}
|
|
free(client);
|
|
num_clients--;
|
|
console_printf("Client disconnected.\n");
|
|
}
|
|
|
|
void cleanup_ftp() {
|
|
int client_index;
|
|
for (client_index = 0; client_index < MAX_CLIENTS; client_index++) {
|
|
client_t *client = clients[client_index];
|
|
if (client) {
|
|
write_reply(client, 421, "Service not available, closing control connection.");
|
|
cleanup_client(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool process_accept_events(int32_t server) {
|
|
int32_t peer;
|
|
struct sockaddr_in client_address;
|
|
int32_t addrlen = sizeof(client_address);
|
|
while ((peer = network_accept(server, (struct sockaddr *)&client_address, &addrlen)) != -WIIU_EAGAIN) {
|
|
if (peer < 0) {
|
|
console_printf("Error accepting connection: [%i] %s\n", -peer, strerror(-peer));
|
|
return false;
|
|
}
|
|
|
|
console_printf("Accepted connection from %s!\n", inet_ntoa(client_address.sin_addr));
|
|
|
|
if (num_clients == MAX_CLIENTS) {
|
|
console_printf("Maximum of %u clients reached, not accepting client.\n", MAX_CLIENTS);
|
|
network_close(peer);
|
|
return true;
|
|
}
|
|
|
|
client_t *client = malloc(sizeof(client_t));
|
|
if (!client) {
|
|
console_printf("Could not allocate memory for client state, not accepting client.\n");
|
|
network_close(peer);
|
|
return true;
|
|
}
|
|
client->socket = peer;
|
|
client->representation_type = 'A';
|
|
client->passive_socket = -1;
|
|
client->data_socket = -1;
|
|
strcpy(client->cwd, "/");
|
|
*client->pending_rename = '\0';
|
|
client->restart_marker = 0;
|
|
client->authenticated = false;
|
|
client->offset = 0;
|
|
client->data_connection_connected = false;
|
|
client->data_callback = NULL;
|
|
client->data_connection_callback_arg = NULL;
|
|
client->data_connection_cleanup = NULL;
|
|
client->data_connection_timer = 0;
|
|
memcpy(&client->address, &client_address, sizeof(client_address));
|
|
int client_index;
|
|
if (write_reply(client, 220, "ftpii") < 0) {
|
|
console_printf("Error writing greeting.\n");
|
|
network_close_blocking(peer);
|
|
free(client);
|
|
} else {
|
|
for (client_index = 0; client_index < MAX_CLIENTS; client_index++) {
|
|
if (!clients[client_index]) {
|
|
clients[client_index] = client;
|
|
break;
|
|
}
|
|
}
|
|
num_clients++;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void process_data_events(client_t *client) {
|
|
int32_t result;
|
|
if (!client->data_connection_connected) {
|
|
if (client->passive_socket >= 0) {
|
|
struct sockaddr_in data_peer_address;
|
|
int32_t addrlen = sizeof(data_peer_address);
|
|
result = network_accept(client->passive_socket, (struct sockaddr *)&data_peer_address,&addrlen);
|
|
if (result >= 0) {
|
|
client->data_socket = result;
|
|
client->data_connection_connected = true;
|
|
}
|
|
} else {
|
|
if ((result = network_connect(client->data_socket, (struct sockaddr *)&client->address, sizeof(client->address))) < 0) {
|
|
if (result == -EINPROGRESS || result == -EALREADY)
|
|
result = -WIIU_EAGAIN;
|
|
if ((result != -WIIU_EAGAIN) && (result != -EISCONN)) {
|
|
console_printf("Unable to connect to client: [%i] %s\n", -result, strerror(-result));
|
|
}
|
|
}
|
|
if (result >= 0 || result == -EISCONN) {
|
|
client->data_connection_connected = true;
|
|
}
|
|
}
|
|
if (client->data_connection_connected) {
|
|
result = 1;
|
|
console_printf("Connected to client! Transferring data...\n");
|
|
} else if (gettime() > client->data_connection_timer) {
|
|
result = -2;
|
|
console_printf("Timed out waiting for data connection.\n");
|
|
}
|
|
} else {
|
|
result = client->data_callback(client->data_socket, client->data_connection_callback_arg);
|
|
}
|
|
|
|
if (result <= 0 && result != -WIIU_EAGAIN) {
|
|
cleanup_data_resources(client);
|
|
if (result < 0) {
|
|
result = write_reply(client, 520, "Closing data connection, error occurred during transfer.");
|
|
} else {
|
|
result = write_reply(client, 226, "Closing data connection, transfer successful.");
|
|
}
|
|
if (result < 0) {
|
|
cleanup_client(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void process_control_events(client_t *client) {
|
|
int32_t bytes_read;
|
|
while (client->offset < (FTP_BUFFER_SIZE - 1)) {
|
|
if (client->data_callback) {
|
|
return;
|
|
}
|
|
char *offset_buf = client->buf + client->offset;
|
|
if ((bytes_read = network_read(client->socket, offset_buf, FTP_BUFFER_SIZE - 1 - client->offset)) < 0) {
|
|
if (bytes_read != -EAGAIN) {
|
|
console_printf("Read error %i occurred, closing client.\n", bytes_read);
|
|
goto recv_loop_end;
|
|
}
|
|
return;
|
|
} else if (bytes_read == 0) {
|
|
goto recv_loop_end; // EOF from client
|
|
}
|
|
client->offset += bytes_read;
|
|
client->buf[client->offset] = '\0';
|
|
|
|
if (strchr(offset_buf, '\0') != (client->buf + client->offset)) {
|
|
console_printf("Received a null byte from client, closing connection ;-)\n"); // i have decided this isn't allowed =P
|
|
goto recv_loop_end;
|
|
}
|
|
|
|
char *next;
|
|
char *end;
|
|
for (next = client->buf; (end = strstr(next, CRLF)) && !client->data_callback; next = end + CRLF_LENGTH) {
|
|
*end = '\0';
|
|
if (strchr(next, '\n')) {
|
|
console_printf("Received a line-feed from client without preceding carriage return, closing connection ;-)\n"); // i have decided this isn't allowed =P
|
|
goto recv_loop_end;
|
|
}
|
|
|
|
if (*next) {
|
|
int32_t result;
|
|
if ((result = process_command(client, next)) < 0) {
|
|
if (result != -EQUIT) {
|
|
console_printf("Closing connection due to error while processing command: %s\n", next);
|
|
}
|
|
goto recv_loop_end;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (next != client->buf) { // some lines were processed
|
|
client->offset = strlen(next);
|
|
char tmp_buf[client->offset];
|
|
memcpy(tmp_buf, next, client->offset);
|
|
memcpy(client->buf, tmp_buf, client->offset);
|
|
}
|
|
}
|
|
console_printf("Received line longer than %u bytes, closing client.\n", FTP_BUFFER_SIZE - 1);
|
|
|
|
recv_loop_end:
|
|
cleanup_client(client);
|
|
}
|
|
|
|
bool process_ftp_events(int32_t server) {
|
|
bool network_down = !process_accept_events(server);
|
|
int client_index;
|
|
for (client_index = 0; client_index < MAX_CLIENTS; client_index++) {
|
|
client_t *client = clients[client_index];
|
|
if (client) {
|
|
if (client->data_callback) {
|
|
process_data_events(client);
|
|
} else {
|
|
process_control_events(client);
|
|
}
|
|
}
|
|
}
|
|
return network_down;
|
|
}
|