/* * 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 \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; }