// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/ftp/ftp_network_transaction.h"

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback_helpers.h"
#include "base/compiler_specific.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "net/base/address_list.h"
#include "net/base/completion_once_callback.h"
#include "net/base/escape.h"
#include "net/base/net_errors.h"
#include "net/base/parse_number.h"
#include "net/base/port_util.h"
#include "net/base/url_util.h"
#include "net/ftp/ftp_request_info.h"
#include "net/ftp/ftp_util.h"
#include "net/log/net_log.h"
#include "net/log/net_log_event_type.h"
#include "net/log/net_log_source.h"
#include "net/socket/client_socket_factory.h"
#include "net/socket/stream_socket.h"
#include "url/url_constants.h"

namespace net {

namespace {

const char kCRLF[] = "\r\n";

const int kCtrlBufLen = 1024;

// Returns true if |input| can be safely used as a part of an FTP command.
bool IsValidFTPCommandSubstring(const std::string& input) {
  // RFC 959 only allows ASCII strings, but at least Firefox can send non-ASCII
  // characters in the command if the request path contains them. To be
  // compatible, we do the same and allow non-ASCII characters in a command.

  // Protect agains newline injection attack.
  if (input.find_first_of("\r\n") != std::string::npos)
    return false;

  return true;
}

enum ErrorClass {
  // The requested action was initiated. The client should expect another
  // reply before issuing the next command.
  ERROR_CLASS_INITIATED,

  // The requested action has been successfully completed.
  ERROR_CLASS_OK,

  // The command has been accepted, but to complete the operation, more
  // information must be sent by the client.
  ERROR_CLASS_INFO_NEEDED,

  // The command was not accepted and the requested action did not take place.
  // This condition is temporary, and the client is encouraged to restart the
  // command sequence.
  ERROR_CLASS_TRANSIENT_ERROR,

  // The command was not accepted and the requested action did not take place.
  // This condition is rather permanent, and the client is discouraged from
  // repeating the exact request.
  ERROR_CLASS_PERMANENT_ERROR,
};

// Returns the error class for given response code. Caller should ensure
// that |response_code| is in range 100-599.
ErrorClass GetErrorClass(int response_code) {
  if (response_code >= 100 && response_code <= 199)
    return ERROR_CLASS_INITIATED;

  if (response_code >= 200 && response_code <= 299)
    return ERROR_CLASS_OK;

  if (response_code >= 300 && response_code <= 399)
    return ERROR_CLASS_INFO_NEEDED;

  if (response_code >= 400 && response_code <= 499)
    return ERROR_CLASS_TRANSIENT_ERROR;

  if (response_code >= 500 && response_code <= 599)
    return ERROR_CLASS_PERMANENT_ERROR;

  // We should not be called on invalid error codes.
  NOTREACHED() << response_code;
  return ERROR_CLASS_PERMANENT_ERROR;
}

// Returns network error code for received FTP |response_code|.
int GetNetErrorCodeForFtpResponseCode(int response_code) {
  switch (response_code) {
    case 421:
      return ERR_FTP_SERVICE_UNAVAILABLE;
    case 426:
      return ERR_FTP_TRANSFER_ABORTED;
    case 450:
      return ERR_FTP_FILE_BUSY;
    case 500:
    case 501:
      return ERR_FTP_SYNTAX_ERROR;
    case 502:
    case 504:
      return ERR_FTP_COMMAND_NOT_SUPPORTED;
    case 503:
      return ERR_FTP_BAD_COMMAND_SEQUENCE;
    default:
      return ERR_FTP_FAILED;
  }
}

// From RFC 2428 Section 3:
//   The text returned in response to the EPSV command MUST be:
//     <some text> (<d><d><d><tcp-port><d>)
//   <d> is a delimiter character, ideally to be |
bool ExtractPortFromEPSVResponse(const FtpCtrlResponse& response, int* port) {
  if (response.lines.size() != 1)
    return false;

  base::StringPiece epsv_line(response.lines[0]);
  size_t start = epsv_line.find('(');
  // If the line doesn't have a '(' or doesn't have enough characters after the
  // first '(', fail.
  if (start == base::StringPiece::npos || epsv_line.length() - start < 7)
    return false;

  char separator = epsv_line[start + 1];

  // Make sure we have "(<d><d><d>...", where <d> is not a number.
  if ((separator >= '0' && separator <= '9') ||
      epsv_line[start + 2] != separator || epsv_line[start + 3] != separator) {
    return false;
  }

  // Skip over those characters.
  start += 4;

  // Make sure there's a terminal <d>.
  size_t end = epsv_line.find(separator, start);
  if (end == base::StringPiece::npos)
    return false;

  return ParseInt32(epsv_line.substr(start, end - start),
                    ParseIntFormat::NON_NEGATIVE, port);
}

// There are two way we can receive IP address and port.
// (127,0,0,1,23,21) IP address and port encapsulated in ().
// 127,0,0,1,23,21  IP address and port without ().
//
// See RFC 959, Section 4.1.2
bool ExtractPortFromPASVResponse(const FtpCtrlResponse& response, int* port) {
  if (response.lines.size() != 1)
    return false;

  std::string line(response.lines[0]);
  if (!base::IsStringASCII(line))
    return false;
  if (line.length() < 2)
    return false;

  size_t paren_pos = line.find('(');
  if (paren_pos == std::string::npos) {
    // Find the first comma and use it to locate the beginning
    // of the response data.
    size_t comma_pos = line.find(',');
    if (comma_pos == std::string::npos)
      return false;

    size_t space_pos = line.rfind(' ', comma_pos);
    if (space_pos != std::string::npos)
      line = line.substr(space_pos + 1);
  } else {
    // Remove the parentheses and use the text inside them.
    size_t closing_paren_pos = line.rfind(')');
    if (closing_paren_pos == std::string::npos)
      return false;
    if (closing_paren_pos <= paren_pos)
      return false;

    line = line.substr(paren_pos + 1, closing_paren_pos - paren_pos - 1);
  }

  // Split the line into comma-separated pieces and extract
  // the last two.
  std::vector<base::StringPiece> pieces = base::SplitStringPiece(
      line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  if (pieces.size() != 6)
    return false;

  // Ignore the IP address supplied in the response. We are always going
  // to connect back to the same server to prevent FTP PASV port scanning.
  uint32_t p0, p1;
  if (!ParseUint32(pieces[4], &p0))
    return false;
  if (!ParseUint32(pieces[5], &p1))
    return false;
  if (p0 > 0xFF || p1 > 0xFF)
    return false;

  *port = (p0 << 8) + p1;
  return true;
}

}  // namespace

FtpNetworkTransaction::FtpNetworkTransaction(
    HostResolver* resolver,
    ClientSocketFactory* socket_factory)
    : command_sent_(COMMAND_NONE),
      io_callback_(base::BindRepeating(&FtpNetworkTransaction::OnIOComplete,
                                       base::Unretained(this))),
      request_(nullptr),
      resolver_(resolver),
      read_ctrl_buf_(new IOBuffer(kCtrlBufLen)),
      read_data_buf_len_(0),
      last_error_(OK),
      system_type_(SYSTEM_TYPE_UNKNOWN),
      // Use image (binary) transfer by default. It should always work,
      // whereas the ascii transfer may damage binary data.
      data_type_(DATA_TYPE_IMAGE),
      resource_type_(RESOURCE_TYPE_UNKNOWN),
      use_epsv_(true),
      data_connection_port_(0),
      socket_factory_(socket_factory),
      next_state_(STATE_NONE),
      state_after_data_connect_complete_(STATE_NONE) {}

FtpNetworkTransaction::~FtpNetworkTransaction() = default;

int FtpNetworkTransaction::Stop(int error) {
  if (command_sent_ == COMMAND_QUIT) {
    if (error != ERR_EMPTY_RESPONSE)
      return error;

    // For empty responses, if this is propagating an error, then it will return
    // the error. If the error occurred during a QUIT command, then this will
    // return OK since there was no previous error. Some FTP servers are lazy
    // and do not bother responding to QUIT commands.
    // See https://crbug.com/633841
    return last_error_;
  }

  next_state_ = STATE_CTRL_WRITE_QUIT;
  last_error_ = error;
  return OK;
}

int FtpNetworkTransaction::Start(
    const FtpRequestInfo* request_info,
    CompletionOnceCallback callback,
    const NetLogWithSource& net_log,
    const NetworkTrafficAnnotationTag& traffic_annotation) {
  net_log_ = net_log;
  request_ = request_info;
  traffic_annotation_ = MutableNetworkTrafficAnnotationTag(traffic_annotation);

  ctrl_response_buffer_ = std::make_unique<FtpCtrlResponseBuffer>(net_log_);

  if (request_->url.has_username()) {
    base::string16 username;
    base::string16 password;
    GetIdentityFromURL(request_->url, &username, &password);
    credentials_.Set(username, password);
  } else {
    credentials_.Set(base::ASCIIToUTF16("anonymous"),
                     base::ASCIIToUTF16("chrome@example.com"));
  }

  DetectTypecode();

  next_state_ = STATE_CTRL_RESOLVE_HOST;
  int rv = DoLoop(OK);
  if (rv == ERR_IO_PENDING)
    user_callback_ = std::move(callback);
  return rv;
}

int FtpNetworkTransaction::RestartWithAuth(const AuthCredentials& credentials,
                                           CompletionOnceCallback callback) {
  ResetStateForRestart();

  credentials_ = credentials;

  next_state_ = STATE_CTRL_RESOLVE_HOST;
  int rv = DoLoop(OK);
  if (rv == ERR_IO_PENDING)
    user_callback_ = std::move(callback);
  return rv;
}

int FtpNetworkTransaction::Read(IOBuffer* buf,
                                int buf_len,
                                CompletionOnceCallback callback) {
  DCHECK(buf);
  DCHECK_GT(buf_len, 0);

  read_data_buf_ = buf;
  read_data_buf_len_ = buf_len;

  next_state_ = STATE_DATA_READ;
  int rv = DoLoop(OK);
  if (rv == ERR_IO_PENDING)
    user_callback_ = std::move(callback);
  return rv;
}

const FtpResponseInfo* FtpNetworkTransaction::GetResponseInfo() const {
  return &response_;
}

LoadState FtpNetworkTransaction::GetLoadState() const {
  if (next_state_ == STATE_CTRL_RESOLVE_HOST_COMPLETE)
    return LOAD_STATE_RESOLVING_HOST;

  if (next_state_ == STATE_CTRL_CONNECT_COMPLETE ||
      next_state_ == STATE_DATA_CONNECT_COMPLETE)
    return LOAD_STATE_CONNECTING;

  if (next_state_ == STATE_DATA_READ_COMPLETE)
    return LOAD_STATE_READING_RESPONSE;

  if (command_sent_ == COMMAND_RETR && read_data_buf_.get())
    return LOAD_STATE_READING_RESPONSE;

  if (command_sent_ == COMMAND_QUIT)
    return LOAD_STATE_IDLE;

  if (command_sent_ != COMMAND_NONE)
    return LOAD_STATE_SENDING_REQUEST;

  return LOAD_STATE_IDLE;
}

uint64_t FtpNetworkTransaction::GetUploadProgress() const {
  return 0;
}

void FtpNetworkTransaction::ResetStateForRestart() {
  command_sent_ = COMMAND_NONE;
  user_callback_.Reset();
  response_ = FtpResponseInfo();
  read_ctrl_buf_ = new IOBuffer(kCtrlBufLen);
  ctrl_response_buffer_ = std::make_unique<FtpCtrlResponseBuffer>(net_log_);
  read_data_buf_ = nullptr;
  read_data_buf_len_ = 0;
  if (write_buf_.get())
    write_buf_->SetOffset(0);
  last_error_ = OK;
  data_connection_port_ = 0;
  ctrl_socket_.reset();
  data_socket_.reset();
  next_state_ = STATE_NONE;
  state_after_data_connect_complete_ = STATE_NONE;
}

void FtpNetworkTransaction::EstablishDataConnection(State state_after_connect) {
  DCHECK(state_after_connect == STATE_CTRL_WRITE_RETR ||
         state_after_connect == STATE_CTRL_WRITE_LIST);
  state_after_data_connect_complete_ = state_after_connect;
  next_state_ = use_epsv_ ? STATE_CTRL_WRITE_EPSV : STATE_CTRL_WRITE_PASV;
}

void FtpNetworkTransaction::DoCallback(int rv) {
  DCHECK(rv != ERR_IO_PENDING);

  std::move(user_callback_).Run(rv);
}

void FtpNetworkTransaction::OnIOComplete(int result) {
  int rv = DoLoop(result);
  if (rv != ERR_IO_PENDING)
    DoCallback(rv);
}

int FtpNetworkTransaction::ProcessCtrlResponse() {
  FtpCtrlResponse response = ctrl_response_buffer_->PopResponse();

  int rv = OK;
  switch (command_sent_) {
    case COMMAND_NONE:
      // TODO(phajdan.jr): https://crbug.com/526721: Check for errors in the
      // welcome message.
      next_state_ = STATE_CTRL_WRITE_USER;
      break;
    case COMMAND_USER:
      rv = ProcessResponseUSER(response);
      break;
    case COMMAND_PASS:
      rv = ProcessResponsePASS(response);
      break;
    case COMMAND_SYST:
      rv = ProcessResponseSYST(response);
      break;
    case COMMAND_PWD:
      rv = ProcessResponsePWD(response);
      break;
    case COMMAND_TYPE:
      rv = ProcessResponseTYPE(response);
      break;
    case COMMAND_EPSV:
      rv = ProcessResponseEPSV(response);
      break;
    case COMMAND_PASV:
      rv = ProcessResponsePASV(response);
      break;
    case COMMAND_SIZE:
      rv = ProcessResponseSIZE(response);
      break;
    case COMMAND_RETR:
      rv = ProcessResponseRETR(response);
      break;
    case COMMAND_CWD:
      rv = ProcessResponseCWD(response);
      break;
    case COMMAND_LIST:
      rv = ProcessResponseLIST(response);
      break;
    case COMMAND_QUIT:
      rv = ProcessResponseQUIT(response);
      break;
    default:
      LOG(DFATAL) << "Unexpected value of command_sent_: " << command_sent_;
      return ERR_UNEXPECTED;
  }

  // We may get multiple responses for some commands,
  // see http://crbug.com/18036. Consume all responses, regardless of whether
  // they make sense. On unexpected responses, SendFtpCommand expects all data
  // to have already been consumed, even when sending the QUIT command.
  while (ctrl_response_buffer_->ResponseAvailable() && rv == OK) {
    response = ctrl_response_buffer_->PopResponse();

    switch (command_sent_) {
      case COMMAND_RETR:
        rv = ProcessResponseRETR(response);
        break;
      case COMMAND_LIST:
        rv = ProcessResponseLIST(response);
        break;
      default:
        // Multiple responses for other commands are invalid.
        rv = Stop(ERR_INVALID_RESPONSE);
        break;
    }
  }

  return rv;
}

// Used to prepare and send FTP command.
int FtpNetworkTransaction::SendFtpCommand(const std::string& command,
                                          const std::string& command_for_log,
                                          Command cmd) {
  // If we send a new command when we still have unprocessed responses
  // for previous commands, the response receiving code will have no way to know
  // which responses are for which command.
  DCHECK(!ctrl_response_buffer_->ResponseAvailable());

  DCHECK(!write_command_buf_.get());
  DCHECK(!write_buf_.get());

  if (!IsValidFTPCommandSubstring(command)) {
    // Callers should validate the command themselves and return a more specific
    // error code.
    NOTREACHED();
    return Stop(ERR_UNEXPECTED);
  }

  command_sent_ = cmd;

  write_command_buf_ = new IOBufferWithSize(command.length() + 2);
  write_buf_ = new DrainableIOBuffer(write_command_buf_.get(),
                                     write_command_buf_->size());
  memcpy(write_command_buf_->data(), command.data(), command.length());
  memcpy(write_command_buf_->data() + command.length(), kCRLF, 2);

  net_log_.AddEvent(NetLogEventType::FTP_COMMAND_SENT,
                    NetLog::StringCallback("command", &command_for_log));

  next_state_ = STATE_CTRL_WRITE;
  return OK;
}

std::string FtpNetworkTransaction::GetRequestPathForFtpCommand(
    bool is_directory) const {
  std::string path(current_remote_directory_);
  if (request_->url.has_path()) {
    std::string gurl_path(request_->url.path());

    // Get rid of the typecode, see RFC 1738 section 3.2.2. FTP url-path.
    std::string::size_type pos = gurl_path.rfind(';');
    if (pos != std::string::npos)
      gurl_path.resize(pos);

    path.append(gurl_path);
  }
  // Make sure that if the path is expected to be a file, it won't end
  // with a trailing slash.
  if (!is_directory && path.length() > 1 && path.back() == '/')
    path.erase(path.length() - 1);
  UnescapeRule::Type unescape_rules =
      UnescapeRule::SPACES |
      UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS;
  // This may unescape to non-ASCII characters, but we allow that. See the
  // comment for IsValidFTPCommandSubstring.
  path = UnescapeURLComponent(path, unescape_rules);

  if (system_type_ == SYSTEM_TYPE_VMS) {
    if (is_directory)
      path = FtpUtil::UnixDirectoryPathToVMS(path);
    else
      path = FtpUtil::UnixFilePathToVMS(path);
  }

  DCHECK(IsValidFTPCommandSubstring(path));
  return path;
}

void FtpNetworkTransaction::DetectTypecode() {
  if (!request_->url.has_path())
    return;
  std::string gurl_path(request_->url.path());

  // Extract the typecode, see RFC 1738 section 3.2.2. FTP url-path.
  std::string::size_type pos = gurl_path.rfind(';');
  if (pos == std::string::npos)
    return;
  std::string typecode_string(gurl_path.substr(pos));
  if (typecode_string == ";type=a") {
    data_type_ = DATA_TYPE_ASCII;
    resource_type_ = RESOURCE_TYPE_FILE;
  } else if (typecode_string == ";type=i") {
    data_type_ = DATA_TYPE_IMAGE;
    resource_type_ = RESOURCE_TYPE_FILE;
  } else if (typecode_string == ";type=d") {
    resource_type_ = RESOURCE_TYPE_DIRECTORY;
  }
}

int FtpNetworkTransaction::DoLoop(int result) {
  DCHECK(next_state_ != STATE_NONE);

  int rv = result;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_CTRL_RESOLVE_HOST:
        DCHECK(rv == OK);
        rv = DoCtrlResolveHost();
        break;
      case STATE_CTRL_RESOLVE_HOST_COMPLETE:
        rv = DoCtrlResolveHostComplete(rv);
        break;
      case STATE_CTRL_CONNECT:
        DCHECK(rv == OK);
        rv = DoCtrlConnect();
        break;
      case STATE_CTRL_CONNECT_COMPLETE:
        rv = DoCtrlConnectComplete(rv);
        break;
      case STATE_CTRL_READ:
        DCHECK(rv == OK);
        rv = DoCtrlRead();
        break;
      case STATE_CTRL_READ_COMPLETE:
        rv = DoCtrlReadComplete(rv);
        break;
      case STATE_CTRL_WRITE:
        DCHECK(rv == OK);
        rv = DoCtrlWrite();
        break;
      case STATE_CTRL_WRITE_COMPLETE:
        rv = DoCtrlWriteComplete(rv);
        break;
      case STATE_CTRL_WRITE_USER:
        DCHECK(rv == OK);
        rv = DoCtrlWriteUSER();
        break;
      case STATE_CTRL_WRITE_PASS:
        DCHECK(rv == OK);
        rv = DoCtrlWritePASS();
        break;
      case STATE_CTRL_WRITE_SYST:
        DCHECK(rv == OK);
        rv = DoCtrlWriteSYST();
        break;
      case STATE_CTRL_WRITE_PWD:
        DCHECK(rv == OK);
        rv = DoCtrlWritePWD();
        break;
      case STATE_CTRL_WRITE_TYPE:
        DCHECK(rv == OK);
        rv = DoCtrlWriteTYPE();
        break;
      case STATE_CTRL_WRITE_EPSV:
        DCHECK(rv == OK);
        rv = DoCtrlWriteEPSV();
        break;
      case STATE_CTRL_WRITE_PASV:
        DCHECK(rv == OK);
        rv = DoCtrlWritePASV();
        break;
      case STATE_CTRL_WRITE_RETR:
        DCHECK(rv == OK);
        rv = DoCtrlWriteRETR();
        break;
      case STATE_CTRL_WRITE_SIZE:
        DCHECK(rv == OK);
        rv = DoCtrlWriteSIZE();
        break;
      case STATE_CTRL_WRITE_CWD:
        DCHECK(rv == OK);
        rv = DoCtrlWriteCWD();
        break;
      case STATE_CTRL_WRITE_LIST:
        DCHECK(rv == OK);
        rv = DoCtrlWriteLIST();
        break;
      case STATE_CTRL_WRITE_QUIT:
        DCHECK(rv == OK);
        rv = DoCtrlWriteQUIT();
        break;
      case STATE_DATA_CONNECT:
        DCHECK(rv == OK);
        rv = DoDataConnect();
        break;
      case STATE_DATA_CONNECT_COMPLETE:
        rv = DoDataConnectComplete(rv);
        break;
      case STATE_DATA_READ:
        DCHECK(rv == OK);
        rv = DoDataRead();
        break;
      case STATE_DATA_READ_COMPLETE:
        rv = DoDataReadComplete(rv);
        break;
      default:
        NOTREACHED() << "bad state";
        rv = ERR_UNEXPECTED;
        break;
    }
  } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
  return rv;
}

int FtpNetworkTransaction::DoCtrlResolveHost() {
  next_state_ = STATE_CTRL_RESOLVE_HOST_COMPLETE;

  HostResolver::RequestInfo info(HostPortPair::FromURL(request_->url));
  // No known referrer.
  return resolver_->Resolve(
      info, DEFAULT_PRIORITY, &addresses_,
      base::Bind(&FtpNetworkTransaction::OnIOComplete, base::Unretained(this)),
      &resolve_request_, net_log_);
}

int FtpNetworkTransaction::DoCtrlResolveHostComplete(int result) {
  if (result == OK)
    next_state_ = STATE_CTRL_CONNECT;
  return result;
}

int FtpNetworkTransaction::DoCtrlConnect() {
  next_state_ = STATE_CTRL_CONNECT_COMPLETE;
  ctrl_socket_ = socket_factory_->CreateTransportClientSocket(
      addresses_, nullptr, net_log_.net_log(), net_log_.source());
  net_log_.AddEvent(
      NetLogEventType::FTP_CONTROL_CONNECTION,
      ctrl_socket_->NetLog().source().ToEventParametersCallback());
  return ctrl_socket_->Connect(io_callback_);
}

int FtpNetworkTransaction::DoCtrlConnectComplete(int result) {
  if (result == OK) {
    // Put the peer's IP address and port into the response.
    IPEndPoint ip_endpoint;
    result = ctrl_socket_->GetPeerAddress(&ip_endpoint);
    if (result == OK) {
      response_.socket_address = HostPortPair::FromIPEndPoint(ip_endpoint);
      next_state_ = STATE_CTRL_READ;

      if (ip_endpoint.GetFamily() == ADDRESS_FAMILY_IPV4) {
        // Do not use EPSV for IPv4 connections. Some servers become confused
        // and we time out while waiting to connect. PASV is perfectly fine for
        // IPv4. Note that this blacklists IPv4 not to use EPSV instead of
        // whitelisting IPv6 to use it, to make the code more future-proof:
        // all future protocols should just use EPSV.
        use_epsv_ = false;
      }
    }
  }
  return result;
}

int FtpNetworkTransaction::DoCtrlRead() {
  next_state_ = STATE_CTRL_READ_COMPLETE;
  return ctrl_socket_->Read(read_ctrl_buf_.get(), kCtrlBufLen, io_callback_);
}

int FtpNetworkTransaction::DoCtrlReadComplete(int result) {
  if (result == 0) {
    // Some servers (for example Pure-FTPd) apparently close the control
    // connection when anonymous login is not permitted. For more details
    // see http://crbug.com/25023.
    if (command_sent_ == COMMAND_USER &&
        credentials_.username() == base::ASCIIToUTF16("anonymous")) {
      response_.needs_auth = true;
    }
    return Stop(ERR_EMPTY_RESPONSE);
  }
  if (result < 0)
    return Stop(result);

  ctrl_response_buffer_->ConsumeData(read_ctrl_buf_->data(), result);

  if (!ctrl_response_buffer_->ResponseAvailable()) {
    // Read more data from the control socket.
    next_state_ = STATE_CTRL_READ;
    return OK;
  }

  return ProcessCtrlResponse();
}

int FtpNetworkTransaction::DoCtrlWrite() {
  next_state_ = STATE_CTRL_WRITE_COMPLETE;

  return ctrl_socket_->Write(write_buf_.get(), write_buf_->BytesRemaining(),
                             io_callback_,
                             NetworkTrafficAnnotationTag(traffic_annotation_));
}

int FtpNetworkTransaction::DoCtrlWriteComplete(int result) {
  if (result < 0)
    return result;

  write_buf_->DidConsume(result);
  if (write_buf_->BytesRemaining() == 0) {
    // Clear the write buffer.
    write_buf_ = nullptr;
    write_command_buf_ = nullptr;

    next_state_ = STATE_CTRL_READ;
  } else {
    next_state_ = STATE_CTRL_WRITE;
  }
  return OK;
}

// FTP Commands and responses

// USER Command.
int FtpNetworkTransaction::DoCtrlWriteUSER() {
  std::string command = "USER " + base::UTF16ToUTF8(credentials_.username());

  if (!IsValidFTPCommandSubstring(command))
    return Stop(ERR_MALFORMED_IDENTITY);

  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, "USER ***", COMMAND_USER);
}

int FtpNetworkTransaction::ProcessResponseUSER(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK:
      next_state_ = STATE_CTRL_WRITE_SYST;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      next_state_ = STATE_CTRL_WRITE_PASS;
      break;
    case ERROR_CLASS_TRANSIENT_ERROR:
    case ERROR_CLASS_PERMANENT_ERROR:
      response_.needs_auth = true;
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// PASS command.
int FtpNetworkTransaction::DoCtrlWritePASS() {
  std::string command = "PASS " + base::UTF16ToUTF8(credentials_.password());

  if (!IsValidFTPCommandSubstring(command))
    return Stop(ERR_MALFORMED_IDENTITY);

  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, "PASS ***", COMMAND_PASS);
}

int FtpNetworkTransaction::ProcessResponsePASS(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK:
      next_state_ = STATE_CTRL_WRITE_SYST;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_TRANSIENT_ERROR:
    case ERROR_CLASS_PERMANENT_ERROR:
      response_.needs_auth = true;
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// SYST command.
int FtpNetworkTransaction::DoCtrlWriteSYST() {
  std::string command = "SYST";
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_SYST);
}

int FtpNetworkTransaction::ProcessResponseSYST(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK: {
      // All important info should be on the first line.
      std::string line = response.lines[0];
      // The response should be ASCII, which allows us to do case-insensitive
      // comparisons easily. If it is not ASCII, we leave the system type
      // as unknown.
      if (base::IsStringASCII(line)) {
        line = base::ToLowerASCII(line);

        // Remove all whitespace, to correctly handle cases like fancy "V M S"
        // response instead of "VMS".
        base::RemoveChars(line, base::kWhitespaceASCII, &line);

        // The "magic" strings we test for below have been gathered by an
        // empirical study. VMS needs to come first because some VMS systems
        // also respond with "UNIX emulation", which is not perfect. It is much
        // more reliable to talk to these servers in their native language.
        if (line.find("vms") != std::string::npos) {
          system_type_ = SYSTEM_TYPE_VMS;
        } else if (line.find("l8") != std::string::npos ||
                   line.find("unix") != std::string::npos ||
                   line.find("bsd") != std::string::npos) {
          system_type_ = SYSTEM_TYPE_UNIX;
        } else if (line.find("win32") != std::string::npos ||
                   line.find("windows") != std::string::npos) {
          system_type_ = SYSTEM_TYPE_WINDOWS;
        } else if (line.find("os/2") != std::string::npos) {
          system_type_ = SYSTEM_TYPE_OS2;
        }
      }
      next_state_ = STATE_CTRL_WRITE_PWD;
      break;
    }
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      // Server does not recognize the SYST command so proceed.
      next_state_ = STATE_CTRL_WRITE_PWD;
      break;
  }
  return OK;
}

// PWD command.
int FtpNetworkTransaction::DoCtrlWritePWD() {
  std::string command = "PWD";
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_PWD);
}

int FtpNetworkTransaction::ProcessResponsePWD(const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK: {
      // The info we look for should be on the first line.
      std::string line = response.lines[0];
      if (line.empty())
        return Stop(ERR_INVALID_RESPONSE);
      std::string::size_type quote_pos = line.find('"');
      if (quote_pos != std::string::npos) {
        line = line.substr(quote_pos + 1);
        quote_pos = line.find('"');
        if (quote_pos == std::string::npos)
          return Stop(ERR_INVALID_RESPONSE);
        line = line.substr(0, quote_pos);
      }
      if (system_type_ == SYSTEM_TYPE_VMS)
        line = FtpUtil::VMSPathToUnix(line);
      if (!line.empty() && line.back() == '/')
        line.erase(line.length() - 1);
      // Fail if the "path" contains characters not allowed in commands.
      // This does mean that files with CRs or LFs in their names aren't
      // handled, but that's probably for the best.
      if (!IsValidFTPCommandSubstring(line))
        return Stop(ERR_INVALID_RESPONSE);
      current_remote_directory_ = line;
      next_state_ = STATE_CTRL_WRITE_TYPE;
      break;
    }
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// TYPE command.
int FtpNetworkTransaction::DoCtrlWriteTYPE() {
  std::string command = "TYPE ";
  if (data_type_ == DATA_TYPE_ASCII) {
    command += "A";
  } else if (data_type_ == DATA_TYPE_IMAGE) {
    command += "I";
  } else {
    NOTREACHED();
    return Stop(ERR_UNEXPECTED);
  }
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_TYPE);
}

int FtpNetworkTransaction::ProcessResponseTYPE(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK:
      next_state_ = STATE_CTRL_WRITE_SIZE;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// EPSV command
int FtpNetworkTransaction::DoCtrlWriteEPSV() {
  const std::string command = "EPSV";
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_EPSV);
}

int FtpNetworkTransaction::ProcessResponseEPSV(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK: {
      int port;
      if (!ExtractPortFromEPSVResponse(response, &port))
        return Stop(ERR_INVALID_RESPONSE);
      if (IsWellKnownPort(port) ||
          !IsPortAllowedForScheme(port, url::kFtpScheme)) {
        return Stop(ERR_UNSAFE_PORT);
      }
      data_connection_port_ = static_cast<uint16_t>(port);
      next_state_ = STATE_DATA_CONNECT;
      break;
    }
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
    case ERROR_CLASS_PERMANENT_ERROR:
      use_epsv_ = false;
      next_state_ = STATE_CTRL_WRITE_PASV;
      return OK;
  }
  return OK;
}

// PASV command
int FtpNetworkTransaction::DoCtrlWritePASV() {
  std::string command = "PASV";
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_PASV);
}

int FtpNetworkTransaction::ProcessResponsePASV(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK: {
      int port;
      if (!ExtractPortFromPASVResponse(response, &port))
        return Stop(ERR_INVALID_RESPONSE);
      if (IsWellKnownPort(port) ||
          !IsPortAllowedForScheme(port, url::kFtpScheme)) {
        return Stop(ERR_UNSAFE_PORT);
      }
      data_connection_port_ = static_cast<uint16_t>(port);
      next_state_ = STATE_DATA_CONNECT;
      break;
    }
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// RETR command
int FtpNetworkTransaction::DoCtrlWriteRETR() {
  std::string command = "RETR " + GetRequestPathForFtpCommand(false);
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_RETR);
}

int FtpNetworkTransaction::ProcessResponseRETR(
    const FtpCtrlResponse& response) {
  // Resource type should be either filled in by DetectTypecode() or
  // detected with CWD. RETR is sent only when the resource is a file.
  DCHECK_EQ(RESOURCE_TYPE_FILE, resource_type_);

  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      // We want the client to start reading the response at this point.
      // It got here either through Start or RestartWithAuth. We want that
      // method to complete. Not setting next state here will make DoLoop exit
      // and in turn make Start/RestartWithAuth complete.
      break;
    case ERROR_CLASS_OK:
      next_state_ = STATE_CTRL_WRITE_QUIT;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }

  return OK;
}

// SIZE command
int FtpNetworkTransaction::DoCtrlWriteSIZE() {
  std::string command = "SIZE " + GetRequestPathForFtpCommand(false);
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_SIZE);
}

int FtpNetworkTransaction::ProcessResponseSIZE(
    const FtpCtrlResponse& response) {
  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      break;
    case ERROR_CLASS_OK:
      if (response.lines.size() != 1)
        return Stop(ERR_INVALID_RESPONSE);
      int64_t size;
      if (!base::StringToInt64(response.lines[0], &size))
        return Stop(ERR_INVALID_RESPONSE);
      if (size < 0)
        return Stop(ERR_INVALID_RESPONSE);

      // A successful response to SIZE does not mean the resource is a file.
      // Some FTP servers (for example, the qnx one) send a SIZE even for
      // directories.
      response_.expected_content_size = size;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      break;
    case ERROR_CLASS_TRANSIENT_ERROR:
      break;
    case ERROR_CLASS_PERMANENT_ERROR:
      // It's possible that SIZE failed because the path is a directory.
      // TODO(xunjieli): https://crbug.com/526724: Add a test for this case.
      if (resource_type_ == RESOURCE_TYPE_UNKNOWN &&
          response.status_code != 550) {
        return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
      }
      break;
  }

  // If the resource is known beforehand to be a file, RETR should be issued,
  // otherwise do CWD which will detect the resource type.
  if (resource_type_ == RESOURCE_TYPE_FILE)
    EstablishDataConnection(STATE_CTRL_WRITE_RETR);
  else
    next_state_ = STATE_CTRL_WRITE_CWD;
  return OK;
}

// CWD command
int FtpNetworkTransaction::DoCtrlWriteCWD() {
  std::string command = "CWD " + GetRequestPathForFtpCommand(true);
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_CWD);
}

int FtpNetworkTransaction::ProcessResponseCWD(const FtpCtrlResponse& response) {
  // CWD should be invoked only when the resource is not a file.
  DCHECK_NE(RESOURCE_TYPE_FILE, resource_type_);

  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_OK:
      resource_type_ = RESOURCE_TYPE_DIRECTORY;
      EstablishDataConnection(STATE_CTRL_WRITE_LIST);
      break;
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      // Some FTP servers send response 451 (not a valid CWD response according
      // to RFC 959) instead of 550.
      if (response.status_code == 451)
        return ProcessResponseCWDNotADirectory();

      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      if (response.status_code == 550)
        return ProcessResponseCWDNotADirectory();

      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }

  return OK;
}

int FtpNetworkTransaction::ProcessResponseCWDNotADirectory() {
  if (resource_type_ == RESOURCE_TYPE_DIRECTORY) {
    // We're assuming that the resource is a directory, but the server
    // says it's not true. The most probable interpretation is that it
    // doesn't exist (with FTP we can't be sure).
    return Stop(ERR_FILE_NOT_FOUND);
  }

  // If it is not a directory, it is probably a file.
  resource_type_ = RESOURCE_TYPE_FILE;

  EstablishDataConnection(STATE_CTRL_WRITE_RETR);
  return OK;
}

// LIST command
int FtpNetworkTransaction::DoCtrlWriteLIST() {
  // Use the -l option for mod_ftp configured in LISTIsNLST mode: the option
  // forces LIST output instead of NLST (which would be ambiguous for us
  // to parse).
  std::string command("LIST -l");
  if (system_type_ == SYSTEM_TYPE_VMS)
    command = "LIST *.*;0";

  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_LIST);
}

int FtpNetworkTransaction::ProcessResponseLIST(
    const FtpCtrlResponse& response) {
  // Resource type should be either filled in by DetectTypecode() or
  // detected with CWD. LIST is sent only when the resource is a directory.
  DCHECK_EQ(RESOURCE_TYPE_DIRECTORY, resource_type_);

  switch (GetErrorClass(response.status_code)) {
    case ERROR_CLASS_INITIATED:
      // We want the client to start reading the response at this point.
      // It got here either through Start or RestartWithAuth. We want that
      // method to complete. Not setting next state here will make DoLoop exit
      // and in turn make Start/RestartWithAuth complete.
      response_.is_directory_listing = true;
      break;
    case ERROR_CLASS_OK:
      response_.is_directory_listing = true;
      next_state_ = STATE_CTRL_WRITE_QUIT;
      break;
    case ERROR_CLASS_INFO_NEEDED:
      return Stop(ERR_INVALID_RESPONSE);
    case ERROR_CLASS_TRANSIENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
    case ERROR_CLASS_PERMANENT_ERROR:
      return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
  }
  return OK;
}

// QUIT command
int FtpNetworkTransaction::DoCtrlWriteQUIT() {
  std::string command = "QUIT";
  next_state_ = STATE_CTRL_READ;
  return SendFtpCommand(command, command, COMMAND_QUIT);
}

int FtpNetworkTransaction::ProcessResponseQUIT(
    const FtpCtrlResponse& response) {
  ctrl_socket_->Disconnect();
  return last_error_;
}

// Data Connection

int FtpNetworkTransaction::DoDataConnect() {
  next_state_ = STATE_DATA_CONNECT_COMPLETE;
  IPEndPoint ip_endpoint;
  AddressList data_address;
  // Connect to the same host as the control socket to prevent PASV port
  // scanning attacks.
  int rv = ctrl_socket_->GetPeerAddress(&ip_endpoint);
  if (rv != OK)
    return Stop(rv);
  data_address = AddressList::CreateFromIPAddress(
      ip_endpoint.address(), data_connection_port_);
  data_socket_ = socket_factory_->CreateTransportClientSocket(
      data_address, nullptr, net_log_.net_log(), net_log_.source());
  net_log_.AddEvent(
      NetLogEventType::FTP_DATA_CONNECTION,
      data_socket_->NetLog().source().ToEventParametersCallback());
  return data_socket_->Connect(io_callback_);
}

int FtpNetworkTransaction::DoDataConnectComplete(int result) {
  if (result != OK && use_epsv_) {
    // It's possible we hit a broken server, sadly. They can break in different
    // ways. Some time out, some reset a connection. Fall back to PASV.
    // TODO(phajdan.jr): https://crbug.com/526723: remember it for future
    // transactions with this server.
    use_epsv_ = false;
    next_state_ = STATE_CTRL_WRITE_PASV;
    return OK;
  }

  // Only record the connection error after we've applied all our fallbacks.
  // We want to capture the final error, one we're not going to recover from.
  RecordDataConnectionError(result);

  if (result != OK)
    return Stop(result);

  next_state_ = state_after_data_connect_complete_;
  return OK;
}

int FtpNetworkTransaction::DoDataRead() {
  DCHECK(read_data_buf_.get());
  DCHECK_GT(read_data_buf_len_, 0);

  if (!data_socket_ || !data_socket_->IsConnected()) {
    // If we don't destroy the data socket completely, some servers will wait
    // for us (http://crbug.com/21127). The half-closed TCP connection needs
    // to be closed on our side too.
    data_socket_.reset();

    if (ctrl_socket_->IsConnected()) {
      // Wait for the server's response, we should get it before sending QUIT.
      next_state_ = STATE_CTRL_READ;
      return OK;
    }

    // We are no longer connected to the server, so just finish the transaction.
    return Stop(OK);
  }

  next_state_ = STATE_DATA_READ_COMPLETE;
  read_data_buf_->data()[0] = 0;
  return data_socket_->Read(
      read_data_buf_.get(), read_data_buf_len_, io_callback_);
}

int FtpNetworkTransaction::DoDataReadComplete(int result) {
  return result;
}

// We're using a histogram as a group of counters, with one bucket for each
// enumeration value.  We're only interested in the values of the counters.
// Ignore the shape, average, and standard deviation of the histograms because
// they are meaningless.
//
// We use two histograms.  In the first histogram we tally whether the user has
// seen an error of that type during the session.  In the second histogram we
// tally the total number of times the users sees each errer.
void FtpNetworkTransaction::RecordDataConnectionError(int result) {
  // Gather data for http://crbug.com/3073. See how many users have trouble
  // establishing FTP data connection in passive FTP mode.
  enum {
    // Data connection successful.
    NET_ERROR_OK = 0,

    // Local firewall blocked the connection.
    NET_ERROR_ACCESS_DENIED = 1,

    // Connection timed out.
    NET_ERROR_TIMED_OUT = 2,

    // Connection has been estabilished, but then got broken (either reset
    // or aborted).
    NET_ERROR_CONNECTION_BROKEN = 3,

    // Connection has been refused.
    NET_ERROR_CONNECTION_REFUSED = 4,

    // No connection to the internet.
    NET_ERROR_INTERNET_DISCONNECTED = 5,

    // Could not reach the destination address.
    NET_ERROR_ADDRESS_UNREACHABLE = 6,

    // A programming error in our network stack.
    NET_ERROR_UNEXPECTED = 7,

    // Other kind of error.
    NET_ERROR_OTHER = 20,

    NUM_OF_NET_ERROR_TYPES
  } type;
  switch (result) {
    case OK:
      type = NET_ERROR_OK;
      break;
    case ERR_ACCESS_DENIED:
    case ERR_NETWORK_ACCESS_DENIED:
      type = NET_ERROR_ACCESS_DENIED;
      break;
    case ERR_TIMED_OUT:
      type = NET_ERROR_TIMED_OUT;
      break;
    case ERR_CONNECTION_ABORTED:
    case ERR_CONNECTION_RESET:
    case ERR_CONNECTION_CLOSED:
      type = NET_ERROR_CONNECTION_BROKEN;
      break;
    case ERR_CONNECTION_FAILED:
    case ERR_CONNECTION_REFUSED:
      type = NET_ERROR_CONNECTION_REFUSED;
      break;
    case ERR_INTERNET_DISCONNECTED:
      type = NET_ERROR_INTERNET_DISCONNECTED;
      break;
    case ERR_ADDRESS_INVALID:
    case ERR_ADDRESS_UNREACHABLE:
      type = NET_ERROR_ADDRESS_UNREACHABLE;
      break;
    case ERR_UNEXPECTED:
      type = NET_ERROR_UNEXPECTED;
      break;
    default:
      type = NET_ERROR_OTHER;
      break;
  };
  static bool had_error_type[NUM_OF_NET_ERROR_TYPES];

  DCHECK(type >= 0 && type < NUM_OF_NET_ERROR_TYPES);
  if (!had_error_type[type]) {
    had_error_type[type] = true;
    UMA_HISTOGRAM_ENUMERATION("Net.FtpDataConnectionErrorHappened",
        type, NUM_OF_NET_ERROR_TYPES);
  }
  UMA_HISTOGRAM_ENUMERATION("Net.FtpDataConnectionErrorCount",
      type, NUM_OF_NET_ERROR_TYPES);
}

}  // namespace net
