#include "ftp_server.h" #include #include #include // ---------- /* FTP response messages */ static const char ftpOpeningDataConn[] = "150 Opening data connection.\r\n"; static const char ftpCmdOk[] = "200 Command OK.\r\n"; static const char ftpGreeting[] = "220 Service ready for new user.\r\n"; static const char ftpClosingDataConn[] = "226 Closing data connection.\r\n"; static const char ftpEnteringPassivePrefix[] = "227 Entering passive mode "; static const char ftpLoginOK[] = "230 User logged in, proceed.\r\n"; static const char ftpFileActionCplt[] = "250 Requested file action complete.\r\n"; static const char ftpRootDirName[] = "257 \"/\" is the current directory\r\n"; static const char ftpSyntaxError[] = "501 Syntax error in command or parameters\r\n"; static const char ftpNotImplemented[] = "502 Command not implemented.\r\n"; static const char ftpCmdNotImplForParam[] = "504 Command not implemented for that parameter\r\n"; #define FTPS_DYNAMIC_RESPONSE_MAXLEN (63) static char ftpsDynResp[FTPS_DYNAMIC_RESPONSE_MAXLEN + 1] = {0}; /* Implemented commands */ static char *ftpImplementedCmdsStr[] = { // this is the smallest standard set of implemented FTP commands "USER", "QUIT", "PORT", "TYPE", "MODE", "STRU", "RETR", "STOR", "NOOP", // ------ additional commands "PWD", "CWD", "PASV", "LIST", "\0" // SENTRY! }; // THIS MUST REMAIN LOCAL! typedef enum { // ordering must be the same as in the string version above USER, QUIT, PORT, TYPE, MODE, STRU, RETR, STOR, NOOP, // ------ additional commands PWD, CWD, PASV, LIST } Ftp_ImplementedCmds; // ----------- static Ftps_State s; // TODO: dynamically allocate this later // TODO deinit! // TODO ambiguous naming between server and client connections int ftps_ctrl_recv(cbd d) { uint32_t event = FTPEVT_CTRL_RECVD; ETHLIB_OS_QUEUE_PUSH(s.eventQ, &event); return 0; } int ftps_ctrl_accept_cb(cbd d) { s.connCtrl = d; uint32_t event = FTPEVT_NEW_CONNECTION; ETHLIB_OS_QUEUE_PUSH(s.eventQ, &event); return 0; } void ftps_thread(void *param); void ftps_init() { osThreadAttr_t attr; memset(&attr, 0, sizeof(osThreadAttr_t)); attr.stack_size = 3072; attr.priority = osPriorityNormal; attr.name = "ftp"; osThreadNew(ftps_thread, NULL, &attr); } char *ftps_get_word(char *word, char *str, uint32_t maxLen) { char *iter = str; uint32_t len = 0; // skip leading whitespaces while ((*iter <= ' ') && (*iter != '\0')) { iter++; } // look for end of word, and copy until copy size is reached while (*iter > ' ') { if (len < maxLen) { word[len] = *iter; } len++; iter++; } word[len] = '\0'; // insert null termination // return the position of the next whitespace return iter; } bool ftps_is_command_implemented(char *cmd, uint32_t len, uint32_t *cmdCode) { char *iter = ftpImplementedCmdsStr[0]; bool ret = false; uint8_t index = 0; while (*iter != '\0') { // compare passed command with the iterator value if (len == 0) { if (!strcmp(iter, cmd)) { ret = true; break; } } else { if (!strncmp(iter, cmd, len)) { ret = true; break; } } // select next command iter = ftpImplementedCmdsStr[++index]; } // if cmdCode pointer is provided, then write back the command code if (ret && (cmdCode != NULL)) { *cmdCode = index; } return ret; } void ftps_rtrim(char *str) { char *iter = str + strlen(str) - 1; while ((*iter <= ' ') && (iter >= str)) { *iter = '\0'; iter--; } } // data reception on data port int ftps_data_recv(cbd d) { return 0; } // accept new data connection int ftps_data_accept_cb(cbd d) { s.connData = d; // set data connection ID tcp_debug(d, true); ETHLIB_OS_SEM_POST(s.dataConnHasOpened); // release data connection semaphore return 0; } #define FTPS_TYPE_ASCII 'A' #define FTPS_TYPE_IMAGE 'I' void ftps_process_event(uint32_t event) { const char *resp = NULL; // response towards the FTP client char *cmdArg = NULL; // command argument int cmdCode = -1; // command code fetched from the string command // is something is received, then read it if (event == FTPEVT_CTRL_RECVD) { // read into the linebuffer uint32_t recvLen = tcp_recv(s.connCtrl, (uint8_t *)s.lineBuffer, FTPS_LINEBUF_LEN); s.lineBuffer[recvLen] = '\0'; // if recv len is zero in dicating that the connection was closed, by the peer, // then reset the server state if (recvLen == 0) { s.connCtrl = 0; s.connState = FTPS_IDLE; // TODO: close data connection as well return; } // right trim the line ftps_rtrim(s.lineBuffer); // MSG("%s\n", s.lineBuffer); // get first word: get pointer to first space and calculate compare length char *spacePos = strchr(s.lineBuffer, ' '); uint32_t compareLen = (spacePos == NULL) ? 0 : (spacePos - s.lineBuffer); // determine if we support this command or not bool cmdImpl = ftps_is_command_implemented(s.lineBuffer, compareLen, (uint32_t *)&cmdCode); // if command is not implemented, then send the "not implemented" answer and return from this function if (!cmdImpl) { resp = ftpNotImplemented; goto resp_and_return; // yes, goto, but it's efficient } // if command is implemented, then set the argument pointer cmdArg = spacePos + 1; // the argument begins right after the first space } // ------ if (s.connState == FTPS_IDLE) { // IDLE... if (event == FTPEVT_NEW_CONNECTION) { // ...and there's an incoming connection // send greeting to the client resp = ftpGreeting; // set state to logging in s.connState = FTPS_LOGGING_IN; } } else if (s.connState == FTPS_LOGGING_IN) { if (cmdCode == USER) { // if user command is issued, the login, no username or password checking // send login ok resp = ftpLoginOK; // switch to established state s.connState = FTPS_ESTABLISHED; } } else if (s.connState == FTPS_ESTABLISHED) { switch (cmdCode) { case PWD: // print working directory name resp = ftpRootDirName; break; case CWD: resp = ftpFileActionCplt; break; case TYPE: { // set transfer format char typeChar[2]; // get type character ftps_get_word(typeChar, cmdArg, 1); // (first parameter) // if no parameter was defined if (typeChar[0] == '\0') { goto syntax_error; } // if parameter is defined, then act accordingly switch (typeChar[0]) { case FTPS_TYPE_ASCII: // ASCII case FTPS_TYPE_IMAGE: // Image (binary) // I don't know if there's any difference between ASCII and Image transfers, since bytes are 8-bit long goto cmd_ok; break; default: // other formats are not implemented goto cmd_param_not_implemented; break; } } break; case PASV: { // enter passive mode if (!s.passiveMode) { // start data connection listener socket uint16_t port; do { // randomize a port number not in use currently port = (rand() % (0xFFFF - 1024)) + 1024; // it's a nice practice if we pick a port number greater than 1024 s.connDataS = tcp_new_connblock(s.intf, IPv4_IF_ADDR, port, ftps_data_recv); } while (s.connDataS == 0); // retry if connection block could not be opened // convert to server socket tcp_set_accept_callback(s.connDataS, ftps_data_accept_cb); tcp_listen(s.connDataS); MSG("port: %u\n", port); // turn passive mode on and save passive mode port s.passiveMode = true; s.passiveModePort = port; } // compile response message SNPRINTF(ftpsDynResp, FTPS_DYNAMIC_RESPONSE_MAXLEN, "%s (%u,%u,%u,%u,%u,%u)\r\n", // no space in the whole address-port compound ftpEnteringPassivePrefix, EXPLODE_IPv4(s.intf->ip), (s.passiveModePort >> 8) & 0xFF, s.passiveModePort & 0xFF); // set response resp = ftpsDynResp; } break; case LIST: { // list files ETHLIB_OS_SEM_WAIT(s.dataConnHasOpened); // wait for data connection to establish tcp_send(s.connCtrl, (const uint8_t *)ftpOpeningDataConn, strlen(ftpOpeningDataConn)); // opening data connection tcp_send(s.connData, (const uint8_t *)"\r\n", 2); // sending listing data tcp_send(s.connCtrl, (const uint8_t *)ftpClosingDataConn, strlen(ftpClosingDataConn)); // closing data connection close_connection(s.connData); // actually close connection s.connData = 0; resp = NULL; // clear data connection CBD } break; default: break; } } // ------ resp_and_return: // send response if instructed if (resp != NULL) { tcp_send(s.connCtrl, (const uint8_t *)resp, strlen(resp)); } return; // ----- syntax_error: resp = ftpSyntaxError; goto resp_and_return; cmd_param_not_implemented: resp = ftpCmdNotImplForParam; goto resp_and_return; cmd_ok: resp = ftpCmdOk; goto resp_and_return; } void ftps_thread(void *param) { // set interface s.intf = get_default_interface(); // initialize event queue s.eventQ = ETHLIB_OS_QUEUE_CREATE(FTPS_EVENT_QUEUE_LENGTH, uint32_t); // initialize control connection s.connCtrlS = tcp_new_connblock(s.intf, IPv4_IF_ADDR, FTPS_CTRL_PORT, ftps_ctrl_recv); tcp_set_accept_callback(s.connCtrlS, ftps_ctrl_accept_cb); // set callback for accepting an incoming connection tcp_listen(s.connCtrlS); // make it listen for incoming connections // initialize connection state s.connState = FTPS_IDLE; s.passiveMode = false; // at the beginning we disable passive mode s.passiveModePort = 0; // initialize semaphore which synchronizes data connection state s.dataConnHasOpened = ETHLIB_OS_SEM_CREATE(1); // process loop for (;;) { // wait for some connection event uint32_t event = FTPEVT_NONE; ETHLIB_OS_QUEUE_POP(s.eventQ, &event); // execute event process ftps_process_event(event); } }