Files
tubestation/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
Narcis Beleuzu 359bfccedf Backed out 8 changesets (bug 1574475, bug 1699222) for build bustages on moz.build . CLOSED TREE
Backed out changeset 6c2c039872b3 (bug 1574475)
Backed out changeset 8a2a04743c5f (bug 1699222)
Backed out changeset 9437c60798d6 (bug 1574475)
Backed out changeset 7ef1884ac11b (bug 1574475)
Backed out changeset ec8c237d5298 (bug 1574475)
Backed out changeset 4a760b3f5d53 (bug 1574475)
Backed out changeset b229b0eea1e7 (bug 1574475)
Backed out changeset 03d34a2f10a6 (bug 1574475)
2021-04-22 18:10:59 +03:00

1951 lines
56 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set tw=80 ts=4 sts=2 sw=2 et cin: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <ctype.h>
#include "prprf.h"
#include "mozilla/Logging.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/TextUtils.h"
#include "prtime.h"
#include "nsIOService.h"
#include "nsFTPChannel.h"
#include "nsFtpConnectionThread.h"
#include "nsFtpControlConnection.h"
#include "nsFtpProtocolHandler.h"
#include "netCore.h"
#include "nsCRT.h"
#include "nsEscape.h"
#include "nsMimeTypes.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsIAsyncStreamCopier.h"
#include "nsThreadUtils.h"
#include "nsStreamUtils.h"
#include "nsIURL.h"
#include "nsISocketTransport.h"
#include "nsIPrefBranch.h"
#include "nsAuthInformationHolder.h"
#include "nsIProtocolProxyService.h"
#include "nsICancelable.h"
#include "nsIOutputStream.h"
#include "nsIProtocolHandler.h"
#include "nsIProxyInfo.h"
#include "nsISocketTransportService.h"
#include "nsIURI.h"
#include "nsILoadInfo.h"
#include "nsIAuthPrompt2.h"
#include "nsIFTPChannelParentInternal.h"
#include "mozilla/Telemetry.h"
using namespace mozilla;
using namespace mozilla::net;
extern LazyLogModule gFTPLog;
#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args)
#define LOG_INFO(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Info, args)
// remove FTP parameters (starting with ";") from the path
static void removeParamsFromPath(nsCString& path) {
int32_t index = path.FindChar(';');
if (index >= 0) {
path.SetLength(index);
}
}
NS_IMPL_ISUPPORTS_INHERITED(nsFtpState, nsBaseContentStream,
nsIInputStreamCallback, nsITransportEventSink,
nsIRequestObserver, nsIProtocolProxyCallback)
nsFtpState::nsFtpState()
: nsBaseContentStream(true),
mState(FTP_INIT),
mNextState(FTP_S_USER),
mKeepRunning(true),
mResponseCode(0),
mReceivedControlData(false),
mTryingCachedControl(false),
mRETRFailed(false),
mFileSize(kJS_MAX_SAFE_UINTEGER),
mServerType(FTP_GENERIC_TYPE),
mAction(GET),
mAnonymous(true),
mRetryPass(false),
mStorReplyReceived(false),
mRlist1xxReceived(false),
mRretr1xxReceived(false),
mRstor1xxReceived(false),
mInternalError(NS_OK),
mReconnectAndLoginAgain(false),
mCacheConnection(true),
mPort(21),
mAddressChecked(false),
mServerIsIPv6(false),
mUseUTF8(false),
mControlStatus(NS_OK),
mDeferredCallbackPending(false) {
this->mServerAddress.raw.family = 0;
this->mServerAddress.inet = {};
LOG_INFO(("FTP:(%p) nsFtpState created", this));
// make sure handler stays around
mHandler = gFtpHandler;
}
nsFtpState::~nsFtpState() {
LOG_INFO(("FTP:(%p) nsFtpState destroyed", this));
if (mProxyRequest) mProxyRequest->Cancel(NS_ERROR_FAILURE);
// release reference to handler
mHandler = nullptr;
}
// nsIInputStreamCallback implementation
NS_IMETHODIMP
nsFtpState::OnInputStreamReady(nsIAsyncInputStream* aInStream) {
LOG(("FTP:(%p) data stream ready\n", this));
// We are receiving a notification from our data stream, so just forward it
// on to our stream callback.
if (HasPendingCallback()) DispatchCallbackSync();
return NS_OK;
}
void nsFtpState::OnControlDataAvailable(const char* aData, uint32_t aDataLen) {
LOG(("FTP:(%p) control data available [%u]\n", this, aDataLen));
mControlConnection->WaitData(this); // queue up another call
if (!mReceivedControlData) {
// parameter can be null cause the channel fills them in.
OnTransportStatus(nullptr, NS_NET_STATUS_BEGIN_FTP_TRANSACTION, 0, 0);
mReceivedControlData = true;
}
// Sometimes we can get two responses in the same packet, eg from LIST.
// So we need to parse the response line by line
nsCString buffer = mControlReadCarryOverBuf;
// Clear the carryover buf - if we still don't have a line, then it will
// be reappended below
mControlReadCarryOverBuf.Truncate();
buffer.Append(aData, aDataLen);
const char* currLine = buffer.get();
while (*currLine && mKeepRunning) {
int32_t eolLength = strcspn(currLine, CRLF);
int32_t currLineLength = strlen(currLine);
// if currLine is empty or only contains CR or LF, then bail. we can
// sometimes get an ODA event with the full response line + CR without
// the trailing LF. the trailing LF might come in the next ODA event.
// because we are happy enough to process a response line ending only
// in CR, we need to take care to discard the extra LF (bug 191220).
if (eolLength == 0 && currLineLength <= 1) break;
if (eolLength == currLineLength) {
mControlReadCarryOverBuf.Assign(currLine);
break;
}
// Append the current segment, including the LF
nsAutoCString line;
int32_t crlfLength = 0;
if ((currLineLength > eolLength) && (currLine[eolLength] == nsCRT::CR) &&
(currLine[eolLength + 1] == nsCRT::LF)) {
crlfLength = 2; // CR +LF
} else {
crlfLength = 1; // + LF or CR
}
line.Assign(currLine, eolLength + crlfLength);
// Does this start with a response code?
bool startNum = (line.Length() >= 3 && IsAsciiDigit(line[0]) &&
IsAsciiDigit(line[1]) && IsAsciiDigit(line[2]));
if (mResponseMsg.IsEmpty()) {
// If we get here, then we know that we have a complete line, and
// that it is the first one
NS_ASSERTION(line.Length() > 4 && startNum,
"Read buffer doesn't include response code");
mResponseCode = atoi(PromiseFlatCString(Substring(line, 0, 3)).get());
}
mResponseMsg.Append(line);
// This is the last line if its 3 numbers followed by a space
if (startNum && line[3] == ' ') {
// yup. last line, let's move on.
if (mState == mNextState) {
NS_ERROR("ftp read state mixup");
mInternalError = NS_ERROR_FAILURE;
mState = FTP_ERROR;
} else {
mState = mNextState;
}
nsCOMPtr<nsIFTPEventSink> ftpSink;
mChannel->GetFTPEventSink(ftpSink);
if (ftpSink) ftpSink->OnFTPControlLog(true, mResponseMsg.get());
nsresult rv = Process();
mResponseMsg.Truncate();
if (NS_FAILED(rv)) {
CloseWithStatus(rv);
return;
}
}
currLine = currLine + eolLength + crlfLength;
}
}
void nsFtpState::OnControlError(nsresult status) {
NS_ASSERTION(NS_FAILED(status), "expecting error condition");
LOG(("FTP:(%p) CC(%p) error [%" PRIx32 " was-cached=%u]\n", this,
mControlConnection.get(), static_cast<uint32_t>(status),
mTryingCachedControl));
mControlStatus = status;
if (mReconnectAndLoginAgain && NS_SUCCEEDED(mInternalError)) {
mReconnectAndLoginAgain = false;
mAnonymous = false;
mControlStatus = NS_OK;
Connect();
} else if (mTryingCachedControl && NS_SUCCEEDED(mInternalError)) {
mTryingCachedControl = false;
Connect();
} else {
CloseWithStatus(status);
}
}
nsresult nsFtpState::EstablishControlConnection() {
NS_ASSERTION(!mControlConnection, "we already have a control connection");
nsresult rv;
LOG(("FTP:(%p) trying cached control\n", this));
// Look to see if we can use a cached control connection:
RefPtr<nsFtpControlConnection> connection;
// Don't use cached control if anonymous (bug #473371)
if (!mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS))
gFtpHandler->RemoveConnection(mChannel->URI(), getter_AddRefs(connection));
if (connection) {
mControlConnection.swap(connection);
if (mControlConnection->IsAlive()) {
// set stream listener of the control connection to be us.
mControlConnection->WaitData(this);
// read cached variables into us.
mServerType = mControlConnection->mServerType;
mPassword = mControlConnection->mPassword;
mPwd = mControlConnection->mPwd;
mUseUTF8 = mControlConnection->mUseUTF8;
mTryingCachedControl = true;
// we have to set charset to connection if server supports utf-8
if (mUseUTF8) mChannel->SetContentCharset("UTF-8"_ns);
// we're already connected to this server, skip login.
mState = FTP_S_PASV;
mResponseCode = 530; // assume the control connection was dropped.
mControlStatus = NS_OK;
mReceivedControlData = false; // For this request, we have not.
// if we succeed, return. Otherwise, we need to create a transport
rv = mControlConnection->Connect(mChannel->ProxyInfo(), this);
if (NS_SUCCEEDED(rv)) return rv;
}
LOG(("FTP:(%p) cached CC(%p) is unusable\n", this,
mControlConnection.get()));
mControlConnection->WaitData(nullptr);
mControlConnection = nullptr;
}
LOG(("FTP:(%p) creating CC\n", this));
mState = FTP_READ_BUF;
mNextState = FTP_S_USER;
nsAutoCString host;
rv = mChannel->URI()->GetAsciiHost(host);
if (NS_FAILED(rv)) return rv;
mControlConnection = new nsFtpControlConnection(host, mPort);
if (!mControlConnection) return NS_ERROR_OUT_OF_MEMORY;
rv = mControlConnection->Connect(mChannel->ProxyInfo(), this);
if (NS_FAILED(rv)) {
LOG(("FTP:(%p) CC(%p) failed to connect [rv=%" PRIx32 "]\n", this,
mControlConnection.get(), static_cast<uint32_t>(rv)));
mControlConnection = nullptr;
return rv;
}
return mControlConnection->WaitData(this);
}
void nsFtpState::MoveToNextState(FTP_STATE nextState) {
if (NS_FAILED(mInternalError)) {
mState = FTP_ERROR;
LOG(("FTP:(%p) FAILED (%" PRIx32 ")\n", this,
static_cast<uint32_t>(mInternalError)));
} else {
mState = FTP_READ_BUF;
mNextState = nextState;
}
}
nsresult nsFtpState::Process() {
nsresult rv = NS_OK;
bool processingRead = true;
while (mKeepRunning && processingRead) {
switch (mState) {
case FTP_COMMAND_CONNECT:
KillControlConnection();
LOG(("FTP:(%p) establishing CC", this));
mInternalError = EstablishControlConnection(); // sets mState
if (NS_FAILED(mInternalError)) {
mState = FTP_ERROR;
LOG(("FTP:(%p) FAILED\n", this));
} else {
LOG(("FTP:(%p) SUCCEEDED\n", this));
}
break;
case FTP_READ_BUF:
LOG(("FTP:(%p) Waiting for CC(%p)\n", this, mControlConnection.get()));
processingRead = false;
break;
case FTP_ERROR: // xx needs more work to handle dropped control
// connection cases
if ((mTryingCachedControl && mResponseCode == 530 &&
mInternalError == NS_ERROR_FTP_PASV) ||
(mResponseCode == 425 && mInternalError == NS_ERROR_FTP_PASV)) {
// The user was logged out during an pasv operation
// we want to restart this request with a new control
// channel.
mState = FTP_COMMAND_CONNECT;
} else if (mResponseCode == 421 &&
mInternalError != NS_ERROR_FTP_LOGIN) {
// The command channel dropped for some reason.
// Fire it back up, unless we were trying to login
// in which case the server might just be telling us
// that the max number of users has been reached...
mState = FTP_COMMAND_CONNECT;
} else if (mAnonymous && mInternalError == NS_ERROR_FTP_LOGIN) {
// If the login was anonymous, and it failed, try again with a
// username Don't reuse old control connection, see #386167
mAnonymous = false;
mState = FTP_COMMAND_CONNECT;
} else {
LOG(("FTP:(%p) FTP_ERROR - calling StopProcessing\n", this));
rv = StopProcessing();
NS_ASSERTION(NS_SUCCEEDED(rv), "StopProcessing failed.");
processingRead = false;
}
break;
case FTP_COMPLETE:
LOG(("FTP:(%p) COMPLETE\n", this));
rv = StopProcessing();
NS_ASSERTION(NS_SUCCEEDED(rv), "StopProcessing failed.");
processingRead = false;
break;
// USER
case FTP_S_USER:
rv = S_user();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN;
MoveToNextState(FTP_R_USER);
break;
case FTP_R_USER:
mState = R_user();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN;
break;
// PASS
case FTP_S_PASS:
rv = S_pass();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN;
MoveToNextState(FTP_R_PASS);
break;
case FTP_R_PASS:
mState = R_pass();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN;
break;
// ACCT
case FTP_S_ACCT:
rv = S_acct();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN;
MoveToNextState(FTP_R_ACCT);
break;
case FTP_R_ACCT:
mState = R_acct();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN;
break;
// SYST
case FTP_S_SYST:
rv = S_syst();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN;
MoveToNextState(FTP_R_SYST);
break;
case FTP_R_SYST:
mState = R_syst();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN;
break;
// TYPE
case FTP_S_TYPE:
rv = S_type();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_TYPE);
break;
case FTP_R_TYPE:
mState = R_type();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// CWD
case FTP_S_CWD:
rv = S_cwd();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_CWD;
MoveToNextState(FTP_R_CWD);
break;
case FTP_R_CWD:
mState = R_cwd();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_CWD;
break;
// LIST
case FTP_S_LIST:
rv = S_list();
if (rv == NS_ERROR_NOT_RESUMABLE) {
mInternalError = rv;
} else if (NS_FAILED(rv)) {
mInternalError = NS_ERROR_FTP_CWD;
}
MoveToNextState(FTP_R_LIST);
break;
case FTP_R_LIST:
mState = R_list();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// SIZE
case FTP_S_SIZE:
rv = S_size();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_SIZE);
break;
case FTP_R_SIZE:
mState = R_size();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// REST
case FTP_S_REST:
rv = S_rest();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_REST);
break;
case FTP_R_REST:
mState = R_rest();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// MDTM
case FTP_S_MDTM:
rv = S_mdtm();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_MDTM);
break;
case FTP_R_MDTM:
mState = R_mdtm();
// Don't want to overwrite a more explicit status code
if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError))
mInternalError = NS_ERROR_FAILURE;
break;
// RETR
case FTP_S_RETR:
rv = S_retr();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_RETR);
break;
case FTP_R_RETR:
mState = R_retr();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// STOR
case FTP_S_STOR:
rv = S_stor();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_STOR);
break;
case FTP_R_STOR:
mState = R_stor();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE;
break;
// PASV
case FTP_S_PASV:
rv = S_pasv();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_PASV;
MoveToNextState(FTP_R_PASV);
break;
case FTP_R_PASV:
mState = R_pasv();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_PASV;
break;
// PWD
case FTP_S_PWD:
rv = S_pwd();
if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_PWD;
MoveToNextState(FTP_R_PWD);
break;
case FTP_R_PWD:
mState = R_pwd();
if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_PWD;
break;
// FEAT for RFC2640 support
case FTP_S_FEAT:
rv = S_feat();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_FEAT);
break;
case FTP_R_FEAT:
mState = R_feat();
// Don't want to overwrite a more explicit status code
if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError))
mInternalError = NS_ERROR_FAILURE;
break;
// OPTS for some non-RFC2640-compliant servers support
case FTP_S_OPTS:
rv = S_opts();
if (NS_FAILED(rv)) mInternalError = rv;
MoveToNextState(FTP_R_OPTS);
break;
case FTP_R_OPTS:
mState = R_opts();
// Don't want to overwrite a more explicit status code
if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError))
mInternalError = NS_ERROR_FAILURE;
break;
default:;
}
}
return rv;
}
///////////////////////////////////
// STATE METHODS
///////////////////////////////////
nsresult nsFtpState::S_user() {
// some servers on connect send us a 421 or 521. (84525) (141784)
if ((mResponseCode == 421) || (mResponseCode == 521)) return NS_ERROR_FAILURE;
nsresult rv;
nsAutoCString usernameStr("USER ");
mResponseMsg = "";
if (mAnonymous) {
mReconnectAndLoginAgain = true;
usernameStr.AppendLiteral("anonymous");
} else {
mReconnectAndLoginAgain = false;
if (mUsername.IsEmpty()) {
// No prompt for anonymous requests (bug #473371)
if (mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS))
return NS_ERROR_FAILURE;
nsCOMPtr<nsIAuthPrompt2> prompter;
NS_QueryAuthPrompt2(static_cast<nsIChannel*>(mChannel),
getter_AddRefs(prompter));
if (!prompter) return NS_ERROR_NOT_INITIALIZED;
RefPtr<nsAuthInformationHolder> info = new nsAuthInformationHolder(
nsIAuthInformation::AUTH_HOST, u""_ns, ""_ns);
bool retval;
rv = prompter->PromptAuth(mChannel, nsIAuthPrompt2::LEVEL_NONE, info,
&retval);
// if the user canceled or didn't supply a username we want to fail
if (NS_FAILED(rv) || !retval || info->User().IsEmpty())
return NS_ERROR_FAILURE;
mUsername = info->User();
mPassword = info->Password();
}
// XXX Is UTF-8 the best choice?
AppendUTF16toUTF8(mUsername, usernameStr);
}
usernameStr.AppendLiteral(CRLF);
return SendFTPCommand(usernameStr);
}
FTP_STATE
nsFtpState::R_user() {
mReconnectAndLoginAgain = false;
if (mResponseCode / 100 == 3) {
// send off the password
return FTP_S_PASS;
}
if (mResponseCode / 100 == 2) {
// no password required, we're already logged in
return FTP_S_SYST;
}
if (mResponseCode / 100 == 5) {
// problem logging in. typically this means the server
// has reached it's user limit.
return FTP_ERROR;
}
// LOGIN FAILED
return FTP_ERROR;
}
nsresult nsFtpState::S_pass() {
nsresult rv;
nsAutoCString passwordStr("PASS ");
mResponseMsg = "";
if (mAnonymous) {
if (!mPassword.IsEmpty()) {
// XXX Is UTF-8 the best choice?
AppendUTF16toUTF8(mPassword, passwordStr);
} else {
nsAutoCString anonPassword;
bool useRealEmail = false;
nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
if (prefs) {
rv = prefs->GetBoolPref("advanced.mailftp", &useRealEmail);
if (NS_SUCCEEDED(rv) && useRealEmail) {
prefs->GetCharPref("network.ftp.anonymous_password", anonPassword);
}
}
if (!anonPassword.IsEmpty()) {
passwordStr.AppendASCII(anonPassword.get());
} else {
// We need to default to a valid email address - bug 101027
// example.com is reserved (rfc2606), so use that
passwordStr.AppendLiteral("mozilla@example.com");
}
}
} else {
if (mPassword.IsEmpty() || mRetryPass) {
// No prompt for anonymous requests (bug #473371)
if (mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS))
return NS_ERROR_FAILURE;
nsCOMPtr<nsIAuthPrompt2> prompter;
NS_QueryAuthPrompt2(static_cast<nsIChannel*>(mChannel),
getter_AddRefs(prompter));
if (!prompter) return NS_ERROR_NOT_INITIALIZED;
RefPtr<nsAuthInformationHolder> info = new nsAuthInformationHolder(
nsIAuthInformation::AUTH_HOST | nsIAuthInformation::ONLY_PASSWORD,
u""_ns, ""_ns);
info->SetUserInternal(mUsername);
bool retval;
rv = prompter->PromptAuth(mChannel, nsIAuthPrompt2::LEVEL_NONE, info,
&retval);
// we want to fail if the user canceled. Note here that if they want
// a blank password, we will pass it along.
if (NS_FAILED(rv) || !retval) return NS_ERROR_FAILURE;
mPassword = info->Password();
}
// XXX Is UTF-8 the best choice?
AppendUTF16toUTF8(mPassword, passwordStr);
}
passwordStr.AppendLiteral(CRLF);
return SendFTPCommand(passwordStr);
}
FTP_STATE
nsFtpState::R_pass() {
if (mResponseCode / 100 == 3) {
// send account info
return FTP_S_ACCT;
}
if (mResponseCode / 100 == 2) {
// logged in
return FTP_S_SYST;
}
if (mResponseCode == 503) {
// start over w/ the user command.
// note: the password was successful, and it's stored in mPassword
mRetryPass = false;
return FTP_S_USER;
}
if (mResponseCode / 100 == 5 || mResponseCode == 421) {
// There is no difference between a too-many-users error,
// a wrong-password error, or any other sort of error
if (!mAnonymous) mRetryPass = true;
return FTP_ERROR;
}
// unexpected response code
return FTP_ERROR;
}
nsresult nsFtpState::S_pwd() {
return SendFTPCommand(nsLiteralCString("PWD" CRLF));
}
FTP_STATE
nsFtpState::R_pwd() {
// Error response to PWD command isn't fatal, but don't cache the connection
// if CWD command is sent since correct mPwd is needed for further requests.
if (mResponseCode / 100 != 2) return FTP_S_TYPE;
nsAutoCString respStr(mResponseMsg);
int32_t pos = respStr.FindChar('"');
if (pos > -1) {
respStr.Cut(0, pos + 1);
pos = respStr.FindChar('"');
if (pos > -1) {
respStr.Truncate(pos);
if (mServerType == FTP_VMS_TYPE) ConvertDirspecFromVMS(respStr);
if (respStr.IsEmpty() || respStr.Last() != '/') respStr.Append('/');
mPwd = respStr;
}
}
return FTP_S_TYPE;
}
nsresult nsFtpState::S_syst() {
return SendFTPCommand(nsLiteralCString("SYST" CRLF));
}
FTP_STATE
nsFtpState::R_syst() {
if (mResponseCode / 100 == 2) {
if ((mResponseMsg.Find("L8") > -1) || (mResponseMsg.Find("UNIX") > -1) ||
(mResponseMsg.Find("BSD") > -1) ||
(mResponseMsg.Find("MACOS Peter's Server") > -1) ||
(mResponseMsg.Find("MACOS WebSTAR FTP") > -1) ||
(mResponseMsg.Find("MVS") > -1) || (mResponseMsg.Find("OS/390") > -1) ||
(mResponseMsg.Find("OS/400") > -1)) {
mServerType = FTP_UNIX_TYPE;
} else if ((mResponseMsg.Find("WIN32", true) > -1) ||
(mResponseMsg.Find("windows", true) > -1)) {
mServerType = FTP_NT_TYPE;
} else if (mResponseMsg.Find("OS/2", true) > -1) {
mServerType = FTP_OS2_TYPE;
} else if (mResponseMsg.Find("VMS", true) > -1) {
mServerType = FTP_VMS_TYPE;
} else {
NS_ERROR("Server type list format unrecognized.");
// clear mResponseMsg, which is displayed to the user.
mResponseMsg = "";
return FTP_ERROR;
}
return FTP_S_FEAT;
}
if (mResponseCode / 100 == 5) {
// server didn't like the SYST command. Probably (500, 501, 502)
// No clue. We will just hope it is UNIX type server.
mServerType = FTP_UNIX_TYPE;
return FTP_S_FEAT;
}
return FTP_ERROR;
}
nsresult nsFtpState::S_acct() {
return SendFTPCommand(nsLiteralCString("ACCT noaccount" CRLF));
}
FTP_STATE
nsFtpState::R_acct() {
if (mResponseCode / 100 == 2) return FTP_S_SYST;
return FTP_ERROR;
}
nsresult nsFtpState::S_type() {
return SendFTPCommand(nsLiteralCString("TYPE I" CRLF));
}
FTP_STATE
nsFtpState::R_type() {
if (mResponseCode / 100 != 2) return FTP_ERROR;
return FTP_S_PASV;
}
nsresult nsFtpState::S_cwd() {
// Don't cache the connection if PWD command failed
if (mPwd.IsEmpty()) mCacheConnection = false;
nsAutoCString cwdStr;
if (mAction != PUT) cwdStr = mPath;
if (cwdStr.IsEmpty() || cwdStr.First() != '/') cwdStr.Insert(mPwd, 0);
if (mServerType == FTP_VMS_TYPE) ConvertDirspecToVMS(cwdStr);
cwdStr.InsertLiteral("CWD ", 0);
cwdStr.AppendLiteral(CRLF);
return SendFTPCommand(cwdStr);
}
FTP_STATE
nsFtpState::R_cwd() {
if (mResponseCode / 100 == 2) {
if (mAction == PUT) return FTP_S_STOR;
return FTP_S_LIST;
}
return FTP_ERROR;
}
nsresult nsFtpState::S_size() {
nsAutoCString sizeBuf(mPath);
if (sizeBuf.IsEmpty() || sizeBuf.First() != '/') sizeBuf.Insert(mPwd, 0);
if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(sizeBuf);
sizeBuf.InsertLiteral("SIZE ", 0);
sizeBuf.AppendLiteral(CRLF);
return SendFTPCommand(sizeBuf);
}
FTP_STATE
nsFtpState::R_size() {
if (mResponseCode / 100 == 2) {
PR_sscanf(mResponseMsg.get() + 4, "%llu", &mFileSize);
mChannel->SetContentLength(mFileSize);
}
// We may want to be able to resume this
return FTP_S_MDTM;
}
nsresult nsFtpState::S_mdtm() {
nsAutoCString mdtmBuf(mPath);
if (mdtmBuf.IsEmpty() || mdtmBuf.First() != '/') mdtmBuf.Insert(mPwd, 0);
if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(mdtmBuf);
mdtmBuf.InsertLiteral("MDTM ", 0);
mdtmBuf.AppendLiteral(CRLF);
return SendFTPCommand(mdtmBuf);
}
FTP_STATE
nsFtpState::R_mdtm() {
if (mResponseCode == 213) {
mResponseMsg.Cut(0, 4);
mResponseMsg.Trim(" \t\r\n");
// yyyymmddhhmmss
if (mResponseMsg.Length() != 14) {
NS_ASSERTION(mResponseMsg.Length() == 14, "Unknown MDTM response");
} else {
mModTime = mResponseMsg;
// Save lastModified time for downloaded files.
nsAutoCString timeString;
nsresult error;
PRExplodedTime exTime;
mResponseMsg.Mid(timeString, 0, 4);
exTime.tm_year = timeString.ToInteger(&error);
mResponseMsg.Mid(timeString, 4, 2);
exTime.tm_month = timeString.ToInteger(&error) - 1; // january = 0
mResponseMsg.Mid(timeString, 6, 2);
exTime.tm_mday = timeString.ToInteger(&error);
mResponseMsg.Mid(timeString, 8, 2);
exTime.tm_hour = timeString.ToInteger(&error);
mResponseMsg.Mid(timeString, 10, 2);
exTime.tm_min = timeString.ToInteger(&error);
mResponseMsg.Mid(timeString, 12, 2);
exTime.tm_sec = timeString.ToInteger(&error);
exTime.tm_usec = 0;
exTime.tm_params.tp_gmt_offset = 0;
exTime.tm_params.tp_dst_offset = 0;
PR_NormalizeTime(&exTime, PR_GMTParameters);
exTime.tm_params = PR_LocalTimeParameters(&exTime);
PRTime time = PR_ImplodeTime(&exTime);
(void)mChannel->SetLastModifiedTime(time);
}
}
nsCString entityID;
entityID.Truncate();
entityID.AppendInt(int64_t(mFileSize));
entityID.Append('/');
entityID.Append(mModTime);
mChannel->SetEntityID(entityID);
// We weren't asked to resume
if (!mChannel->ResumeRequested()) return FTP_S_RETR;
// if (our entityID == supplied one (if any))
if (mSuppliedEntityID.IsEmpty() || entityID.Equals(mSuppliedEntityID))
return FTP_S_REST;
mInternalError = NS_ERROR_ENTITY_CHANGED;
mResponseMsg.Truncate();
return FTP_ERROR;
}
nsresult nsFtpState::SetContentType() {
// FTP directory URLs don't always end in a slash. Make sure they do.
// This check needs to be here rather than a more obvious place
// (e.g. LIST command processing) so that it ensures the terminating
// slash is appended for the new request case.
if (!mPath.IsEmpty() && mPath.Last() != '/') {
nsCOMPtr<nsIURL> url = (do_QueryInterface(mChannel->URI()));
nsAutoCString filePath;
if (NS_SUCCEEDED(url->GetFilePath(filePath))) {
filePath.Append('/');
nsresult rv = NS_MutateURI(url).SetFilePath(filePath).Finalize(url);
if (NS_SUCCEEDED(rv)) {
mChannel->UpdateURI(url);
}
}
}
return mChannel->SetContentType(
nsLiteralCString(APPLICATION_HTTP_INDEX_FORMAT));
}
nsresult nsFtpState::S_list() {
nsresult rv = SetContentType();
if (NS_FAILED(rv))
// XXX Invalid cast of FTP_STATE to nsresult -- FTP_ERROR has
// value < 0x80000000 and will pass NS_SUCCEEDED() (bug 778109)
return (nsresult)FTP_ERROR;
rv = mChannel->PushStreamConverter("text/ftp-dir",
APPLICATION_HTTP_INDEX_FORMAT);
if (NS_FAILED(rv)) {
// clear mResponseMsg which is displayed to the user.
// TODO: we should probably set this to something meaningful.
mResponseMsg = "";
return rv;
}
// dir listings aren't resumable
NS_ENSURE_TRUE(!mChannel->ResumeRequested(), NS_ERROR_NOT_RESUMABLE);
mChannel->SetEntityID(""_ns);
const char* listString;
if (mServerType == FTP_VMS_TYPE) {
listString = "LIST *.*;0" CRLF;
} else {
listString = "LIST" CRLF;
}
return SendFTPCommand(nsDependentCString(listString));
}
FTP_STATE
nsFtpState::R_list() {
if (mResponseCode / 100 == 1) {
mRlist1xxReceived = true;
// OK, time to start reading from the data connection.
if (mDataStream && HasPendingCallback())
mDataStream->AsyncWait(this, 0, 0, CallbackTarget());
return FTP_READ_BUF;
}
if (mResponseCode / 100 == 2 && mRlist1xxReceived) {
//(DONE)
mNextState = FTP_COMPLETE;
mRlist1xxReceived = false;
return FTP_COMPLETE;
}
return FTP_ERROR;
}
nsresult nsFtpState::S_retr() {
nsAutoCString retrStr(mPath);
if (retrStr.IsEmpty() || retrStr.First() != '/') retrStr.Insert(mPwd, 0);
if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(retrStr);
retrStr.InsertLiteral("RETR ", 0);
retrStr.AppendLiteral(CRLF);
return SendFTPCommand(retrStr);
}
FTP_STATE
nsFtpState::R_retr() {
if (mResponseCode / 100 == 2) {
if (!mRretr1xxReceived) return FTP_ERROR;
//(DONE)
mNextState = FTP_COMPLETE;
mRretr1xxReceived = false;
return FTP_COMPLETE;
}
if (mResponseCode / 100 == 1) {
mChannel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM));
mRretr1xxReceived = true;
if (mDataStream && HasPendingCallback())
mDataStream->AsyncWait(this, 0, 0, CallbackTarget());
return FTP_READ_BUF;
}
// These error codes are related to problems with the connection.
// If we encounter any at this point, do not try CWD and abort.
if (mResponseCode == 421 || mResponseCode == 425 || mResponseCode == 426)
return FTP_ERROR;
if (mResponseCode / 100 == 5) {
mRETRFailed = true;
return FTP_S_PASV;
}
return FTP_S_CWD;
}
nsresult nsFtpState::S_rest() {
nsAutoCString restString("REST ");
// The int64_t cast is needed to avoid ambiguity
restString.AppendInt(int64_t(mChannel->StartPos()), 10);
restString.AppendLiteral(CRLF);
return SendFTPCommand(restString);
}
FTP_STATE
nsFtpState::R_rest() {
if (mResponseCode / 100 == 4) {
// If REST fails, then we can't resume
mChannel->SetEntityID(""_ns);
mInternalError = NS_ERROR_NOT_RESUMABLE;
mResponseMsg.Truncate();
return FTP_ERROR;
}
return FTP_S_RETR;
}
nsresult nsFtpState::S_stor() {
NS_ENSURE_STATE(mChannel->UploadStream());
NS_ASSERTION(mAction == PUT, "Wrong state to be here");
nsCOMPtr<nsIURL> url = do_QueryInterface(mChannel->URI());
NS_ASSERTION(url, "I thought you were a nsStandardURL");
nsAutoCString storStr;
url->GetFilePath(storStr);
NS_ASSERTION(!storStr.IsEmpty(), "What does it mean to store a empty path");
// kill the first slash since we want to be relative to CWD.
if (storStr.First() == '/') storStr.Cut(0, 1);
if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(storStr);
NS_UnescapeURL(storStr);
storStr.InsertLiteral("STOR ", 0);
storStr.AppendLiteral(CRLF);
return SendFTPCommand(storStr);
}
FTP_STATE
nsFtpState::R_stor() {
if (mResponseCode / 100 == 2 && mRstor1xxReceived) {
//(DONE)
mNextState = FTP_COMPLETE;
mStorReplyReceived = true;
mRstor1xxReceived = false;
// Call Close() if it was not called in nsFtpState::OnStoprequest()
if (!mUploadRequest && !IsClosed()) Close();
return FTP_COMPLETE;
}
if (mResponseCode / 100 == 1) {
LOG(("FTP:(%p) writing on DT\n", this));
mRstor1xxReceived = true;
return FTP_READ_BUF;
}
mStorReplyReceived = true;
return FTP_ERROR;
}
nsresult nsFtpState::S_pasv() {
if (!mAddressChecked) {
// Find socket address
mAddressChecked = true;
mServerAddress.raw.family = AF_INET;
mServerAddress.inet.ip = htonl(INADDR_ANY);
mServerAddress.inet.port = htons(0);
nsITransport* controlSocket = mControlConnection->Transport();
if (!controlSocket)
// XXX Invalid cast of FTP_STATE to nsresult -- FTP_ERROR has
// value < 0x80000000 and will pass NS_SUCCEEDED() (bug 778109)
return (nsresult)FTP_ERROR;
nsCOMPtr<nsISocketTransport> sTrans = do_QueryInterface(controlSocket);
if (sTrans) {
nsresult rv = sTrans->GetPeerAddr(&mServerAddress);
if (NS_SUCCEEDED(rv)) {
if (!mServerAddress.IsIPAddrAny())
mServerIsIPv6 = (mServerAddress.raw.family == AF_INET6) &&
!mServerAddress.IsIPAddrV4Mapped();
else {
/*
* In case of SOCKS5 remote DNS resolution, we do
* not know the remote IP address. Still, if it is
* an IPV6 host, then the external address of the
* socks server should also be IPv6, and this is the
* self address of the transport.
*/
NetAddr selfAddress;
rv = sTrans->GetSelfAddr(&selfAddress);
if (NS_SUCCEEDED(rv))
mServerIsIPv6 = (selfAddress.raw.family == AF_INET6) &&
!selfAddress.IsIPAddrV4Mapped();
}
}
}
}
const char* string;
if (mServerIsIPv6) {
string = "EPSV" CRLF;
} else {
string = "PASV" CRLF;
}
return SendFTPCommand(nsDependentCString(string));
}
FTP_STATE
nsFtpState::R_pasv() {
if (mResponseCode / 100 != 2) return FTP_ERROR;
nsresult rv;
int32_t port;
nsAutoCString responseCopy(mResponseMsg);
char* response = responseCopy.BeginWriting();
char* ptr = response;
// Make sure to ignore the address in the PASV response (bug 370559)
if (mServerIsIPv6) {
// The returned string is of the form
// text (|||ppp|)
// Where '|' can be any single character
char delim;
while (*ptr && *ptr != '(') ptr++;
if (*ptr++ != '(') return FTP_ERROR;
delim = *ptr++;
if (!delim || *ptr++ != delim || *ptr++ != delim || *ptr < '0' ||
*ptr > '9')
return FTP_ERROR;
port = 0;
do {
port = port * 10 + *ptr++ - '0';
} while (*ptr >= '0' && *ptr <= '9');
if (*ptr++ != delim || *ptr != ')') return FTP_ERROR;
} else {
// The returned address string can be of the form
// (xxx,xxx,xxx,xxx,ppp,ppp) or
// xxx,xxx,xxx,xxx,ppp,ppp (without parens)
int32_t h0, h1, h2, h3, p0, p1;
int32_t fields = 0;
// First try with parens
while (*ptr && *ptr != '(') ++ptr;
if (*ptr) {
++ptr;
fields = PR_sscanf(ptr, "%ld,%ld,%ld,%ld,%ld,%ld", &h0, &h1, &h2, &h3,
&p0, &p1);
}
if (!*ptr || fields < 6) {
// OK, lets try w/o parens
ptr = response;
while (*ptr && *ptr != ',') ++ptr;
if (*ptr) {
// backup to the start of the digits
do {
ptr--;
} while ((ptr >= response) && (*ptr >= '0') && (*ptr <= '9'));
ptr++; // get back onto the numbers
fields = PR_sscanf(ptr, "%ld,%ld,%ld,%ld,%ld,%ld", &h0, &h1, &h2, &h3,
&p0, &p1);
}
}
NS_ASSERTION(fields == 6, "Can't parse PASV response");
if (fields < 6) return FTP_ERROR;
port = ((int32_t)(p0 << 8)) + p1;
}
bool newDataConn = true;
if (mDataTransport) {
// Reuse this connection only if its still alive, and the port
// is the same
nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(mDataTransport);
if (strans) {
int32_t oldPort;
nsresult rv = strans->GetPort(&oldPort);
if (NS_SUCCEEDED(rv)) {
if (oldPort == port) {
bool isAlive;
if (NS_SUCCEEDED(strans->IsAlive(&isAlive)) && isAlive)
newDataConn = false;
}
}
}
if (newDataConn) {
mDataTransport->Close(NS_ERROR_ABORT);
mDataTransport = nullptr;
if (mDataStream) {
mDataStream->CloseWithStatus(NS_ERROR_ABORT);
mDataStream = nullptr;
}
}
}
if (newDataConn) {
// now we know where to connect our data channel
nsCOMPtr<nsISocketTransportService> sts =
do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
if (!sts) return FTP_ERROR;
nsCOMPtr<nsISocketTransport> strans;
nsAutoCString host;
if (!mServerAddress.IsIPAddrAny()) {
char buf[kIPv6CStrBufSize];
mServerAddress.ToStringBuffer(buf, sizeof(buf));
host.Assign(buf);
} else {
/*
* In case of SOCKS5 remote DNS resolving, the peer address
* fetched previously will be invalid (0.0.0.0): it is unknown
* to us. But we can pass on the original hostname to the
* connect for the data connection.
*/
rv = mChannel->URI()->GetAsciiHost(host);
if (NS_FAILED(rv)) return FTP_ERROR;
}
rv = sts->CreateTransport(nsTArray<nsCString>(), host, port,
mChannel->ProxyInfo(), nullptr,
getter_AddRefs(strans)); // the data socket
if (NS_FAILED(rv)) return FTP_ERROR;
mDataTransport = strans;
strans->SetQoSBits(gFtpHandler->GetDataQoSBits());
LOG(("FTP:(%p) created DT (%s:%x)\n", this, host.get(), port));
// hook ourself up as a proxy for status notifications
rv = mDataTransport->SetEventSink(this, GetCurrentEventTarget());
NS_ENSURE_SUCCESS(rv, FTP_ERROR);
if (mAction == PUT) {
NS_ASSERTION(!mRETRFailed, "Failed before uploading");
// nsIUploadChannel requires the upload stream to support ReadSegments.
// therefore, we can open an unbuffered socket output stream.
nsCOMPtr<nsIOutputStream> output;
rv = mDataTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0,
getter_AddRefs(output));
if (NS_FAILED(rv)) return FTP_ERROR;
// perform the data copy on the socket transport thread. we do this
// because "output" is a socket output stream, so the result is that
// all work will be done on the socket transport thread.
nsCOMPtr<nsIEventTarget> stEventTarget =
do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
if (!stEventTarget) return FTP_ERROR;
nsCOMPtr<nsIAsyncStreamCopier> copier =
do_CreateInstance(NS_ASYNCSTREAMCOPIER_CONTRACTID, &rv);
if (NS_SUCCEEDED(rv)) {
rv = copier->Init(mChannel->UploadStream(), output, stEventTarget, true,
false /* output is NOT buffered */, 0, true, true);
}
if (NS_FAILED(rv)) return FTP_ERROR;
rv = copier->AsyncCopy(this, nullptr);
if (NS_FAILED(rv)) return FTP_ERROR;
// hold a reference to the copier so we can cancel it if necessary.
mUploadRequest = copier;
// update the current working directory before sending the STOR
// command. this is needed since we might be reusing a control
// connection.
return FTP_S_CWD;
}
//
// else, we are reading from the data connection...
//
// open a buffered, asynchronous socket input stream
nsCOMPtr<nsIInputStream> input;
rv = mDataTransport->OpenInputStream(0, nsIOService::gDefaultSegmentSize,
nsIOService::gDefaultSegmentCount,
getter_AddRefs(input));
NS_ENSURE_SUCCESS(rv, FTP_ERROR);
mDataStream = do_QueryInterface(input);
}
if (mRETRFailed || mPath.IsEmpty() || mPath.Last() == '/') return FTP_S_CWD;
return FTP_S_SIZE;
}
nsresult nsFtpState::S_feat() {
return SendFTPCommand(nsLiteralCString("FEAT" CRLF));
}
FTP_STATE
nsFtpState::R_feat() {
if (mResponseCode / 100 == 2) {
if (mResponseMsg.Find(nsLiteralCString(CRLF " UTF8" CRLF), true) > -1) {
// This FTP server supports UTF-8 encoding
mChannel->SetContentCharset("UTF-8"_ns);
mUseUTF8 = true;
return FTP_S_OPTS;
}
}
mUseUTF8 = false;
return FTP_S_PWD;
}
nsresult nsFtpState::S_opts() {
// This command is for compatibility of old FTP spec (IETF Draft)
return SendFTPCommand(nsLiteralCString("OPTS UTF8 ON" CRLF));
}
FTP_STATE
nsFtpState::R_opts() {
// Ignore error code because "OPTS UTF8 ON" is for compatibility of
// FTP server using IETF draft
return FTP_S_PWD;
}
////////////////////////////////////////////////////////////////////////////////
// nsIRequest methods:
nsresult nsFtpState::Init(nsFtpChannel* channel) {
// parameter validation
NS_ASSERTION(channel, "FTP: needs a channel");
mChannel = channel; // a straight ref ptr to the channel
mKeepRunning = true;
mSuppliedEntityID = channel->EntityID();
if (channel->UploadStream()) mAction = PUT;
nsresult rv;
nsCOMPtr<nsIURL> url = do_QueryInterface(mChannel->URI());
nsAutoCString host;
if (url) {
rv = url->GetAsciiHost(host);
} else {
rv = mChannel->URI()->GetAsciiHost(host);
}
if (NS_FAILED(rv) || host.IsEmpty()) {
return NS_ERROR_MALFORMED_URI;
}
nsAutoCString path;
if (url) {
rv = url->GetFilePath(path);
} else {
rv = mChannel->URI()->GetPathQueryRef(path);
}
if (NS_FAILED(rv)) return rv;
removeParamsFromPath(path);
nsCOMPtr<nsIURI> outURI;
// FTP parameters such as type=i are ignored
if (url) {
rv = NS_MutateURI(url).SetFilePath(path).Finalize(outURI);
} else {
rv = NS_MutateURI(mChannel->URI()).SetPathQueryRef(path).Finalize(outURI);
}
if (NS_SUCCEEDED(rv)) {
mChannel->UpdateURI(outURI);
}
// Skip leading slash
char* fwdPtr = path.BeginWriting();
if (!fwdPtr) return NS_ERROR_OUT_OF_MEMORY;
if (*fwdPtr == '/') fwdPtr++;
if (*fwdPtr != '\0') {
// now unescape it... %xx reduced inline to resulting character
int32_t len = NS_UnescapeURL(fwdPtr);
mPath.Assign(fwdPtr, len);
if (mPath.FindCharInSet(CRLF) != kNotFound ||
mPath.FindChar('\0') != kNotFound) {
mPath.Truncate();
return NS_ERROR_MALFORMED_URI;
}
}
// pull any username and/or password out of the uri
nsAutoCString uname;
rv = mChannel->URI()->GetUsername(uname);
if (NS_FAILED(rv)) return rv;
if (!uname.IsEmpty() && !uname.EqualsLiteral("anonymous")) {
mAnonymous = false;
CopyUTF8toUTF16(NS_UnescapeURL(uname), mUsername);
// return an error if we find a CR or LF in the username
if (uname.FindCharInSet(CRLF) >= 0) return NS_ERROR_MALFORMED_URI;
}
nsAutoCString password;
rv = mChannel->URI()->GetPassword(password);
if (NS_FAILED(rv)) return rv;
CopyUTF8toUTF16(NS_UnescapeURL(password), mPassword);
// return an error if we find a CR or LF in the password
if (mPassword.FindCharInSet(CRLF) >= 0) return NS_ERROR_MALFORMED_URI;
int32_t port;
rv = mChannel->URI()->GetPort(&port);
if (NS_FAILED(rv)) return rv;
if (port > 0) mPort = port;
// Lookup Proxy information asynchronously if it isn't already set
// on the channel and if we aren't configured explicitly to go directly
nsCOMPtr<nsIProtocolProxyService> pps =
do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID);
if (pps && !mChannel->ProxyInfo()) {
pps->AsyncResolve(static_cast<nsIChannel*>(mChannel), 0, this, nullptr,
getter_AddRefs(mProxyRequest));
}
return NS_OK;
}
void nsFtpState::Connect() {
mState = FTP_COMMAND_CONNECT;
mNextState = FTP_S_USER;
nsresult rv = Process();
// check for errors.
if (NS_FAILED(rv)) {
LOG(("FTP:Process() failed: %" PRIx32 "\n", static_cast<uint32_t>(rv)));
mInternalError = NS_ERROR_FAILURE;
mState = FTP_ERROR;
CloseWithStatus(mInternalError);
}
}
void nsFtpState::KillControlConnection() {
mControlReadCarryOverBuf.Truncate(0);
mAddressChecked = false;
mServerIsIPv6 = false;
// if everything went okay, save the connection.
// FIX: need a better way to determine if we can cache the connections.
// there are some errors which do not mean that we need to kill the
// connection e.g. fnf.
if (!mControlConnection) return;
// kill the reference to ourselves in the control connection.
mControlConnection->WaitData(nullptr);
if (NS_SUCCEEDED(mInternalError) && NS_SUCCEEDED(mControlStatus) &&
mControlConnection->IsAlive() && mCacheConnection) {
LOG_INFO(("FTP:(%p) caching CC(%p)", this, mControlConnection.get()));
// Store connection persistent data
mControlConnection->mServerType = mServerType;
mControlConnection->mPassword = mPassword;
mControlConnection->mPwd = mPwd;
mControlConnection->mUseUTF8 = mUseUTF8;
nsresult rv = NS_OK;
// Don't cache controlconnection if anonymous (bug #473371)
if (!mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS))
rv = gFtpHandler->InsertConnection(mChannel->URI(), mControlConnection);
// Can't cache it? Kill it then.
mControlConnection->Disconnect(rv);
} else {
mControlConnection->Disconnect(NS_BINDING_ABORTED);
}
mControlConnection = nullptr;
}
nsresult nsFtpState::StopProcessing() {
// Only do this function once.
if (!mKeepRunning) return NS_OK;
mKeepRunning = false;
LOG_INFO(("FTP:(%p) nsFtpState stopping", this));
if (NS_FAILED(mInternalError) && !mResponseMsg.IsEmpty()) {
// check to see if the control status is bad, forward the error message.
nsCOMPtr<nsIFTPChannelParentInternal> ftpChanP;
mChannel->GetCallback(ftpChanP);
if (ftpChanP) {
ftpChanP->SetErrorMsg(mResponseMsg.get(), mUseUTF8);
}
}
nsresult broadcastErrorCode = mControlStatus;
if (NS_SUCCEEDED(broadcastErrorCode)) broadcastErrorCode = mInternalError;
mInternalError = broadcastErrorCode;
KillControlConnection();
// XXX This can fire before we are done loading data. Is that a problem?
OnTransportStatus(nullptr, NS_NET_STATUS_END_FTP_TRANSACTION, 0, 0);
if (NS_FAILED(broadcastErrorCode)) CloseWithStatus(broadcastErrorCode);
return NS_OK;
}
nsresult nsFtpState::SendFTPCommand(const nsACString& command) {
NS_ASSERTION(mControlConnection, "null control connection");
// we don't want to log the password:
nsAutoCString logcmd(command);
if (StringBeginsWith(command, "PASS "_ns)) logcmd = "PASS xxxxx";
LOG(("FTP:(%p) writing \"%s\"\n", this, logcmd.get()));
nsCOMPtr<nsIFTPEventSink> ftpSink;
mChannel->GetFTPEventSink(ftpSink);
if (ftpSink) ftpSink->OnFTPControlLog(false, logcmd.get());
if (mControlConnection) return mControlConnection->Write(command);
return NS_ERROR_FAILURE;
}
// Convert a unix-style filespec to VMS format
// /foo/fred/barney/file.txt -> foo:[fred.barney]file.txt
// /foo/file.txt -> foo:[000000]file.txt
void nsFtpState::ConvertFilespecToVMS(nsCString& fileString) {
int ntok = 1;
char *t, *nextToken;
nsAutoCString fileStringCopy;
// Get a writeable copy we can strtok with.
fileStringCopy = fileString;
t = nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken);
if (t)
while (nsCRT::strtok(nextToken, "/", &nextToken))
ntok++; // count number of terms (tokens)
LOG(("FTP:(%p) ConvertFilespecToVMS ntok: %d\n", this, ntok));
LOG(("FTP:(%p) ConvertFilespecToVMS from: \"%s\"\n", this, fileString.get()));
if (fileString.First() == '/') {
// absolute filespec
// / -> []
// /a -> a (doesn't really make much sense)
// /a/b -> a:[000000]b
// /a/b/c -> a:[b]c
// /a/b/c/d -> a:[b.c]d
if (ntok == 1) {
if (fileString.Length() == 1) {
// Just a slash
fileString.Truncate();
fileString.AppendLiteral("[]");
} else {
// just copy the name part (drop the leading slash)
fileStringCopy = fileString;
fileString = Substring(fileStringCopy, 1, fileStringCopy.Length() - 1);
}
} else {
// Get another copy since the last one was written to.
fileStringCopy = fileString;
fileString.Truncate();
fileString.Append(
nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken));
fileString.AppendLiteral(":[");
if (ntok > 2) {
for (int i = 2; i < ntok; i++) {
if (i > 2) fileString.Append('.');
fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken));
}
} else {
fileString.AppendLiteral("000000");
}
fileString.Append(']');
fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken));
}
} else {
// relative filespec
// a -> a
// a/b -> [.a]b
// a/b/c -> [.a.b]c
if (ntok == 1) {
// no slashes, just use the name as is
} else {
// Get another copy since the last one was written to.
fileStringCopy = fileString;
fileString.Truncate();
fileString.AppendLiteral("[.");
fileString.Append(
nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken));
if (ntok > 2) {
for (int i = 2; i < ntok; i++) {
fileString.Append('.');
fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken));
}
}
fileString.Append(']');
fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken));
}
}
LOG(("FTP:(%p) ConvertFilespecToVMS to: \"%s\"\n", this, fileString.get()));
}
// Convert a unix-style dirspec to VMS format
// /foo/fred/barney/rubble -> foo:[fred.barney.rubble]
// /foo/fred -> foo:[fred]
// /foo -> foo:[000000]
// (null) -> (null)
void nsFtpState::ConvertDirspecToVMS(nsCString& dirSpec) {
LOG(("FTP:(%p) ConvertDirspecToVMS from: \"%s\"\n", this, dirSpec.get()));
if (!dirSpec.IsEmpty()) {
if (dirSpec.Last() != '/') dirSpec.Append('/');
// we can use the filespec routine if we make it look like a file name
dirSpec.Append('x');
ConvertFilespecToVMS(dirSpec);
dirSpec.Truncate(dirSpec.Length() - 1);
}
LOG(("FTP:(%p) ConvertDirspecToVMS to: \"%s\"\n", this, dirSpec.get()));
}
// Convert an absolute VMS style dirspec to UNIX format
void nsFtpState::ConvertDirspecFromVMS(nsCString& dirSpec) {
LOG(("FTP:(%p) ConvertDirspecFromVMS from: \"%s\"\n", this, dirSpec.get()));
if (dirSpec.IsEmpty()) {
dirSpec.Insert('.', 0);
} else {
dirSpec.Insert('/', 0);
dirSpec.ReplaceSubstring(":[", "/");
dirSpec.ReplaceChar('.', '/');
dirSpec.ReplaceChar(']', '/');
}
LOG(("FTP:(%p) ConvertDirspecFromVMS to: \"%s\"\n", this, dirSpec.get()));
}
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsFtpState::OnTransportStatus(nsITransport* transport, nsresult status,
int64_t progress, int64_t progressMax) {
// Mix signals from both the control and data connections.
// Ignore data transfer events on the control connection.
if (mControlConnection && transport == mControlConnection->Transport()) {
switch (status) {
case NS_NET_STATUS_RESOLVING_HOST:
case NS_NET_STATUS_RESOLVED_HOST:
case NS_NET_STATUS_CONNECTING_TO:
case NS_NET_STATUS_CONNECTED_TO:
case NS_NET_STATUS_TLS_HANDSHAKE_STARTING:
case NS_NET_STATUS_TLS_HANDSHAKE_ENDED:
break;
default:
return NS_OK;
}
}
// Ignore the progressMax value from the socket. We know the true size of
// the file based on the response from our SIZE request. Additionally, only
// report the max progress based on where we started/resumed.
mChannel->OnTransportStatus(nullptr, status, progress,
mFileSize - mChannel->StartPos());
return NS_OK;
}
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsFtpState::OnStartRequest(nsIRequest* request) {
mStorReplyReceived = false;
return NS_OK;
}
NS_IMETHODIMP
nsFtpState::OnStopRequest(nsIRequest* request, nsresult status) {
mUploadRequest = nullptr;
// Close() will be called when reply to STOR command is received
// see bug #389394
if (!mStorReplyReceived) return NS_OK;
// We're done uploading. Let our consumer know that we're done.
Close();
return NS_OK;
}
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsFtpState::Available(uint64_t* result) {
if (mDataStream) return mDataStream->Available(result);
return nsBaseContentStream::Available(result);
}
NS_IMETHODIMP
nsFtpState::ReadSegments(nsWriteSegmentFun writer, void* closure,
uint32_t count, uint32_t* result) {
// Insert a thunk here so that the input stream passed to the writer is this
// input stream instead of mDataStream.
if (mDataStream) {
nsWriteSegmentThunk thunk = {this, writer, closure};
nsresult rv;
rv = mDataStream->ReadSegments(NS_WriteSegmentThunk, &thunk, count, result);
return rv;
}
return nsBaseContentStream::ReadSegments(writer, closure, count, result);
}
NS_IMETHODIMP
nsFtpState::CloseWithStatus(nsresult status) {
LOG(("FTP:(%p) close [%" PRIx32 "]\n", this, static_cast<uint32_t>(status)));
// Shutdown the control connection processing if we are being closed with an
// error. Note: This method may be called several times.
if (!IsClosed() && NS_FAILED(status)) {
if (NS_SUCCEEDED(mInternalError)) mInternalError = status;
StopProcessing();
}
if (mUploadRequest) {
mUploadRequest->Cancel(NS_ERROR_ABORT);
mUploadRequest = nullptr;
}
if (mDataTransport) {
// Shutdown the data transport.
mDataTransport->Close(NS_ERROR_ABORT);
mDataTransport = nullptr;
}
if (mDataStream) {
mDataStream->CloseWithStatus(NS_ERROR_ABORT);
mDataStream = nullptr;
}
return nsBaseContentStream::CloseWithStatus(status);
}
static nsresult CreateHTTPProxiedChannel(nsIChannel* channel, nsIProxyInfo* pi,
nsIChannel** newChannel) {
nsresult rv;
nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIProtocolHandler> handler;
rv = ioService->GetProtocolHandler("http", getter_AddRefs(handler));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIProxiedProtocolHandler> pph = do_QueryInterface(handler, &rv);
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIURI> uri;
channel->GetURI(getter_AddRefs(uri));
nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
return pph->NewProxiedChannel(uri, pi, 0, nullptr, loadInfo, newChannel);
}
NS_IMETHODIMP
nsFtpState::OnProxyAvailable(nsICancelable* request, nsIChannel* channel,
nsIProxyInfo* pi, nsresult status) {
mProxyRequest = nullptr;
// failed status code just implies DIRECT processing
if (NS_SUCCEEDED(status)) {
nsAutoCString type;
if (pi && NS_SUCCEEDED(pi->GetType(type)) && type.EqualsLiteral("http")) {
// Proxy the FTP url via HTTP
// This would have been easier to just return a HTTP channel directly
// from nsIIOService::NewChannelFromURI(), but the proxy type cannot
// be reliabliy determined synchronously without jank due to pac, etc..
LOG(("FTP:(%p) Configured to use a HTTP proxy channel\n", this));
nsCOMPtr<nsIChannel> newChannel;
if (NS_SUCCEEDED(CreateHTTPProxiedChannel(channel, pi,
getter_AddRefs(newChannel))) &&
NS_SUCCEEDED(mChannel->Redirect(
newChannel, nsIChannelEventSink::REDIRECT_INTERNAL, true))) {
LOG(("FTP:(%p) Redirected to use a HTTP proxy channel\n", this));
return NS_OK;
}
} else if (pi) {
// Proxy using the FTP protocol routed through a socks proxy
LOG(("FTP:(%p) Configured to use a SOCKS proxy channel\n", this));
mChannel->SetProxyInfo(pi);
}
}
if (mDeferredCallbackPending) {
mDeferredCallbackPending = false;
OnCallbackPending();
}
return NS_OK;
}
void nsFtpState::OnCallbackPending() {
if (mState == FTP_INIT) {
if (mProxyRequest) {
mDeferredCallbackPending = true;
return;
}
Connect();
} else if (mDataStream) {
mDataStream->AsyncWait(this, 0, 0, CallbackTarget());
}
}