Implement FTP client.

This commit is contained in:
TomAwezome 2022-01-10 00:05:51 -05:00
parent dedb922567
commit afc785ba27
2 changed files with 510 additions and 0 deletions

View file

@ -0,0 +1,508 @@
#define FTP_BUF_SIZE 1024
I64 FTPMessageGet(CTCPSocket *message_socket, U8 *buf)
{ // returns FTP status code of message
U8 *temp;
Bool first = TRUE;
I64 status;
"\n";
while (TCPSocketReceive(message_socket, buf, FTP_BUF_SIZE) > 0)
{
if (first)
{
status = Str2I64(buf);
first = FALSE;
}
temp = MStrPrint("%Q", buf);
temp = StrReplace(temp, "\\r", "",, TRUE);
temp = StrReplace(temp, "\\n", "\n",, TRUE);
temp = StrReplace(temp, "\\t", "\t",, TRUE);
temp = StrReplace(temp, "\\\"", "\"",, TRUE);
"%s", temp;
MemSet(buf, 0, FTP_BUF_SIZE);
Free(temp);
}
"\n";
return status;
}
I64 FTPReplyPassiveParse(CTCPSocket *message_socket, CSocketAddressIPV4 *dest_addr_out)
{ // Parse an FTP reply message to the PASV command, destination written to arg.
U8 buf[8192], *str;
I64 tk, cur_section = 0, recv = 0, recv_sum = 0, res;
U32 ip_addr = 0;
U16 port = 0;
CCompCtrl *cc;
while ((recv = TCPSocketReceive(message_socket, buf + recv_sum, 8192 - recv_sum)) > 0)
{
recv_sum += recv;
}
str = StrNew(buf);
cc = CompCtrlNew(str);
while ((tk = Lex(cc)))
{
switch (tk)
{
case TK_IDENT:
cur_section = 0;
ip_addr = 0;
port = 0;
break;
case ',':
break; // skip
case TK_I64:
if (cur_section < 4)
{
ip_addr.u8[3 - cur_section] = cc->cur_i64;
cur_section++;
}
else if (4 <= cur_section < 6)
{
port.u8[5 - cur_section] = cc->cur_i64;
cur_section++;
}
if (cur_section >= 6)
goto parse_done;
break;
default:
break;
}
}
parse_done:
if (cur_section < 6)
res = -1;
else
{
dest_addr_out->family = AF_INET;
dest_addr_out->port = EndianU16(port);
dest_addr_out->address.address = ip_addr;
res = 0;
}
CompCtrlDel(cc);
return res;
}
I64 FTPFileDownload(CTCPSocket *data_socket, U8 *dest)
{
CFile *f;
I64 data_len = 0, recv_sum = 0, recv;
U8 buf[BLK_SIZE];
progress4 = 0;
f = FOpen(dest, "w");
if (!f)
{
ST_ERR_ST "Failed to open %s for writing\n", dest;
return -1;
}
while (TRUE)
{
recv = TCPSocketReceive(data_socket, buf + data_len, sizeof(buf) - data_len);
if (recv <= 0)
{
if (recv < 0)
ST_ERR_ST "Failed to receive TCP data\n";
if (data_len != 0 && !FBlkWrite(f, buf))
break;
f->de.size = recv_sum;
FClose(f);
TCPSocketClose(data_socket);
return recv;
}
data_len += recv;
recv_sum += recv;
progress4 += recv;
if (data_len == BLK_SIZE)
{
if (!FBlkWrite(f, buf))
break;
data_len = 0;
}
}
TCPSocketClose(data_socket);
ST_ERR_ST "Write failed, %s may be corrupted\n", dest;
FClose(f);
return -1;
}
I64 FTPFileView(U8 *filename=NULL, CTask *parent=NULL, CTask **_pu_task=NULL)
{
U8 *st = MStrPrint("Cd(\"%Q\");Plain(\"%Q\");", __DIR__, filename);
I64 res = PopUp(st, parent, _pu_task);
Free(st);
return res;
}
U8 *FTPBasename(U8 *path)
{
U8 *lastslash = StrLastOcc(path, "/"), *result;
if (lastslash == NULL)
result = path;
else
result = lastslash + 1;
//BAD FOR FILENAMES: ? / | = % : ; * + " < > space
result = StrReplace(result, "?", "");
result = StrReplace(result, "/", "",, TRUE);
result = StrReplace(result, "|", "",, TRUE);
result = StrReplace(result, "=", "",, TRUE);
result = StrReplace(result, "%", "",, TRUE);
result = StrReplace(result, ":", "",, TRUE);
result = StrReplace(result, ";", "",, TRUE);
result = StrReplace(result, "*", "",, TRUE);
result = StrReplace(result, "+", "",, TRUE);
result = StrReplace(result, "\"", "",, TRUE);
result = StrReplace(result, "<", "",, TRUE);
result = StrReplace(result, ">", "",, TRUE);
result = StrReplace(result, " ", "",, TRUE);
if (StrLen(result) > 22)
result[21] = 0; // truncate filename len
SysLog("%s\n", result);
return result;
}
class CFTPFilePrompt
{
U8 name[256] format "$$DA-P,LEN=255,A=\"FileName:%s\"$$";
};
U8 *FTPFilePrompt(U8 *path)
{
CFTPFilePrompt form;
U8 *basename = FTPBasename(path);
MemSet(form.name, 0, 256);
MemCopy(form.name, basename, MinI64(StrLen(basename), sizeof(form.name) - 1));
form.name[255] = 0;
if (PopUpForm(&form))
{
if (StrLen(form.name) >= 26)
form.name[25] = 0;
return StrNew(form.name);
}
return NULL;
}
I64 FTPClient(U8 *hostname=NULL, U16 port=21)
{
U32 addr;
CAddressInfo *current;
CAddressInfo *result = NULL;
I64 error, tk, i;
CSocketAddressIPV4 ipv4_address, *temp_ipv4, data_ipv4;
CTCPSocket *message_socket = TCPSocket(AF_INET), *data_socket;
I64 status = 0;
U8 buf[FTP_BUF_SIZE], *temp, *input_str, *dest;
CCompCtrl *cc;
if (!hostname)
hostname = StrGet("\nEnter FTP server address (URL or IPV4): ");
if (!IPV4AddressParse(hostname, &addr))
{
error = DNSAddressInfoGet(hostname, NULL, &result);
if (error < 0)
{
NetErr("FTP Client: Failed at DNS Get Address Info.");
return -1;
}
current = result;
while (current)
{
if (current->family == AF_INET)
{
temp_ipv4 = current->address;
addr = EndianU32(temp_ipv4->address); // why does it need EndianU32
break;
}
current = current->next;
}
if (!current)
{
NetErr("FTP Client: Failed to resolve address.");
return -1;
}
}
ipv4_address.port = EndianU16(port);
ipv4_address.family = AF_INET;
ipv4_address.address.address = addr;
message_socket->timeout = TCP_TIMEOUT;
if (TCPSocketConnect(message_socket, &ipv4_address) != 0)
{
"\nFailed to connect to server.\n";
TCPSocketClose(message_socket);
return -1;
}
else
"\nSuccessfully connected.\n";
message_socket->timeout = 2 * JIFFY_FREQ;
FTPMessageGet(message_socket, buf);
"\n\nType HELP for command list.\n\n";
while (TRUE)
{
input_str = StrGet(">");
cc = CompCtrlNew(input_str);
while ((tk = Lex(cc)))
{
switch (tk)
{
case TK_IDENT: // command
"COMMAND:%s\n", cc->cur_str;
for (i = 0; i < StrLen(cc->cur_str); i++)
cc->cur_str[i] = ToUpper(cc->cur_str[i]);
if (!StrCompare(cc->cur_str, "CWD") ||
!StrCompare(cc->cur_str, "CD"))
{
StrFirstRemove(input_str, " ");
if (!StrCompare(input_str, ""))
{
ST_ERR_ST "Must provide argument!\n";
goto lex_done;
}
"ARG:%s\n", input_str;
temp = MStrPrint("CWD %s\r\n", input_str);
TCPSocketSendString(message_socket, temp);
FTPMessageGet(message_socket, buf);
Free(temp);
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "LIST") ||
!StrCompare(cc->cur_str, "DIR") ||
!StrCompare(cc->cur_str, "LS"))
{
TCPSocketSendString(message_socket, "PASV\r\n");
if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Error parsing server response to PASV command!\n";
goto lex_done;
}
data_socket = TCPSocket(AF_INET);
data_socket->timeout = 2 * JIFFY_FREQ;
if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Failed at data socket connect!";
TCPSocketClose(data_socket);
goto lex_done;
}
TCPSocketSendString(message_socket, "LIST\r\n");
FTPMessageGet(data_socket, buf);
FTPMessageGet(message_socket, buf);
if (TCPSocketClose(data_socket) != 0)
ST_ERR_ST "Failed at data socket close!";
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "PWD"))
{
TCPSocketSendString(message_socket, "PWD\r\n");
FTPMessageGet(message_socket, buf);
}
else if (!StrCompare(cc->cur_str, "RETR") ||
!StrCompare(cc->cur_str, "GET"))
{
StrFirstRemove(input_str, " ");
if (!StrCompare(input_str, ""))
{
ST_ERR_ST "Must provide argument!\n";
goto lex_done;
}
TCPSocketSendString(message_socket, "PASV\r\n");
if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Error parsing server response to PASV command!\n";
goto lex_done;
}
data_socket = TCPSocket(AF_INET);
data_socket->timeout = 2 * JIFFY_FREQ;
if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Failed at data socket connect!";
TCPSocketClose(data_socket);
goto lex_done;
}
dest = FTPFilePrompt(input_str);
if (dest == NULL)
{
ST_ERR_ST "Download filename cannot be empty!";
TCPSocketClose(data_socket);
goto lex_done;
}
temp = MStrPrint("RETR %s\r\n", input_str);
TCPSocketSendString(message_socket, temp);
FTPFileDownload(data_socket, dest);
FTPMessageGet(message_socket, buf);
"\nOpen file with Ed? ";
if (YorN)
FTPFileView(dest);
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "VIEW") ||
!StrCompare(cc->cur_str, "CAT"))
{
StrFirstRemove(input_str, " ");
if (!StrCompare(input_str, ""))
{
ST_ERR_ST "Must provide argument!\n";
goto lex_done;
}
TCPSocketSendString(message_socket, "PASV\r\n");
if (FTPReplyPassiveParse(message_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Error parsing server response to PASV command!\n";
goto lex_done;
}
data_socket = TCPSocket(AF_INET);
data_socket->timeout = 2 * JIFFY_FREQ;
if (TCPSocketConnect(data_socket, &data_ipv4) != 0)
{
ST_ERR_ST "Failed at data socket connect!";
TCPSocketClose(data_socket);
goto lex_done;
}
temp = MStrPrint("RETR %s\r\n", input_str);
TCPSocketSendString(message_socket, temp);
FTPMessageGet(data_socket, buf);
FTPMessageGet(message_socket, buf);
if (TCPSocketClose(data_socket) != 0)
ST_ERR_ST "Failed at data socket close!";
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "USER"))
{
StrFirstRemove(input_str, " ");
if (!StrCompare(input_str, ""))
{
ST_ERR_ST "Must provide argument!\n";
goto lex_done;
}
"ARG:%s\n", input_str;
temp = MStrPrint("USER %s\r\n", input_str);
TCPSocketSendString(message_socket, temp);
FTPMessageGet(message_socket, buf);
Free(temp);
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "PASS"))
{
StrFirstRemove(input_str, " ");
if (!StrCompare(input_str, ""))
{
ST_ERR_ST "Must provide argument!\n";
goto lex_done;
}
"ARG:%s\n", input_str;
temp = MStrPrint("PASS %s\r\n", input_str);
TCPSocketSendString(message_socket, temp);
FTPMessageGet(message_socket, buf);
Free(temp);
goto lex_done;
}
else if (!StrCompare(cc->cur_str, "QUIT") ||
!StrCompare(cc->cur_str, "EXIT") ||
!StrCompare(cc->cur_str, "BYE"))
{
TCPSocketSendString(message_socket, "QUIT\r\n");
status = FTPMessageGet(message_socket, buf);
if (status == 221)
"Server closed successfully, exiting normally...\n\n";
else
ST_WARN_ST "Server error during close, force exit...\n\n";
TCPSocketClose(message_socket);
"See ya later!\n\n";
CompCtrlDel(cc);
return 0;
}
else
{
"
Command List:
(Alternate names separated by '/'; names case-insensitive)
CWD/CD <path> = Change Working Directory to <path>.
LIST/DIR/LS = List directory contents.
PWD = Print name of current directory.
RETR/GET <file> = Download copy of <file> from server.
VIEW/CAT <file> = Print the contents of <file> to the screen.
USER <username> = Set username to <username>.
PASS <password> = Set password to <password>.
QUIT/EXIT/BYE = End FTP session.\n\n";
}
break;
default:
"\nCommand expected. Type HELP for command list.\n";
break;
}
}
lex_done:
CompCtrlDel(cc);
}
};
FTPClient;

View file

@ -304,6 +304,8 @@ I64 TCPSendFlags(CTCPSocket *tcp_socket, U8 flags)
NetErr("TCP SEND FLAGS: Error, TCP Send to AF_UNSPEC\n");
return -1;
}
return 0;
}
I64 TCPSendData(CTCPSocket *tcp_socket, U8 flags, U8 *data, I64 length)