LwFTP/lwftpc.c

659 lines
17 KiB
C

/*
* lwftpc.c
*
* Created on: Feb 20, 2024
* Author: "SeungJu Lim"
*/
#include "lwftpc.h"
/**
*=============================================
* Debugging
*=============================================
*/
/*
* @brief Debug print for LwFTP
*/
void debugPrint(const char *format, ...) {
#ifdef LWFTP_DEBUG
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
#endif
}
/**
*=============================================
* Debugging
*=============================================
*/
/**
* @brief Send command to connected FTP connection
*
* @param conn Target netconn structure
* @param data
*/
err_t lwftp_send(struct netconn *conn, char *data) {
err_t err = 0;
if (strcmp(data, "\r\n") == 0)
debugPrint(">> lwftp: ----> send <CRLF>\r\n");
else
debugPrint(">> lwftp: ----> send %s", data);
err = netconn_write(conn, data, strlen(data), NETCONN_COPY);
return err;
}
/*
* @brief Send PASV command to server
*/
err_t lwftp_send_pasv(lwftp_session_t *s){
s->ctrl_state = LWFTP_PASV_SENT;
return lwftp_send(s->conn, "PASV\r\n");
}
/*
* @brief Attempt login to the server
*/
err_t lwftp_login(lwftp_session_t *s) {
char cmd[256];
if (s->ctrl_state == LWFTP_CLOSED) {
return ERR_CONN;
} else if (s->ctrl_state == LWFTP_CONNECTED
|| s->ctrl_state == LWFTP_LOGGED) {
s->ctrl_state = LWFTP_USER_SENT;
snprintf(cmd, sizeof(cmd), "USER %s\r\n", s->user);
return lwftp_send(s->conn, cmd);
}
return ERR_USE;
}
/*
* @brief
*/
err_t lwftp_data_open(lwftp_session_t *s, char *response) {
err_t err = ERR_VAL;
char *ptr;
ip_addr_t addr_d;
ptr = strchr(response, '(');
if (!ptr) return ERR_BUF;
do {
unsigned int a = strtoul(ptr + 1, &ptr, 10);
unsigned int b = strtoul(ptr + 1, &ptr, 10);
unsigned int c = strtoul(ptr + 1, &ptr, 10);
unsigned int d = strtoul(ptr + 1, &ptr, 10);
IP4_ADDR(&addr_d, a, b, c, d);
} while (0);
s->data_port = strtoul(ptr + 1, &ptr, 10) << 8;
s->data_port |= strtoul(ptr + 1, &ptr, 10) & 255;
if (*ptr != ')') return ERR_BUF;
debugPrint(">> lwftp: new data port: '%d'\r\n", s->data_port);
// if data connection already exist:
if (s->data_conn != NULL) {
debugPrint(">> lwftp: existing data connection found, closing\r\n");
netconn_close(s->data_conn);
netconn_delete(s->data_conn);
s->data_state = LWFTP_CLOSED;
s->ctrl_state = LWFTP_LOGGED;
}
// create new data connection
s->data_conn = netconn_new(NETCONN_TCP);
if (s->data_conn == NULL) {
debugPrint(">> lwftp: Failed to create new netconn for data connection\r\n");
return ERR_MEM;
}
// do connect
err = netconn_connect(s->data_conn, &addr_d, s->data_port);
return err;
}
/**
*=============================================
* FTP Commands
*=============================================
*/
/*
* @brief Close FTP connection
*/
err_t lwftp_close(lwftp_session_t *s) {
err_t err;
if (s->data_conn != NULL && s->data_state != LWFTP_DATA_CLOSED) {
netconn_close(s->data_conn);
err = netconn_delete(s->data_conn);
if (err != ERR_OK) {
debugPrint(">> lwftp: cannot close data connection with code %d\r\n", err);
return err;
}
debugPrint(">> lwftp: data connection closed successfully\r\n");
s->data_state = LWFTP_CLOSED;
}
if (s->conn != NULL && s->ctrl_state != LWFTP_CLOSED) {
netconn_close(s->conn);
err = netconn_delete(s->conn);
if (err != ERR_OK) {
debugPrint(">> lwftp: cannot close control connection with code %d\r\n", err);
return err;
}
debugPrint(">> lwftp: control connection closed successfully\r\n");
s->ctrl_state = LWFTP_CLOSED;
}
return err;
}
/*
* @brief Store data to FTP server
* caller func
*/
err_t lwftp_store(lwftp_session_t *s, char *filename, char *data) {
err_t err;
// init error state
s->result = ERR_INPROGRESS;
// set semaphore
s->xfer_semaphore = xSemaphoreCreateBinary();
// get data port and open it
if (s->ctrl_state == LWFTP_LOGGED && s->data_state == LWFTP_DATA_CLOSED) {
// set callback
s->data_callback = lwftp_store_callback;
s->filename = filename;
s->data = data;
lwftp_send_pasv(s);
// send data immediately if data port is already opened
} else if (s->ctrl_state == LWFTP_PASV_MODE && s->data_state == LWFTP_DATA_XFER_READY) {
lwftp_store_callback(s, filename, data);
// unexpected state - busy data port or something
// need to code more exceptions
} else {
debugPrint(">> lwftp_ERROR: unexpected state\r\n");
xSemaphoreGive(s->xfer_semaphore);
}
// wait semaphore - get result from callback
if (xSemaphoreTake(s->xfer_semaphore, FTP_TIMEOUT) == pdTRUE) {
err = s->result;
s->result = ERR_OK; // reset state
vSemaphoreDelete(s->xfer_semaphore); // reset semaphore
return err;
}
return ERR_TIMEOUT;
}
/*
* @brief Store command callback
* callback func
*/
void lwftp_store_callback(lwftp_session_t *s, char *filename, char *data) {
err_t err;
char cmd[256];
size_t data_len = strlen(data);
// send STOR command
snprintf(cmd, sizeof(cmd), "STOR %s\r\n", filename);
err = lwftp_send(s->conn, cmd);
if (err == ERR_OK) {
err = netconn_write(s->data_conn, data, data_len, NETCONN_COPY);
if (err == ERR_OK) {
debugPrint(">> lwftp: data sent successfully\r\n");
netconn_close(s->data_conn);
} else {
debugPrint(">> lwftp_ERROR: send data failed with code %d\r\n", err);
}
} else {
debugPrint(">> lwftp_ERROR: send command failed with code %d\r\n", err);
}
// Reset callback data from FTP server
s->data_callback = NULL;
s->filename = NULL;
s->data = NULL;
// return result to caller using FTP session structure
s->result = err;
}
/*
* @brief Retrieve data from
* caller func
*/
err_t lwftp_retrieve(lwftp_session_t *s, char *filename) {
char cmd[256];
err_t err;
// Init data portz
if (s->ctrl_state == LWFTP_LOGGED) {
s->data_callback = lwftp_retrieve_callback;
s->filename = strdup(filename);
lwftp_send(s->conn, "PASV\r\n");
// Send data
} else if (s->ctrl_state == LWFTP_PASV_SENT && s->data_state == LWFTP_DATA_XFER_READY) {
snprintf(cmd, sizeof(cmd), "RETR %s\r\n", filename);
err = lwftp_send(s->conn, cmd);
if (err == ERR_OK) {
debugPrint(">> lwftp: command sent successfully\r\n");
} else {
debugPrint(">> lwftp: send command failed with code %d\r\n", err);
}
// Init callback data
s->data_callback = NULL;
s->filename = NULL;
s->data = NULL;
}
return err;
}
/*
* @brief Retrieve command callback
* callback func
*/
void lwftp_retrieve_callback(lwftp_session_t *s, char *filename, char *data) {
lwftp_retrieve(s, filename);
}
/*
* @brief Get a list of files in the root directory of the server.
* caller func
* @param s Session structure
* @param outStr output parameter to return result of LIST command
*/
err_t lwftp_list(lwftp_session_t *s, char **outStr) {
err_t err;
// init
s->result = ERR_INPROGRESS;
*outStr = NULL;
s->outStr = outStr;
s->xfer_semaphore = xSemaphoreCreateBinary();
// open data port
if (s->ctrl_state == LWFTP_LOGGED && s->data_state == LWFTP_DATA_CLOSED) {
s->data_callback = lwftp_list_callback;
lwftp_send_pasv(s);
// send data if data port already opened
} else if (s->ctrl_state == LWFTP_PASV_SENT
&& s->data_state == LWFTP_DATA_XFER_READY) {
lwftp_list_callback(s, NULL, NULL);
// unexpected state - busy data port or something
// need to code more exceptions
} else {
debugPrint(">> lwftp_ERROR: data port is busy\r\n");
xSemaphoreGive(s->xfer_semaphore);
}
// wait semaphore - get result from callback
if (xSemaphoreTake(s->xfer_semaphore, FTP_TIMEOUT) == pdTRUE) {
err = s->result;
if (err == ERR_OK) {
size_t length = strlen(*outStr);
if (length == 0) {
debugPrint(">> lwftp: server directory is empty.\r\n");
}
} else {
debugPrint(">> lwftp_ERROR: failed to get file list.\r\n");
}
// reset session & semaphore
resetSession(s);
vSemaphoreDelete(s->xfer_semaphore);
return err;
}
return ERR_TIMEOUT;
}
/*
* @brief List command callback
* callback func
*/
void lwftp_list_callback(lwftp_session_t *s, char *filename, char *data) {
err_t err;
// set state & send command
s->ctrl_state = LWFTP_LIST_SENT;
s->data_state = LWFTP_DATA_XFER_READY;
err = lwftp_send(s->conn, "LIST\r\n");
if (err == ERR_OK) {
debugPrint(">> lwftp: command sent successfully\r\n");
// the rest of the process will be done in the data thread.
// failed to send command to the server.
// event listener also will not catch anything.
} else {
debugPrint(">> lwftp_ERROR: send command failed with code %d\r\n", err);
s->result = err;
xSemaphoreGive(s->xfer_semaphore);
}
}
/**
*=============================================
* Data reception & processing
*=============================================
*/
/**
* @brief Called when data is finished receiving.
* @param s Session structure
* @param data Received data
* @param size Size of received data (length)
*/
void onDataReceived(lwftp_session_t *s, void *data, size_t size) {
// debugPrint(">> lwftp: Received data:\r\n%.*s\r\n", (int) size, (char*) data);
printf(">> %d\r\n", s->data_state);
if (s->data_state == LWFTP_DATA_XFERING) {
if (s->ctrl_state == LWFTP_LIST_SENT || s->ctrl_state == LWFTP_RETR_SENT) {
char *newData = (char*) malloc(size + 1);
if (newData == NULL) {
debugPrint(">> lwftp_ERROR: memory allocation failed\r\n");
s->result = ERR_MEM;
} else {
memcpy(newData, data, size);
newData[size] = '\0';
if (*(s->outStr) != NULL) {
free(*(s->outStr));
*(s->outStr) = NULL;
}
*(s->outStr) = newData;
s->result = ERR_OK;
}
s->ctrl_state = LWFTP_LOGGED;
s->data_state = LWFTP_DATA_CLOSED;
xSemaphoreGive(s->xfer_semaphore);
}
}
}
/**
*=============================================
* Threads (Event listener)
*=============================================
*/
/**
* @brief Data connection event listener
* @param arg Session structure
*/
void lwftp_data_thread(void *arg) {
lwftp_session_t *s = (lwftp_session_t*) arg;
static struct netbuf *d_buf;
static char *total_data = NULL;
static size_t total_len = 0;
while (1) {
if (s->data_conn != NULL) {
while (netconn_recv(s->data_conn, &d_buf) == ERR_OK) {
void *data;
u16_t len;
do {
netbuf_data(d_buf, &data, &len);
char *new_data = (char*) realloc(total_data, total_len + len);
if (!new_data) {
debugPrint("lwftp: memory reallocation failed\r\n");
break;
} else {
total_data = new_data;
memcpy(total_data + total_len, data, len);
total_len += len;
}
} while (netbuf_next(d_buf) >= 0);
netbuf_delete(d_buf);
}
if (total_data != NULL && total_len > 0) {
onDataReceived(s, total_data, total_len);
free(total_data);
total_data = NULL;
total_len = 0;
}
}
}
}
/**
* @brief Control connection event listener
* @param arg Session structure
*/
void lwftp_ctrl_thread(void *arg) {
lwftp_session_t *s = (lwftp_session_t*) arg;
static struct netbuf *buf;
uint response = 0;
char cmd[256];
err_t err;
// check that the session data is valid.
if ((s->ctrl_state != LWFTP_CLOSED) || s->data_state != LWFTP_DATA_CLOSED
|| s->conn || s->data_conn || !s->user || !s->pass) {
debugPrint(">> lwftp: invalid session data\r\n");
return;
}
// create new TCP connection
s->conn = netconn_new(NETCONN_TCP);
err = netconn_bind(s->conn, &s->cli_ip, 0);
if (err != ERR_OK) {
debugPrint(">> lwftp: client IP binding failed with code %d\r\n", err);
s->conn = NULL;
} else {
debugPrint(">> lwftp: client IP bind OK\r\n");
err = netconn_connect(s->conn, &s->svr_ip, s->svr_port);
// If the connection to the server is established, the following will continue, else delete the connection
if (err != ERR_OK) {
debugPrint(">> lwftp: server connection failed with code %d\r\n", err);
s->conn = NULL;
} else {
while (1) {
/* wait until the data is sent by the server*/
if (netconn_recv(s->conn, &buf) == ERR_OK) {
if (buf) {
response = strtoul(buf->p->payload, NULL, 10);
debugPrint("\n>> lwftp: <==== resp '%d'\r\n", response);
/** ============================================ */
/** ========= Response code processing ========= */
/** [Response 220] Service ready for new user.*/
if (response == 220) {
if (s->ctrl_state == LWFTP_CLOSED) {
debugPrint(">> lwftp: server connect OK\r\n");
s->ctrl_state = LWFTP_CONNECTED;
lwftp_login(s);
}
}
/** [Response 331] User name okay, need password.*/
else if (response == 331) {
if (s->ctrl_state == LWFTP_USER_SENT) {
s->ctrl_state = LWFTP_PASS_SENT;
snprintf(cmd, sizeof(cmd), "PASS %s\r\n", s->pass);
lwftp_send(s->conn, cmd);
}
}
/** [Response 230] User logged in, proceed.*/
else if (response == 230) {
if (s->ctrl_state == LWFTP_PASS_SENT) {
s->ctrl_state = LWFTP_LOGGED;
debugPrint(">> lwftp: now logged in\r\n");
}
}
/** [Response 227] Entering Passive Mode (h1,h2,h3,h4,p1,p2).*/
else if (response == 227) {
if (s->ctrl_state == LWFTP_PASV_SENT) {
debugPrint(">> lwftp: entering passive Mode\r\n");
err = lwftp_data_open(s, buf->p->payload);
if (err == ERR_OK) {
debugPrint(">> lwftp: data port connect OK\r\n");
s->data_state = LWFTP_DATA_XFER_READY;
// if callback exists, execute.
if (s->data_callback != NULL) {
s->data_callback(s, s->filename, s->data);
}
} else {
debugPrint(">> lwftp_ERROR: failed to connect to server data port with code %d\r\n", err);
netconn_delete(s->data_conn);
s->data_conn = NULL;
s->ctrl_state = LWFTP_LOGGED;
s->data_state = LWFTP_DATA_CLOSED;
// if callback exists, don't execute and release semaphore.
if (s->data_callback != NULL) {
xSemaphoreGive(s->xfer_semaphore);
}
}
}
}
/** [Response 125] Data connection already open; transfer starting.*/
else if (response == 125) {
printf(">>c: %d / d: %d\r\n", s->ctrl_state,s->data_state);
if (s->data_state == LWFTP_DATA_XFER_READY) {
s->data_state = LWFTP_DATA_XFERING;
debugPrint(">> lwftp: data transfer starting.\r\n");
}
}
/** [Response 226] Closing data connection. Requested file action successful*/
else if (response == 226) {
// if (s->data_state == LWFTP_DATA_XFERING) {}
debugPrint(">> lwftp: transfer complete, closing connection\r\n");
}
/** [Response 332] Need account for login.*/
else if (response == 332) {
debugPrint("\n>> lwftp: need account for login.\r\n");
s->ctrl_state = LWFTP_CONNECTED;
}
/** [Response 421] Service not available, closing.*/
else if (response == 421) {
debugPrint("\n>> lwftp: service not available, closing.\r\n");
break;
}
/** [Response 500] Syntax error, command unrecognized.*/
else if (response == 500 || response == 503) {
debugPrint("\n>> lwftp: syntax error or bad sequence of commands\r\n");
if (s->ctrl_state == LWFTP_USER_SENT || s->ctrl_state == LWFTP_PASS_SENT) {
s->ctrl_state = LWFTP_CONNECTED;
} else {
s->ctrl_state = LWFTP_LOGGED;
}
}
/** [Response 530] Not logged in.*/
else if (response == 530) {
if (s->ctrl_state == LWFTP_PASS_SENT) {
s->ctrl_state = LWFTP_CONNECTED;
debugPrint(">> lwftp: invalid user data or not logged in yet\r\n");
}
}
/** ============================================ */
}
}
memset(cmd, 0, sizeof(cmd));
if (buf != NULL) {
netbuf_delete(buf);
buf = NULL;
}
}
}
}
lwftp_close(s);
return;
}
/**
*=============================================
* Utils
*=============================================
*/
void resetSession(lwftp_session_t *s) {
s->data_callback = NULL;
s->filename = NULL;
s->data = NULL;
s->outStr = NULL;
s->result = ERR_OK;
}
/** Extract file names from result of FTP LIST command */
char** extractFileNames(const char *input, int *fileCount) {
int count = 0;
char **fileNames = NULL;
char *inputCopy = strdup(input);
char *line = strtok(inputCopy, "\n");
while (line != NULL) {
char *fileName = strrchr(line, ' ');
if (fileName && *(fileName + 1)) {
fileName++;
fileNames = realloc(fileNames, sizeof(char*) * (count + 1));
fileNames[count++] = strdup(fileName);
}
line = strtok(NULL, "\n");
}
free(inputCopy);
// *fileCount = count;
return fileNames;
}
/** Generate unique file name if duplicate filenames exist */
char* genUniqFilName(char **fileNames, int fileCount, const char *targetFileName) {
int suffix = 1;
char baseFileName[256] = { 0 };
char extension[256] = { 0 };
char *newFileName = NULL;
int nameExists;
const char *dot = strrchr(targetFileName, '.');
if (dot) {
memcpy(baseFileName, targetFileName, dot - targetFileName);
strcpy(extension, dot);
} else {
strcpy(baseFileName, targetFileName);
}
do {
nameExists = 0;
free(newFileName);
newFileName = (char*) malloc(strlen(baseFileName) + strlen(extension) + 15);
if (!newFileName) {
debugPrint(">> lwftp: memory allocation failed\r\n");
return NULL;
}
sprintf(newFileName, "%s(%d)%s", baseFileName, suffix, extension);
for (int i = 0; i < fileCount; i++) {
if (strcmp(fileNames[i], newFileName) == 0) {
nameExists = 1;
break;
}
}
suffix++;
} while (nameExists);
return newFileName;
}