in libs/curl/lib/ftp.c [2813:3265]
static CURLcode ftp_statemachine(struct Curl_easy *data,
struct connectdata *conn)
{
CURLcode result;
int ftpcode;
struct ftp_conn *ftpc = &conn->proto.ftpc;
struct pingpong *pp = &ftpc->pp;
static const char * const ftpauth[] = { "SSL", "TLS" };
size_t nread = 0;
if(pp->sendleft)
return Curl_pp_flushsend(data, pp);
result = ftp_readresp(data, FIRSTSOCKET, pp, &ftpcode, &nread);
if(result)
return result;
if(ftpcode) {
/* we have now received a full FTP server response */
switch(ftpc->state) {
case FTP_WAIT220:
if(ftpcode == 230) {
/* 230 User logged in - already! Take as 220 if TLS required. */
if(data->set.use_ssl <= CURLUSESSL_TRY ||
conn->bits.ftp_use_control_ssl)
return ftp_state_user_resp(data, ftpcode);
}
else if(ftpcode != 220) {
failf(data, "Got a %03d ftp-server response when 220 was expected",
ftpcode);
return CURLE_WEIRD_SERVER_REPLY;
}
/* We have received a 220 response fine, now we proceed. */
#ifdef HAVE_GSSAPI
if(data->set.krb) {
/* If not anonymous login, try a secure login. Note that this
procedure is still BLOCKING. */
Curl_sec_request_prot(conn, "private");
/* We set private first as default, in case the line below fails to
set a valid level */
Curl_sec_request_prot(conn, data->set.str[STRING_KRB_LEVEL]);
if(Curl_sec_login(data, conn)) {
failf(data, "secure login failed");
return CURLE_WEIRD_SERVER_REPLY;
}
infof(data, "Authentication successful");
}
#endif
if(data->set.use_ssl && !conn->bits.ftp_use_control_ssl) {
/* We do not have a SSL/TLS control connection yet, but FTPS is
requested. Try a FTPS connection now */
ftpc->count3 = 0;
switch(data->set.ftpsslauth) {
case CURLFTPAUTH_DEFAULT:
case CURLFTPAUTH_SSL:
ftpc->count2 = 1; /* add one to get next */
ftpc->count1 = 0;
break;
case CURLFTPAUTH_TLS:
ftpc->count2 = -1; /* subtract one to get next */
ftpc->count1 = 1;
break;
default:
failf(data, "unsupported parameter to CURLOPT_FTPSSLAUTH: %d",
(int)data->set.ftpsslauth);
return CURLE_UNKNOWN_OPTION; /* we do not know what to do */
}
result = Curl_pp_sendf(data, &ftpc->pp, "AUTH %s",
ftpauth[ftpc->count1]);
if(!result)
ftp_state(data, FTP_AUTH);
}
else
result = ftp_state_user(data, conn);
break;
case FTP_AUTH:
/* we have gotten the response to a previous AUTH command */
if(pp->overflow)
return CURLE_WEIRD_SERVER_REPLY; /* Forbid pipelining in response. */
/* RFC2228 (page 5) says:
*
* If the server is willing to accept the named security mechanism,
* and does not require any security data, it must respond with
* reply code 234/334.
*/
if((ftpcode == 234) || (ftpcode == 334)) {
/* this was BLOCKING, keep it so for now */
bool done;
if(!Curl_conn_is_ssl(conn, FIRSTSOCKET)) {
result = Curl_ssl_cfilter_add(data, conn, FIRSTSOCKET);
if(result) {
/* we failed and bail out */
return CURLE_USE_SSL_FAILED;
}
}
result = Curl_conn_connect(data, FIRSTSOCKET, TRUE, &done);
if(!result) {
conn->bits.ftp_use_data_ssl = FALSE; /* clear-text data */
conn->bits.ftp_use_control_ssl = TRUE; /* SSL on control */
result = ftp_state_user(data, conn);
}
}
else if(ftpc->count3 < 1) {
ftpc->count3++;
ftpc->count1 += ftpc->count2; /* get next attempt */
result = Curl_pp_sendf(data, &ftpc->pp, "AUTH %s",
ftpauth[ftpc->count1]);
/* remain in this same state */
}
else {
if(data->set.use_ssl > CURLUSESSL_TRY)
/* we failed and CURLUSESSL_CONTROL or CURLUSESSL_ALL is set */
result = CURLE_USE_SSL_FAILED;
else
/* ignore the failure and continue */
result = ftp_state_user(data, conn);
}
break;
case FTP_USER:
case FTP_PASS:
result = ftp_state_user_resp(data, ftpcode);
break;
case FTP_ACCT:
result = ftp_state_acct_resp(data, ftpcode);
break;
case FTP_PBSZ:
result =
Curl_pp_sendf(data, &ftpc->pp, "PROT %c",
data->set.use_ssl == CURLUSESSL_CONTROL ? 'C' : 'P');
if(!result)
ftp_state(data, FTP_PROT);
break;
case FTP_PROT:
if(ftpcode/100 == 2)
/* We have enabled SSL for the data connection! */
conn->bits.ftp_use_data_ssl =
(data->set.use_ssl != CURLUSESSL_CONTROL) ? TRUE : FALSE;
/* FTP servers typically responds with 500 if they decide to reject
our 'P' request */
else if(data->set.use_ssl > CURLUSESSL_CONTROL)
/* we failed and bails out */
return CURLE_USE_SSL_FAILED;
if(data->set.ftp_ccc) {
/* CCC - Clear Command Channel
*/
result = Curl_pp_sendf(data, &ftpc->pp, "%s", "CCC");
if(!result)
ftp_state(data, FTP_CCC);
}
else
result = ftp_state_pwd(data, conn);
break;
case FTP_CCC:
if(ftpcode < 500) {
/* First shut down the SSL layer (note: this call will block) */
/* This has only been tested on the proftpd server, and the mod_tls
* code sends a close notify alert without waiting for a close notify
* alert in response. Thus we wait for a close notify alert from the
* server, but we do not send one. Let's hope other servers do
* the same... */
result = Curl_ssl_cfilter_remove(data, FIRSTSOCKET,
(data->set.ftp_ccc == CURLFTPSSL_CCC_ACTIVE));
if(result)
failf(data, "Failed to clear the command channel (CCC)");
}
if(!result)
/* Then continue as normal */
result = ftp_state_pwd(data, conn);
break;
case FTP_PWD:
if(ftpcode == 257) {
char *ptr = Curl_dyn_ptr(&pp->recvbuf) + 4; /* start on the first
letter */
bool entry_extracted = FALSE;
struct dynbuf out;
Curl_dyn_init(&out, 1000);
/* Reply format is like
257<space>[rubbish]"<directory-name>"<space><commentary> and the
RFC959 says
The directory name can contain any character; embedded
double-quotes should be escaped by double-quotes (the
"quote-doubling" convention).
*/
/* scan for the first double-quote for non-standard responses */
while(*ptr != '\n' && *ptr != '\0' && *ptr != '"')
ptr++;
if('\"' == *ptr) {
/* it started good */
for(ptr++; *ptr; ptr++) {
if('\"' == *ptr) {
if('\"' == ptr[1]) {
/* "quote-doubling" */
result = Curl_dyn_addn(&out, &ptr[1], 1);
ptr++;
}
else {
/* end of path */
if(Curl_dyn_len(&out))
entry_extracted = TRUE;
break; /* get out of this loop */
}
}
else
result = Curl_dyn_addn(&out, ptr, 1);
if(result)
return result;
}
}
if(entry_extracted) {
/* If the path name does not look like an absolute path (i.e.: it
does not start with a '/'), we probably need some server-dependent
adjustments. For example, this is the case when connecting to
an OS400 FTP server: this server supports two name syntaxes,
the default one being incompatible with standard paths. In
addition, this server switches automatically to the regular path
syntax when one is encountered in a command: this results in
having an entrypath in the wrong syntax when later used in CWD.
The method used here is to check the server OS: we do it only
if the path name looks strange to minimize overhead on other
systems. */
char *dir = Curl_dyn_ptr(&out);
if(!ftpc->server_os && dir[0] != '/') {
result = Curl_pp_sendf(data, &ftpc->pp, "%s", "SYST");
if(result) {
free(dir);
return result;
}
Curl_safefree(ftpc->entrypath);
ftpc->entrypath = dir; /* remember this */
infof(data, "Entry path is '%s'", ftpc->entrypath);
/* also save it where getinfo can access it: */
data->state.most_recent_ftp_entrypath = ftpc->entrypath;
ftp_state(data, FTP_SYST);
break;
}
Curl_safefree(ftpc->entrypath);
ftpc->entrypath = dir; /* remember this */
infof(data, "Entry path is '%s'", ftpc->entrypath);
/* also save it where getinfo can access it: */
data->state.most_recent_ftp_entrypath = ftpc->entrypath;
}
else {
/* could not get the path */
Curl_dyn_free(&out);
infof(data, "Failed to figure out path");
}
}
ftp_state(data, FTP_STOP); /* we are done with the CONNECT phase! */
CURL_TRC_FTP(data, "[%s] protocol connect phase DONE", FTP_DSTATE(data));
break;
case FTP_SYST:
if(ftpcode == 215) {
char *ptr = Curl_dyn_ptr(&pp->recvbuf) + 4; /* start on the first
letter */
char *os;
char *start;
/* Reply format is like
215<space><OS-name><space><commentary>
*/
while(*ptr == ' ')
ptr++;
for(start = ptr; *ptr && *ptr != ' '; ptr++)
;
os = Curl_memdup0(start, ptr - start);
if(!os)
return CURLE_OUT_OF_MEMORY;
/* Check for special servers here. */
if(strcasecompare(os, "OS/400")) {
/* Force OS400 name format 1. */
result = Curl_pp_sendf(data, &ftpc->pp, "%s", "SITE NAMEFMT 1");
if(result) {
free(os);
return result;
}
/* remember target server OS */
Curl_safefree(ftpc->server_os);
ftpc->server_os = os;
ftp_state(data, FTP_NAMEFMT);
break;
}
/* Nothing special for the target server. */
/* remember target server OS */
Curl_safefree(ftpc->server_os);
ftpc->server_os = os;
}
else {
/* Cannot identify server OS. Continue anyway and cross fingers. */
}
ftp_state(data, FTP_STOP); /* we are done with the CONNECT phase! */
CURL_TRC_FTP(data, "[%s] protocol connect phase DONE", FTP_DSTATE(data));
break;
case FTP_NAMEFMT:
if(ftpcode == 250) {
/* Name format change successful: reload initial path. */
ftp_state_pwd(data, conn);
break;
}
ftp_state(data, FTP_STOP); /* we are done with the CONNECT phase! */
CURL_TRC_FTP(data, "[%s] protocol connect phase DONE", FTP_DSTATE(data));
break;
case FTP_QUOTE:
case FTP_POSTQUOTE:
case FTP_RETR_PREQUOTE:
case FTP_STOR_PREQUOTE:
if((ftpcode >= 400) && !ftpc->count2) {
/* failure response code, and not allowed to fail */
failf(data, "QUOT command failed with %03d", ftpcode);
result = CURLE_QUOTE_ERROR;
}
else
result = ftp_state_quote(data, FALSE, ftpc->state);
break;
case FTP_CWD:
if(ftpcode/100 != 2) {
/* failure to CWD there */
if(data->set.ftp_create_missing_dirs &&
ftpc->cwdcount && !ftpc->count2) {
/* try making it */
ftpc->count2++; /* counter to prevent CWD-MKD loops */
/* count3 is set to allow MKD to fail once per dir. In the case when
CWD fails and then MKD fails (due to another session raced it to
create the dir) this then allows for a second try to CWD to it. */
ftpc->count3 = (data->set.ftp_create_missing_dirs == 2) ? 1 : 0;
result = Curl_pp_sendf(data, &ftpc->pp, "MKD %s",
ftpc->dirs[ftpc->cwdcount - 1]);
if(!result)
ftp_state(data, FTP_MKD);
}
else {
/* return failure */
failf(data, "Server denied you to change to the given directory");
ftpc->cwdfail = TRUE; /* do not remember this path as we failed
to enter it */
result = CURLE_REMOTE_ACCESS_DENIED;
}
}
else {
/* success */
ftpc->count2 = 0;
if(++ftpc->cwdcount <= ftpc->dirdepth)
/* send next CWD */
result = Curl_pp_sendf(data, &ftpc->pp, "CWD %s",
ftpc->dirs[ftpc->cwdcount - 1]);
else
result = ftp_state_mdtm(data);
}
break;
case FTP_MKD:
if((ftpcode/100 != 2) && !ftpc->count3--) {
/* failure to MKD the dir */
failf(data, "Failed to MKD dir: %03d", ftpcode);
result = CURLE_REMOTE_ACCESS_DENIED;
}
else {
ftp_state(data, FTP_CWD);
/* send CWD */
result = Curl_pp_sendf(data, &ftpc->pp, "CWD %s",
ftpc->dirs[ftpc->cwdcount - 1]);
}
break;
case FTP_MDTM:
result = ftp_state_mdtm_resp(data, ftpcode);
break;
case FTP_TYPE:
case FTP_LIST_TYPE:
case FTP_RETR_TYPE:
case FTP_STOR_TYPE:
result = ftp_state_type_resp(data, ftpcode, ftpc->state);
break;
case FTP_SIZE:
case FTP_RETR_SIZE:
case FTP_STOR_SIZE:
result = ftp_state_size_resp(data, ftpcode, ftpc->state);
break;
case FTP_REST:
case FTP_RETR_REST:
result = ftp_state_rest_resp(data, conn, ftpcode, ftpc->state);
break;
case FTP_PRET:
if(ftpcode != 200) {
/* there only is this one standard OK return code. */
failf(data, "PRET command not accepted: %03d", ftpcode);
return CURLE_FTP_PRET_FAILED;
}
result = ftp_state_use_pasv(data, conn);
break;
case FTP_PASV:
result = ftp_state_pasv_resp(data, ftpcode);
break;
case FTP_PORT:
result = ftp_state_port_resp(data, ftpcode);
break;
case FTP_LIST:
case FTP_RETR:
result = ftp_state_get_resp(data, ftpcode, ftpc->state);
break;
case FTP_STOR:
result = ftp_state_stor_resp(data, ftpcode, ftpc->state);
break;
case FTP_QUIT:
default:
/* internal error */
ftp_state(data, FTP_STOP);
break;
}
} /* if(ftpcode) */
return result;
}