EtherLib/apps/ftp_server.c
2024-04-24 22:23:08 +02:00

351 lines
11 KiB
C

#include "ftp_server.h"
#include <stdbool.h>
#include <stdlib.h>
#include <etherlib/prefab/conn_blocks/tcp_connblock.h>
// ----------
/* 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
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';
// 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; // 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);
}
}