diff options
author | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
---|---|---|
committer | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
commit | 679147eead574d186ebf3069647b4c23e8ccace6 (patch) | |
tree | fc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/net/ftp | |
download | qtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz |
Initial import.
Diffstat (limited to 'chromium/net/ftp')
43 files changed, 7083 insertions, 0 deletions
diff --git a/chromium/net/ftp/OWNERS b/chromium/net/ftp/OWNERS new file mode 100644 index 00000000000..92ecc889fd3 --- /dev/null +++ b/chromium/net/ftp/OWNERS @@ -0,0 +1 @@ +phajdan.jr@chromium.org diff --git a/chromium/net/ftp/ftp_auth_cache.cc b/chromium/net/ftp/ftp_auth_cache.cc new file mode 100644 index 00000000000..370cee57379 --- /dev/null +++ b/chromium/net/ftp/ftp_auth_cache.cc @@ -0,0 +1,62 @@ +// Copyright (c) 2011 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_auth_cache.h" + +#include "base/logging.h" +#include "url/gurl.h" + +namespace net { + +// static +const size_t FtpAuthCache::kMaxEntries = 10; + +FtpAuthCache::Entry::Entry(const GURL& origin, + const AuthCredentials& credentials) + : origin(origin), + credentials(credentials) { +} + +FtpAuthCache::Entry::~Entry() {} + +FtpAuthCache::FtpAuthCache() {} + +FtpAuthCache::~FtpAuthCache() {} + +FtpAuthCache::Entry* FtpAuthCache::Lookup(const GURL& origin) { + for (EntryList::iterator it = entries_.begin(); it != entries_.end(); ++it) { + if (it->origin == origin) + return &(*it); + } + return NULL; +} + +void FtpAuthCache::Add(const GURL& origin, const AuthCredentials& credentials) { + DCHECK(origin.SchemeIs("ftp")); + DCHECK_EQ(origin.GetOrigin(), origin); + + Entry* entry = Lookup(origin); + if (entry) { + entry->credentials = credentials; + } else { + entries_.push_front(Entry(origin, credentials)); + + // Prevent unbound memory growth of the cache. + if (entries_.size() > kMaxEntries) + entries_.pop_back(); + } +} + +void FtpAuthCache::Remove(const GURL& origin, + const AuthCredentials& credentials) { + for (EntryList::iterator it = entries_.begin(); it != entries_.end(); ++it) { + if (it->origin == origin && it->credentials.Equals(credentials)) { + entries_.erase(it); + DCHECK(!Lookup(origin)); + return; + } + } +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_auth_cache.h b/chromium/net/ftp/ftp_auth_cache.h new file mode 100644 index 00000000000..526b3588782 --- /dev/null +++ b/chromium/net/ftp/ftp_auth_cache.h @@ -0,0 +1,61 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_AUTH_CACHE_H_ +#define NET_FTP_FTP_AUTH_CACHE_H_ + +#include <list> + +#include "net/base/auth.h" +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace net { + +// The FtpAuthCache class is a simple cache structure to store authentication +// information for ftp. Provides lookup, insertion, and deletion of entries. +// The parameter for doing lookups, insertions, and deletions is a GURL of the +// server's address (not a full URL with path, since FTP auth isn't per path). +// For example: +// GURL("ftp://myserver") -- OK (implied port of 21) +// GURL("ftp://myserver:21") -- OK +// GURL("ftp://myserver/PATH") -- WRONG, paths not allowed +class NET_EXPORT_PRIVATE FtpAuthCache { + public: + // Maximum number of entries we allow in the cache. + static const size_t kMaxEntries; + + struct Entry { + Entry(const GURL& origin, const AuthCredentials& credentials); + ~Entry(); + + GURL origin; + AuthCredentials credentials; + }; + + FtpAuthCache(); + ~FtpAuthCache(); + + // Return Entry corresponding to given |origin| or NULL if not found. + Entry* Lookup(const GURL& origin); + + // Add an entry for |origin| to the cache using |credentials|. If there is + // already an entry for |origin|, it will be overwritten. + void Add(const GURL& origin, const AuthCredentials& credentials); + + // Remove the entry for |origin| from the cache, if one exists and matches + // |credentials|. + void Remove(const GURL& origin, const AuthCredentials& credentials); + + private: + typedef std::list<Entry> EntryList; + + // Internal representation of cache, an STL list. This makes lookups O(n), + // but we expect n to be very low. + EntryList entries_; +}; + +} // namespace net + +#endif // NET_FTP_FTP_AUTH_CACHE_H_ diff --git a/chromium/net/ftp/ftp_auth_cache_unittest.cc b/chromium/net/ftp/ftp_auth_cache_unittest.cc new file mode 100644 index 00000000000..153d08be49f --- /dev/null +++ b/chromium/net/ftp/ftp_auth_cache_unittest.cc @@ -0,0 +1,159 @@ +// Copyright (c) 2011 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_auth_cache.h" + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/auth.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using net::FtpAuthCache; + +namespace { + +const base::string16 kBogus(ASCIIToUTF16("bogus")); +const base::string16 kOthername(ASCIIToUTF16("othername")); +const base::string16 kOtherword(ASCIIToUTF16("otherword")); +const base::string16 kPassword(ASCIIToUTF16("password")); +const base::string16 kPassword1(ASCIIToUTF16("password1")); +const base::string16 kPassword2(ASCIIToUTF16("password2")); +const base::string16 kPassword3(ASCIIToUTF16("password3")); +const base::string16 kUsername(ASCIIToUTF16("username")); +const base::string16 kUsername1(ASCIIToUTF16("username1")); +const base::string16 kUsername2(ASCIIToUTF16("username2")); +const base::string16 kUsername3(ASCIIToUTF16("username3")); + +} // namespace + +TEST(FtpAuthCacheTest, LookupAddRemove) { + FtpAuthCache cache; + + GURL origin1("ftp://foo1"); + GURL origin2("ftp://foo2"); + + // Lookup non-existent entry. + EXPECT_TRUE(cache.Lookup(origin1) == NULL); + + // Add entry for origin1. + cache.Add(origin1, net::AuthCredentials(kUsername1, kPassword1)); + FtpAuthCache::Entry* entry1 = cache.Lookup(origin1); + ASSERT_TRUE(entry1); + EXPECT_EQ(origin1, entry1->origin); + EXPECT_EQ(kUsername1, entry1->credentials.username()); + EXPECT_EQ(kPassword1, entry1->credentials.password()); + + // Add an entry for origin2. + cache.Add(origin2, net::AuthCredentials(kUsername2, kPassword2)); + FtpAuthCache::Entry* entry2 = cache.Lookup(origin2); + ASSERT_TRUE(entry2); + EXPECT_EQ(origin2, entry2->origin); + EXPECT_EQ(kUsername2, entry2->credentials.username()); + EXPECT_EQ(kPassword2, entry2->credentials.password()); + + // The original entry1 should still be there. + EXPECT_EQ(entry1, cache.Lookup(origin1)); + + // Overwrite the entry for origin1. + cache.Add(origin1, net::AuthCredentials(kUsername3, kPassword3)); + FtpAuthCache::Entry* entry3 = cache.Lookup(origin1); + ASSERT_TRUE(entry3); + EXPECT_EQ(origin1, entry3->origin); + EXPECT_EQ(kUsername3, entry3->credentials.username()); + EXPECT_EQ(kPassword3, entry3->credentials.password()); + + // Remove entry of origin1. + cache.Remove(origin1, net::AuthCredentials(kUsername3, kPassword3)); + EXPECT_TRUE(cache.Lookup(origin1) == NULL); + + // Remove non-existent entry. + cache.Remove(origin1, net::AuthCredentials(kUsername3, kPassword3)); + EXPECT_TRUE(cache.Lookup(origin1) == NULL); +} + +// Check that if the origin differs only by port number, it is considered +// a separate origin. +TEST(FtpAuthCacheTest, LookupWithPort) { + FtpAuthCache cache; + + GURL origin1("ftp://foo:80"); + GURL origin2("ftp://foo:21"); + + cache.Add(origin1, net::AuthCredentials(kUsername, kPassword)); + cache.Add(origin2, net::AuthCredentials(kUsername, kPassword)); + + EXPECT_NE(cache.Lookup(origin1), cache.Lookup(origin2)); +} + +TEST(FtpAuthCacheTest, NormalizedKey) { + // GURL is automatically canonicalized. Hence the following variations in + // url format should all map to the same entry (case insensitive host, + // default port of 21). + + FtpAuthCache cache; + + // Add. + cache.Add(GURL("ftp://HoSt:21"), net::AuthCredentials(kUsername, kPassword)); + + // Lookup. + FtpAuthCache::Entry* entry1 = cache.Lookup(GURL("ftp://HoSt:21")); + ASSERT_TRUE(entry1); + EXPECT_EQ(entry1, cache.Lookup(GURL("ftp://host:21"))); + EXPECT_EQ(entry1, cache.Lookup(GURL("ftp://host"))); + + // Overwrite. + cache.Add(GURL("ftp://host"), net::AuthCredentials(kOthername, kOtherword)); + FtpAuthCache::Entry* entry2 = cache.Lookup(GURL("ftp://HoSt:21")); + ASSERT_TRUE(entry2); + EXPECT_EQ(GURL("ftp://host"), entry2->origin); + EXPECT_EQ(kOthername, entry2->credentials.username()); + EXPECT_EQ(kOtherword, entry2->credentials.password()); + + // Remove + cache.Remove(GURL("ftp://HOsT"), + net::AuthCredentials(kOthername, kOtherword)); + EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == NULL); +} + +TEST(FtpAuthCacheTest, OnlyRemoveMatching) { + FtpAuthCache cache; + + cache.Add(GURL("ftp://host"), net::AuthCredentials(kUsername, kPassword)); + EXPECT_TRUE(cache.Lookup(GURL("ftp://host"))); + + // Auth data doesn't match, shouldn't remove. + cache.Remove(GURL("ftp://host"), net::AuthCredentials(kBogus, kBogus)); + EXPECT_TRUE(cache.Lookup(GURL("ftp://host"))); + + // Auth data matches, should remove. + cache.Remove(GURL("ftp://host"), net::AuthCredentials(kUsername, kPassword)); + EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == NULL); +} + +TEST(FtpAuthCacheTest, EvictOldEntries) { + FtpAuthCache cache; + + for (size_t i = 0; i < FtpAuthCache::kMaxEntries; i++) { + cache.Add(GURL("ftp://host" + base::IntToString(i)), + net::AuthCredentials(kUsername, kPassword)); + } + + // No entries should be evicted before reaching the limit. + for (size_t i = 0; i < FtpAuthCache::kMaxEntries; i++) { + EXPECT_TRUE(cache.Lookup(GURL("ftp://host" + base::IntToString(i)))); + } + + // Adding one entry should cause eviction of the first entry. + cache.Add(GURL("ftp://last_host"), + net::AuthCredentials(kUsername, kPassword)); + EXPECT_TRUE(cache.Lookup(GURL("ftp://host0")) == NULL); + + // Remaining entries should not get evicted. + for (size_t i = 1; i < FtpAuthCache::kMaxEntries; i++) { + EXPECT_TRUE(cache.Lookup(GURL("ftp://host" + base::IntToString(i)))); + } + EXPECT_TRUE(cache.Lookup(GURL("ftp://last_host"))); +} diff --git a/chromium/net/ftp/ftp_ctrl_response_buffer.cc b/chromium/net/ftp/ftp_ctrl_response_buffer.cc new file mode 100644 index 00000000000..f7b14678095 --- /dev/null +++ b/chromium/net/ftp/ftp_ctrl_response_buffer.cc @@ -0,0 +1,152 @@ +// Copyright (c) 2011 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_ctrl_response_buffer.h" + +#include "base/bind.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/values.h" +#include "net/base/net_errors.h" + +namespace net { + +// static +const int FtpCtrlResponse::kInvalidStatusCode = -1; + +FtpCtrlResponse::FtpCtrlResponse() : status_code(kInvalidStatusCode) {} + +FtpCtrlResponse::~FtpCtrlResponse() {} + +FtpCtrlResponseBuffer::FtpCtrlResponseBuffer(const BoundNetLog& net_log) + : multiline_(false), + net_log_(net_log) { +} + +FtpCtrlResponseBuffer::~FtpCtrlResponseBuffer() {} + +int FtpCtrlResponseBuffer::ConsumeData(const char* data, int data_length) { + buffer_.append(data, data_length); + ExtractFullLinesFromBuffer(); + + while (!lines_.empty()) { + ParsedLine line = lines_.front(); + lines_.pop(); + + if (multiline_) { + if (!line.is_complete || line.status_code != response_buf_.status_code) { + line_buf_.append(line.raw_text); + continue; + } + + response_buf_.lines.push_back(line_buf_); + + line_buf_ = line.status_text; + DCHECK_EQ(line.status_code, response_buf_.status_code); + + if (!line.is_multiline) { + response_buf_.lines.push_back(line_buf_); + responses_.push(response_buf_); + + // Prepare to handle following lines. + response_buf_ = FtpCtrlResponse(); + line_buf_.clear(); + multiline_ = false; + } + } else { + if (!line.is_complete) + return ERR_INVALID_RESPONSE; + + response_buf_.status_code = line.status_code; + if (line.is_multiline) { + line_buf_ = line.status_text; + multiline_ = true; + } else { + response_buf_.lines.push_back(line.status_text); + responses_.push(response_buf_); + + // Prepare to handle following lines. + response_buf_ = FtpCtrlResponse(); + line_buf_.clear(); + } + } + } + + return OK; +} + +namespace { + +base::Value* NetLogFtpCtrlResponseCallback(const FtpCtrlResponse* response, + NetLog::LogLevel log_level) { + base::ListValue* lines = new base::ListValue(); + lines->AppendStrings(response->lines); + + base::DictionaryValue* dict = new base::DictionaryValue(); + dict->SetInteger("status_code", response->status_code); + dict->Set("lines", lines); + return dict; +} + +} // namespace + +FtpCtrlResponse FtpCtrlResponseBuffer::PopResponse() { + FtpCtrlResponse result = responses_.front(); + responses_.pop(); + + net_log_.AddEvent(NetLog::TYPE_FTP_CONTROL_RESPONSE, + base::Bind(&NetLogFtpCtrlResponseCallback, &result)); + + return result; +} + +FtpCtrlResponseBuffer::ParsedLine::ParsedLine() + : has_status_code(false), + is_multiline(false), + is_complete(false), + status_code(FtpCtrlResponse::kInvalidStatusCode) { +} + +// static +FtpCtrlResponseBuffer::ParsedLine FtpCtrlResponseBuffer::ParseLine( + const std::string& line) { + ParsedLine result; + + if (line.length() >= 3) { + if (base::StringToInt(base::StringPiece(line.begin(), line.begin() + 3), + &result.status_code)) + result.has_status_code = (100 <= result.status_code && + result.status_code <= 599); + if (result.has_status_code && line.length() >= 4 && line[3] == ' ') { + result.is_complete = true; + } else if (result.has_status_code && line.length() >= 4 && line[3] == '-') { + result.is_complete = true; + result.is_multiline = true; + } + } + + if (result.is_complete) { + result.status_text = line.substr(4); + } else { + result.status_text = line; + } + + result.raw_text = line; + + return result; +} + +void FtpCtrlResponseBuffer::ExtractFullLinesFromBuffer() { + int cut_pos = 0; + for (size_t i = 0; i < buffer_.length(); i++) { + if (i >= 1 && buffer_[i - 1] == '\r' && buffer_[i] == '\n') { + lines_.push(ParseLine(buffer_.substr(cut_pos, i - cut_pos - 1))); + cut_pos = i + 1; + } + } + buffer_.erase(0, cut_pos); +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_ctrl_response_buffer.h b/chromium/net/ftp/ftp_ctrl_response_buffer.h new file mode 100644 index 00000000000..fa5c03115b3 --- /dev/null +++ b/chromium/net/ftp/ftp_ctrl_response_buffer.h @@ -0,0 +1,101 @@ +// Copyright (c) 2011 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. + + +#ifndef NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_ +#define NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_ + +#include <queue> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "net/base/net_export.h" +#include "net/base/net_log.h" + +namespace net { + +struct NET_EXPORT_PRIVATE FtpCtrlResponse { + static const int kInvalidStatusCode; + + FtpCtrlResponse(); + ~FtpCtrlResponse(); + + int status_code; // Three-digit status code. + std::vector<std::string> lines; // Response lines, without CRLFs. +}; + +class NET_EXPORT_PRIVATE FtpCtrlResponseBuffer { + public: + FtpCtrlResponseBuffer(const BoundNetLog& net_log); + ~FtpCtrlResponseBuffer(); + + // Called when data is received from the control socket. Returns error code. + int ConsumeData(const char* data, int data_length); + + bool ResponseAvailable() const { + return !responses_.empty(); + } + + // Returns the next response. It is an error to call this function + // unless ResponseAvailable returns true. + FtpCtrlResponse PopResponse(); + + private: + struct ParsedLine { + ParsedLine(); + + // Indicates that this line begins with a valid 3-digit status code. + bool has_status_code; + + // Indicates that this line has the dash (-) after the code, which + // means a multiline response. + bool is_multiline; + + // Indicates that this line could be parsed as a complete and valid + // response line, without taking into account preceding lines (which + // may change its meaning into a continuation of the previous line). + bool is_complete; + + // Part of response parsed as status code. + int status_code; + + // Part of response parsed as status text. + std::string status_text; + + // Text before parsing, without terminating CRLF. + std::string raw_text; + }; + + static ParsedLine ParseLine(const std::string& line); + + void ExtractFullLinesFromBuffer(); + + // We keep not-yet-parsed data in a string buffer. + std::string buffer_; + + std::queue<ParsedLine> lines_; + + // True if we are in the middle of parsing a multi-line response. + bool multiline_; + + // When parsing a multiline response, we don't know beforehand if a line + // will have a continuation. So always store last line of multiline response + // so we can append the continuation to it. + std::string line_buf_; + + // Keep the response data while we add all lines to it. + FtpCtrlResponse response_buf_; + + // As we read full responses (possibly multiline), we add them to the queue. + std::queue<FtpCtrlResponse> responses_; + + BoundNetLog net_log_; + + DISALLOW_COPY_AND_ASSIGN(FtpCtrlResponseBuffer); +}; + +} // namespace net + +#endif // NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_ diff --git a/chromium/net/ftp/ftp_ctrl_response_buffer_unittest.cc b/chromium/net/ftp/ftp_ctrl_response_buffer_unittest.cc new file mode 100644 index 00000000000..f74b28724a9 --- /dev/null +++ b/chromium/net/ftp/ftp_ctrl_response_buffer_unittest.cc @@ -0,0 +1,175 @@ +// Copyright (c) 2009 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_ctrl_response_buffer.h" + +#include <string.h> + +#include "net/base/net_errors.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class FtpCtrlResponseBufferTest : public testing::Test { + public: + FtpCtrlResponseBufferTest() : buffer_(net::BoundNetLog()) { + } + + protected: + int PushDataToBuffer(const char* data) { + return buffer_.ConsumeData(data, strlen(data)); + } + + net::FtpCtrlResponseBuffer buffer_; +}; + +TEST_F(FtpCtrlResponseBufferTest, Basic) { + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("200 Status Text\r\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(200, response.status_code); + ASSERT_EQ(1U, response.lines.size()); + EXPECT_EQ("Status Text", response.lines[0]); +} + +TEST_F(FtpCtrlResponseBufferTest, Chunks) { + EXPECT_EQ(net::OK, PushDataToBuffer("20")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(net::OK, PushDataToBuffer("0 Status")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(net::OK, PushDataToBuffer(" Text")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(net::OK, PushDataToBuffer("\r")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(net::OK, PushDataToBuffer("\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(200, response.status_code); + ASSERT_EQ(1U, response.lines.size()); + EXPECT_EQ("Status Text", response.lines[0]); +} + +TEST_F(FtpCtrlResponseBufferTest, Continuation) { + EXPECT_EQ(net::OK, PushDataToBuffer("230-FirstLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230-SecondLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230 LastLine\r\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(230, response.status_code); + ASSERT_EQ(3U, response.lines.size()); + EXPECT_EQ("FirstLine", response.lines[0]); + EXPECT_EQ("SecondLine", response.lines[1]); + EXPECT_EQ("LastLine", response.lines[2]); +} + +TEST_F(FtpCtrlResponseBufferTest, MultilineContinuation) { + EXPECT_EQ(net::OK, PushDataToBuffer("230-FirstLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("Continued\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230-SecondLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("215 Continued\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230 LastLine\r\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(230, response.status_code); + ASSERT_EQ(3U, response.lines.size()); + EXPECT_EQ("FirstLineContinued", response.lines[0]); + EXPECT_EQ("SecondLine215 Continued", response.lines[1]); + EXPECT_EQ("LastLine", response.lines[2]); +} + +TEST_F(FtpCtrlResponseBufferTest, MultilineContinuationZeroLength) { + // For the corner case from bug 29322. + EXPECT_EQ(net::OK, PushDataToBuffer("230-\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("example.com\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230 LastLine\r\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(230, response.status_code); + ASSERT_EQ(2U, response.lines.size()); + EXPECT_EQ("example.com", response.lines[0]); + EXPECT_EQ("LastLine", response.lines[1]); +} + +TEST_F(FtpCtrlResponseBufferTest, SimilarContinuation) { + EXPECT_EQ(net::OK, PushDataToBuffer("230-FirstLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + // Notice the space at the start of the line. It should be recognized + // as a continuation, and not the last line. + EXPECT_EQ(net::OK, PushDataToBuffer(" 230 Continued\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230 TrueLastLine\r\n")); + EXPECT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(230, response.status_code); + ASSERT_EQ(2U, response.lines.size()); + EXPECT_EQ("FirstLine 230 Continued", response.lines[0]); + EXPECT_EQ("TrueLastLine", response.lines[1]); +} + +// The nesting of multi-line responses is not allowed. +TEST_F(FtpCtrlResponseBufferTest, NoNesting) { + EXPECT_EQ(net::OK, PushDataToBuffer("230-FirstLine\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("300-Continuation\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("300 Still continuation\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); + + EXPECT_EQ(net::OK, PushDataToBuffer("230 Real End\r\n")); + ASSERT_TRUE(buffer_.ResponseAvailable()); + + net::FtpCtrlResponse response = buffer_.PopResponse(); + EXPECT_FALSE(buffer_.ResponseAvailable()); + EXPECT_EQ(230, response.status_code); + ASSERT_EQ(2U, response.lines.size()); + EXPECT_EQ("FirstLine300-Continuation300 Still continuation", + response.lines[0]); + EXPECT_EQ("Real End", response.lines[1]); +} + +TEST_F(FtpCtrlResponseBufferTest, NonNumericResponse) { + EXPECT_EQ(net::ERR_INVALID_RESPONSE, PushDataToBuffer("Non-numeric\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); +} + +TEST_F(FtpCtrlResponseBufferTest, OutOfRangeResponse) { + EXPECT_EQ(net::ERR_INVALID_RESPONSE, PushDataToBuffer("777 OK?\r\n")); + EXPECT_FALSE(buffer_.ResponseAvailable()); +} + +} // namespace diff --git a/chromium/net/ftp/ftp_directory_listing_parser.cc b/chromium/net/ftp/ftp_directory_listing_parser.cc new file mode 100644 index 00000000000..03b77bb8205 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser.cc @@ -0,0 +1,145 @@ +// Copyright (c) 2011 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_directory_listing_parser.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/i18n/icu_encoding_detection.h" +#include "base/i18n/icu_string_conversions.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/ftp/ftp_directory_listing_parser_ls.h" +#include "net/ftp/ftp_directory_listing_parser_netware.h" +#include "net/ftp/ftp_directory_listing_parser_os2.h" +#include "net/ftp/ftp_directory_listing_parser_vms.h" +#include "net/ftp/ftp_directory_listing_parser_windows.h" +#include "net/ftp/ftp_server_type_histograms.h" + +namespace net { + +namespace { + +// Fills in |raw_name| for all |entries| using |encoding|. Returns network +// error code. +int FillInRawName(const std::string& encoding, + std::vector<FtpDirectoryListingEntry>* entries) { + for (size_t i = 0; i < entries->size(); i++) { + if (!base::UTF16ToCodepage(entries->at(i).name, encoding.c_str(), + base::OnStringConversionError::FAIL, + &entries->at(i).raw_name)) { + return ERR_ENCODING_CONVERSION_FAILED; + } + } + + return OK; +} + +// Parses |text| as an FTP directory listing. Fills in |entries| +// and |server_type| and returns network error code. +int ParseListing(const base::string16& text, + const base::string16& newline_separator, + const std::string& encoding, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries, + FtpServerType* server_type) { + std::vector<base::string16> lines; + base::SplitStringUsingSubstr(text, newline_separator, &lines); + + struct { + base::Callback<bool(void)> callback; + FtpServerType server_type; + } parsers[] = { + { + base::Bind(&ParseFtpDirectoryListingLs, lines, current_time, entries), + SERVER_LS + }, + { + base::Bind(&ParseFtpDirectoryListingWindows, lines, entries), + SERVER_WINDOWS + }, + { + base::Bind(&ParseFtpDirectoryListingVms, lines, entries), + SERVER_VMS + }, + { + base::Bind(&ParseFtpDirectoryListingNetware, + lines, current_time, entries), + SERVER_NETWARE + }, + { + base::Bind(&ParseFtpDirectoryListingOS2, lines, entries), + SERVER_OS2 + } + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(parsers); i++) { + entries->clear(); + if (parsers[i].callback.Run()) { + *server_type = parsers[i].server_type; + return FillInRawName(encoding, entries); + } + } + + entries->clear(); + return ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT; +} + +// Detects encoding of |text| and parses it as an FTP directory listing. +// Fills in |entries| and |server_type| and returns network error code. +int DecodeAndParse(const std::string& text, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries, + FtpServerType* server_type) { + const char* kNewlineSeparators[] = { "\n", "\r\n" }; + + std::vector<std::string> encodings; + if (!base::DetectAllEncodings(text, &encodings)) + return ERR_ENCODING_DETECTION_FAILED; + + // Use first encoding that can be used to decode the text. + for (size_t i = 0; i < encodings.size(); i++) { + base::string16 converted_text; + if (base::CodepageToUTF16(text, + encodings[i].c_str(), + base::OnStringConversionError::FAIL, + &converted_text)) { + for (size_t j = 0; j < arraysize(kNewlineSeparators); j++) { + int rv = ParseListing(converted_text, + ASCIIToUTF16(kNewlineSeparators[j]), + encodings[i], + current_time, + entries, + server_type); + if (rv == OK) + return rv; + } + } + } + + entries->clear(); + *server_type = SERVER_UNKNOWN; + return ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT; +} + +} // namespace + +FtpDirectoryListingEntry::FtpDirectoryListingEntry() + : type(UNKNOWN), + size(-1) { +} + +int ParseFtpDirectoryListing(const std::string& text, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries) { + FtpServerType server_type = SERVER_UNKNOWN; + int rv = DecodeAndParse(text, current_time, entries, &server_type); + UpdateFtpServerTypeHistograms(server_type); + return rv; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser.h b/chromium/net/ftp/ftp_directory_listing_parser.h new file mode 100644 index 00000000000..d2dd677bb09 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser.h @@ -0,0 +1,46 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "net/base/net_export.h" + +namespace net { + +struct FtpDirectoryListingEntry { + enum Type { + UNKNOWN, + FILE, + DIRECTORY, + SYMLINK, + }; + + FtpDirectoryListingEntry(); + + Type type; + base::string16 name; // Name (UTF-16-encoded). + std::string raw_name; // Name in original character encoding. + int64 size; // File size, in bytes. -1 if not applicable. + + // Last modified time, in local time zone. + base::Time last_modified; +}; + +// Parses an FTP directory listing |text|. On success fills in |entries|. +// Returns network error code. +NET_EXPORT int ParseFtpDirectoryListing( + const std::string& text, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_ls.cc b/chromium/net/ftp/ftp_directory_listing_parser_ls.cc new file mode 100644 index 00000000000..41e29b60a9b --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_ls.cc @@ -0,0 +1,233 @@ +// 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_directory_listing_parser_ls.h" + +#include <vector> + +#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/time/time.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "net/ftp/ftp_util.h" + +namespace { + +bool TwoColumnDateListingToTime(const base::string16& date, + const base::string16& time, + base::Time* result) { + base::Time::Exploded time_exploded = { 0 }; + + // Date should be in format YYYY-MM-DD. + std::vector<base::string16> date_parts; + base::SplitString(date, '-', &date_parts); + if (date_parts.size() != 3) + return false; + if (!base::StringToInt(date_parts[0], &time_exploded.year)) + return false; + if (!base::StringToInt(date_parts[1], &time_exploded.month)) + return false; + if (!base::StringToInt(date_parts[2], &time_exploded.day_of_month)) + return false; + + // Time should be in format HH:MM + if (time.length() != 5) + return false; + + std::vector<base::string16> time_parts; + base::SplitString(time, ':', &time_parts); + if (time_parts.size() != 2) + return false; + if (!base::StringToInt(time_parts[0], &time_exploded.hour)) + return false; + if (!base::StringToInt(time_parts[1], &time_exploded.minute)) + return false; + if (!time_exploded.HasValidValues()) + return false; + + // We don't know the time zone of the server, so just use local time. + *result = base::Time::FromLocalExploded(time_exploded); + return true; +} + +// Returns the column index of the end of the date listing and detected +// last modification time. +bool DetectColumnOffsetSizeAndModificationTime( + const std::vector<base::string16>& columns, + const base::Time& current_time, + size_t* offset, + base::string16* size, + base::Time* modification_time) { + // The column offset can be arbitrarily large if some fields + // like owner or group name contain spaces. Try offsets from left to right + // and use the first one that matches a date listing. + // + // Here is how a listing line should look like. A star ("*") indicates + // a required field: + // + // * 1. permission listing + // 2. number of links (optional) + // * 3. owner name (may contain spaces) + // 4. group name (optional, may contain spaces) + // * 5. size in bytes + // * 6. month + // * 7. day of month + // * 8. year or time <-- column_offset will be the index of this column + // 9. file name (optional, may contain spaces) + for (size_t i = 5U; i < columns.size(); i++) { + if (net::FtpUtil::LsDateListingToTime(columns[i - 2], + columns[i - 1], + columns[i], + current_time, + modification_time)) { + *size = columns[i - 3]; + *offset = i; + return true; + } + } + + // Some FTP listings have swapped the "month" and "day of month" columns + // (for example Russian listings). We try to recognize them only after making + // sure no column offset works above (this is a more strict way). + for (size_t i = 5U; i < columns.size(); i++) { + if (net::FtpUtil::LsDateListingToTime(columns[i - 1], + columns[i - 2], + columns[i], + current_time, + modification_time)) { + *size = columns[i - 3]; + *offset = i; + return true; + } + } + + // Some FTP listings use a different date format. + for (size_t i = 5U; i < columns.size(); i++) { + if (TwoColumnDateListingToTime(columns[i - 1], + columns[i], + modification_time)) { + *size = columns[i - 2]; + *offset = i; + return true; + } + } + + return false; +} + +} // namespace + +namespace net { + +bool ParseFtpDirectoryListingLs( + const std::vector<base::string16>& lines, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries) { + // True after we have received a "total n" listing header, where n is an + // integer. Only one such header is allowed per listing. + bool received_total_line = false; + + for (size_t i = 0; i < lines.size(); i++) { + if (lines[i].empty()) + continue; + + std::vector<base::string16> columns; + base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); + + // Some FTP servers put a "total n" line at the beginning of the listing + // (n is an integer). Allow such a line, but only once, and only if it's + // the first non-empty line. Do not match the word exactly, because it may + // be in different languages (at least English and German have been seen + // in the field). + if (columns.size() == 2 && !received_total_line) { + received_total_line = true; + + int64 total_number; + if (!base::StringToInt64(columns[1], &total_number)) + return false; + if (total_number < 0) + return false; + + continue; + } + + FtpDirectoryListingEntry entry; + + size_t column_offset; + base::string16 size; + if (!DetectColumnOffsetSizeAndModificationTime(columns, + current_time, + &column_offset, + &size, + &entry.last_modified)) { + // Some servers send a message in one of the first few lines. + // All those messages have in common is the string ".:", + // where "." means the current directory, and ":" separates it + // from the rest of the message, which may be empty. + if (lines[i].find(ASCIIToUTF16(".:")) != base::string16::npos) + continue; + + return false; + } + + // Do not check "validity" of the permission listing. It's quirky, + // and some servers send garbage here while other parts of the line are OK. + + if (!columns[0].empty() && columns[0][0] == 'l') { + entry.type = FtpDirectoryListingEntry::SYMLINK; + } else if (!columns[0].empty() && columns[0][0] == 'd') { + entry.type = FtpDirectoryListingEntry::DIRECTORY; + } else { + entry.type = FtpDirectoryListingEntry::FILE; + } + + if (!base::StringToInt64(size, &entry.size)) { + // Some FTP servers do not separate owning group name from file size, + // like "group1234". We still want to display the file name for that + // entry, but can't really get the size (What if the group is named + // "group1", and the size is in fact 234? We can't distinguish between + // that and "group" with size 1234). Use a dummy value for the size. + // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes. + entry.size = 0; + } + if (entry.size < 0) { + // Some FTP servers have bugs that cause them to display the file size + // as negative. They're most likely big files like DVD ISO images. + // We still want to display them, so just say the real file size + // is unknown. + entry.size = -1; + } + if (entry.type != FtpDirectoryListingEntry::FILE) + entry.size = -1; + + if (column_offset == columns.size() - 1) { + // If the end of the date listing is the last column, there is no file + // name. Some FTP servers send listing entries with empty names. + // It's not obvious how to display such an entry, so we ignore them. + // We don't want to make the parsing fail at this point though. + // Other entries can still be useful. + continue; + } + + entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], + column_offset + 1); + + if (entry.type == FtpDirectoryListingEntry::SYMLINK) { + base::string16::size_type pos = entry.name.rfind(ASCIIToUTF16(" -> ")); + + // We don't require the " -> " to be present. Some FTP servers don't send + // the symlink target, possibly for security reasons. + if (pos != base::string16::npos) + entry.name = entry.name.substr(0, pos); + } + + entries->push_back(entry); + } + + return true; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_ls.h b/chromium/net/ftp/ftp_directory_listing_parser_ls.h new file mode 100644 index 00000000000..9f7020f8ac9 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_ls.h @@ -0,0 +1,29 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace base { +class Time; +} + +namespace net { + +struct FtpDirectoryListingEntry; + +// Parses "ls -l" FTP directory listing. Returns true on success. +NET_EXPORT_PRIVATE bool ParseFtpDirectoryListingLs( + const std::vector<base::string16>& lines, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_ls_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_ls_unittest.cc new file mode 100644 index 00000000000..a7389443500 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_ls_unittest.cc @@ -0,0 +1,218 @@ +// Copyright (c) 2011 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_directory_listing_parser_unittest.h" + +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/ftp/ftp_directory_listing_parser_ls.h" + +namespace net { + +namespace { + +typedef FtpDirectoryListingParserTest FtpDirectoryListingParserLsTest; + +TEST_F(FtpDirectoryListingParserLsTest, Good) { + const struct SingleLineTestData good_cases[] = { + { "-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README", + FtpDirectoryListingEntry::FILE, "README", 528, + 2007, 11, 1, 0, 0 }, + { "drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 directory", + FtpDirectoryListingEntry::DIRECTORY, "directory", -1, + 1994, 5, 15, 18, 11 }, + { "lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub", + FtpDirectoryListingEntry::SYMLINK, "pub", -1, + 2008, 9, 18, 0, 0 }, + { "lrwxrwxrwx 1 0 0 3 Oct 12 13:37 mirror -> pub", + FtpDirectoryListingEntry::SYMLINK, "mirror", -1, + 1994, 10, 12, 13, 37 }, + { "drwxrwsr-x 4 501 501 4096 Feb 20 2007 pub", + FtpDirectoryListingEntry::DIRECTORY, "pub", -1, + 2007, 2, 20, 0, 0 }, + { "drwxr-xr-x 4 (?) (?) 4096 Apr 8 2007 jigdo", + FtpDirectoryListingEntry::DIRECTORY, "jigdo", -1, + 2007, 4, 8, 0, 0 }, + { "drwx-wx-wt 2 root wheel 512 Jul 1 02:15 incoming", + FtpDirectoryListingEntry::DIRECTORY, "incoming", -1, + 1994, 7, 1, 2, 15 }, + { "-rw-r--r-- 1 2 3 3447432 May 18 2009 Foo - Manual.pdf", + FtpDirectoryListingEntry::FILE, "Foo - Manual.pdf", 3447432, + 2009, 5, 18, 0, 0 }, + { "d-wx-wx-wt+ 4 ftp 989 512 Dec 8 15:54 incoming", + FtpDirectoryListingEntry::DIRECTORY, "incoming", -1, + 1993, 12, 8, 15, 54 }, + { "drwxrwxrwx 1 owner group 1024 Sep 13 0:30 audio", + FtpDirectoryListingEntry::DIRECTORY, "audio", -1, + 1994, 9, 13, 0, 30 }, + { "lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub", + FtpDirectoryListingEntry::SYMLINK, "pub", -1, + 2008, 9, 18, 0, 0 }, + { "-rw-r--r-- 1 ftp ftp -528 Nov 01 2007 README", + FtpDirectoryListingEntry::FILE, "README", -1, + 2007, 11, 1, 0, 0 }, + + // Tests for the wu-ftpd variant: + { "drwxr-xr-x 2 sys 512 Mar 27 2009 pub", + FtpDirectoryListingEntry::DIRECTORY, "pub", -1, + 2009, 3, 27, 0, 0 }, + { "lrwxrwxrwx 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub", + FtpDirectoryListingEntry::SYMLINK, "pub", -1, + 2008, 9, 18, 0, 0 }, + { "drwxr-xr-x (?) (?) 4096 Apr 8 2007 jigdo", + FtpDirectoryListingEntry::DIRECTORY, "jigdo", -1, + 2007, 4, 8, 0, 0 }, + { "-rw-r--r-- 2 3 3447432 May 18 2009 Foo - Manual.pdf", + FtpDirectoryListingEntry::FILE, "Foo - Manual.pdf", 3447432, + 2009, 5, 18, 0, 0 }, + + // Tests for "ls -l" style listings sent by an OS/2 server (FtpServer): + { "-r--r--r-- 1 ftp -A--- 13274 Mar 1 2006 UpTime.exe", + FtpDirectoryListingEntry::FILE, "UpTime.exe", 13274, + 2006, 3, 1, 0, 0 }, + { "dr--r--r-- 1 ftp ----- 0 Nov 17 17:08 kernels", + FtpDirectoryListingEntry::DIRECTORY, "kernels", -1, + 1993, 11, 17, 17, 8 }, + + // Tests for "ls -l" style listing sent by Xplain FTP Server. + { "drwxr-xr-x folder 0 Jul 17 2006 online", + FtpDirectoryListingEntry::DIRECTORY, "online", -1, + 2006, 7, 17, 0, 0 }, + + // Tests for "ls -l" style listing with owning group name + // not separated from file size (http://crbug.com/58963). + { "-rw-r--r-- 1 ftpadmin ftpadmin125435904 Apr 9 2008 .pureftpd-upload", + FtpDirectoryListingEntry::FILE, ".pureftpd-upload", 0, + 2008, 4, 9, 0, 0 }, + + // Tests for "ls -l" style listing with number of links + // not separated from permission listing (http://crbug.com/70394). + { "drwxr-xr-x1732 266 111 90112 Jun 21 2001 .rda_2", + FtpDirectoryListingEntry::DIRECTORY, ".rda_2", -1, + 2001, 6, 21, 0, 0 }, + + // Tests for "ls -l" style listing with group name containing spaces. + { "drwxrwxr-x 3 %%%% Domain Users 4096 Dec 9 2009 %%%%%", + FtpDirectoryListingEntry::DIRECTORY, "%%%%%", -1, + 2009, 12, 9, 0, 0 }, + + // Tests for "ls -l" style listing in Russian locale (note the swapped + // parts order: the day of month is the first, before month). + { "-rwxrwxr-x 1 ftp ftp 123 23 \xd0\xbc\xd0\xb0\xd0\xb9 2011 test", + FtpDirectoryListingEntry::FILE, "test", 123, + 2011, 5, 23, 0, 0 }, + { "drwxrwxr-x 1 ftp ftp 4096 19 \xd0\xbe\xd0\xba\xd1\x82 2011 dir", + FtpDirectoryListingEntry::DIRECTORY, "dir", -1, + 2011, 10, 19, 0, 0 }, + + // Plan9 sends entry type "a" for append-only files. + { "ar-xr-xr-x 2 none none 512 Apr 26 17:52 plan9", + FtpDirectoryListingEntry::FILE, "plan9", 512, + 1994, 4, 26, 17, 52 }, + + // Hylafax sends a shorter permission listing. + { "drwxrwx 2 10 4096 Jul 28 02:41 tmp", + FtpDirectoryListingEntry::DIRECTORY, "tmp", -1, + 1994, 7, 28, 2, 41 }, + + // Completely different date format (YYYY-MM-DD). + { "drwxrwxrwx 2 root root 4096 2012-02-07 00:31 notas_servico", + FtpDirectoryListingEntry::DIRECTORY, "notas_servico", -1, + 2012, 2, 7, 0, 31 }, + { "-rwxrwxrwx 2 root root 4096 2012-02-07 00:31 notas_servico", + FtpDirectoryListingEntry::FILE, "notas_servico", 4096, + 2012, 2, 7, 0, 31 }, + + // Weird permission bits. + { "drwx--l--- 2 0 10 512 Dec 22 1994 swetzel", + FtpDirectoryListingEntry::DIRECTORY, "swetzel", -1, + 1994, 12, 22, 0, 0 }, + + // Garbage in date (but still parseable). + { "lrw-rw-rw- 1 user group 542 " + "/t11/member/incomingFeb 8 2007 " + "Shortcut to incoming.lnk -> /t11/member/incoming", + FtpDirectoryListingEntry::SYMLINK, "Shortcut to incoming.lnk", -1, + 2007, 2, 8, 0, 0 }, + + // Garbage in permissions (with no effect on other bits). + // Also test multiple "columns" resulting from the garbage. + { "garbage 1 ftp ftp 528 Nov 01 2007 README", + FtpDirectoryListingEntry::FILE, "README", 528, + 2007, 11, 1, 0, 0 }, + { "gar bage 1 ftp ftp 528 Nov 01 2007 README", + FtpDirectoryListingEntry::FILE, "README", 528, + 2007, 11, 1, 0, 0 }, + { "g a r b a g e 1 ftp ftp 528 Nov 01 2007 README", + FtpDirectoryListingEntry::FILE, "README", 528, + 2007, 11, 1, 0, 0 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + good_cases[i].input)); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingLs( + GetSingleLineTestCase(good_cases[i].input), + GetMockCurrentTime(), + &entries)); + VerifySingleLineTestCase(good_cases[i], entries); + } +} + +TEST_F(FtpDirectoryListingParserLsTest, Ignored) { + const char* ignored_cases[] = { + "drwxr-xr-x 2 0 0 4096 Mar 18 2007 ", // http://crbug.com/60065 + + "ftpd: .: Permission denied", + "ftpd-BSD: .: Permission denied", + "ls: .: EDC5111I Permission denied.", + + // Tests important for security: verify that after we detect the column + // offset we don't try to access invalid memory on malformed input. + "drwxr-xr-x 3 ftp ftp 4096 May 15 18:11", + "drwxr-xr-x 3 ftp 4096 May 15 18:11", + "drwxr-xr-x folder 0 May 15 18:11", + }; + for (size_t i = 0; i < arraysize(ignored_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + ignored_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingLs( + GetSingleLineTestCase(ignored_cases[i]), + GetMockCurrentTime(), + &entries)); + EXPECT_EQ(0U, entries.size()); + } +} + +TEST_F(FtpDirectoryListingParserLsTest, Bad) { + const char* bad_cases[] = { + " foo", + "garbage", + "-rw-r--r-- ftp ftp", + "-rw-r--r-- ftp ftp 528 Foo 01 2007 README", + "-rw-r--r-- 1 ftp ftp", + "-rw-r--r-- 1 ftp ftp 528 Foo 01 2007 README", + "drwxrwxrwx 1 owner group 1024 Sep 13 0:3 audio", + + // Invalid month value (30). + "drwxrwxrwx 2 root root 4096 2012-30-07 00:31 notas_servico", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + bad_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingLs(GetSingleLineTestCase(bad_cases[i]), + GetMockCurrentTime(), + &entries)); + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_netware.cc b/chromium/net/ftp/ftp_directory_listing_parser_netware.cc new file mode 100644 index 00000000000..2306513fa60 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_netware.cc @@ -0,0 +1,94 @@ +// Copyright (c) 2011 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_directory_listing_parser_netware.h" + +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "net/ftp/ftp_util.h" + +namespace { + +bool LooksLikeNetwarePermissionsListing(const base::string16& text) { + if (text.length() != 10) + return false; + + if (text[0] != '[' || text[9] != ']') + return false; + return (text[1] == 'R' || text[1] == '-') && + (text[2] == 'W' || text[2] == '-') && + (text[3] == 'C' || text[3] == '-') && + (text[4] == 'E' || text[4] == '-') && + (text[5] == 'A' || text[5] == '-') && + (text[6] == 'F' || text[6] == '-') && + (text[7] == 'M' || text[7] == '-') && + (text[8] == 'S' || text[8] == '-'); +} + +} // namespace + +namespace net { + +bool ParseFtpDirectoryListingNetware( + const std::vector<base::string16>& lines, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries) { + if (!lines.empty() && !StartsWith(lines[0], ASCIIToUTF16("total "), true)) + return false; + + for (size_t i = 1U; i < lines.size(); i++) { + if (lines[i].empty()) + continue; + + std::vector<base::string16> columns; + base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); + + if (columns.size() < 8) + return false; + + FtpDirectoryListingEntry entry; + + if (columns[0].length() != 1) + return false; + if (columns[0][0] == 'd') { + entry.type = FtpDirectoryListingEntry::DIRECTORY; + } else if (columns[0][0] == '-') { + entry.type = FtpDirectoryListingEntry::FILE; + } else { + return false; + } + + // Note: on older Netware systems the permissions listing is in the same + // column as the entry type (just there is no space between them). We do not + // support the older format here for simplicity. + if (!LooksLikeNetwarePermissionsListing(columns[1])) + return false; + + if (!base::StringToInt64(columns[3], &entry.size)) + return false; + if (entry.size < 0) + return false; + if (entry.type != FtpDirectoryListingEntry::FILE) + entry.size = -1; + + // Netware uses the same date listing format as Unix "ls -l". + if (!FtpUtil::LsDateListingToTime(columns[4], columns[5], columns[6], + current_time, &entry.last_modified)) { + return false; + } + + entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], 7); + + entries->push_back(entry); + } + + return true; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_netware.h b/chromium/net/ftp/ftp_directory_listing_parser_netware.h new file mode 100644 index 00000000000..a70e21718d7 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_netware.h @@ -0,0 +1,29 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_NETWARE_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_NETWARE_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace base { +class Time; +} + +namespace net { + +struct FtpDirectoryListingEntry; + +// Parses Netware FTP directory listing. Returns true on success. +NET_EXPORT_PRIVATE bool ParseFtpDirectoryListingNetware( + const std::vector<base::string16>& lines, + const base::Time& current_time, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_NETWARE_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_netware_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_netware_unittest.cc new file mode 100644 index 00000000000..4863713ca86 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_netware_unittest.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2011 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_directory_listing_parser_unittest.h" + +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "net/ftp/ftp_directory_listing_parser_netware.h" + +namespace net { + +namespace { + +typedef FtpDirectoryListingParserTest FtpDirectoryListingParserNetwareTest; + +TEST_F(FtpDirectoryListingParserNetwareTest, Good) { + const struct SingleLineTestData good_cases[] = { + { "d [RWCEAFMS] ftpadmin 512 Jan 29 2004 pub", + FtpDirectoryListingEntry::DIRECTORY, "pub", -1, + 2004, 1, 29, 0, 0 }, + { "- [RW------] ftpadmin 123 Nov 11 18:25 afile", + FtpDirectoryListingEntry::FILE, "afile", 123, + 1994, 11, 11, 18, 25 }, + { "d [RWCEAFMS] daniel 512 May 17 2010 NVP anyagok", + FtpDirectoryListingEntry::DIRECTORY, "NVP anyagok", -1, + 2010, 5, 17, 0, 0 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + good_cases[i].input)); + + std::vector<base::string16> lines( + GetSingleLineTestCase(good_cases[i].input)); + + // The parser requires a "total n" line before accepting regular input. + lines.insert(lines.begin(), ASCIIToUTF16("total 1")); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingNetware(lines, + GetMockCurrentTime(), + &entries)); + VerifySingleLineTestCase(good_cases[i], entries); + } +} + +TEST_F(FtpDirectoryListingParserNetwareTest, Bad) { + const char* bad_cases[] = { + " foo", + "garbage", + "d [] ftpadmin 512 Jan 29 2004 pub", + "d [XGARBAGE] ftpadmin 512 Jan 29 2004 pub", + "d [RWCEAFMS] 512 Jan 29 2004 pub", + "d [RWCEAFMS] ftpadmin -1 Jan 29 2004 pub", + "l [RW------] ftpadmin 512 Jan 29 2004 pub", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + bad_cases[i])); + + std::vector<base::string16> lines(GetSingleLineTestCase(bad_cases[i])); + + // The parser requires a "total n" line before accepting regular input. + lines.insert(lines.begin(), ASCIIToUTF16("total 1")); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingNetware(lines, + GetMockCurrentTime(), + &entries)); + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_os2.cc b/chromium/net/ftp/ftp_directory_listing_parser_os2.cc new file mode 100644 index 00000000000..9a0cce53c81 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_os2.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2011 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_directory_listing_parser_os2.h" + +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/time/time.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "net/ftp/ftp_util.h" + +namespace net { + +bool ParseFtpDirectoryListingOS2( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries) { + for (size_t i = 0; i < lines.size(); i++) { + if (lines[i].empty()) + continue; + + std::vector<base::string16> columns; + base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); + + // Every line of the listing consists of the following: + // + // 1. size in bytes (0 for directories) + // 2. type (A for files, DIR for directories) + // 3. date + // 4. time + // 5. filename (may be empty or contain spaces) + // + // For now, make sure we have 1-4, and handle 5 later. + if (columns.size() < 4) + return false; + + FtpDirectoryListingEntry entry; + if (!base::StringToInt64(columns[0], &entry.size)) + return false; + if (EqualsASCII(columns[1], "DIR")) { + if (entry.size != 0) + return false; + entry.type = FtpDirectoryListingEntry::DIRECTORY; + entry.size = -1; + } else if (EqualsASCII(columns[1], "A")) { + entry.type = FtpDirectoryListingEntry::FILE; + if (entry.size < 0) + return false; + } else { + return false; + } + + if (!FtpUtil::WindowsDateListingToTime(columns[2], + columns[3], + &entry.last_modified)) { + return false; + } + + entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], 4); + if (entry.name.empty()) { + // Some FTP servers send listing entries with empty names. + // It's not obvious how to display such an entry, so ignore them. + // We don't want to make the parsing fail at this point though. + // Other entries can still be useful. + continue; + } + + entries->push_back(entry); + } + + return true; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_os2.h b/chromium/net/ftp/ftp_directory_listing_parser_os2.h new file mode 100644 index 00000000000..9cd471fb0b1 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_os2.h @@ -0,0 +1,24 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_OS2_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_OS2_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace net { + +struct FtpDirectoryListingEntry; + +// Parses OS/2 FTP directory listing. Returns true on success. +NET_EXPORT_PRIVATE bool ParseFtpDirectoryListingOS2( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_OS2_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_os2_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_os2_unittest.cc new file mode 100644 index 00000000000..7302f4c9a9d --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_os2_unittest.cc @@ -0,0 +1,116 @@ +// Copyright (c) 2011 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_directory_listing_parser_unittest.h" + +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/ftp/ftp_directory_listing_parser_os2.h" + +namespace net { + +namespace { + +typedef FtpDirectoryListingParserTest FtpDirectoryListingParserOS2Test; + +TEST_F(FtpDirectoryListingParserOS2Test, Good) { + const struct SingleLineTestData good_cases[] = { + { "0 DIR 11-02-09 17:32 NT", + FtpDirectoryListingEntry::DIRECTORY, "NT", -1, + 2009, 11, 2, 17, 32 }, + { "458 A 01-06-09 14:42 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 2009, 1, 6, 14, 42 }, + { "1 A 01-06-09 02:42 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 1, + 2009, 1, 6, 2, 42 }, + { "458 A 01-06-01 02:42 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 2001, 1, 6, 2, 42 }, + { "458 A 01-06-00 02:42 Corner1.txt", + FtpDirectoryListingEntry::FILE, "Corner1.txt", 458, + 2000, 1, 6, 2, 42 }, + { "458 A 01-06-99 02:42 Corner2.txt", + FtpDirectoryListingEntry::FILE, "Corner2.txt", 458, + 1999, 1, 6, 2, 42 }, + { "458 A 01-06-80 02:42 Corner3.txt", + FtpDirectoryListingEntry::FILE, "Corner3.txt", 458, + 1980, 1, 6, 2, 42 }, + { "458 A 01-06-1979 02:42 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 1979, 1, 6, 2, 42 }, + { "0 DIR 11-02-09 17:32 My Directory", + FtpDirectoryListingEntry::DIRECTORY, "My Directory", -1, + 2009, 11, 2, 17, 32 }, + { "0 DIR 12-25-10 00:00 Christmas Midnight", + FtpDirectoryListingEntry::DIRECTORY, "Christmas Midnight", -1, + 2010, 12, 25, 0, 0 }, + { "0 DIR 12-25-10 12:00 Christmas Midday", + FtpDirectoryListingEntry::DIRECTORY, "Christmas Midday", -1, + 2010, 12, 25, 12, 0 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + good_cases[i].input)); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingOS2( + GetSingleLineTestCase(good_cases[i].input), + &entries)); + VerifySingleLineTestCase(good_cases[i], entries); + } +} + +TEST_F(FtpDirectoryListingParserOS2Test, Ignored) { + const char* ignored_cases[] = { + "1234 A 12-07-10 12:05", + "0 DIR 11-02-09 05:32", + }; + for (size_t i = 0; i < arraysize(ignored_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + ignored_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingOS2( + GetSingleLineTestCase(ignored_cases[i]), + &entries)); + EXPECT_EQ(0U, entries.size()); + } +} + +TEST_F(FtpDirectoryListingParserOS2Test, Bad) { + const char* bad_cases[] = { + "garbage", + "0 GARBAGE 11-02-09 05:32", + "0 GARBAGE 11-02-09 05:32 NT", + "0 DIR 11-FEB-09 05:32", + "0 DIR 11-02 05:32", + "-1 A 11-02-09 05:32", + "0 DIR 11-FEB-09 05:32", + "0 DIR 11-02 05:32 NT", + "-1 A 11-02-09 05:32 NT", + "0 A 99-25-10 12:00", + "0 A 12-99-10 12:00", + "0 A 12-25-10 99:00", + "0 A 12-25-10 12:99", + "0 A 99-25-10 12:00 months out of range", + "0 A 12-99-10 12:00 days out of range", + "0 A 12-25-10 99:00 hours out of range", + "0 A 12-25-10 12:99 minutes out of range", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + bad_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingOS2( + GetSingleLineTestCase(bad_cases[i]), + &entries)); + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_unittest.cc new file mode 100644 index 00000000000..4eaf1dd97e4 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_unittest.cc @@ -0,0 +1,166 @@ +// 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_directory_listing_parser.h" + +#include "base/file_util.h" +#include "base/format_macros.h" +#include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/net_errors.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +namespace { + +class FtpDirectoryListingParserTest + : public testing::TestWithParam<const char*> { +}; + +TEST_P(FtpDirectoryListingParserTest, Parse) { + base::FilePath test_dir; + PathService::Get(base::DIR_SOURCE_ROOT, &test_dir); + test_dir = test_dir.AppendASCII("net"); + test_dir = test_dir.AppendASCII("data"); + test_dir = test_dir.AppendASCII("ftp"); + + base::Time::Exploded mock_current_time_exploded = { 0 }; + mock_current_time_exploded.year = 1994; + mock_current_time_exploded.month = 11; + mock_current_time_exploded.day_of_month = 15; + mock_current_time_exploded.hour = 12; + mock_current_time_exploded.minute = 45; + base::Time mock_current_time( + base::Time::FromLocalExploded(mock_current_time_exploded)); + + SCOPED_TRACE(base::StringPrintf("Test case: %s", GetParam())); + + std::string test_listing; + EXPECT_TRUE(file_util::ReadFileToString(test_dir.AppendASCII(GetParam()), + &test_listing)); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_EQ(OK, ParseFtpDirectoryListing(test_listing, + mock_current_time, + &entries)); + + std::string expected_listing; + ASSERT_TRUE(file_util::ReadFileToString( + test_dir.AppendASCII(std::string(GetParam()) + ".expected"), + &expected_listing)); + + std::vector<std::string> lines; + base::SplitStringUsingSubstr(expected_listing, "\r\n", &lines); + + // Special case for empty listings. + if (lines.size() == 1 && lines[0].empty()) + lines.clear(); + + ASSERT_EQ(9 * entries.size(), lines.size()); + + for (size_t i = 0; i < lines.size() / 9; i++) { + std::string type(lines[9 * i]); + std::string name(lines[9 * i + 1]); + int64 size; + base::StringToInt64(lines[9 * i + 2], &size); + + SCOPED_TRACE(base::StringPrintf("Filename: %s", name.c_str())); + + int year, month, day_of_month, hour, minute; + base::StringToInt(lines[9 * i + 3], &year); + base::StringToInt(lines[9 * i + 4], &month); + base::StringToInt(lines[9 * i + 5], &day_of_month); + base::StringToInt(lines[9 * i + 6], &hour); + base::StringToInt(lines[9 * i + 7], &minute); + + const FtpDirectoryListingEntry& entry = entries[i]; + + if (type == "d") { + EXPECT_EQ(FtpDirectoryListingEntry::DIRECTORY, entry.type); + } else if (type == "-") { + EXPECT_EQ(FtpDirectoryListingEntry::FILE, entry.type); + } else if (type == "l") { + EXPECT_EQ(FtpDirectoryListingEntry::SYMLINK, entry.type); + } else { + ADD_FAILURE() << "invalid gold test data: " << type; + } + + EXPECT_EQ(UTF8ToUTF16(name), entry.name); + EXPECT_EQ(size, entry.size); + + base::Time::Exploded time_exploded; + entry.last_modified.LocalExplode(&time_exploded); + EXPECT_EQ(year, time_exploded.year); + EXPECT_EQ(month, time_exploded.month); + EXPECT_EQ(day_of_month, time_exploded.day_of_month); + EXPECT_EQ(hour, time_exploded.hour); + EXPECT_EQ(minute, time_exploded.minute); + } +} + +const char* kTestFiles[] = { + "dir-listing-ls-1", + "dir-listing-ls-1-utf8", + "dir-listing-ls-2", + "dir-listing-ls-3", + "dir-listing-ls-4", + "dir-listing-ls-5", + "dir-listing-ls-6", + "dir-listing-ls-7", + "dir-listing-ls-8", + "dir-listing-ls-9", + "dir-listing-ls-10", + "dir-listing-ls-11", + "dir-listing-ls-12", + "dir-listing-ls-13", + "dir-listing-ls-14", + "dir-listing-ls-15", + "dir-listing-ls-16", + "dir-listing-ls-17", + "dir-listing-ls-18", + "dir-listing-ls-19", + "dir-listing-ls-20", // TODO(phajdan.jr): should use windows-1251 encoding. + "dir-listing-ls-21", // TODO(phajdan.jr): should use windows-1251 encoding. + "dir-listing-ls-22", // TODO(phajdan.jr): should use windows-1251 encoding. + "dir-listing-ls-23", + "dir-listing-ls-24", + + // Tests for Russian listings. The only difference between those + // files is character encoding: + "dir-listing-ls-25", // UTF-8 + "dir-listing-ls-26", // KOI8-R + "dir-listing-ls-27", // windows-1251 + + "dir-listing-ls-28", // Hylafax FTP server + "dir-listing-ls-29", + "dir-listing-ls-30", + "dir-listing-ls-31", + + "dir-listing-netware-1", + "dir-listing-netware-2", + "dir-listing-netware-3", // Spaces in file names. + "dir-listing-os2-1", + "dir-listing-vms-1", + "dir-listing-vms-2", + "dir-listing-vms-3", + "dir-listing-vms-4", + "dir-listing-vms-5", + "dir-listing-vms-6", + "dir-listing-vms-7", + "dir-listing-vms-8", + "dir-listing-windows-1", + "dir-listing-windows-2", +}; + +INSTANTIATE_TEST_CASE_P(, FtpDirectoryListingParserTest, + testing::ValuesIn(kTestFiles)); + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_unittest.h b/chromium/net/ftp/ftp_directory_listing_parser_unittest.h new file mode 100644 index 00000000000..9ac42d867b5 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_unittest.h @@ -0,0 +1,76 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_ + +#include <vector> + +#include "base/strings/utf_string_conversions.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +class FtpDirectoryListingParserTest : public testing::Test { + public: + struct SingleLineTestData { + const char* input; + FtpDirectoryListingEntry::Type type; + const char* filename; + int64 size; + int year; + int month; + int day_of_month; + int hour; + int minute; + }; + + protected: + FtpDirectoryListingParserTest() {} + + std::vector<base::string16> GetSingleLineTestCase(const std::string& text) { + std::vector<base::string16> lines; + lines.push_back(UTF8ToUTF16(text)); + return lines; + } + + void VerifySingleLineTestCase( + const SingleLineTestData& test_case, + const std::vector<FtpDirectoryListingEntry>& entries) { + ASSERT_FALSE(entries.empty()); + + FtpDirectoryListingEntry entry = entries[0]; + EXPECT_EQ(test_case.type, entry.type); + EXPECT_EQ(UTF8ToUTF16(test_case.filename), entry.name); + EXPECT_EQ(test_case.size, entry.size); + + base::Time::Exploded time_exploded; + entry.last_modified.LocalExplode(&time_exploded); + + // Only test members displayed on the directory listing. + EXPECT_EQ(test_case.year, time_exploded.year); + EXPECT_EQ(test_case.month, time_exploded.month); + EXPECT_EQ(test_case.day_of_month, time_exploded.day_of_month); + EXPECT_EQ(test_case.hour, time_exploded.hour); + EXPECT_EQ(test_case.minute, time_exploded.minute); + + EXPECT_EQ(1U, entries.size()); + } + + base::Time GetMockCurrentTime() { + base::Time::Exploded mock_current_time_exploded = { 0 }; + mock_current_time_exploded.year = 1994; + mock_current_time_exploded.month = 11; + mock_current_time_exploded.day_of_month = 15; + mock_current_time_exploded.hour = 12; + mock_current_time_exploded.minute = 45; + return base::Time::FromLocalExploded(mock_current_time_exploded); + } +}; + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_ + diff --git a/chromium/net/ftp/ftp_directory_listing_parser_vms.cc b/chromium/net/ftp/ftp_directory_listing_parser_vms.cc new file mode 100644 index 00000000000..4b44d73b5b5 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_vms.cc @@ -0,0 +1,293 @@ +// 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_directory_listing_parser_vms.h" + +#include <vector> + +#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/time/time.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "net/ftp/ftp_util.h" + +namespace net { + +namespace { + +// Converts the filename component in listing to the filename we can display. +// Returns true on success. +bool ParseVmsFilename(const base::string16& raw_filename, + base::string16* parsed_filename, + FtpDirectoryListingEntry::Type* type) { + // On VMS, the files and directories are versioned. The version number is + // separated from the file name by a semicolon. Example: ANNOUNCE.TXT;2. + std::vector<base::string16> listing_parts; + base::SplitString(raw_filename, ';', &listing_parts); + if (listing_parts.size() != 2) + return false; + int version_number; + if (!base::StringToInt(listing_parts[1], &version_number)) + return false; + if (version_number < 0) + return false; + + // Even directories have extensions in the listings. Don't display extensions + // for directories; it's awkward for non-VMS users. Also, VMS is + // case-insensitive, but generally uses uppercase characters. This may look + // awkward, so we convert them to lower case. + std::vector<base::string16> filename_parts; + base::SplitString(listing_parts[0], '.', &filename_parts); + if (filename_parts.size() != 2) + return false; + if (EqualsASCII(filename_parts[1], "DIR")) { + *parsed_filename = StringToLowerASCII(filename_parts[0]); + *type = FtpDirectoryListingEntry::DIRECTORY; + } else { + *parsed_filename = StringToLowerASCII(listing_parts[0]); + *type = FtpDirectoryListingEntry::FILE; + } + return true; +} + +bool ParseVmsFilesize(const base::string16& input, int64* size) { + if (ContainsOnlyChars(input, ASCIIToUTF16("*"))) { + // Response consisting of asterisks means unknown size. + *size = -1; + return true; + } + + // VMS's directory listing gives us file size in blocks. We assume that + // the block size is 512 bytes. It doesn't give accurate file size, but is the + // best information we have. + const int kBlockSize = 512; + + if (base::StringToInt64(input, size)) { + if (*size < 0) + return false; + *size *= kBlockSize; + return true; + } + + std::vector<base::string16> parts; + base::SplitString(input, '/', &parts); + if (parts.size() != 2) + return false; + + int64 blocks_used, blocks_allocated; + if (!base::StringToInt64(parts[0], &blocks_used)) + return false; + if (!base::StringToInt64(parts[1], &blocks_allocated)) + return false; + if (blocks_used > blocks_allocated) + return false; + if (blocks_used < 0 || blocks_allocated < 0) + return false; + + *size = blocks_used * kBlockSize; + return true; +} + +bool LooksLikeVmsFileProtectionListingPart(const base::string16& input) { + if (input.length() > 4) + return false; + + // On VMS there are four different permission bits: Read, Write, Execute, + // and Delete. They appear in that order in the permission listing. + std::string pattern("RWED"); + base::string16 match(input); + while (!match.empty() && !pattern.empty()) { + if (match[0] == pattern[0]) + match = match.substr(1); + pattern = pattern.substr(1); + } + return match.empty(); +} + +bool LooksLikeVmsFileProtectionListing(const base::string16& input) { + if (input.length() < 2) + return false; + if (input[0] != '(' || input[input.length() - 1] != ')') + return false; + + // We expect four parts of the file protection listing: for System, Owner, + // Group, and World. + std::vector<base::string16> parts; + base::SplitString(input.substr(1, input.length() - 2), ',', &parts); + if (parts.size() != 4) + return false; + + return LooksLikeVmsFileProtectionListingPart(parts[0]) && + LooksLikeVmsFileProtectionListingPart(parts[1]) && + LooksLikeVmsFileProtectionListingPart(parts[2]) && + LooksLikeVmsFileProtectionListingPart(parts[3]); +} + +bool LooksLikeVmsUserIdentificationCode(const base::string16& input) { + if (input.length() < 2) + return false; + return input[0] == '[' && input[input.length() - 1] == ']'; +} + +bool LooksLikeVMSError(const base::string16& text) { + static const char* kPermissionDeniedMessages[] = { + "%RMS-E-FNF", // File not found. + "%RMS-E-PRV", // Access denied. + "%SYSTEM-F-NOPRIV", + "privilege", + }; + + for (size_t i = 0; i < arraysize(kPermissionDeniedMessages); i++) { + if (text.find(ASCIIToUTF16(kPermissionDeniedMessages[i])) != + base::string16::npos) + return true; + } + + return false; +} + +bool VmsDateListingToTime(const std::vector<base::string16>& columns, + base::Time* time) { + DCHECK_EQ(4U, columns.size()); + + base::Time::Exploded time_exploded = { 0 }; + + // Date should be in format DD-MMM-YYYY. + std::vector<base::string16> date_parts; + base::SplitString(columns[2], '-', &date_parts); + if (date_parts.size() != 3) + return false; + if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month)) + return false; + if (!FtpUtil::AbbreviatedMonthToNumber(date_parts[1], + &time_exploded.month)) + return false; + if (!base::StringToInt(date_parts[2], &time_exploded.year)) + return false; + + // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the + // last type first. Do not parse the seconds, they will be ignored anyway. + base::string16 time_column(columns[3]); + if (time_column.length() == 11 && time_column[8] == '.') + time_column = time_column.substr(0, 8); + if (time_column.length() == 8 && time_column[5] == ':') + time_column = time_column.substr(0, 5); + if (time_column.length() != 5) + return false; + std::vector<base::string16> time_parts; + base::SplitString(time_column, ':', &time_parts); + if (time_parts.size() != 2) + return false; + if (!base::StringToInt(time_parts[0], &time_exploded.hour)) + return false; + if (!base::StringToInt(time_parts[1], &time_exploded.minute)) + return false; + + // We don't know the time zone of the server, so just use local time. + *time = base::Time::FromLocalExploded(time_exploded); + return true; +} + +} // namespace + +bool ParseFtpDirectoryListingVms( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries) { + // The first non-empty line is the listing header. It often + // starts with "Directory ", but not always. We set a flag after + // seing the header. + bool seen_header = false; + + // Sometimes the listing doesn't end with a "Total" line, but + // it's only okay when it contains some errors (it's needed + // to distinguish it from "ls -l" format). + bool seen_error = false; + + for (size_t i = 0; i < lines.size(); i++) { + if (lines[i].empty()) + continue; + + if (StartsWith(lines[i], ASCIIToUTF16("Total of "), true)) { + // After the "total" line, all following lines must be empty. + for (size_t j = i + 1; j < lines.size(); j++) + if (!lines[j].empty()) + return false; + + return true; + } + + if (!seen_header) { + seen_header = true; + continue; + } + + if (LooksLikeVMSError(lines[i])) { + seen_error = true; + continue; + } + + std::vector<base::string16> columns; + base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); + + if (columns.size() == 1) { + // There can be no continuation if the current line is the last one. + if (i == lines.size() - 1) + return false; + + // Skip the next line. + i++; + + // This refers to the continuation line. + if (LooksLikeVMSError(lines[i])) { + seen_error = true; + continue; + } + + // Join the current and next line and split them into columns. + base::SplitString( + CollapseWhitespace(lines[i - 1] + ASCIIToUTF16(" ") + lines[i], + false), + ' ', + &columns); + } + + FtpDirectoryListingEntry entry; + if (!ParseVmsFilename(columns[0], &entry.name, &entry.type)) + return false; + + // There are different variants of a VMS listing. Some display + // the protection listing and user identification code, some do not. + if (columns.size() == 6) { + if (!LooksLikeVmsFileProtectionListing(columns[5])) + return false; + if (!LooksLikeVmsUserIdentificationCode(columns[4])) + return false; + + // Drop the unneeded data, so that the following code can always expect + // just four columns. + columns.resize(4); + } + + if (columns.size() != 4) + return false; + + if (!ParseVmsFilesize(columns[1], &entry.size)) + return false; + if (entry.type != FtpDirectoryListingEntry::FILE) + entry.size = -1; + if (!VmsDateListingToTime(columns, &entry.last_modified)) + return false; + + entries->push_back(entry); + } + + // The only place where we return true is after receiving the "Total" line, + // that should be present in every VMS listing. Alternatively, if the listing + // contains error messages, it's OK not to have the "Total" line. + return seen_error; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_vms.h b/chromium/net/ftp/ftp_directory_listing_parser_vms.h new file mode 100644 index 00000000000..98e6a9275bd --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_vms.h @@ -0,0 +1,24 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace net { + +struct FtpDirectoryListingEntry; + +// Parses VMS FTP directory listing. Returns true on success. +NET_EXPORT_PRIVATE bool ParseFtpDirectoryListingVms( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_vms_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_vms_unittest.cc new file mode 100644 index 00000000000..3690f7e18c6 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_vms_unittest.cc @@ -0,0 +1,168 @@ +// Copyright (c) 2011 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_directory_listing_parser_unittest.h" + +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "net/ftp/ftp_directory_listing_parser_vms.h" + +namespace net { + +namespace { + +typedef FtpDirectoryListingParserTest FtpDirectoryListingParserVmsTest; + +TEST_F(FtpDirectoryListingParserVmsTest, Good) { + const struct SingleLineTestData good_cases[] = { + { "README.TXT;4 2 18-APR-2000 10:40:39.90", + FtpDirectoryListingEntry::FILE, "readme.txt", 1024, + 2000, 4, 18, 10, 40 }, + { ".WELCOME;1 2 13-FEB-2002 23:32:40.47", + FtpDirectoryListingEntry::FILE, ".welcome", 1024, + 2002, 2, 13, 23, 32 }, + { "FILE.;1 2 13-FEB-2002 23:32:40.47", + FtpDirectoryListingEntry::FILE, "file.", 1024, + 2002, 2, 13, 23, 32 }, + { "EXAMPLE.TXT;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,)", + FtpDirectoryListingEntry::FILE, "example.txt", 512, + 2009, 11, 4, 6, 2 }, + { "ANNOUNCE.TXT;2 1/16 12-MAR-2005 08:44:57 [SYSTEM] (RWED,RWED,RE,RE)", + FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "TEST.DIR;1 1 4-MAR-1999 22:14:34 [UCX$NOBO,ANONYMOUS] (RWE,RWE,RWE,RWE)", + FtpDirectoryListingEntry::DIRECTORY, "test", -1, + 1999, 3, 4, 22, 14 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (,,,)", + FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (R,RW,RWD,RE)", + FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (ED,RED,WD,WED)", + FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "VMS721.ISO;2 ****** 6-MAY-2008 09:29 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)", + FtpDirectoryListingEntry::FILE, "vms721.iso", -1, + 2008, 5, 6, 9, 29 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + good_cases[i].input)); + + std::vector<base::string16> lines( + GetSingleLineTestCase(good_cases[i].input)); + + // The parser requires a directory header before accepting regular input. + lines.insert(lines.begin(), + ASCIIToUTF16("Directory ANONYMOUS_ROOT:[000000]")); + + // A valid listing must also have a "Total" line at the end. + lines.insert(lines.end(), + ASCIIToUTF16("Total of 1 file, 2 blocks.")); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingVms(lines, + &entries)); + VerifySingleLineTestCase(good_cases[i], entries); + } +} + +TEST_F(FtpDirectoryListingParserVmsTest, Bad) { + const char* bad_cases[] = { + "garbage", + + // Missing file version number. + "README.TXT 2 18-APR-2000 10:40:39", + + // Missing extension. + "README;1 2 18-APR-2000 10:40:39", + + // Malformed file size. + "README.TXT;1 garbage 18-APR-2000 10:40:39", + "README.TXT;1 -2 18-APR-2000 10:40:39", + + // Malformed date. + "README.TXT;1 2 APR-2000 10:40:39", + "README.TXT;1 2 -18-APR-2000 10:40:39", + "README.TXT;1 2 18-APR 10:40:39", + "README.TXT;1 2 18-APR-2000 10", + "README.TXT;1 2 18-APR-2000 10:40.25", + "README.TXT;1 2 18-APR-2000 10:40.25.25", + + // Malformed security information. + "X.TXT;2 1 12-MAR-2005 08:44:57 (RWED,RWED,RE,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [SYSTEM]", + "X.TXT;2 1 12-MAR-2005 08:44:57 (SYSTEM) (RWED,RWED,RE,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [SYSTEM] [RWED,RWED,RE,RE]", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWED,RE,RE,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWEDRWED,RE,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,DEWR,RE,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWED,Q,RE)", + "X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RRWWEEDD,RE,RE)", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, bad_cases[i])); + + std::vector<base::string16> lines(GetSingleLineTestCase(bad_cases[i])); + + // The parser requires a directory header before accepting regular input. + lines.insert(lines.begin(), + ASCIIToUTF16("Directory ANONYMOUS_ROOT:[000000]")); + + // A valid listing must also have a "Total" line at the end. + lines.insert(lines.end(), + ASCIIToUTF16("Total of 1 file, 2 blocks.")); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, + &entries)); + } +} + +TEST_F(FtpDirectoryListingParserVmsTest, BadDataAfterFooter) { + const char* bad_cases[] = { + "garbage", + "Total of 1 file, 2 blocks.", + "Directory ANYNYMOUS_ROOT:[000000]", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, bad_cases[i])); + + std::vector<base::string16> lines( + GetSingleLineTestCase("README.TXT;4 2 18-APR-2000 10:40:39.90")); + + // The parser requires a directory header before accepting regular input. + lines.insert(lines.begin(), + ASCIIToUTF16("Directory ANONYMOUS_ROOT:[000000]")); + + // A valid listing must also have a "Total" line at the end. + lines.insert(lines.end(), + ASCIIToUTF16("Total of 1 file, 2 blocks.")); + + { + // Make sure the listing is valid before we add data after footer. + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingVms(lines, + &entries)); + } + + { + // Insert a line at the end of the listing that should make it invalid. + lines.insert(lines.end(), + ASCIIToUTF16(bad_cases[i])); + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, + &entries)); + } + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_windows.cc b/chromium/net/ftp/ftp_directory_listing_parser_windows.cc new file mode 100644 index 00000000000..5ec4a523434 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_windows.cc @@ -0,0 +1,72 @@ +// Copyright (c) 2011 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_directory_listing_parser_windows.h" + +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/time/time.h" +#include "net/ftp/ftp_directory_listing_parser.h" +#include "net/ftp/ftp_util.h" + +namespace net { + +bool ParseFtpDirectoryListingWindows( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries) { + for (size_t i = 0; i < lines.size(); i++) { + if (lines[i].empty()) + continue; + + std::vector<base::string16> columns; + base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); + + // Every line of the listing consists of the following: + // + // 1. date + // 2. time + // 3. size in bytes (or "<DIR>" for directories) + // 4. filename (may be empty or contain spaces) + // + // For now, make sure we have 1-3, and handle 4 later. + if (columns.size() < 3) + return false; + + FtpDirectoryListingEntry entry; + if (EqualsASCII(columns[2], "<DIR>")) { + entry.type = FtpDirectoryListingEntry::DIRECTORY; + entry.size = -1; + } else { + entry.type = FtpDirectoryListingEntry::FILE; + if (!base::StringToInt64(columns[2], &entry.size)) + return false; + if (entry.size < 0) + return false; + } + + if (!FtpUtil::WindowsDateListingToTime(columns[0], + columns[1], + &entry.last_modified)) { + return false; + } + + entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], 3); + if (entry.name.empty()) { + // Some FTP servers send listing entries with empty names. + // It's not obvious how to display such an entry, so ignore them. + // We don't want to make the parsing fail at this point though. + // Other entries can still be useful. + continue; + } + + entries->push_back(entry); + } + + return true; +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_directory_listing_parser_windows.h b/chromium/net/ftp/ftp_directory_listing_parser_windows.h new file mode 100644 index 00000000000..e66f8e5d051 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_windows.h @@ -0,0 +1,24 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_ +#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_ + +#include <vector> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace net { + +struct FtpDirectoryListingEntry; + +// Parses Windows FTP directory listing. Returns true on success. +NET_EXPORT_PRIVATE bool ParseFtpDirectoryListingWindows( + const std::vector<base::string16>& lines, + std::vector<FtpDirectoryListingEntry>* entries); + +} // namespace net + +#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_ diff --git a/chromium/net/ftp/ftp_directory_listing_parser_windows_unittest.cc b/chromium/net/ftp/ftp_directory_listing_parser_windows_unittest.cc new file mode 100644 index 00000000000..d3d47da1529 --- /dev/null +++ b/chromium/net/ftp/ftp_directory_listing_parser_windows_unittest.cc @@ -0,0 +1,126 @@ +// Copyright (c) 2011 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_directory_listing_parser_unittest.h" + +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "net/ftp/ftp_directory_listing_parser_windows.h" + +namespace net { + +namespace { + +typedef FtpDirectoryListingParserTest FtpDirectoryListingParserWindowsTest; + +TEST_F(FtpDirectoryListingParserWindowsTest, Good) { + const struct SingleLineTestData good_cases[] = { + { "11-02-09 05:32PM <DIR> NT", + FtpDirectoryListingEntry::DIRECTORY, "NT", -1, + 2009, 11, 2, 17, 32 }, + { "01-06-09 02:42PM 458 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 2009, 1, 6, 14, 42 }, + { "01-06-09 02:42AM 1 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 1, + 2009, 1, 6, 2, 42 }, + { "01-06-01 02:42AM 458 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 2001, 1, 6, 2, 42 }, + { "01-06-00 02:42AM 458 Corner1.txt", + FtpDirectoryListingEntry::FILE, "Corner1.txt", 458, + 2000, 1, 6, 2, 42 }, + { "01-06-99 02:42AM 458 Corner2.txt", + FtpDirectoryListingEntry::FILE, "Corner2.txt", 458, + 1999, 1, 6, 2, 42 }, + { "01-06-80 02:42AM 458 Corner3.txt", + FtpDirectoryListingEntry::FILE, "Corner3.txt", 458, + 1980, 1, 6, 2, 42 }, +#if !defined(OS_LINUX) && !defined(OS_ANDROID) + // TODO(phajdan.jr): Re-enable when 2038-year problem is fixed on Linux. + { "01-06-79 02:42AM 458 Corner4", + FtpDirectoryListingEntry::FILE, "Corner4", 458, + 2079, 1, 6, 2, 42 }, +#endif // !defined (OS_LINUX) && !defined(OS_ANDROID) + { "01-06-1979 02:42AM 458 Readme.txt", + FtpDirectoryListingEntry::FILE, "Readme.txt", 458, + 1979, 1, 6, 2, 42 }, + { "11-02-09 05:32PM <DIR> My Directory", + FtpDirectoryListingEntry::DIRECTORY, "My Directory", -1, + 2009, 11, 2, 17, 32 }, + { "12-25-10 12:00AM <DIR> Christmas Midnight", + FtpDirectoryListingEntry::DIRECTORY, "Christmas Midnight", -1, + 2010, 12, 25, 0, 0 }, + { "12-25-10 12:00PM <DIR> Christmas Midday", + FtpDirectoryListingEntry::DIRECTORY, "Christmas Midday", -1, + 2010, 12, 25, 12, 0 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + good_cases[i].input)); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingWindows( + GetSingleLineTestCase(good_cases[i].input), + &entries)); + VerifySingleLineTestCase(good_cases[i], entries); + } +} + +TEST_F(FtpDirectoryListingParserWindowsTest, Ignored) { + const char* ignored_cases[] = { + "12-07-10 12:05AM <DIR> ", // http://crbug.com/66097 + "12-07-10 12:05AM 1234 ", + "11-02-09 05:32 <DIR>", + "11-02-09 05:32PM <DIR>", + }; + for (size_t i = 0; i < arraysize(ignored_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + ignored_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_TRUE(ParseFtpDirectoryListingWindows( + GetSingleLineTestCase(ignored_cases[i]), + &entries)); + EXPECT_EQ(0U, entries.size()); + } +} + +TEST_F(FtpDirectoryListingParserWindowsTest, Bad) { + const char* bad_cases[] = { + "garbage", + "11-02-09 05:32PM <GARBAGE>", + "11-02-09 05:32PM <GARBAGE> NT", + "11-FEB-09 05:32PM <DIR>", + "11-02 05:32PM <DIR>", + "11-02-09 05:32PM -1", + "11-FEB-09 05:32PM <DIR> NT", + "11-02 05:32PM <DIR> NT", + "11-02-09 05:32PM -1 NT", + "99-25-10 12:00AM 0", + "12-99-10 12:00AM 0", + "12-25-10 99:00AM 0", + "12-25-10 12:99AM 0", + "12-25-10 12:00ZM 0", + "99-25-10 12:00AM 0 months out of range", + "12-99-10 12:00AM 0 days out of range", + "12-25-10 99:00AM 0 hours out of range", + "12-25-10 12:99AM 0 minutes out of range", + "12-25-10 12:00ZM 0 what does ZM mean", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, + bad_cases[i])); + + std::vector<FtpDirectoryListingEntry> entries; + EXPECT_FALSE(ParseFtpDirectoryListingWindows( + GetSingleLineTestCase(bad_cases[i]), + &entries)); + } +} + +} // namespace + +} // namespace net diff --git a/chromium/net/ftp/ftp_network_layer.cc b/chromium/net/ftp/ftp_network_layer.cc new file mode 100644 index 00000000000..098fb9fb0a6 --- /dev/null +++ b/chromium/net/ftp/ftp_network_layer.cc @@ -0,0 +1,44 @@ +// Copyright (c) 2008 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_layer.h" + +#include "net/ftp/ftp_network_session.h" +#include "net/ftp/ftp_network_transaction.h" +#include "net/socket/client_socket_factory.h" + +namespace net { + +FtpNetworkLayer::FtpNetworkLayer(HostResolver* host_resolver) + : session_(new FtpNetworkSession(host_resolver)), + suspended_(false) { +} + +FtpNetworkLayer::~FtpNetworkLayer() { +} + +// static +FtpTransactionFactory* FtpNetworkLayer::CreateFactory( + HostResolver* host_resolver) { + return new FtpNetworkLayer(host_resolver); +} + +FtpTransaction* FtpNetworkLayer::CreateTransaction() { + if (suspended_) + return NULL; + + return new FtpNetworkTransaction(session_.get(), + ClientSocketFactory::GetDefaultFactory()); +} + +void FtpNetworkLayer::Suspend(bool suspend) { + suspended_ = suspend; + + /* TODO(darin): We'll need this code once we have a connection manager. + if (suspend) + session_->connection_manager()->CloseIdleSockets(); + */ +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_network_layer.h b/chromium/net/ftp/ftp_network_layer.h new file mode 100644 index 00000000000..6040d029415 --- /dev/null +++ b/chromium/net/ftp/ftp_network_layer.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef NET_FTP_FTP_NETWORK_LAYER_H_ +#define NET_FTP_FTP_NETWORK_LAYER_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "net/base/net_export.h" +#include "net/ftp/ftp_transaction_factory.h" + +namespace net { + +class FtpNetworkSession; +class HostResolver; + +class NET_EXPORT FtpNetworkLayer : public FtpTransactionFactory { + public: + explicit FtpNetworkLayer(HostResolver* host_resolver); + virtual ~FtpNetworkLayer(); + + static FtpTransactionFactory* CreateFactory(HostResolver* host_resolver); + + // FtpTransactionFactory methods: + virtual FtpTransaction* CreateTransaction() OVERRIDE; + virtual void Suspend(bool suspend) OVERRIDE; + + private: + scoped_refptr<FtpNetworkSession> session_; + bool suspended_; + DISALLOW_COPY_AND_ASSIGN(FtpNetworkLayer); +}; + +} // namespace net + +#endif // NET_FTP_FTP_NETWORK_LAYER_H_ diff --git a/chromium/net/ftp/ftp_network_session.cc b/chromium/net/ftp/ftp_network_session.cc new file mode 100644 index 00000000000..65dd218a4d4 --- /dev/null +++ b/chromium/net/ftp/ftp_network_session.cc @@ -0,0 +1,15 @@ +// Copyright (c) 2010 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_session.h" + +namespace net { + +FtpNetworkSession::FtpNetworkSession(HostResolver* host_resolver) + : host_resolver_(host_resolver) {} + +FtpNetworkSession::~FtpNetworkSession() {} + +} // namespace net + diff --git a/chromium/net/ftp/ftp_network_session.h b/chromium/net/ftp/ftp_network_session.h new file mode 100644 index 00000000000..e92bb06643f --- /dev/null +++ b/chromium/net/ftp/ftp_network_session.h @@ -0,0 +1,33 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_NETWORK_SESSION_H_ +#define NET_FTP_FTP_NETWORK_SESSION_H_ + +#include "base/memory/ref_counted.h" +#include "net/base/net_export.h" + +namespace net { + +class HostResolver; + +// This class holds session objects used by FtpNetworkTransaction objects. +class NET_EXPORT_PRIVATE FtpNetworkSession + : public base::RefCounted<FtpNetworkSession> { + public: + explicit FtpNetworkSession(HostResolver* host_resolver); + + HostResolver* host_resolver() { return host_resolver_; } + + private: + friend class base::RefCounted<FtpNetworkSession>; + + virtual ~FtpNetworkSession(); + + HostResolver* const host_resolver_; +}; + +} // namespace net + +#endif // NET_FTP_FTP_NETWORK_SESSION_H_ diff --git a/chromium/net/ftp/ftp_network_transaction.cc b/chromium/net/ftp/ftp_network_transaction.cc new file mode 100644 index 00000000000..f9f7b820168 --- /dev/null +++ b/chromium/net/ftp/ftp_network_transaction.cc @@ -0,0 +1,1400 @@ +// 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/compiler_specific.h" +#include "base/metrics/histogram.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/string_split.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "net/base/address_list.h" +#include "net/base/connection_type_histograms.h" +#include "net/base/escape.h" +#include "net/base/net_errors.h" +#include "net/base/net_log.h" +#include "net/base/net_util.h" +#include "net/ftp/ftp_network_session.h" +#include "net/ftp/ftp_request_info.h" +#include "net/ftp/ftp_util.h" +#include "net/socket/client_socket_factory.h" +#include "net/socket/stream_socket.h" + +const char kCRLF[] = "\r\n"; + +const int kCtrlBufLen = 1024; + +namespace { + +// Returns true if |input| can be safely used as a part of FTP command. +bool IsValidFTPCommandString(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 net::ERR_FTP_SERVICE_UNAVAILABLE; + case 426: + return net::ERR_FTP_TRANSFER_ABORTED; + case 450: + return net::ERR_FTP_FILE_BUSY; + case 500: + case 501: + return net::ERR_FTP_SYNTAX_ERROR; + case 502: + case 504: + return net::ERR_FTP_COMMAND_NOT_SUPPORTED; + case 503: + return net::ERR_FTP_BAD_COMMAND_SEQUENCE; + default: + return net::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 net::FtpCtrlResponse& response, + int* port) { + if (response.lines.size() != 1) + return false; + const char* ptr = response.lines[0].c_str(); + while (*ptr && *ptr != '(') + ++ptr; + if (!*ptr) + return false; + char sep = *(++ptr); + if (!sep || isdigit(sep) || *(++ptr) != sep || *(++ptr) != sep) + return false; + if (!isdigit(*(++ptr))) + return false; + *port = *ptr - '0'; + while (isdigit(*(++ptr))) { + *port *= 10; + *port += *ptr - '0'; + } + if (*ptr != sep) + return false; + + return true; +} + +// 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 net::FtpCtrlResponse& response, + int* port) { + if (response.lines.size() != 1) + return false; + + std::string line(response.lines[0]); + if (!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<std::string> pieces; + base::SplitString(line, ',', &pieces); + 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. + int p0, p1; + if (!base::StringToInt(pieces[4], &p0)) + return false; + if (!base::StringToInt(pieces[5], &p1)) + return false; + *port = (p0 << 8) + p1; + + return true; +} + +} // namespace + +namespace net { + +FtpNetworkTransaction::FtpNetworkTransaction( + FtpNetworkSession* session, + ClientSocketFactory* socket_factory) + : command_sent_(COMMAND_NONE), + io_callback_(base::Bind(&FtpNetworkTransaction::OnIOComplete, + base::Unretained(this))), + session_(session), + request_(NULL), + resolver_(session->host_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_CTRL_WRITE_SIZE) {} + +FtpNetworkTransaction::~FtpNetworkTransaction() { +} + +int FtpNetworkTransaction::Stop(int error) { + if (command_sent_ == COMMAND_QUIT) + return error; + + next_state_ = STATE_CTRL_WRITE_QUIT; + last_error_ = error; + return OK; +} + +int FtpNetworkTransaction::RestartIgnoringLastError( + const CompletionCallback& callback) { + return ERR_NOT_IMPLEMENTED; +} + +int FtpNetworkTransaction::Start(const FtpRequestInfo* request_info, + const CompletionCallback& callback, + const BoundNetLog& net_log) { + net_log_ = net_log; + request_ = request_info; + + ctrl_response_buffer_.reset(new 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(ASCIIToUTF16("anonymous"), + ASCIIToUTF16("chrome@example.com")); + } + + DetectTypecode(); + + next_state_ = STATE_CTRL_RESOLVE_HOST; + int rv = DoLoop(OK); + if (rv == ERR_IO_PENDING) + user_callback_ = callback; + return rv; +} + +int FtpNetworkTransaction::RestartWithAuth(const AuthCredentials& credentials, + const CompletionCallback& callback) { + ResetStateForRestart(); + + credentials_ = credentials; + + next_state_ = STATE_CTRL_RESOLVE_HOST; + int rv = DoLoop(OK); + if (rv == ERR_IO_PENDING) + user_callback_ = callback; + return rv; +} + +int FtpNetworkTransaction::Read(IOBuffer* buf, + int buf_len, + const CompletionCallback& 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_ = 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 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_.reset(new FtpCtrlResponseBuffer(net_log_)); + read_data_buf_ = NULL; + 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_CTRL_WRITE_SIZE; +} + +void FtpNetworkTransaction::ResetDataConnectionAfterError(State next_state) { + // The server _might_ have reset the data connection + // (see RFC 959 3.2. ESTABLISHING DATA CONNECTIONS: + // "The server MUST close the data connection under the following + // conditions: + // ... + // 5. An irrecoverable error condition occurs.") + // + // It is ambiguous what an irrecoverable error condition is, + // so we take no chances. + state_after_data_connect_complete_ = next_state; + next_state_ = use_epsv_ ? STATE_CTRL_WRITE_EPSV : STATE_CTRL_WRITE_PASV; +} + +void FtpNetworkTransaction::DoCallback(int rv) { + DCHECK(rv != ERR_IO_PENDING); + DCHECK(!user_callback_.is_null()); + + // Since Run may result in Read being called, clear callback_ up front. + CompletionCallback c = user_callback_; + user_callback_.Reset(); + c.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): 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. + 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. + return Stop(ERR_INVALID_RESPONSE); + } + } + + 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 (!IsValidFTPCommandString(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(NetLog::TYPE_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[path.length() - 1] == '/') + path.erase(path.length() - 1); + UnescapeRule::Type unescape_rules = UnescapeRule::SPACES | + UnescapeRule::URL_SPECIAL_CHARS; + // This may unescape to non-ASCII characters, but we allow that. See the + // comment for IsValidFTPCommandString. + path = net::UnescapeURLComponent(path, unescape_rules); + + if (system_type_ == SYSTEM_TYPE_VMS) { + if (is_directory) + path = FtpUtil::UnixDirectoryPathToVMS(path); + else + path = FtpUtil::UnixFilePathToVMS(path); + } + + DCHECK(IsValidFTPCommandString(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, &addresses_, + base::Bind(&FtpNetworkTransaction::OnIOComplete, base::Unretained(this)), + 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_, net_log_.net_log(), net_log_.source()); + net_log_.AddEvent( + NetLog::TYPE_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() == 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_); +} + +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_ = NULL; + write_command_buf_ = NULL; + + 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 " + UTF16ToUTF8(credentials_.username()); + + if (!IsValidFTPCommandString(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_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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + return OK; +} + +// PASS command. +int FtpNetworkTransaction::DoCtrlWritePASS() { + std::string command = "PASS " + UTF16ToUTF8(credentials_.password()); + + if (!IsValidFTPCommandString(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_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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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 (IsStringASCII(line)) { + line = StringToLowerASCII(line); + + // Remove all whitespace, to correctly handle cases like fancy "V M S" + // response instead of "VMS". + RemoveChars(line, 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; + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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.length() && line[line.length() - 1] == '/') + line.erase(line.length() - 1); + 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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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_ = use_epsv_ ? STATE_CTRL_WRITE_EPSV : STATE_CTRL_WRITE_PASV; + 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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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: + if (!ExtractPortFromEPSVResponse( response, &data_connection_port_)) + return Stop(ERR_INVALID_RESPONSE); + if (data_connection_port_ < 1024 || + !IsPortAllowedByFtp(data_connection_port_)) + return Stop(ERR_UNSAFE_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; + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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: + if (!ExtractPortFromPASVResponse(response, &data_connection_port_)) + return Stop(ERR_INVALID_RESPONSE); + if (data_connection_port_ < 1024 || + !IsPortAllowedByFtp(data_connection_port_)) + return Stop(ERR_UNSAFE_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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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) { + 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. + resource_type_ = RESOURCE_TYPE_FILE; + break; + case ERROR_CLASS_OK: + resource_type_ = RESOURCE_TYPE_FILE; + 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: + // Code 550 means "Failed to open file". Other codes are unrelated, + // like "Not logged in" etc. + if (response.status_code != 550 || resource_type_ == RESOURCE_TYPE_FILE) + return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code)); + + // It's possible that RETR failed because the path is a directory. + resource_type_ = RESOURCE_TYPE_DIRECTORY; + + // We're going to try CWD next, but first send a PASV one more time, + // because some FTP servers, including FileZilla, require that. + // See http://crbug.com/25316. + next_state_ = use_epsv_ ? STATE_CTRL_WRITE_EPSV : STATE_CTRL_WRITE_PASV; + break; + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + + // We should be sure about our resource type now. Otherwise we risk + // an infinite loop (RETR can later send CWD, and CWD can later send RETR). + DCHECK_NE(RESOURCE_TYPE_UNKNOWN, resource_type_); + + 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) { + State state_after_size; + if (resource_type_ == RESOURCE_TYPE_FILE) + state_after_size = STATE_CTRL_WRITE_RETR; + else + state_after_size = STATE_CTRL_WRITE_CWD; + + switch (GetErrorClass(response.status_code)) { + case ERROR_CLASS_INITIATED: + next_state_ = state_after_size; + break; + case ERROR_CLASS_OK: + if (response.lines.size() != 1) + return Stop(ERR_INVALID_RESPONSE); + int64 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; + + next_state_ = state_after_size; + break; + case ERROR_CLASS_INFO_NEEDED: + next_state_ = state_after_size; + break; + case ERROR_CLASS_TRANSIENT_ERROR: + ResetDataConnectionAfterError(state_after_size); + break; + case ERROR_CLASS_PERMANENT_ERROR: + // It's possible that SIZE failed because the path is a directory. + if (resource_type_ == RESOURCE_TYPE_UNKNOWN && + response.status_code != 550) { + return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code)); + } + + ResetDataConnectionAfterError(state_after_size); + break; + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + + 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) { + // We should never issue CWD if we know the target resource is 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: + next_state_ = 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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + + 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); + } + + // We are here because SIZE failed and we are not sure what the resource + // type is. It could still be file, and SIZE could fail because of + // an access error (http://crbug.com/56734). Try RETR just to be sure. + resource_type_ = RESOURCE_TYPE_FILE; + + ResetDataConnectionAfterError(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) { + 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)); + default: + NOTREACHED(); + return Stop(ERR_UNEXPECTED); + } + 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, net_log_.net_log(), net_log_.source()); + net_log_.AddEvent( + NetLog::TYPE_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): remember it for future transactions with this server. + // TODO(phajdan.jr): write a test for this code path. + 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_ == NULL || !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 diff --git a/chromium/net/ftp/ftp_network_transaction.h b/chromium/net/ftp/ftp_network_transaction.h new file mode 100644 index 00000000000..5eb6aae7361 --- /dev/null +++ b/chromium/net/ftp/ftp_network_transaction.h @@ -0,0 +1,262 @@ +// 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. + +#ifndef NET_FTP_FTP_NETWORK_TRANSACTION_H_ +#define NET_FTP_FTP_NETWORK_TRANSACTION_H_ + +#include <string> +#include <utility> + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "net/base/address_list.h" +#include "net/base/auth.h" +#include "net/base/net_log.h" +#include "net/dns/host_resolver.h" +#include "net/dns/single_request_host_resolver.h" +#include "net/ftp/ftp_ctrl_response_buffer.h" +#include "net/ftp/ftp_response_info.h" +#include "net/ftp/ftp_transaction.h" + +namespace net { + +class ClientSocketFactory; +class FtpNetworkSession; +class StreamSocket; + +class NET_EXPORT_PRIVATE FtpNetworkTransaction : public FtpTransaction { + public: + FtpNetworkTransaction(FtpNetworkSession* session, + ClientSocketFactory* socket_factory); + virtual ~FtpNetworkTransaction(); + + virtual int Stop(int error); + virtual int RestartIgnoringLastError(const CompletionCallback& callback); + + // FtpTransaction methods: + virtual int Start(const FtpRequestInfo* request_info, + const CompletionCallback& callback, + const BoundNetLog& net_log) OVERRIDE; + virtual int RestartWithAuth(const AuthCredentials& credentials, + const CompletionCallback& callback) OVERRIDE; + virtual int Read(IOBuffer* buf, int buf_len, + const CompletionCallback& callback) OVERRIDE; + virtual const FtpResponseInfo* GetResponseInfo() const OVERRIDE; + virtual LoadState GetLoadState() const OVERRIDE; + virtual uint64 GetUploadProgress() const OVERRIDE; + + private: + FRIEND_TEST_ALL_PREFIXES(FtpNetworkTransactionTest, + DownloadTransactionEvilPasvUnsafeHost); + + enum Command { + COMMAND_NONE, + COMMAND_USER, + COMMAND_PASS, + COMMAND_SYST, + COMMAND_TYPE, + COMMAND_EPSV, + COMMAND_PASV, + COMMAND_PWD, + COMMAND_SIZE, + COMMAND_RETR, + COMMAND_CWD, + COMMAND_LIST, + COMMAND_QUIT, + }; + + // Major categories of remote system types, as returned by SYST command. + enum SystemType { + SYSTEM_TYPE_UNKNOWN, + SYSTEM_TYPE_UNIX, + SYSTEM_TYPE_WINDOWS, + SYSTEM_TYPE_OS2, + SYSTEM_TYPE_VMS, + }; + + // Data representation type, see RFC 959 section 3.1.1. Data Types. + // We only support the two most popular data types. + enum DataType { + DATA_TYPE_ASCII, + DATA_TYPE_IMAGE, + }; + + // In FTP we need to issue different commands depending on whether a resource + // is a file or directory. If we don't know that, we're going to autodetect + // it. + enum ResourceType { + RESOURCE_TYPE_UNKNOWN, + RESOURCE_TYPE_FILE, + RESOURCE_TYPE_DIRECTORY, + }; + + enum State { + // Control connection states: + STATE_CTRL_RESOLVE_HOST, + STATE_CTRL_RESOLVE_HOST_COMPLETE, + STATE_CTRL_CONNECT, + STATE_CTRL_CONNECT_COMPLETE, + STATE_CTRL_READ, + STATE_CTRL_READ_COMPLETE, + STATE_CTRL_WRITE, + STATE_CTRL_WRITE_COMPLETE, + STATE_CTRL_WRITE_USER, + STATE_CTRL_WRITE_PASS, + STATE_CTRL_WRITE_SYST, + STATE_CTRL_WRITE_TYPE, + STATE_CTRL_WRITE_EPSV, + STATE_CTRL_WRITE_PASV, + STATE_CTRL_WRITE_PWD, + STATE_CTRL_WRITE_RETR, + STATE_CTRL_WRITE_SIZE, + STATE_CTRL_WRITE_CWD, + STATE_CTRL_WRITE_LIST, + STATE_CTRL_WRITE_QUIT, + // Data connection states: + STATE_DATA_CONNECT, + STATE_DATA_CONNECT_COMPLETE, + STATE_DATA_READ, + STATE_DATA_READ_COMPLETE, + STATE_NONE + }; + + // Resets the members of the transaction so it can be restarted. + void ResetStateForRestart(); + + // Resets the data connection after an error and switches to |next_state|. + void ResetDataConnectionAfterError(State next_state); + + void DoCallback(int result); + void OnIOComplete(int result); + + // Executes correct ProcessResponse + command_name function based on last + // issued command. Returns error code. + int ProcessCtrlResponse(); + + int SendFtpCommand(const std::string& command, + const std::string& command_for_log, + Command cmd); + + // Returns request path suitable to be included in an FTP command. If the path + // will be used as a directory, |is_directory| should be true. + std::string GetRequestPathForFtpCommand(bool is_directory) const; + + // See if the request URL contains a typecode and make us respect it. + void DetectTypecode(); + + // Runs the state transition loop. + int DoLoop(int result); + + // Each of these methods corresponds to a State value. Those with an input + // argument receive the result from the previous state. If a method returns + // ERR_IO_PENDING, then the result from OnIOComplete will be passed to the + // next state method as the result arg. + int DoCtrlResolveHost(); + int DoCtrlResolveHostComplete(int result); + int DoCtrlConnect(); + int DoCtrlConnectComplete(int result); + int DoCtrlRead(); + int DoCtrlReadComplete(int result); + int DoCtrlWrite(); + int DoCtrlWriteComplete(int result); + int DoCtrlWriteUSER(); + int ProcessResponseUSER(const FtpCtrlResponse& response); + int DoCtrlWritePASS(); + int ProcessResponsePASS(const FtpCtrlResponse& response); + int DoCtrlWriteSYST(); + int ProcessResponseSYST(const FtpCtrlResponse& response); + int DoCtrlWritePWD(); + int ProcessResponsePWD(const FtpCtrlResponse& response); + int DoCtrlWriteTYPE(); + int ProcessResponseTYPE(const FtpCtrlResponse& response); + int DoCtrlWriteEPSV(); + int ProcessResponseEPSV(const FtpCtrlResponse& response); + int DoCtrlWritePASV(); + int ProcessResponsePASV(const FtpCtrlResponse& response); + int DoCtrlWriteRETR(); + int ProcessResponseRETR(const FtpCtrlResponse& response); + int DoCtrlWriteSIZE(); + int ProcessResponseSIZE(const FtpCtrlResponse& response); + int DoCtrlWriteCWD(); + int ProcessResponseCWD(const FtpCtrlResponse& response); + int ProcessResponseCWDNotADirectory(); + int DoCtrlWriteLIST(); + int ProcessResponseLIST(const FtpCtrlResponse& response); + int DoCtrlWriteQUIT(); + int ProcessResponseQUIT(const FtpCtrlResponse& response); + + int DoDataConnect(); + int DoDataConnectComplete(int result); + int DoDataRead(); + int DoDataReadComplete(int result); + + void RecordDataConnectionError(int result); + + Command command_sent_; + + CompletionCallback io_callback_; + CompletionCallback user_callback_; + + scoped_refptr<FtpNetworkSession> session_; + + BoundNetLog net_log_; + const FtpRequestInfo* request_; + FtpResponseInfo response_; + + // Cancels the outstanding request on destruction. + SingleRequestHostResolver resolver_; + AddressList addresses_; + + // User buffer passed to the Read method for control socket. + scoped_refptr<IOBuffer> read_ctrl_buf_; + + scoped_ptr<FtpCtrlResponseBuffer> ctrl_response_buffer_; + + scoped_refptr<IOBuffer> read_data_buf_; + int read_data_buf_len_; + + // Buffer holding the command line to be written to the control socket. + scoped_refptr<IOBufferWithSize> write_command_buf_; + + // Buffer passed to the Write method of control socket. It actually writes + // to the write_command_buf_ at correct offset. + scoped_refptr<DrainableIOBuffer> write_buf_; + + int last_error_; + + SystemType system_type_; + + // Data type to be used for the TYPE command. + DataType data_type_; + + // Detected resource type (file or directory). + ResourceType resource_type_; + + // Initially we favour EPSV over PASV for transfers but should any + // EPSV fail, we fall back to PASV for the duration of connection. + bool use_epsv_; + + AuthCredentials credentials_; + + // Current directory on the remote server, as returned by last PWD command, + // with any trailing slash removed. + std::string current_remote_directory_; + + int data_connection_port_; + + ClientSocketFactory* socket_factory_; + + scoped_ptr<StreamSocket> ctrl_socket_; + scoped_ptr<StreamSocket> data_socket_; + + State next_state_; + + // State to switch to after data connection is complete. + State state_after_data_connect_complete_; +}; + +} // namespace net + +#endif // NET_FTP_FTP_NETWORK_TRANSACTION_H_ diff --git a/chromium/net/ftp/ftp_network_transaction_unittest.cc b/chromium/net/ftp/ftp_network_transaction_unittest.cc new file mode 100644 index 00000000000..a244cc51c5a --- /dev/null +++ b/chromium/net/ftp/ftp_network_transaction_unittest.cc @@ -0,0 +1,1602 @@ +// 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 "build/build_config.h" + +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "net/base/host_port_pair.h" +#include "net/base/io_buffer.h" +#include "net/base/net_util.h" +#include "net/base/test_completion_callback.h" +#include "net/dns/mock_host_resolver.h" +#include "net/ftp/ftp_network_session.h" +#include "net/ftp/ftp_request_info.h" +#include "net/socket/socket_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace { + +// Size we use for IOBuffers used to receive data from the test data socket. +const int kBufferSize = 128; + +} // namespace + +namespace net { + +class FtpSocketDataProvider : public DynamicSocketDataProvider { + public: + enum State { + NONE, + PRE_USER, + PRE_PASSWD, + PRE_SYST, + PRE_PWD, + PRE_TYPE, + PRE_SIZE, + PRE_EPSV, + PRE_PASV, + PRE_LIST, + PRE_RETR, + PRE_RETR_EPSV, + PRE_RETR_PASV, + PRE_CWD_EPSV, + PRE_CWD_PASV, + PRE_CWD, + PRE_QUIT, + PRE_NOPASV, + QUIT + }; + + FtpSocketDataProvider() + : failure_injection_state_(NONE), + multiline_welcome_(false), + use_epsv_(true), + data_type_('I') { + Init(); + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_USER: + return Verify("USER anonymous\r\n", data, PRE_PASSWD, + "331 Password needed\r\n"); + case PRE_PASSWD: + { + const char* response_one = "230 Welcome\r\n"; + const char* response_multi = "230- One\r\n230- Two\r\n230 Three\r\n"; + return Verify("PASS chrome@example.com\r\n", data, PRE_SYST, + multiline_welcome_ ? response_multi : response_one); + } + case PRE_SYST: + return Verify("SYST\r\n", data, PRE_PWD, "215 UNIX\r\n"); + case PRE_PWD: + return Verify("PWD\r\n", data, PRE_TYPE, + "257 \"/\" is your current location\r\n"); + case PRE_TYPE: + return Verify(std::string("TYPE ") + data_type_ + "\r\n", data, + use_epsv_ ? PRE_EPSV : PRE_PASV, + "200 TYPE set successfully\r\n"); + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_SIZE, + "227 Entering Extended Passive Mode (|||31744|)\r\n"); + case PRE_CWD_EPSV: + return Verify("EPSV\r\n", data, PRE_CWD, + "227 Entering Extended Passive Mode (|||31744|)\r\n"); + case PRE_RETR_EPSV: + return Verify("EPSV\r\n", data, PRE_RETR, + "227 Entering Extended Passive Mode (|||31744|)\r\n"); + case PRE_CWD_PASV: + return Verify("PASV\r\n", data, PRE_CWD, + "227 Entering Passive Mode 127,0,0,1,123,456\r\n"); + case PRE_RETR_PASV: + return Verify("PASV\r\n", data, PRE_RETR, + "227 Entering Passive Mode 127,0,0,1,123,456\r\n"); + case PRE_PASV: + return Verify("PASV\r\n", data, PRE_SIZE, + "227 Entering Passive Mode 127,0,0,1,123,456\r\n"); + case PRE_NOPASV: + // Use unallocated 599 FTP error code to make sure it falls into the + // generic ERR_FTP_FAILED bucket. + return Verify("PASV\r\n", data, PRE_QUIT, + "599 fail\r\n"); + case PRE_QUIT: + return Verify("QUIT\r\n", data, QUIT, "221 Goodbye.\r\n"); + default: + NOTREACHED() << "State not handled " << state(); + return MockWriteResult(ASYNC, ERR_UNEXPECTED); + } + } + + void InjectFailure(State state, State next_state, const char* response) { + DCHECK_EQ(NONE, failure_injection_state_); + DCHECK_NE(NONE, state); + DCHECK_NE(NONE, next_state); + DCHECK_NE(state, next_state); + failure_injection_state_ = state; + failure_injection_next_state_ = next_state; + fault_response_ = response; + } + + State state() const { + return state_; + } + + virtual void Reset() OVERRIDE { + DynamicSocketDataProvider::Reset(); + Init(); + } + + void set_multiline_welcome(bool multiline) { multiline_welcome_ = multiline; } + + bool use_epsv() const { return use_epsv_; } + void set_use_epsv(bool use_epsv) { use_epsv_ = use_epsv; } + + void set_data_type(char data_type) { data_type_ = data_type; } + + protected: + void Init() { + state_ = PRE_USER; + SimulateRead("220 host TestFTPd\r\n"); + } + + // If protocol fault injection has been requested, adjusts state and mocked + // read and returns true. + bool InjectFault() { + if (state_ != failure_injection_state_) + return false; + SimulateRead(fault_response_); + state_ = failure_injection_next_state_; + return true; + } + + MockWriteResult Verify(const std::string& expected, + const std::string& data, + State next_state, + const char* next_read, + const size_t next_read_length) { + EXPECT_EQ(expected, data); + if (expected == data) { + state_ = next_state; + SimulateRead(next_read, next_read_length); + return MockWriteResult(ASYNC, data.length()); + } + return MockWriteResult(ASYNC, ERR_UNEXPECTED); + } + + MockWriteResult Verify(const std::string& expected, + const std::string& data, + State next_state, + const char* next_read) { + return Verify(expected, data, next_state, + next_read, std::strlen(next_read)); + } + + + private: + State state_; + State failure_injection_state_; + State failure_injection_next_state_; + const char* fault_response_; + + // If true, we will send multiple 230 lines as response after PASS. + bool multiline_welcome_; + + // If true, we will use EPSV command. + bool use_epsv_; + + // Data type to be used for TYPE command. + char data_type_; + + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProvider); +}; + +class FtpSocketDataProviderDirectoryListing : public FtpSocketDataProvider { + public: + FtpSocketDataProviderDirectoryListing() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /\r\n", data, + use_epsv() ? PRE_CWD_EPSV : PRE_CWD_PASV, + "550 I can only retrieve regular files\r\n"); + case PRE_CWD: + return Verify("CWD /\r\n", data, PRE_LIST, "200 OK\r\n"); + case PRE_LIST: + return Verify("LIST -l\r\n", data, PRE_QUIT, "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderDirectoryListing); +}; + +class FtpSocketDataProviderDirectoryListingWithPasvFallback + : public FtpSocketDataProviderDirectoryListing { + public: + FtpSocketDataProviderDirectoryListingWithPasvFallback() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_PASV, + "500 no EPSV for you\r\n"); + case PRE_SIZE: + return Verify("SIZE /\r\n", data, PRE_CWD_PASV, + "550 I can only retrieve regular files\r\n"); + default: + return FtpSocketDataProviderDirectoryListing::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN( + FtpSocketDataProviderDirectoryListingWithPasvFallback); +}; + +class FtpSocketDataProviderDirectoryListingZeroSize + : public FtpSocketDataProviderDirectoryListing { + public: + FtpSocketDataProviderDirectoryListingZeroSize() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /\r\n", data, PRE_CWD, "213 0\r\n"); + default: + return FtpSocketDataProviderDirectoryListing::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderDirectoryListingZeroSize); +}; + +class FtpSocketDataProviderVMSDirectoryListing : public FtpSocketDataProvider { + public: + FtpSocketDataProviderVMSDirectoryListing() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SYST: + return Verify("SYST\r\n", data, PRE_PWD, "215 VMS\r\n"); + case PRE_PWD: + return Verify("PWD\r\n", data, PRE_TYPE, + "257 \"ANONYMOUS_ROOT:[000000]\"\r\n"); + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_PASV, "500 Invalid command\r\n"); + case PRE_SIZE: + return Verify("SIZE ANONYMOUS_ROOT:[000000]dir\r\n", data, PRE_CWD_PASV, + "550 I can only retrieve regular files\r\n"); + case PRE_CWD: + return Verify("CWD ANONYMOUS_ROOT:[dir]\r\n", data, PRE_LIST, + "200 OK\r\n"); + case PRE_LIST: + return Verify("LIST *.*;0\r\n", data, PRE_QUIT, "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderVMSDirectoryListing); +}; + +class FtpSocketDataProviderVMSDirectoryListingRootDirectory + : public FtpSocketDataProvider { + public: + FtpSocketDataProviderVMSDirectoryListingRootDirectory() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SYST: + return Verify("SYST\r\n", data, PRE_PWD, "215 VMS\r\n"); + case PRE_PWD: + return Verify("PWD\r\n", data, PRE_TYPE, + "257 \"ANONYMOUS_ROOT:[000000]\"\r\n"); + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_PASV, + "500 EPSV command unknown\r\n"); + case PRE_SIZE: + return Verify("SIZE ANONYMOUS_ROOT\r\n", data, PRE_CWD_PASV, + "550 I can only retrieve regular files\r\n"); + case PRE_CWD: + return Verify("CWD ANONYMOUS_ROOT:[000000]\r\n", data, PRE_LIST, + "200 OK\r\n"); + case PRE_LIST: + return Verify("LIST *.*;0\r\n", data, PRE_QUIT, "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN( + FtpSocketDataProviderVMSDirectoryListingRootDirectory); +}; + +class FtpSocketDataProviderFileDownloadWithFileTypecode + : public FtpSocketDataProvider { + public: + FtpSocketDataProviderFileDownloadWithFileTypecode() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /file\r\n", data, PRE_RETR, + "213 18\r\n"); + case PRE_RETR: + return Verify("RETR /file\r\n", data, PRE_QUIT, "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadWithFileTypecode); +}; + +class FtpSocketDataProviderFileDownload : public FtpSocketDataProvider { + public: + FtpSocketDataProviderFileDownload() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /file\r\n", data, PRE_CWD, + "213 18\r\n"); + case PRE_CWD: + return Verify("CWD /file\r\n", data, + use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV, + "550 Not a directory\r\n"); + case PRE_RETR: + return Verify("RETR /file\r\n", data, PRE_QUIT, "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownload); +}; + +class FtpSocketDataProviderFileNotFound : public FtpSocketDataProvider { + public: + FtpSocketDataProviderFileNotFound() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /file\r\n", data, + use_epsv() ? PRE_CWD_EPSV : PRE_CWD_PASV, + "550 File Not Found\r\n"); + case PRE_CWD: + return Verify("CWD /file\r\n", data, + use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV, + "550 File Not Found\r\n"); + case PRE_RETR: + return Verify("RETR /file\r\n", data, PRE_QUIT, + "550 File Not Found\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileNotFound); +}; + +class FtpSocketDataProviderFileDownloadWithPasvFallback + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderFileDownloadWithPasvFallback() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_PASV, + "500 No can do\r\n"); + case PRE_CWD: + return Verify("CWD /file\r\n", data, PRE_RETR_PASV, + "550 Not a directory\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadWithPasvFallback); +}; + +class FtpSocketDataProviderFileDownloadZeroSize + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderFileDownloadZeroSize() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /file\r\n", data, PRE_CWD, + "213 0\r\n"); + case PRE_CWD: + return Verify("CWD /file\r\n", data, + use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV, + "550 not a directory\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadZeroSize); +}; + +class FtpSocketDataProviderFileDownloadCWD451 + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderFileDownloadCWD451() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_CWD: + return Verify("CWD /file\r\n", data, + use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV, + "451 not a directory\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadCWD451); +}; + +class FtpSocketDataProviderVMSFileDownload : public FtpSocketDataProvider { + public: + FtpSocketDataProviderVMSFileDownload() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SYST: + return Verify("SYST\r\n", data, PRE_PWD, "215 VMS\r\n"); + case PRE_PWD: + return Verify("PWD\r\n", data, PRE_TYPE, + "257 \"ANONYMOUS_ROOT:[000000]\"\r\n"); + case PRE_EPSV: + return Verify("EPSV\r\n", data, PRE_PASV, + "500 EPSV command unknown\r\n"); + case PRE_SIZE: + return Verify("SIZE ANONYMOUS_ROOT:[000000]file\r\n", data, PRE_CWD, + "213 18\r\n"); + case PRE_CWD: + return Verify("CWD ANONYMOUS_ROOT:[file]\r\n", data, PRE_RETR_PASV, + "550 Not a directory\r\n"); + case PRE_RETR: + return Verify("RETR ANONYMOUS_ROOT:[000000]file\r\n", data, PRE_QUIT, + "200 OK\r\n"); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderVMSFileDownload); +}; + +class FtpSocketDataProviderEscaping : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderEscaping() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE / !\"#$%y\200\201\r\n", data, PRE_CWD, + "213 18\r\n"); + case PRE_CWD: + return Verify("CWD / !\"#$%y\200\201\r\n", data, + use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV, + "550 Not a directory\r\n"); + case PRE_RETR: + return Verify("RETR / !\"#$%y\200\201\r\n", data, PRE_QUIT, + "200 OK\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEscaping); +}; + +class FtpSocketDataProviderFileDownloadTransferStarting + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderFileDownloadTransferStarting() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_RETR: + return Verify("RETR /file\r\n", data, PRE_QUIT, + "125-Data connection already open.\r\n" + "125 Transfer starting.\r\n" + "226 Transfer complete.\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadTransferStarting); +}; + +class FtpSocketDataProviderDirectoryListingTransferStarting + : public FtpSocketDataProviderDirectoryListing { + public: + FtpSocketDataProviderDirectoryListingTransferStarting() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_LIST: + return Verify("LIST -l\r\n", data, PRE_QUIT, + "125-Data connection already open.\r\n" + "125 Transfer starting.\r\n" + "226 Transfer complete.\r\n"); + default: + return FtpSocketDataProviderDirectoryListing::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN( + FtpSocketDataProviderDirectoryListingTransferStarting); +}; + +class FtpSocketDataProviderFileDownloadInvalidResponse + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderFileDownloadInvalidResponse() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + // Use unallocated 599 FTP error code to make sure it falls into the + // generic ERR_FTP_FAILED bucket. + return Verify("SIZE /file\r\n", data, PRE_QUIT, + "599 Evil Response\r\n" + "599 More Evil\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownloadInvalidResponse); +}; + +class FtpSocketDataProviderEvilEpsv : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderEvilEpsv(const char* epsv_response, + State expected_state) + : epsv_response_(epsv_response), + epsv_response_length_(std::strlen(epsv_response)), + expected_state_(expected_state) {} + + FtpSocketDataProviderEvilEpsv(const char* epsv_response, + size_t epsv_response_length, + State expected_state) + : epsv_response_(epsv_response), + epsv_response_length_(epsv_response_length), + expected_state_(expected_state) {} + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_EPSV: + return Verify("EPSV\r\n", data, expected_state_, + epsv_response_, epsv_response_length_); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + const char* epsv_response_; + const size_t epsv_response_length_; + const State expected_state_; + + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEvilEpsv); +}; + +class FtpSocketDataProviderEvilPasv + : public FtpSocketDataProviderFileDownloadWithPasvFallback { + public: + FtpSocketDataProviderEvilPasv(const char* pasv_response, State expected_state) + : pasv_response_(pasv_response), + expected_state_(expected_state) { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_PASV: + return Verify("PASV\r\n", data, expected_state_, pasv_response_); + default: + return FtpSocketDataProviderFileDownloadWithPasvFallback::OnWrite(data); + } + } + + private: + const char* pasv_response_; + const State expected_state_; + + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEvilPasv); +}; + +class FtpSocketDataProviderEvilSize : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderEvilSize(const char* size_response, State expected_state) + : size_response_(size_response), + expected_state_(expected_state) { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_SIZE: + return Verify("SIZE /file\r\n", data, expected_state_, size_response_); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + const char* size_response_; + const State expected_state_; + + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEvilSize); +}; + +class FtpSocketDataProviderEvilLogin + : public FtpSocketDataProviderFileDownload { + public: + FtpSocketDataProviderEvilLogin(const char* expected_user, + const char* expected_password) + : expected_user_(expected_user), + expected_password_(expected_password) { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_USER: + return Verify(std::string("USER ") + expected_user_ + "\r\n", data, + PRE_PASSWD, "331 Password needed\r\n"); + case PRE_PASSWD: + return Verify(std::string("PASS ") + expected_password_ + "\r\n", data, + PRE_SYST, "230 Welcome\r\n"); + default: + return FtpSocketDataProviderFileDownload::OnWrite(data); + } + } + + private: + const char* expected_user_; + const char* expected_password_; + + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEvilLogin); +}; + +class FtpSocketDataProviderCloseConnection : public FtpSocketDataProvider { + public: + FtpSocketDataProviderCloseConnection() { + } + + virtual MockWriteResult OnWrite(const std::string& data) OVERRIDE { + if (InjectFault()) + return MockWriteResult(ASYNC, data.length()); + switch (state()) { + case PRE_USER: + return Verify("USER anonymous\r\n", data, + PRE_QUIT, ""); + default: + return FtpSocketDataProvider::OnWrite(data); + } + } + + private: + DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderCloseConnection); +}; + +class FtpNetworkTransactionTest + : public PlatformTest, + public ::testing::WithParamInterface<int> { + public: + FtpNetworkTransactionTest() + : host_resolver_(new MockHostResolver), + session_(new FtpNetworkSession(host_resolver_.get())), + transaction_(session_.get(), &mock_socket_factory_) { + scoped_refptr<RuleBasedHostResolverProc> rules( + new RuleBasedHostResolverProc(NULL)); + if (GetFamily() == AF_INET) { + rules->AddIPLiteralRule("*", "127.0.0.1", "127.0.0.1"); + } else if (GetFamily() == AF_INET6) { + rules->AddIPLiteralRule("*", "::1", "::1"); + } else { + NOTREACHED(); + } + host_resolver_->set_rules(rules.get()); + } + + protected: + // Accessor to make code refactoring-friendly, e.g. when we change the way + // parameters are passed (like more parameters). + int GetFamily() { + return GetParam(); + } + + FtpRequestInfo GetRequestInfo(const std::string& url) { + FtpRequestInfo info; + info.url = GURL(url); + return info; + } + + void ExecuteTransaction(FtpSocketDataProvider* ctrl_socket, + const char* request, + int data_socket, + int expected_result) { + // Expect EPSV usage for non-IPv4 control connections. + ctrl_socket->set_use_epsv((GetFamily() != AF_INET)); + + mock_socket_factory_.AddSocketDataProvider(ctrl_socket); + + std::string mock_data("mock-data"); + MockRead data_reads[] = { + // Usually FTP servers close the data connection after the entire data has + // been received. + MockRead(SYNCHRONOUS, ERR_TEST_PEER_CLOSE_AFTER_NEXT_MOCK_READ), + MockRead(mock_data.c_str()), + }; + + ScopedVector<StaticSocketDataProvider> data_sockets; + data_sockets.reserve(data_socket); + for (int i = 0; i < data_socket + 1; i++) { + // We only read from one data socket, other ones are dummy. + if (i == data_socket) { + data_sockets.push_back(new StaticSocketDataProvider( + data_reads, arraysize(data_reads), NULL, 0)); + } else { + data_sockets.push_back(new StaticSocketDataProvider); + } + mock_socket_factory_.AddSocketDataProvider(data_sockets[i]); + } + + FtpRequestInfo request_info = GetRequestInfo(request); + EXPECT_EQ(LOAD_STATE_IDLE, transaction_.GetLoadState()); + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Start(&request_info, callback_.callback(), + BoundNetLog())); + EXPECT_NE(LOAD_STATE_IDLE, transaction_.GetLoadState()); + ASSERT_EQ(expected_result, callback_.WaitForResult()); + if (expected_result == OK) { + scoped_refptr<IOBuffer> io_buffer(new IOBuffer(kBufferSize)); + memset(io_buffer->data(), 0, kBufferSize); + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Read(io_buffer.get(), kBufferSize, + callback_.callback())); + ASSERT_EQ(static_cast<int>(mock_data.length()), + callback_.WaitForResult()); + EXPECT_EQ(mock_data, std::string(io_buffer->data(), mock_data.length())); + + // Do another Read to detect that the data socket is now closed. + int rv = transaction_.Read(io_buffer.get(), kBufferSize, + callback_.callback()); + if (rv == ERR_IO_PENDING) { + EXPECT_EQ(0, callback_.WaitForResult()); + } else { + EXPECT_EQ(0, rv); + } + } + EXPECT_EQ(FtpSocketDataProvider::QUIT, ctrl_socket->state()); + EXPECT_EQ(LOAD_STATE_IDLE, transaction_.GetLoadState()); + } + + void TransactionFailHelper(FtpSocketDataProvider* ctrl_socket, + const char* request, + FtpSocketDataProvider::State state, + FtpSocketDataProvider::State next_state, + const char* response, + int expected_result) { + ctrl_socket->InjectFailure(state, next_state, response); + ExecuteTransaction(ctrl_socket, request, 1, expected_result); + } + + scoped_ptr<MockHostResolver> host_resolver_; + scoped_refptr<FtpNetworkSession> session_; + MockClientSocketFactory mock_socket_factory_; + FtpNetworkTransaction transaction_; + TestCompletionCallback callback_; +}; + +TEST_P(FtpNetworkTransactionTest, FailedLookup) { + FtpRequestInfo request_info = GetRequestInfo("ftp://badhost"); + scoped_refptr<RuleBasedHostResolverProc> rules( + new RuleBasedHostResolverProc(NULL)); + rules->AddSimulatedFailure("badhost"); + host_resolver_->set_rules(rules.get()); + + EXPECT_EQ(LOAD_STATE_IDLE, transaction_.GetLoadState()); + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Start(&request_info, callback_.callback(), + BoundNetLog())); + ASSERT_EQ(ERR_NAME_NOT_RESOLVED, callback_.WaitForResult()); + EXPECT_EQ(LOAD_STATE_IDLE, transaction_.GetLoadState()); +} + +// Check that when determining the host, the square brackets decorating IPv6 +// literals in URLs are stripped. +TEST_P(FtpNetworkTransactionTest, StripBracketsFromIPv6Literals) { + // This test only makes sense for IPv6 connections. + if (GetFamily() != AF_INET6) + return; + + host_resolver_->rules()->AddSimulatedFailure("[::1]"); + + // We start a transaction that is expected to fail with ERR_INVALID_RESPONSE. + // The important part of this test is to make sure that we don't fail with + // ERR_NAME_NOT_RESOLVED, since that would mean the decorated hostname + // was used. + FtpSocketDataProviderEvilSize ctrl_socket( + "213 99999999999999999999999999999999\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://[::1]/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransaction) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); + + EXPECT_TRUE(transaction_.GetResponseInfo()->is_directory_listing); + EXPECT_EQ(-1, transaction_.GetResponseInfo()->expected_content_size); + EXPECT_EQ((GetFamily() == AF_INET) ? "127.0.0.1" : "::1", + transaction_.GetResponseInfo()->socket_address.host()); + EXPECT_EQ(21, transaction_.GetResponseInfo()->socket_address.port()); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionWithPasvFallback) { + FtpSocketDataProviderDirectoryListingWithPasvFallback ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); + + EXPECT_TRUE(transaction_.GetResponseInfo()->is_directory_listing); + EXPECT_EQ(-1, transaction_.GetResponseInfo()->expected_content_size); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionWithTypecode) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host;type=d", 1, OK); + + EXPECT_TRUE(transaction_.GetResponseInfo()->is_directory_listing); + EXPECT_EQ(-1, transaction_.GetResponseInfo()->expected_content_size); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionMultilineWelcome) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + ctrl_socket.set_multiline_welcome(true); + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionShortReads2) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + ctrl_socket.set_short_read_limit(2); + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionShortReads5) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + ctrl_socket.set_short_read_limit(5); + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionMultilineWelcomeShort) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // The client will not consume all three 230 lines. That's good, we want to + // test that scenario. + ctrl_socket.allow_unconsumed_reads(true); + ctrl_socket.set_multiline_welcome(true); + ctrl_socket.set_short_read_limit(5); + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +// Regression test for http://crbug.com/60555. +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionZeroSize) { + FtpSocketDataProviderDirectoryListingZeroSize ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 0, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionVMS) { + FtpSocketDataProviderVMSDirectoryListing ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/dir", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionVMSRootDirectory) { + FtpSocketDataProviderVMSDirectoryListingRootDirectory ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionTransferStarting) { + FtpSocketDataProviderDirectoryListingTransferStarting ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransaction) { + FtpSocketDataProviderFileDownload ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); + + // We pass an artificial value of 18 as a response to the SIZE command. + EXPECT_EQ(18, transaction_.GetResponseInfo()->expected_content_size); + EXPECT_EQ((GetFamily() == AF_INET) ? "127.0.0.1" : "::1", + transaction_.GetResponseInfo()->socket_address.host()); + EXPECT_EQ(21, transaction_.GetResponseInfo()->socket_address.port()); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionWithPasvFallback) { + FtpSocketDataProviderFileDownloadWithPasvFallback ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); + + // We pass an artificial value of 18 as a response to the SIZE command. + EXPECT_EQ(18, transaction_.GetResponseInfo()->expected_content_size); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionWithTypecodeA) { + FtpSocketDataProviderFileDownloadWithFileTypecode ctrl_socket; + ctrl_socket.set_data_type('A'); + ExecuteTransaction(&ctrl_socket, "ftp://host/file;type=a", 0, OK); + + // We pass an artificial value of 18 as a response to the SIZE command. + EXPECT_EQ(18, transaction_.GetResponseInfo()->expected_content_size); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionWithTypecodeI) { + FtpSocketDataProviderFileDownloadWithFileTypecode ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file;type=i", 0, OK); + + // We pass an artificial value of 18 as a response to the SIZE command. + EXPECT_EQ(18, transaction_.GetResponseInfo()->expected_content_size); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionMultilineWelcome) { + FtpSocketDataProviderFileDownload ctrl_socket; + ctrl_socket.set_multiline_welcome(true); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionShortReads2) { + FtpSocketDataProviderFileDownload ctrl_socket; + ctrl_socket.set_short_read_limit(2); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionShortReads5) { + FtpSocketDataProviderFileDownload ctrl_socket; + ctrl_socket.set_short_read_limit(5); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionZeroSize) { + FtpSocketDataProviderFileDownloadZeroSize ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionCWD451) { + FtpSocketDataProviderFileDownloadCWD451 ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionVMS) { + FtpSocketDataProviderVMSFileDownload ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionTransferStarting) { + FtpSocketDataProviderFileDownloadTransferStarting ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionInvalidResponse) { + FtpSocketDataProviderFileDownloadInvalidResponse ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvReallyBadFormat) { + FtpSocketDataProviderEvilPasv ctrl_socket("227 Portscan (127,0,0,\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafePort1) { + FtpSocketDataProviderEvilPasv ctrl_socket("227 Portscan (127,0,0,1,0,22)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafePort2) { + // Still unsafe. 1 * 256 + 2 = 258, which is < 1024. + FtpSocketDataProviderEvilPasv ctrl_socket("227 Portscan (127,0,0,1,1,2)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafePort3) { + // Still unsafe. 3 * 256 + 4 = 772, which is < 1024. + FtpSocketDataProviderEvilPasv ctrl_socket("227 Portscan (127,0,0,1,3,4)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafePort4) { + // Unsafe. 8 * 256 + 1 = 2049, which is used by nfs. + FtpSocketDataProviderEvilPasv ctrl_socket("227 Portscan (127,0,0,1,8,1)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafeHost) { + FtpSocketDataProviderEvilPasv ctrl_socket( + "227 Portscan (10,1,2,3,123,456)\r\n", FtpSocketDataProvider::PRE_SIZE); + ctrl_socket.set_use_epsv(GetFamily() != AF_INET); + std::string mock_data("mock-data"); + MockRead data_reads[] = { + MockRead(mock_data.c_str()), + }; + StaticSocketDataProvider data_socket1; + StaticSocketDataProvider data_socket2(data_reads, arraysize(data_reads), + NULL, 0); + mock_socket_factory_.AddSocketDataProvider(&ctrl_socket); + mock_socket_factory_.AddSocketDataProvider(&data_socket1); + mock_socket_factory_.AddSocketDataProvider(&data_socket2); + FtpRequestInfo request_info = GetRequestInfo("ftp://host/file"); + + // Start the transaction. + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Start(&request_info, callback_.callback(), + BoundNetLog())); + ASSERT_EQ(OK, callback_.WaitForResult()); + + // The transaction fires the callback when we can start reading data. That + // means that the data socket should be open. + MockTCPClientSocket* data_socket = + static_cast<MockTCPClientSocket*>(transaction_.data_socket_.get()); + ASSERT_TRUE(data_socket); + ASSERT_TRUE(data_socket->IsConnected()); + + // Even if the PASV response specified some other address, we connect + // to the address we used for control connection (which could be 127.0.0.1 + // or ::1 depending on whether we use IPv6). + for (AddressList::const_iterator it = data_socket->addresses().begin(); + it != data_socket->addresses().end(); ++it) { + EXPECT_NE("10.1.2.3", it->ToStringWithoutPort()); + } +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat1) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||22)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat2) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (||\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat3) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat4) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (||||)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat5) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + const char response[] = "227 Portscan (\0\0\031773\0)\r\n"; + FtpSocketDataProviderEvilEpsv ctrl_socket(response, sizeof(response)-1, + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort1) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||22|)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort2) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||258|)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort3) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||772|)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort4) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||2049|)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvWeirdSep) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan ($$$31744$)\r\n", + FtpSocketDataProvider::PRE_SIZE); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, + DownloadTransactionEvilEpsvWeirdSepUnsafePort) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan ($$$317$)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_UNSAFE_PORT); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvIllegalHost) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|2|::1|31744|)\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadUsername) { + FtpSocketDataProviderEvilLogin ctrl_socket("hello%0Aworld", "test"); + ExecuteTransaction(&ctrl_socket, "ftp://hello%0Aworld:test@host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadPassword) { + FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello%0Dworld"); + ExecuteTransaction(&ctrl_socket, "ftp://test:hello%0Dworld@host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInLogin) { + FtpSocketDataProviderEvilLogin ctrl_socket("hello world", "test"); + ExecuteTransaction(&ctrl_socket, "ftp://hello%20world:test@host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInPassword) { + FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello world"); + ExecuteTransaction(&ctrl_socket, "ftp://test:hello%20world@host/file", 1, OK); +} + +TEST_P(FtpNetworkTransactionTest, EvilRestartUser) { + FtpSocketDataProvider ctrl_socket1; + ctrl_socket1.InjectFailure(FtpSocketDataProvider::PRE_PASSWD, + FtpSocketDataProvider::PRE_QUIT, + "530 Login authentication failed\r\n"); + mock_socket_factory_.AddSocketDataProvider(&ctrl_socket1); + + FtpRequestInfo request_info = GetRequestInfo("ftp://host/file"); + + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Start(&request_info, callback_.callback(), + BoundNetLog())); + ASSERT_EQ(ERR_FTP_FAILED, callback_.WaitForResult()); + + MockRead ctrl_reads[] = { + MockRead("220 host TestFTPd\r\n"), + MockRead("221 Goodbye!\r\n"), + MockRead(SYNCHRONOUS, OK), + }; + MockWrite ctrl_writes[] = { + MockWrite("QUIT\r\n"), + }; + StaticSocketDataProvider ctrl_socket2(ctrl_reads, arraysize(ctrl_reads), + ctrl_writes, arraysize(ctrl_writes)); + mock_socket_factory_.AddSocketDataProvider(&ctrl_socket2); + ASSERT_EQ(ERR_IO_PENDING, + transaction_.RestartWithAuth( + AuthCredentials( + ASCIIToUTF16("foo\nownz0red"), + ASCIIToUTF16("innocent")), + callback_.callback())); + EXPECT_EQ(ERR_MALFORMED_IDENTITY, callback_.WaitForResult()); +} + +TEST_P(FtpNetworkTransactionTest, EvilRestartPassword) { + FtpSocketDataProvider ctrl_socket1; + ctrl_socket1.InjectFailure(FtpSocketDataProvider::PRE_PASSWD, + FtpSocketDataProvider::PRE_QUIT, + "530 Login authentication failed\r\n"); + mock_socket_factory_.AddSocketDataProvider(&ctrl_socket1); + + FtpRequestInfo request_info = GetRequestInfo("ftp://host/file"); + + ASSERT_EQ(ERR_IO_PENDING, + transaction_.Start(&request_info, callback_.callback(), + BoundNetLog())); + ASSERT_EQ(ERR_FTP_FAILED, callback_.WaitForResult()); + + MockRead ctrl_reads[] = { + MockRead("220 host TestFTPd\r\n"), + MockRead("331 User okay, send password\r\n"), + MockRead("221 Goodbye!\r\n"), + MockRead(SYNCHRONOUS, OK), + }; + MockWrite ctrl_writes[] = { + MockWrite("USER innocent\r\n"), + MockWrite("QUIT\r\n"), + }; + StaticSocketDataProvider ctrl_socket2(ctrl_reads, arraysize(ctrl_reads), + ctrl_writes, arraysize(ctrl_writes)); + mock_socket_factory_.AddSocketDataProvider(&ctrl_socket2); + ASSERT_EQ(ERR_IO_PENDING, + transaction_.RestartWithAuth( + AuthCredentials(ASCIIToUTF16("innocent"), + ASCIIToUTF16("foo\nownz0red")), + callback_.callback())); + EXPECT_EQ(ERR_MALFORMED_IDENTITY, callback_.WaitForResult()); +} + +TEST_P(FtpNetworkTransactionTest, Escaping) { + FtpSocketDataProviderEscaping ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/%20%21%22%23%24%25%79%80%81", + 1, OK); +} + +// Test for http://crbug.com/23794. +TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilSize) { + // Try to overflow int64 in the response. + FtpSocketDataProviderEvilSize ctrl_socket( + "213 99999999999999999999999999999999\r\n", + FtpSocketDataProvider::PRE_QUIT); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, ERR_INVALID_RESPONSE); +} + +// Test for http://crbug.com/36360. +TEST_P(FtpNetworkTransactionTest, DownloadTransactionBigSize) { + // Pass a valid, but large file size. The transaction should not fail. + FtpSocketDataProviderEvilSize ctrl_socket( + "213 3204427776\r\n", + FtpSocketDataProvider::PRE_CWD); + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 1, OK); + EXPECT_EQ(3204427776LL, + transaction_.GetResponseInfo()->expected_content_size); +} + +// Regression test for http://crbug.com/25023. +TEST_P(FtpNetworkTransactionTest, CloseConnection) { + FtpSocketDataProviderCloseConnection ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host", 1, ERR_EMPTY_RESPONSE); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailUser) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_USER, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPass) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_PASSWD, + FtpSocketDataProvider::PRE_QUIT, + "530 Login authentication failed\r\n", + ERR_FTP_FAILED); +} + +// Regression test for http://crbug.com/38707. +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPass503) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_PASSWD, + FtpSocketDataProvider::PRE_QUIT, + "503 Bad sequence of commands\r\n", + ERR_FTP_BAD_COMMAND_SEQUENCE); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailSyst) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_SYST, + FtpSocketDataProvider::PRE_PWD, + "599 fail\r\n", + OK); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPwd) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_PWD, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailType) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_TYPE, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailEpsv) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_EPSV, + FtpSocketDataProvider::PRE_NOPASV, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailCwd) { + FtpSocketDataProviderDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host", + FtpSocketDataProvider::PRE_CWD, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailList) { + FtpSocketDataProviderVMSDirectoryListing ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/dir", + FtpSocketDataProvider::PRE_LIST, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailUser) { + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_USER, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailPass) { + FtpSocketDataProviderFileDownload ctrl_socket; + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_PASSWD, + FtpSocketDataProvider::PRE_QUIT, + "530 Login authentication failed\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailSyst) { + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_SYST, + FtpSocketDataProvider::PRE_PWD, + "599 fail\r\n", + OK); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailPwd) { + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_PWD, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailType) { + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_TYPE, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailEpsv) { + // This test makes no sense for IPv4 connections (we don't use EPSV there). + if (GetFamily() == AF_INET) + return; + + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_EPSV, + FtpSocketDataProvider::PRE_NOPASV, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailRetr) { + FtpSocketDataProviderFileDownload ctrl_socket; + // Use unallocated 599 FTP error code to make sure it falls into the generic + // ERR_FTP_FAILED bucket. + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_RETR, + FtpSocketDataProvider::PRE_QUIT, + "599 fail\r\n", + ERR_FTP_FAILED); +} + +TEST_P(FtpNetworkTransactionTest, FileNotFound) { + FtpSocketDataProviderFileNotFound ctrl_socket; + ExecuteTransaction(&ctrl_socket, "ftp://host/file", 2, ERR_FTP_FAILED); +} + +// Test for http://crbug.com/38845. +TEST_P(FtpNetworkTransactionTest, ZeroLengthDirInPWD) { + FtpSocketDataProviderFileDownload ctrl_socket; + TransactionFailHelper(&ctrl_socket, + "ftp://host/file", + FtpSocketDataProvider::PRE_PWD, + FtpSocketDataProvider::PRE_TYPE, + "257 \"\"\r\n", + OK); +} + +INSTANTIATE_TEST_CASE_P(FTP, + FtpNetworkTransactionTest, + ::testing::Values(AF_INET, AF_INET6)); + +} // namespace net diff --git a/chromium/net/ftp/ftp_request_info.h b/chromium/net/ftp/ftp_request_info.h new file mode 100644 index 00000000000..be7702fd3d9 --- /dev/null +++ b/chromium/net/ftp/ftp_request_info.h @@ -0,0 +1,20 @@ +// Copyright (c) 2010 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. + +#ifndef NET_FTP_FTP_REQUEST_INFO_H_ +#define NET_FTP_FTP_REQUEST_INFO_H_ + +#include "url/gurl.h" + +namespace net { + +class FtpRequestInfo { + public: + // The requested URL. + GURL url; +}; + +} // namespace net + +#endif // NET_FTP_FTP_REQUEST_INFO_H_ diff --git a/chromium/net/ftp/ftp_response_info.cc b/chromium/net/ftp/ftp_response_info.cc new file mode 100644 index 00000000000..ddbd1edf3a4 --- /dev/null +++ b/chromium/net/ftp/ftp_response_info.cc @@ -0,0 +1,17 @@ +// Copyright (c) 2011 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_response_info.h" + +namespace net { + +FtpResponseInfo::FtpResponseInfo() + : needs_auth(false), + expected_content_size(-1), + is_directory_listing(false) { +} + +FtpResponseInfo::~FtpResponseInfo() {} + +} // namespace net diff --git a/chromium/net/ftp/ftp_response_info.h b/chromium/net/ftp/ftp_response_info.h new file mode 100644 index 00000000000..8bae8c19931 --- /dev/null +++ b/chromium/net/ftp/ftp_response_info.h @@ -0,0 +1,43 @@ +// Copyright (c) 2010 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. + +#ifndef NET_FTP_FTP_RESPONSE_INFO_H_ +#define NET_FTP_FTP_RESPONSE_INFO_H_ + +#include "base/time/time.h" +#include "net/base/host_port_pair.h" + +namespace net { + +class FtpResponseInfo { + public: + FtpResponseInfo(); + ~FtpResponseInfo(); + + // True if authentication failed and valid authentication credentials are + // needed. + bool needs_auth; + + // The time at which the request was made that resulted in this response. + // For cached responses, this time could be "far" in the past. + base::Time request_time; + + // The time at which the response headers were received. For cached + // responses, this time could be "far" in the past. + base::Time response_time; + + // Expected content size, in bytes, as reported by SIZE command. Only valid + // for file downloads. -1 means unknown size. + int64 expected_content_size; + + // True if the response data is of a directory listing. + bool is_directory_listing; + + // Remote address of the socket which fetched this resource. + HostPortPair socket_address; +}; + +} // namespace net + +#endif // NET_FTP_FTP_RESPONSE_INFO_H_ diff --git a/chromium/net/ftp/ftp_server_type_histograms.cc b/chromium/net/ftp/ftp_server_type_histograms.cc new file mode 100644 index 00000000000..84592ed023f --- /dev/null +++ b/chromium/net/ftp/ftp_server_type_histograms.cc @@ -0,0 +1,33 @@ +// Copyright (c) 2009 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_server_type_histograms.h" + +#include "base/metrics/histogram.h" + +namespace net { + +// 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 FTP server of a given type during that session. In the second +// histogram we tally the number of transactions with FTP server of a given type +// the user has made during that session. +void UpdateFtpServerTypeHistograms(FtpServerType type) { + static bool had_server_type[NUM_OF_SERVER_TYPES]; + if (type >= 0 && type < NUM_OF_SERVER_TYPES) { + if (!had_server_type[type]) { + had_server_type[type] = true; + UMA_HISTOGRAM_ENUMERATION("Net.HadFtpServerType2", + type, NUM_OF_SERVER_TYPES); + } + } + UMA_HISTOGRAM_ENUMERATION("Net.FtpServerTypeCount2", + type, NUM_OF_SERVER_TYPES); +} + +} // namespace net diff --git a/chromium/net/ftp/ftp_server_type_histograms.h b/chromium/net/ftp/ftp_server_type_histograms.h new file mode 100644 index 00000000000..5ae862cd21a --- /dev/null +++ b/chromium/net/ftp/ftp_server_type_histograms.h @@ -0,0 +1,32 @@ +// Copyright (c) 2009 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. + +#ifndef NET_FTP_FTP_SERVER_TYPE_HISTOGRAMS_H_ +#define NET_FTP_FTP_SERVER_TYPE_HISTOGRAMS_H_ + +// The UpdateFtpServerTypeHistograms function collects statistics related +// to the types of FTP servers that our users are encountering. + +namespace net { + +enum FtpServerType { + // Record cases in which we couldn't parse the server's response. That means + // a server type we don't recognize, a security attack (when what we're + // connecting to isn't an FTP server), or a broken server. + SERVER_UNKNOWN = 0, + + SERVER_LS = 1, // Server using /bin/ls -l listing style. + SERVER_WINDOWS = 2, // Server using Windows listing style. + SERVER_VMS = 3, // Server using VMS listing style. + SERVER_NETWARE = 4, // Server using Netware listing style. + SERVER_OS2 = 5, // Server using OS/2 listing style. + + NUM_OF_SERVER_TYPES +}; + +void UpdateFtpServerTypeHistograms(FtpServerType type); + +} // namespace net + +#endif // NET_FTP_FTP_SERVER_TYPE_HISTOGRAMS_H_ diff --git a/chromium/net/ftp/ftp_transaction.h b/chromium/net/ftp/ftp_transaction.h new file mode 100644 index 00000000000..f0e1b41f26d --- /dev/null +++ b/chromium/net/ftp/ftp_transaction.h @@ -0,0 +1,80 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_TRANSACTION_H_ +#define NET_FTP_FTP_TRANSACTION_H_ + +#include "net/base/completion_callback.h" +#include "net/base/io_buffer.h" +#include "net/base/load_states.h" +#include "net/base/net_export.h" + +namespace net { + +class AuthCredentials; +class FtpResponseInfo; +class FtpRequestInfo; +class BoundNetLog; + +// Represents a single FTP transaction. +class NET_EXPORT_PRIVATE FtpTransaction { + public: + // Stops any pending IO and destroys the transaction object. + virtual ~FtpTransaction() {} + + // Starts the FTP transaction (i.e., sends the FTP request). + // + // Returns OK if the transaction could be started synchronously, which means + // that the request was served from the cache (only supported for directory + // listings). ERR_IO_PENDING is returned to indicate that the + // CompletionCallback will be notified once response info is available or if + // an IO error occurs. Any other return value indicates that the transaction + // could not be started. + // + // Regardless of the return value, the caller is expected to keep the + // request_info object alive until Destroy is called on the transaction. + // + // NOTE: The transaction is not responsible for deleting the callback object. + // + // Profiling information for the request is saved to |net_log| if non-NULL. + virtual int Start(const FtpRequestInfo* request_info, + const CompletionCallback& callback, + const BoundNetLog& net_log) = 0; + + // Restarts the FTP transaction with authentication credentials. + virtual int RestartWithAuth(const AuthCredentials& credentials, + const CompletionCallback& callback) = 0; + + // Once response info is available for the transaction, response data may be + // read by calling this method. + // + // Response data is copied into the given buffer and the number of bytes + // copied is returned. ERR_IO_PENDING is returned if response data is not + // yet available. The CompletionCallback is notified when the data copy + // completes, and it is passed the number of bytes that were successfully + // copied. Or, if a read error occurs, the CompletionCallback is notified of + // the error. Any other negative return value indicates that the transaction + // could not be read. + // + // NOTE: The transaction is not responsible for deleting the callback object. + // + virtual int Read(IOBuffer* buf, + int buf_len, + const CompletionCallback& callback) = 0; + + // Returns the response info for this transaction or NULL if the response + // info is not available. + virtual const FtpResponseInfo* GetResponseInfo() const = 0; + + // Returns the load state for this transaction. + virtual LoadState GetLoadState() const = 0; + + // Returns the upload progress in bytes. If there is no upload data, + // zero will be returned. + virtual uint64 GetUploadProgress() const = 0; +}; + +} // namespace net + +#endif // NET_FTP_FTP_TRANSACTION_H_ diff --git a/chromium/net/ftp/ftp_transaction_factory.h b/chromium/net/ftp/ftp_transaction_factory.h new file mode 100644 index 00000000000..db32fbeeac5 --- /dev/null +++ b/chromium/net/ftp/ftp_transaction_factory.h @@ -0,0 +1,29 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_TRANSACTION_FACTORY_H_ +#define NET_FTP_FTP_TRANSACTION_FACTORY_H_ + +#include "net/base/net_export.h" + +namespace net { + +class FtpTransaction; + +// An interface to a class that can create FtpTransaction objects. +class NET_EXPORT FtpTransactionFactory { + public: + virtual ~FtpTransactionFactory() {} + + // Creates a FtpTransaction object. + virtual FtpTransaction* CreateTransaction() = 0; + + // Suspends the creation of new transactions. If |suspend| is false, creation + // of new transactions is resumed. + virtual void Suspend(bool suspend) = 0; +}; + +} // namespace net + +#endif // NET_FTP_FTP_TRANSACTION_FACTORY_H_ diff --git a/chromium/net/ftp/ftp_util.cc b/chromium/net/ftp/ftp_util.cc new file mode 100644 index 00000000000..c5e18c8f79d --- /dev/null +++ b/chromium/net/ftp/ftp_util.cc @@ -0,0 +1,376 @@ +// Copyright (c) 2011 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_util.h" + +#include <map> +#include <vector> + +#include "base/i18n/case_conversion.h" +#include "base/i18n/char_iterator.h" +#include "base/logging.h" +#include "base/memory/singleton.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/strings/string_tokenizer.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "third_party/icu/source/common/unicode/uchar.h" +#include "third_party/icu/source/i18n/unicode/datefmt.h" +#include "third_party/icu/source/i18n/unicode/dtfmtsym.h" + +using base::StringPiece16; + +// For examples of Unix<->VMS path conversions, see the unit test file. On VMS +// a path looks differently depending on whether it's a file or directory. + +namespace net { + +// static +std::string FtpUtil::UnixFilePathToVMS(const std::string& unix_path) { + if (unix_path.empty()) + return std::string(); + + base::StringTokenizer tokenizer(unix_path, "/"); + std::vector<std::string> tokens; + while (tokenizer.GetNext()) + tokens.push_back(tokenizer.token()); + + if (unix_path[0] == '/') { + // It's an absolute path. + + if (tokens.empty()) { + DCHECK_EQ(1U, unix_path.length()); + return "[]"; + } + + if (tokens.size() == 1) + return unix_path.substr(1); // Drop the leading slash. + + std::string result(tokens[0] + ":["); + if (tokens.size() == 2) { + // Don't ask why, it just works that way on VMS. + result.append("000000"); + } else { + result.append(tokens[1]); + for (size_t i = 2; i < tokens.size() - 1; i++) + result.append("." + tokens[i]); + } + result.append("]" + tokens[tokens.size() - 1]); + return result; + } + + if (tokens.size() == 1) + return unix_path; + + std::string result("["); + for (size_t i = 0; i < tokens.size() - 1; i++) + result.append("." + tokens[i]); + result.append("]" + tokens[tokens.size() - 1]); + return result; +} + +// static +std::string FtpUtil::UnixDirectoryPathToVMS(const std::string& unix_path) { + if (unix_path.empty()) + return std::string(); + + std::string path(unix_path); + + if (path[path.length() - 1] != '/') + path.append("/"); + + // Reuse logic from UnixFilePathToVMS by appending a fake file name to the + // real path and removing it after conversion. + path.append("x"); + path = UnixFilePathToVMS(path); + return path.substr(0, path.length() - 1); +} + +// static +std::string FtpUtil::VMSPathToUnix(const std::string& vms_path) { + if (vms_path.empty()) + return "."; + + if (vms_path[0] == '/') { + // This is not really a VMS path. Most likely the server is emulating UNIX. + // Return path as-is. + return vms_path; + } + + if (vms_path == "[]") + return "/"; + + std::string result(vms_path); + if (vms_path[0] == '[') { + // It's a relative path. + ReplaceFirstSubstringAfterOffset(&result, 0, "[.", std::string()); + } else { + // It's an absolute path. + result.insert(0, "/"); + ReplaceSubstringsAfterOffset(&result, 0, ":[000000]", "/"); + ReplaceSubstringsAfterOffset(&result, 0, ":[", "/"); + } + std::replace(result.begin(), result.end(), '.', '/'); + std::replace(result.begin(), result.end(), ']', '/'); + + // Make sure the result doesn't end with a slash. + if (result.length() && result[result.length() - 1] == '/') + result = result.substr(0, result.length() - 1); + + return result; +} + +namespace { + +// Lazy-initialized map of abbreviated month names. +class AbbreviatedMonthsMap { + public: + static AbbreviatedMonthsMap* GetInstance() { + return Singleton<AbbreviatedMonthsMap>::get(); + } + + // Converts abbreviated month name |text| to its number (in range 1-12). + // On success returns true and puts the number in |number|. + bool GetMonthNumber(const base::string16& text, int* number) { + // Ignore the case of the month names. The simplest way to handle that + // is to make everything lowercase. + base::string16 text_lower(base::i18n::ToLower(text)); + + if (map_.find(text_lower) == map_.end()) + return false; + + *number = map_[text_lower]; + return true; + } + + private: + friend struct DefaultSingletonTraits<AbbreviatedMonthsMap>; + + // Constructor, initializes the map based on ICU data. It is much faster + // to do that just once. + AbbreviatedMonthsMap() { + int32_t locales_count; + const icu::Locale* locales = + icu::DateFormat::getAvailableLocales(locales_count); + + for (int32_t locale = 0; locale < locales_count; locale++) { + UErrorCode status(U_ZERO_ERROR); + + icu::DateFormatSymbols format_symbols(locales[locale], status); + + // If we cannot get format symbols for some locale, it's not a fatal + // error. Just try another one. + if (U_FAILURE(status)) + continue; + + int32_t months_count; + const icu::UnicodeString* months = + format_symbols.getShortMonths(months_count); + + for (int32_t month = 0; month < months_count; month++) { + base::string16 month_name(months[month].getBuffer(), + static_cast<size_t>(months[month].length())); + + // Ignore the case of the month names. The simplest way to handle that + // is to make everything lowercase. + month_name = base::i18n::ToLower(month_name); + + map_[month_name] = month + 1; + + // Sometimes ICU returns longer strings, but in FTP listings a shorter + // abbreviation is used (for example for the Russian locale). Make sure + // we always have a map entry for a three-letter abbreviation. + map_[month_name.substr(0, 3)] = month + 1; + } + } + + // Fail loudly if the data returned by ICU is obviously incomplete. + // This is intended to catch cases like http://crbug.com/177428 + // much earlier. Note that the issue above turned out to be non-trivial + // to reproduce - crash data is much better indicator of a problem + // than incomplete bug reports. + CHECK_EQ(1, map_[ASCIIToUTF16("jan")]); + CHECK_EQ(2, map_[ASCIIToUTF16("feb")]); + CHECK_EQ(3, map_[ASCIIToUTF16("mar")]); + CHECK_EQ(4, map_[ASCIIToUTF16("apr")]); + CHECK_EQ(5, map_[ASCIIToUTF16("may")]); + CHECK_EQ(6, map_[ASCIIToUTF16("jun")]); + CHECK_EQ(7, map_[ASCIIToUTF16("jul")]); + CHECK_EQ(8, map_[ASCIIToUTF16("aug")]); + CHECK_EQ(9, map_[ASCIIToUTF16("sep")]); + CHECK_EQ(10, map_[ASCIIToUTF16("oct")]); + CHECK_EQ(11, map_[ASCIIToUTF16("nov")]); + CHECK_EQ(12, map_[ASCIIToUTF16("dec")]); + } + + // Maps lowercase month names to numbers in range 1-12. + std::map<base::string16, int> map_; + + DISALLOW_COPY_AND_ASSIGN(AbbreviatedMonthsMap); +}; + +} // namespace + +// static +bool FtpUtil::AbbreviatedMonthToNumber(const base::string16& text, + int* number) { + return AbbreviatedMonthsMap::GetInstance()->GetMonthNumber(text, number); +} + +// static +bool FtpUtil::LsDateListingToTime(const base::string16& month, + const base::string16& day, + const base::string16& rest, + const base::Time& current_time, + base::Time* result) { + base::Time::Exploded time_exploded = { 0 }; + + if (!AbbreviatedMonthToNumber(month, &time_exploded.month)) { + // Work around garbage sent by some servers in the same column + // as the month. Take just last 3 characters of the string. + if (month.length() < 3 || + !AbbreviatedMonthToNumber(month.substr(month.length() - 3), + &time_exploded.month)) { + return false; + } + } + + if (!base::StringToInt(day, &time_exploded.day_of_month)) + return false; + if (time_exploded.day_of_month > 31) + return false; + + if (!base::StringToInt(rest, &time_exploded.year)) { + // Maybe it's time. Does it look like time (HH:MM)? + if (rest.length() == 5 && rest[2] == ':') { + if (!base::StringToInt(StringPiece16(rest.begin(), rest.begin() + 2), + &time_exploded.hour)) { + return false; + } + + if (!base::StringToInt(StringPiece16(rest.begin() + 3, rest.begin() + 5), + &time_exploded.minute)) { + return false; + } + } else if (rest.length() == 4 && rest[1] == ':') { + // Sometimes it's just H:MM. + if (!base::StringToInt(StringPiece16(rest.begin(), rest.begin() + 1), + &time_exploded.hour)) { + return false; + } + + if (!base::StringToInt(StringPiece16(rest.begin() + 2, rest.begin() + 4), + &time_exploded.minute)) { + return false; + } + } else { + return false; + } + + // Guess the year. + base::Time::Exploded current_exploded; + current_time.LocalExplode(¤t_exploded); + + // If it's not possible for the parsed date to be in the current year, + // use the previous year. + if (time_exploded.month > current_exploded.month || + (time_exploded.month == current_exploded.month && + time_exploded.day_of_month > current_exploded.day_of_month)) { + time_exploded.year = current_exploded.year - 1; + } else { + time_exploded.year = current_exploded.year; + } + } + + // We don't know the time zone of the listing, so just use local time. + *result = base::Time::FromLocalExploded(time_exploded); + return true; +} + +// static +bool FtpUtil::WindowsDateListingToTime(const base::string16& date, + const base::string16& time, + base::Time* result) { + base::Time::Exploded time_exploded = { 0 }; + + // Date should be in format MM-DD-YY[YY]. + std::vector<base::string16> date_parts; + base::SplitString(date, '-', &date_parts); + if (date_parts.size() != 3) + return false; + if (!base::StringToInt(date_parts[0], &time_exploded.month)) + return false; + if (!base::StringToInt(date_parts[1], &time_exploded.day_of_month)) + return false; + if (!base::StringToInt(date_parts[2], &time_exploded.year)) + return false; + if (time_exploded.year < 0) + return false; + // If year has only two digits then assume that 00-79 is 2000-2079, + // and 80-99 is 1980-1999. + if (time_exploded.year < 80) + time_exploded.year += 2000; + else if (time_exploded.year < 100) + time_exploded.year += 1900; + + // Time should be in format HH:MM[(AM|PM)] + if (time.length() < 5) + return false; + + std::vector<base::string16> time_parts; + base::SplitString(time.substr(0, 5), ':', &time_parts); + if (time_parts.size() != 2) + return false; + if (!base::StringToInt(time_parts[0], &time_exploded.hour)) + return false; + if (!base::StringToInt(time_parts[1], &time_exploded.minute)) + return false; + if (!time_exploded.HasValidValues()) + return false; + + if (time.length() > 5) { + if (time.length() != 7) + return false; + base::string16 am_or_pm(time.substr(5, 2)); + if (EqualsASCII(am_or_pm, "PM")) { + if (time_exploded.hour < 12) + time_exploded.hour += 12; + } else if (EqualsASCII(am_or_pm, "AM")) { + if (time_exploded.hour == 12) + time_exploded.hour = 0; + } else { + return false; + } + } + + // We don't know the time zone of the server, so just use local time. + *result = base::Time::FromLocalExploded(time_exploded); + return true; +} + +// static +base::string16 FtpUtil::GetStringPartAfterColumns(const base::string16& text, + int columns) { + base::i18n::UTF16CharIterator iter(&text); + + // TODO(jshin): Is u_isspace the right function to use here? + for (int i = 0; i < columns; i++) { + // Skip the leading whitespace. + while (!iter.end() && u_isspace(iter.get())) + iter.Advance(); + + // Skip the actual text of i-th column. + while (!iter.end() && !u_isspace(iter.get())) + iter.Advance(); + } + + base::string16 result(text.substr(iter.array_pos())); + TrimWhitespace(result, TRIM_ALL, &result); + return result; +} + +} // namespace diff --git a/chromium/net/ftp/ftp_util.h b/chromium/net/ftp/ftp_util.h new file mode 100644 index 00000000000..3e8a899c7d4 --- /dev/null +++ b/chromium/net/ftp/ftp_util.h @@ -0,0 +1,58 @@ +// Copyright (c) 2011 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. + +#ifndef NET_FTP_FTP_UTIL_H_ +#define NET_FTP_FTP_UTIL_H_ + +#include <string> + +#include "base/strings/string16.h" +#include "net/base/net_export.h" + +namespace base { +class Time; +} + +namespace net { + +class NET_EXPORT_PRIVATE FtpUtil { + public: + // Converts Unix file path to VMS path (must be a file, and not a directory). + static std::string UnixFilePathToVMS(const std::string& unix_path); + + // Converts Unix directory path to VMS path (must be a directory). + static std::string UnixDirectoryPathToVMS(const std::string& unix_path); + + // Converts VMS path to Unix-style path. + static std::string VMSPathToUnix(const std::string& vms_path); + + // Converts abbreviated month (like Nov) to its number (in range 1-12). + // Note: in some locales abbreviations are more than three letters long, + // and this function also handles them correctly. + static bool AbbreviatedMonthToNumber(const base::string16& text, int* number); + + // Converts a "ls -l" date listing to time. The listing comes in three + // columns. The first one contains month, the second one contains day + // of month. The third one is either a time (and then we guess the year based + // on |current_time|), or is a year (and then we don't know the time). + static bool LsDateListingToTime(const base::string16& month, + const base::string16& day, + const base::string16& rest, + const base::Time& current_time, + base::Time* result); + + // Converts a Windows date listing to time. Returns true on success. + static bool WindowsDateListingToTime(const base::string16& date, + const base::string16& time, + base::Time* result); + + // Skips |columns| columns from |text| (whitespace-delimited), and returns the + // remaining part, without leading/trailing whitespace. + static base::string16 GetStringPartAfterColumns(const base::string16& text, + int columns); +}; + +} // namespace net + +#endif // NET_FTP_FTP_UTIL_H_ diff --git a/chromium/net/ftp/ftp_util_unittest.cc b/chromium/net/ftp/ftp_util_unittest.cc new file mode 100644 index 00000000000..2aab7f49f5c --- /dev/null +++ b/chromium/net/ftp/ftp_util_unittest.cc @@ -0,0 +1,253 @@ +// Copyright (c) 2011 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_util.h" + +#include "base/basictypes.h" +#include "base/format_macros.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +TEST(FtpUtilTest, UnixFilePathToVMS) { + const struct { + const char* input; + const char* expected_output; + } kTestCases[] = { + { "", "" }, + { "/", "[]" }, + { "/a", "a" }, + { "/a/b", "a:[000000]b" }, + { "/a/b/c", "a:[b]c" }, + { "/a/b/c/d", "a:[b.c]d" }, + { "/a/b/c/d/e", "a:[b.c.d]e" }, + { "a", "a" }, + { "a/b", "[.a]b" }, + { "a/b/c", "[.a.b]c" }, + { "a/b/c/d", "[.a.b.c]d" }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + EXPECT_EQ(kTestCases[i].expected_output, + net::FtpUtil::UnixFilePathToVMS(kTestCases[i].input)) + << kTestCases[i].input; + } +} + +TEST(FtpUtilTest, UnixDirectoryPathToVMS) { + const struct { + const char* input; + const char* expected_output; + } kTestCases[] = { + { "", "" }, + { "/", "" }, + { "/a", "a:[000000]" }, + { "/a/", "a:[000000]" }, + { "/a/b", "a:[b]" }, + { "/a/b/", "a:[b]" }, + { "/a/b/c", "a:[b.c]" }, + { "/a/b/c/", "a:[b.c]" }, + { "/a/b/c/d", "a:[b.c.d]" }, + { "/a/b/c/d/", "a:[b.c.d]" }, + { "/a/b/c/d/e", "a:[b.c.d.e]" }, + { "/a/b/c/d/e/", "a:[b.c.d.e]" }, + { "a", "[.a]" }, + { "a/", "[.a]" }, + { "a/b", "[.a.b]" }, + { "a/b/", "[.a.b]" }, + { "a/b/c", "[.a.b.c]" }, + { "a/b/c/", "[.a.b.c]" }, + { "a/b/c/d", "[.a.b.c.d]" }, + { "a/b/c/d/", "[.a.b.c.d]" }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + EXPECT_EQ(kTestCases[i].expected_output, + net::FtpUtil::UnixDirectoryPathToVMS(kTestCases[i].input)) + << kTestCases[i].input; + } +} + +TEST(FtpUtilTest, VMSPathToUnix) { + const struct { + const char* input; + const char* expected_output; + } kTestCases[] = { + { "", "." }, + { "[]", "/" }, + { "a", "/a" }, + { "a:[000000]", "/a" }, + { "a:[000000]b", "/a/b" }, + { "a:[b]", "/a/b" }, + { "a:[b]c", "/a/b/c" }, + { "a:[b.c]", "/a/b/c" }, + { "a:[b.c]d", "/a/b/c/d" }, + { "a:[b.c.d]", "/a/b/c/d" }, + { "a:[b.c.d]e", "/a/b/c/d/e" }, + { "a:[b.c.d.e]", "/a/b/c/d/e" }, + { "[.a]", "a" }, + { "[.a]b", "a/b" }, + { "[.a.b]", "a/b" }, + { "[.a.b]c", "a/b/c" }, + { "[.a.b.c]", "a/b/c" }, + { "[.a.b.c]d", "a/b/c/d" }, + { "[.a.b.c.d]", "a/b/c/d" }, + { "[.", "" }, + + // UNIX emulation: + { "/", "/" }, + { "/a", "/a" }, + { "/a/b", "/a/b" }, + { "/a/b/c", "/a/b/c" }, + { "/a/b/c/d", "/a/b/c/d" }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + EXPECT_EQ(kTestCases[i].expected_output, + net::FtpUtil::VMSPathToUnix(kTestCases[i].input)) + << kTestCases[i].input; + } +} + +TEST(FtpUtilTest, LsDateListingToTime) { + base::Time mock_current_time; + ASSERT_TRUE(base::Time::FromString("Tue, 15 Nov 1994 12:45:26 GMT", + &mock_current_time)); + + const struct { + // Input. + const char* month; + const char* day; + const char* rest; + + // Expected output. + int expected_year; + int expected_month; + int expected_day_of_month; + int expected_hour; + int expected_minute; + } kTestCases[] = { + { "Nov", "01", "2007", 2007, 11, 1, 0, 0 }, + { "Jul", "25", "13:37", 1994, 7, 25, 13, 37 }, + + // Test date listings in German. + { "M\xc3\xa4r", "13", "2009", 2009, 3, 13, 0, 0 }, + { "Mai", "1", "10:10", 1994, 5, 1, 10, 10 }, + { "Okt", "14", "21:18", 1994, 10, 14, 21, 18 }, + { "Dez", "25", "2008", 2008, 12, 25, 0, 0 }, + + // Test date listings in Russian. + { "\xd1\x8f\xd0\xbd\xd0\xb2", "1", "2011", 2011, 1, 1, 0, 0 }, + { "\xd1\x84\xd0\xb5\xd0\xb2", "1", "2011", 2011, 2, 1, 0, 0 }, + { "\xd0\xbc\xd0\xb0\xd1\x80", "1", "2011", 2011, 3, 1, 0, 0 }, + { "\xd0\xb0\xd0\xbf\xd1\x80", "1", "2011", 2011, 4, 1, 0, 0 }, + { "\xd0\xbc\xd0\xb0\xd0\xb9", "1", "2011", 2011, 5, 1, 0, 0 }, + { "\xd0\xb8\xd1\x8e\xd0\xbd", "1", "2011", 2011, 6, 1, 0, 0 }, + { "\xd0\xb8\xd1\x8e\xd0\xbb", "1", "2011", 2011, 7, 1, 0, 0 }, + { "\xd0\xb0\xd0\xb2\xd0\xb3", "1", "2011", 2011, 8, 1, 0, 0 }, + { "\xd1\x81\xd0\xb5\xd0\xbd", "1", "2011", 2011, 9, 1, 0, 0 }, + { "\xd0\xbe\xd0\xba\xd1\x82", "1", "2011", 2011, 10, 1, 0, 0 }, + { "\xd0\xbd\xd0\xbe\xd1\x8f", "1", "2011", 2011, 11, 1, 0, 0 }, + { "\xd0\xb4\xd0\xb5\xd0\xba", "1", "2011", 2011, 12, 1, 0, 0 }, + + // Test current year detection. + { "Nov", "01", "12:00", 1994, 11, 1, 12, 0 }, + { "Nov", "15", "12:00", 1994, 11, 15, 12, 0 }, + { "Nov", "16", "12:00", 1993, 11, 16, 12, 0 }, + { "Jan", "01", "08:30", 1994, 1, 1, 8, 30 }, + { "Sep", "02", "09:00", 1994, 9, 2, 9, 0 }, + { "Dec", "06", "21:00", 1993, 12, 6, 21, 0 }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s %s %s", i, + kTestCases[i].month, kTestCases[i].day, + kTestCases[i].rest)); + + base::Time time; + ASSERT_TRUE(net::FtpUtil::LsDateListingToTime( + UTF8ToUTF16(kTestCases[i].month), UTF8ToUTF16(kTestCases[i].day), + UTF8ToUTF16(kTestCases[i].rest), mock_current_time, &time)); + + base::Time::Exploded time_exploded; + time.LocalExplode(&time_exploded); + EXPECT_EQ(kTestCases[i].expected_year, time_exploded.year); + EXPECT_EQ(kTestCases[i].expected_month, time_exploded.month); + EXPECT_EQ(kTestCases[i].expected_day_of_month, time_exploded.day_of_month); + EXPECT_EQ(kTestCases[i].expected_hour, time_exploded.hour); + EXPECT_EQ(kTestCases[i].expected_minute, time_exploded.minute); + EXPECT_EQ(0, time_exploded.second); + EXPECT_EQ(0, time_exploded.millisecond); + } +} + +TEST(FtpUtilTest, WindowsDateListingToTime) { + const struct { + // Input. + const char* date; + const char* time; + + // Expected output. + int expected_year; + int expected_month; + int expected_day_of_month; + int expected_hour; + int expected_minute; + } kTestCases[] = { + { "11-01-07", "12:42", 2007, 11, 1, 12, 42 }, + { "11-01-07", "12:42AM", 2007, 11, 1, 0, 42 }, + { "11-01-07", "12:42PM", 2007, 11, 1, 12, 42 }, + + { "11-01-2007", "12:42", 2007, 11, 1, 12, 42 }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s %s", i, + kTestCases[i].date, kTestCases[i].time)); + + base::Time time; + ASSERT_TRUE(net::FtpUtil::WindowsDateListingToTime( + UTF8ToUTF16(kTestCases[i].date), + UTF8ToUTF16(kTestCases[i].time), + &time)); + + base::Time::Exploded time_exploded; + time.LocalExplode(&time_exploded); + EXPECT_EQ(kTestCases[i].expected_year, time_exploded.year); + EXPECT_EQ(kTestCases[i].expected_month, time_exploded.month); + EXPECT_EQ(kTestCases[i].expected_day_of_month, time_exploded.day_of_month); + EXPECT_EQ(kTestCases[i].expected_hour, time_exploded.hour); + EXPECT_EQ(kTestCases[i].expected_minute, time_exploded.minute); + EXPECT_EQ(0, time_exploded.second); + EXPECT_EQ(0, time_exploded.millisecond); + } +} + +TEST(FtpUtilTest, GetStringPartAfterColumns) { + const struct { + const char* text; + int column; + const char* expected_result; + } kTestCases[] = { + { "", 0, "" }, + { "", 1, "" }, + { "foo abc", 0, "foo abc" }, + { "foo abc", 1, "abc" }, + { " foo abc", 0, "foo abc" }, + { " foo abc", 1, "abc" }, + { " foo abc", 2, "" }, + { " foo abc ", 0, "foo abc" }, + { " foo abc ", 1, "abc" }, + { " foo abc ", 2, "" }, + }; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestCases); i++) { + SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s %d", i, + kTestCases[i].text, kTestCases[i].column)); + + EXPECT_EQ(ASCIIToUTF16(kTestCases[i].expected_result), + net::FtpUtil::GetStringPartAfterColumns( + ASCIIToUTF16(kTestCases[i].text), kTestCases[i].column)); + } +} + +} // namespace |