mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 22:56:30 -10:00
574 lines
11 KiB
C++
574 lines
11 KiB
C++
/*
|
|
* Copyright (C) Volition, Inc. 1999. All rights reserved.
|
|
*
|
|
* All source code herein is the property of Volition, Inc. You may not sell
|
|
* or otherwise commercially exploit the source or things you created based on the
|
|
* source.
|
|
*
|
|
*/
|
|
|
|
#include <cstdio>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#else
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/select.h>
|
|
#include <cerrno>
|
|
#include <arpa/inet.h>
|
|
#include <netdb.h>
|
|
#endif
|
|
|
|
#include "globalincs/pstypes.h"
|
|
#include "osapi/osapi.h"
|
|
#include "inetfile/cftp.h"
|
|
#include "network/psnet2.h"
|
|
|
|
|
|
int FTPObjThread(void *obj)
|
|
{
|
|
(reinterpret_cast<CFtpGet *>(obj))->WorkerThread();
|
|
|
|
return (reinterpret_cast<CFtpGet *>(obj))->GetStatus();
|
|
}
|
|
|
|
void CFtpGet::AbortGet()
|
|
{
|
|
m_Aborting = true;
|
|
|
|
while ( !m_Aborted ) {
|
|
os_sleep(10); // Wait for the thread to end
|
|
}
|
|
|
|
if (LOCALFILE != nullptr) {
|
|
fclose(LOCALFILE);
|
|
LOCALFILE = nullptr;
|
|
}
|
|
}
|
|
|
|
CFtpGet::CFtpGet(const char *URL, const char *localfile, const char *Username, const char *Password)
|
|
{
|
|
m_State = FTP_STATE_STARTUP;
|
|
|
|
m_DataSock = INVALID_SOCKET;
|
|
m_ControlSock = INVALID_SOCKET;
|
|
m_iBytesIn = 0;
|
|
m_iBytesTotal = 0;
|
|
m_Aborting = false;
|
|
m_Aborted = false;
|
|
|
|
LOCALFILE = fopen(localfile, "wb");
|
|
|
|
if (LOCALFILE == nullptr) {
|
|
m_State = FTP_STATE_CANT_WRITE_FILE;
|
|
m_Aborted = true;
|
|
|
|
return;
|
|
}
|
|
|
|
if (Username) {
|
|
m_szUserName = Username;
|
|
} else {
|
|
m_szUserName = "anonymous";
|
|
}
|
|
|
|
if (Password) {
|
|
m_szPassword = Password;
|
|
} else {
|
|
m_szPassword = "[email protected]";
|
|
}
|
|
|
|
m_ControlSock = socket(AF_INET6, SOCK_STREAM, 0);
|
|
|
|
if (m_ControlSock == INVALID_SOCKET) {
|
|
m_State = FTP_STATE_SOCKET_ERROR;
|
|
m_Aborted = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// make sure we are in dual-stack mode (not the default on Windows)
|
|
int i_opt = 0;
|
|
setsockopt(m_ControlSock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&i_opt), sizeof(i_opt));
|
|
|
|
// Parse the URL
|
|
// Get rid of any extra ftp:// stuff
|
|
const char *pURL = URL;
|
|
|
|
if ( strnicmp(URL, "ftp:", 4) == 0 ) {
|
|
pURL += 4;
|
|
|
|
while (*pURL == '/') {
|
|
pURL++;
|
|
}
|
|
}
|
|
|
|
// There shouldn't be any : in this string
|
|
if ( strchr(pURL, ':') ) {
|
|
m_State = FTP_STATE_URL_PARSING_ERROR;
|
|
m_Aborted = true;
|
|
return;
|
|
}
|
|
|
|
// parse out host, directory, and filename
|
|
SCP_string url_str(pURL);
|
|
|
|
size_t filestart = url_str.find_last_of('/');
|
|
size_t dirstart = url_str.find_first_of('/');
|
|
|
|
if ( (filestart == SCP_string::npos) || (dirstart == SCP_string::npos) ) {
|
|
m_State = FTP_STATE_URL_PARSING_ERROR;
|
|
m_Aborted = true;
|
|
return;
|
|
}
|
|
|
|
m_szHost = url_str.substr(0, dirstart);
|
|
|
|
if (dirstart != filestart) {
|
|
m_szDir = url_str.substr(dirstart+1, filestart);
|
|
}
|
|
|
|
m_szFilename = url_str.substr(filestart+1);
|
|
|
|
|
|
SDL_Thread *thread = SDL_CreateThread(FTPObjThread, "FTPObjThread", this);
|
|
|
|
if (thread == nullptr) {
|
|
m_State = FTP_STATE_INTERNAL_ERROR;
|
|
m_Aborted = true;
|
|
return;
|
|
} else {
|
|
SDL_DetachThread(thread);
|
|
}
|
|
|
|
m_State = FTP_STATE_CONNECTING;
|
|
}
|
|
|
|
|
|
|
|
CFtpGet::~CFtpGet()
|
|
{
|
|
if (m_DataSock != INVALID_SOCKET) {
|
|
shutdown(m_DataSock, 2);
|
|
closesocket(m_DataSock);
|
|
}
|
|
|
|
if (m_ControlSock != INVALID_SOCKET) {
|
|
shutdown(m_ControlSock, 2);
|
|
closesocket(m_ControlSock);
|
|
}
|
|
|
|
if (LOCALFILE != nullptr) {
|
|
fclose(LOCALFILE);
|
|
LOCALFILE = nullptr;
|
|
}
|
|
}
|
|
|
|
//Returns a value to specify the status (ie. connecting/connected/transferring/done)
|
|
int CFtpGet::GetStatus()
|
|
{
|
|
return m_State;
|
|
}
|
|
|
|
uint CFtpGet::GetBytesIn()
|
|
{
|
|
return m_iBytesIn;
|
|
}
|
|
|
|
uint CFtpGet::GetTotalBytes()
|
|
{
|
|
|
|
return m_iBytesTotal;
|
|
}
|
|
|
|
//This function does all the work -- connects on a blocking socket
|
|
//then sends the appropriate user and password commands
|
|
//and then the cwd command, the port command then get and finally the quit
|
|
void CFtpGet::WorkerThread()
|
|
{
|
|
ConnectControlSocket();
|
|
|
|
if (m_State != FTP_STATE_LOGGING_IN) {
|
|
return;
|
|
}
|
|
|
|
LoginHost();
|
|
|
|
if (m_State != FTP_STATE_LOGGED_IN) {
|
|
return;
|
|
}
|
|
|
|
GetFile();
|
|
|
|
LogoutHost();
|
|
|
|
// We are all done now, and state has the current state.
|
|
m_Aborted = true;
|
|
}
|
|
|
|
uint CFtpGet::GetFile()
|
|
{
|
|
// Start off by changing into the proper dir.
|
|
SCP_string response;
|
|
SCP_string szCommandString;
|
|
int rcode;
|
|
|
|
rcode = SendFTPCommand("TYPE I");
|
|
|
|
if (rcode >=400) {
|
|
m_State = FTP_STATE_UNKNOWN_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
if ( !m_szDir.empty() ) {
|
|
szCommandString = "CWD "; // end space is intentional!
|
|
szCommandString += m_szDir;
|
|
|
|
rcode = SendFTPCommand(szCommandString.c_str());
|
|
|
|
if (rcode >=400) {
|
|
m_State = FTP_STATE_DIRECTORY_INVALID;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
if ( !IssuePasv() ) {
|
|
m_State = FTP_STATE_UNKNOWN_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
szCommandString = "RETR "; // end space is intentional!
|
|
szCommandString += m_szFilename;
|
|
|
|
rcode = SendFTPCommand(szCommandString.c_str(), &response);
|
|
|
|
if (rcode >=400) {
|
|
m_State = FTP_STATE_FILE_NOT_FOUND;
|
|
return 0;
|
|
}
|
|
|
|
// 150 Opening BINARY mode data connection for filename (num bytes)
|
|
size_t offset = response.find('(');
|
|
|
|
if (offset != SCP_string::npos) {
|
|
SCP_string val = response.substr(offset+1);
|
|
|
|
sscanf(val.c_str(), "%u ", &m_iBytesTotal);
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
ReadDataChannel();
|
|
|
|
m_State = FTP_STATE_FILE_RECEIVED;
|
|
|
|
return 1;
|
|
}
|
|
|
|
bool CFtpGet::IssuePasv()
|
|
{
|
|
SOCKADDR_STORAGE dataaddr;
|
|
SCP_string response;
|
|
SCP_string val;
|
|
int rcode;
|
|
uint16_t port;
|
|
|
|
// try extended passive mode first, then fall back to regular,
|
|
if ( (rcode = SendFTPCommand("EPSV", &response)) >= 400 ) {
|
|
if ( (rcode = SendFTPCommand("PASV", &response)) >= 400 ) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return false;
|
|
}
|
|
|
|
size_t offset = response.find('(');
|
|
|
|
if (offset == SCP_string::npos) {
|
|
return false;
|
|
}
|
|
|
|
val = response.substr(offset+1);
|
|
|
|
// 229 Entering Extended Passive Mode (|||port|)
|
|
if (rcode == 229) {
|
|
unsigned int i_port;
|
|
|
|
sscanf(val.c_str(), "|||%u|", &i_port);
|
|
port = static_cast<uint16_t>(i_port);
|
|
}
|
|
// 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
|
|
else if (rcode == 227) {
|
|
unsigned int p1, p2;
|
|
|
|
// per the spec for IPv6 compat we'll skip host and only grab port
|
|
sscanf(val.c_str(), ",%u,%u)", &p1, &p2);
|
|
port = static_cast<uint16_t>(p1 * 256 + p2);
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return false;
|
|
}
|
|
|
|
memcpy(&dataaddr, &m_ServerAddr, sizeof(dataaddr));
|
|
|
|
switch (dataaddr.ss_family) {
|
|
case AF_INET: {
|
|
auto *sa4 = reinterpret_cast<SOCKADDR_IN *>(&dataaddr);
|
|
|
|
sa4->sin_port = htons(port);
|
|
|
|
break;
|
|
}
|
|
|
|
case AF_INET6: {
|
|
auto *sa6 = reinterpret_cast<SOCKADDR_IN6 *>(&dataaddr);
|
|
|
|
sa6->sin6_port = htons(port);
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
Int3();
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return false;
|
|
}
|
|
|
|
m_DataSock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
|
|
|
|
if (m_DataSock == INVALID_SOCKET) {
|
|
return false;
|
|
}
|
|
|
|
// make sure we are in dual-stack mode (not the default on Windows)
|
|
int i_opt = 0;
|
|
setsockopt(m_DataSock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast<const char *>(&i_opt), sizeof(i_opt));
|
|
|
|
if (m_Aborting) {
|
|
return false;
|
|
}
|
|
|
|
if ( connect(m_DataSock, reinterpret_cast<LPSOCKADDR>(&dataaddr), psnet_get_sockaddr_len(&dataaddr)) == SOCKET_ERROR ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int CFtpGet::ConnectControlSocket()
|
|
{
|
|
// server address will be needed again for data socket so be sure it's saved
|
|
if ( !psnet_get_addr(m_szHost.c_str(), "ftp", &m_ServerAddr) ) {
|
|
m_State = FTP_STATE_HOST_NOT_FOUND;
|
|
return 0;
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
// Now we will connect to the host
|
|
if ( connect(m_ControlSock, reinterpret_cast<LPSOCKADDR>(&m_ServerAddr), psnet_get_sockaddr_len(&m_ServerAddr)) ) {
|
|
m_State = FTP_STATE_CANT_CONNECT;
|
|
return 0;
|
|
}
|
|
|
|
m_State = FTP_STATE_LOGGING_IN;
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
int CFtpGet::LoginHost()
|
|
{
|
|
SCP_string szLoginString;
|
|
int rcode;
|
|
|
|
szLoginString = "USER "; // end space is intentional!
|
|
szLoginString += m_szUserName;
|
|
|
|
rcode = SendFTPCommand(szLoginString.c_str());
|
|
|
|
if (rcode >= 400) {
|
|
m_State = FTP_STATE_LOGIN_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
szLoginString = "PASS "; // end space is intentional!
|
|
szLoginString += m_szPassword;
|
|
|
|
rcode = SendFTPCommand(szLoginString.c_str());
|
|
|
|
if (rcode >= 400) {
|
|
m_State = FTP_STATE_LOGIN_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
m_State = FTP_STATE_LOGGED_IN;
|
|
|
|
return 1;
|
|
}
|
|
|
|
void CFtpGet::LogoutHost()
|
|
{
|
|
SendFTPCommand("QUIT");
|
|
}
|
|
|
|
int CFtpGet::SendFTPCommand(const char *command, SCP_string *response)
|
|
{
|
|
const SCP_string eol = "\r\n"; // CRLF
|
|
SCP_string cmd;
|
|
|
|
if (command == nullptr) {
|
|
return 999;
|
|
}
|
|
|
|
cmd = command;
|
|
cmd += eol;
|
|
|
|
// Send the FTP command
|
|
if ( send(m_ControlSock, cmd.c_str(), static_cast<int>(cmd.length()), 0) == SOCKET_ERROR ) {
|
|
// Return 999 to indicate an error has occurred
|
|
return 999;
|
|
}
|
|
|
|
// Read the server's reply and return the reply code as an integer
|
|
return ReadFTPServerReply(response);
|
|
}
|
|
|
|
int CFtpGet::ReadFTPServerReply(SCP_string *str_reply)
|
|
{
|
|
SSIZE_T rval;
|
|
SCP_string code_str;
|
|
const size_t RECV_BUF_SIZE = 1024;
|
|
char recv_buffer[RECV_BUF_SIZE];
|
|
bool multi_line = false;
|
|
int code;
|
|
|
|
char *p_buf = nullptr;
|
|
char *p_save = nullptr;
|
|
|
|
do {
|
|
memset(recv_buffer, 0, RECV_BUF_SIZE);
|
|
|
|
rval = recv(m_ControlSock, recv_buffer, RECV_BUF_SIZE, 0);
|
|
|
|
if (rval <= 0) {
|
|
break;
|
|
}
|
|
|
|
if (m_Aborting) {
|
|
break;
|
|
}
|
|
|
|
// buffer could contain multiple lines so loop through them all
|
|
p_buf = strtok_r(recv_buffer, "\r", &p_save);
|
|
|
|
while (p_buf) {
|
|
// response eol is CRLF, so skip the LF too
|
|
if (*p_buf == '\n') {
|
|
++p_buf;
|
|
}
|
|
|
|
if ( strlen(p_buf) < 4 ) {
|
|
break;
|
|
}
|
|
|
|
// skip muli-line responses
|
|
// 230-start
|
|
// ...
|
|
// 230 end
|
|
if (p_buf[3] == '-') {
|
|
multi_line = true;
|
|
}
|
|
// normal response or end of multi-line
|
|
else if (p_buf[3] == ' ') {
|
|
if (multi_line) {
|
|
multi_line = false;
|
|
} else {
|
|
code_str = SCP_string(p_buf, 0, 3);
|
|
code = std::stoi(code_str);
|
|
|
|
if (str_reply) {
|
|
str_reply->assign(p_buf+4);
|
|
}
|
|
|
|
return code;
|
|
}
|
|
}
|
|
|
|
p_buf = strtok_r(nullptr, "\r", &p_save);
|
|
}
|
|
|
|
os_sleep(1);
|
|
} while (true);
|
|
|
|
return 999;
|
|
}
|
|
|
|
uint CFtpGet::ReadDataChannel()
|
|
{
|
|
const size_t BUF_SIZE = 4096;
|
|
char sDataBuffer[BUF_SIZE];
|
|
SSIZE_T nBytesRecv;
|
|
|
|
m_State = FTP_STATE_RECEIVING;
|
|
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
do {
|
|
if (m_Aborting) {
|
|
return 0;
|
|
}
|
|
|
|
nBytesRecv = recv(m_DataSock, sDataBuffer, BUF_SIZE, 0);
|
|
|
|
if (nBytesRecv > 0) {
|
|
m_iBytesIn += static_cast<uint>(nBytesRecv);
|
|
fwrite(sDataBuffer, static_cast<size_t>(nBytesRecv), 1, LOCALFILE);
|
|
}
|
|
|
|
os_sleep(1);
|
|
} while (nBytesRecv > 0);
|
|
|
|
fclose(LOCALFILE);
|
|
LOCALFILE = nullptr;
|
|
|
|
// Close the file and check for error returns.
|
|
if (nBytesRecv == SOCKET_ERROR) {
|
|
// Ok, we got a socket error -- xfer aborted?
|
|
m_State = FTP_STATE_RECV_FAILED;
|
|
return 0;
|
|
} else {
|
|
// done!
|
|
m_State = FTP_STATE_FILE_RECEIVED;
|
|
return 1;
|
|
}
|
|
}
|