thorium-mirror/other/ftp-support-thorium.patch
Alexander Frick 81c5a21723 min
2025-01-04 21:59:21 -06:00

14183 lines
421 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/android_webview/browser/aw_browser_context.cc b/android_webview/browser/aw_browser_context.cc
index 6167ef5ed42af..6af9e477c4c8a 100644
--- a/android_webview/browser/aw_browser_context.cc
+++ b/android_webview/browser/aw_browser_context.cc
@@ -574,6 +574,9 @@ void AwBrowserContext::ConfigureNetworkContextParams(
// (http://crbug.com/921750).
context_params->enforce_chrome_ct_policy = false;
+ // WebView does not support ftp yet.
+ context_params->enable_ftp_url_support = false;
+
context_params->enable_brotli = true;
context_params->enable_zstd =
base::FeatureList::IsEnabled(net::features::kZstdContentEncoding);
diff --git a/chrome/app/app-Info.plist b/chrome/app/app-Info.plist
index 5654e5c9d5858..08d00c2005d7b 100644
--- a/chrome/app/app-Info.plist
+++ b/chrome/app/app-Info.plist
@@ -233,6 +233,14 @@
<string>https</string>
</array>
</dict>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>FTP site URL</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>ftp</string>
+ </array>
+ </dict>
<dict>
<key>CFBundleURLName</key>
<string>Local file URL</string>
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index ceabbced0f67e..c298005d46bc3 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -13574,6 +13574,9 @@ This can include information about installed software, files, your browser, and
<message name="IDS_DIRECTORY_LISTING_DATE_MODIFIED" desc="When viewing a local directory, this is the text for the column above the last modified dates.">
Date Modified
</message>
+ <message name="IDS_DIRECTORY_LISTING_PARSING_ERROR_BOX_TEXT" desc="Text to show in a box when we failed to parse FTP directory listing.">
+ Oh, no! This server is sending data <ph name="PRODUCT_NAME">$1<ex>Thorium</ex></ph> can't understand. Please <ph name="BEGIN_LINK">&lt;a href="https://issues.chromium.org/issues/new"&gt;</ph>report a bug<ph name="END_LINK">&lt;/a&gt;<ex>&lt;/a&gt;</ex></ph>, and include the <ph name="BEGIN2_LINK">&lt;a href="LOCATION"&gt;</ph>raw listing<ph name="END2_LINK">&lt;/a&gt;</ph>.
+ </message>
<!-- Saving Page-->
<message name="IDS_SAVE_PAGE_DESC_HTML_ONLY" desc="In the Save Page dialog, the description of saving only the HTML of a webpage.">
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index c9e74052c7afb..d271ccc0dca71 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -8542,6 +8542,12 @@ const FeatureEntry kFeatureEntries[] = {
FEATURE_VALUE_TYPE(
heavy_ad_intervention::features::kHeavyAdPrivacyMitigations)},
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ {"enable-ftp", flag_descriptions::kEnableFtpName,
+ flag_descriptions::kEnableFtpDescription, kOsAll,
+ FEATURE_VALUE_TYPE(network::features::kFtpProtocol)},
+#endif
+
#if BUILDFLAG(IS_CHROMEOS_ASH)
{"crostini-container-install",
flag_descriptions::kCrostiniContainerInstallName,
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index e2a95fa3aadaa..5b608e9272838 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -355,6 +355,13 @@ const char kEnableDrDcDescription[] =
"(raster, webgl, video) "
" continues using the gpu main thread.";
+const char kEnableFtpName[] = "Enable support for FTP URLs. Enabled by default. Thorium Flag.";
+const char kEnableFtpDescription[] =
+ "When enabled, the browser will handle navigations to ftp:// URLs by "
+ "either showing a directory listing or downloading the resource over FTP. "
+ "When disabled, the browser has no special handling for ftp:// URLs and "
+ "by default defers handling of the URL to the underlying platform.";
+
const char kTextBasedAudioDescriptionName[] = "Enable audio descriptions.";
const char kTextBasedAudioDescriptionDescription[] =
"When enabled, HTML5 video elements with a 'descriptions' WebVTT track "
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 4ef2b219f1d74..69f9c4f7d9e1a 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -255,6 +255,9 @@ extern const char
extern const char
kEnableExtensionsPermissionsForSupervisedUsersOnDesktopDescription[];
+extern const char kEnableFtpName[];
+extern const char kEnableFtpDescription[];
+
extern const char
kEnableSupervisedUserSkipParentApprovalToInstallExtensionsName[];
extern const char
diff --git a/chrome/browser/net/profile_network_context_service.cc b/chrome/browser/net/profile_network_context_service.cc
index e50641649a959..0f0ef6bd2b0fd 100644
--- a/chrome/browser/net/profile_network_context_service.cc
+++ b/chrome/browser/net/profile_network_context_service.cc
@@ -1190,6 +1190,15 @@ void ProfileNetworkContextService::ConfigureNetworkContextParamsInternal(
network_context_params->hsts_policy_bypass_list.push_back(*string_value);
}
+ // NOTE(mmenke): Keep these protocol handlers and
+ // ProfileIOData::SetUpJobFactoryDefaultsForBuilder in sync with
+ // ProfileIOData::IsHandledProtocol().
+ // TODO(mmenke): Find a better way of handling tracking supported schemes.
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ network_context_params->enable_ftp_url_support =
+ base::FeatureList::IsEnabled(network::features::kFtpProtocol);
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
proxy_config_monitor_.AddToNetworkContextParams(network_context_params);
network_context_params->enable_certificate_reporting = true;
diff --git a/chrome/browser/net/system_network_context_manager.cc b/chrome/browser/net/system_network_context_manager.cc
index 22a62ad698fdb..ec30208a38bf0 100644
--- a/chrome/browser/net/system_network_context_manager.cc
+++ b/chrome/browser/net/system_network_context_manager.cc
@@ -1042,6 +1042,12 @@ SystemNetworkContextManager::CreateNetworkContextParams() {
network_context_params->http_cache_enabled = false;
+ // These are needed for PAC scripts that use FTP URLs.
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ network_context_params->enable_ftp_url_support =
+ base::FeatureList::IsEnabled(network::features::kFtpProtocol);
+#endif
+
proxy_config_monitor_.AddToNetworkContextParams(network_context_params.get());
return network_context_params;
diff --git a/chrome/browser/profiles/profile_io_data.cc b/chrome/browser/profiles/profile_io_data.cc
index 39f96f45bca3a..123e400761e0f 100644
--- a/chrome/browser/profiles/profile_io_data.cc
+++ b/chrome/browser/profiles/profile_io_data.cc
@@ -15,6 +15,7 @@
#include "components/dom_distiller/core/url_constants.h"
#include "extensions/buildflags/buildflags.h"
#include "net/net_buildflags.h"
+#include "services/network/public/cpp/features.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
@@ -54,6 +55,9 @@ bool ProfileIOData::IsHandledProtocol(const std::string& scheme) {
#if !BUILDFLAG(IS_ANDROID)
chrome::kIsolatedAppScheme,
#endif // !BUILDFLAG(IS_ANDROID)
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ url::kFtpScheme,
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
});
return kProtocolList.contains(scheme);
diff --git a/chrome/browser/ui/login/login_tab_helper.cc b/chrome/browser/ui/login/login_tab_helper.cc
index f6702ac512f8d..a54956a7ec00a 100644
--- a/chrome/browser/ui/login/login_tab_helper.cc
+++ b/chrome/browser/ui/login/login_tab_helper.cc
@@ -74,13 +74,16 @@ void LoginTabHelper::DidFinishNavigation(
return;
}
- // Show a login prompt with the navigation's AuthChallengeInfo on HTTP 401/407
- // responses.
- int response_code = navigation_handle->GetResponseHeaders()->response_code();
- if (response_code !=
- net::HttpStatusCode::HTTP_PROXY_AUTHENTICATION_REQUIRED &&
- response_code != net::HttpStatusCode::HTTP_UNAUTHORIZED) {
- return;
+ // Show a login prompt with the navigation's AuthChallengeInfo on FTP
+ // navigations and on HTTP 401/407 responses.
+ if (!navigation_handle->GetURL().SchemeIs(url::kFtpScheme)) {
+ int response_code =
+ navigation_handle->GetResponseHeaders()->response_code();
+ if (response_code !=
+ net::HttpStatusCode::HTTP_PROXY_AUTHENTICATION_REQUIRED &&
+ response_code != net::HttpStatusCode::HTTP_UNAUTHORIZED) {
+ return;
+ }
}
challenge_ = navigation_handle->GetAuthChallengeInfo().value();
diff --git a/chrome/common/net/net_resource_provider.cc b/chrome/common/net/net_resource_provider.cc
index 88d43505881b5..92a452fc5f9e9 100644
--- a/chrome/common/net/net_resource_provider.cc
+++ b/chrome/common/net/net_resource_provider.cc
@@ -35,6 +35,8 @@ struct LazyDirectoryListerCacher {
l10n_util::GetStringUTF8(IDS_DIRECTORY_LISTING_DATE_MODIFIED));
value.Set("language",
l10n_util::GetLanguage(base::i18n::GetConfiguredLocale()));
+ value.Set("listingParsingErrorBoxText",
+ l10n_util::GetStringUTF8(IDS_DIRECTORY_LISTING_PARSING_ERROR_BOX_TEXT));
value.Set("textdirection", base::i18n::IsRTL() ? "rtl" : "ltr");
std::string str = webui::GetI18nTemplateHtml(
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
diff --git a/chrome/installer/util/shell_util.cc b/chrome/installer/util/shell_util.cc
index c087d1c02bca4..aa5d721c5ea93 100644
--- a/chrome/installer/util/shell_util.cc
+++ b/chrome/installer/util/shell_util.cc
@@ -1,4 +1,4 @@
-// Copyright 2012 The Chromium Authors
+// Copyright 2024 The Chromium Authors and Alex313031
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
@@ -1545,10 +1545,10 @@ const wchar_t* ShellUtil::kDefaultFileAssociations[] = {
const wchar_t* ShellUtil::kPotentialFileAssociations[] = {
L".htm", L".html", L".mhtml", L".pdf", L".shtml",
L".svg", L".xht", L".xhtml", L".webp", nullptr};
-const wchar_t* ShellUtil::kBrowserProtocolAssociations[] = {L"http", L"https",
+const wchar_t* ShellUtil::kBrowserProtocolAssociations[] = {L"ftp", L"http", L"https",
nullptr};
const wchar_t* ShellUtil::kPotentialProtocolAssociations[] = {
- L"http", L"https", L"irc", L"mailto", L"mms", L"news", L"nntp",
+ L"ftp", L"http", L"https", L"irc", L"mailto", L"mms", L"news", L"nntp",
L"sms", L"smsto", L"snews", L"tel", L"urn", L"webcal", nullptr};
const wchar_t* ShellUtil::kRegUrlProtocol = L"URL Protocol";
const wchar_t* ShellUtil::kRegApplication = L"\\Application";
diff --git a/chrome/test/data/ftp/dir1/test.html b/chrome/test/data/ftp/dir1/test.html
new file mode 100644
index 0000000000000..ce0c4c91818cd
--- /dev/null
+++ b/chrome/test/data/ftp/dir1/test.html
@@ -0,0 +1,5 @@
+<html>
+<head>
+<title>PASS</title>
+</head>
+</html>
diff --git a/components/safe_browsing/content/browser/safe_browsing_network_context.cc b/components/safe_browsing/content/browser/safe_browsing_network_context.cc
index 37852c47b90d4..8be654c440fc9 100644
--- a/components/safe_browsing/content/browser/safe_browsing_network_context.cc
+++ b/components/safe_browsing/content/browser/safe_browsing_network_context.cc
@@ -133,6 +133,10 @@ class SafeBrowsingNetworkContext::SharedURLLoaderFactory
network_context_params->file_paths->trigger_migration = trigger_migration_;
network_context_params->file_paths->cookie_database_name = base::FilePath(
base::FilePath::StringType(kSafeBrowsingBaseFilename) + kCookiesFile);
+ // These are needed for PAC scripts that use FTP URLs.
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ network_context_params->enable_ftp_url_support = true;
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
network_context_params->enable_encrypted_cookies = false;
return network_context_params;
diff --git a/content/browser/browser_url_handler_impl.cc b/content/browser/browser_url_handler_impl.cc
index 3158ee1460b5b..8f62b65b2ee3e 100644
--- a/content/browser/browser_url_handler_impl.cc
+++ b/content/browser/browser_url_handler_impl.cc
@@ -34,6 +34,7 @@ static bool HandleViewSource(GURL* url, BrowserContext* browser_context) {
std::vector<std::string> all_allowed_sub_schemes({
url::kHttpScheme,
url::kHttpsScheme,
+ url::kFtpScheme,
kChromeUIScheme,
url::kFileScheme,
url::kFileSystemScheme,
diff --git a/content/public/common/content_switches.cc b/content/public/common/content_switches.cc
index 30f63bfc1750f..a38a1839d7207 100644
--- a/content/public/common/content_switches.cc
+++ b/content/public/common/content_switches.cc
@@ -329,6 +329,9 @@ const char kEnableFakeNoAllocDirectCallForTesting[] =
// status:"experimental", which are enabled when running web tests.
const char kEnableBlinkTestFeatures[] = "enable-blink-test-features";
+// Enables support for FTP URLs. See https://crbug.com/333943.
+const char kEnableFtp[] = "enable-ftp";
+
// Disables all RuntimeEnabledFeatures that can be enabled via OriginTrials.
const char kDisableOriginTrialControlledBlinkFeatures[] =
"disable-origin-trial-controlled-blink-features";
diff --git a/content/public/common/content_switches.h b/content/public/common/content_switches.h
index b6f3b21a2b1ff..d01af10dcec28 100644
--- a/content/public/common/content_switches.h
+++ b/content/public/common/content_switches.h
@@ -106,6 +106,7 @@ CONTENT_EXPORT extern const char kEnableExperimentalWebAssemblyFeatures[];
CONTENT_EXPORT extern const char kEnableExperimentalWebAssemblyFeatures[];
CONTENT_EXPORT extern const char kEnableExperimentalWebPlatformFeatures[];
CONTENT_EXPORT extern const char kEnableBlinkTestFeatures[];
+CONTENT_EXPORT extern const char kEnableFtp[];
CONTENT_EXPORT extern const char kEnableGpuMemoryBufferVideoFrames[];
CONTENT_EXPORT extern const char kEnableIsolatedWebAppsInRenderer[];
CONTENT_EXPORT extern const char kEnableLCDText[];
diff --git a/content/public/renderer/render_frame.h b/content/public/renderer/render_frame.h
index c7aa04a3f3edb..61b37176a24ab 100644
--- a/content/public/renderer/render_frame.h
+++ b/content/public/renderer/render_frame.h
@@ -179,6 +179,9 @@ class CONTENT_EXPORT RenderFrame :
virtual blink::AssociatedInterfaceProvider*
GetRemoteAssociatedInterfaces() = 0;
+ // Returns true if this frame is a FTP directory listing.
+ virtual bool IsFTPDirectoryListing() = 0;
+
// Notifies the browser of text selection changes made.
virtual void SetSelectedText(const std::u16string& selection_text,
size_t offset,
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index f362ee7c23241..d34f9fbe475b2 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -2500,6 +2500,10 @@ RenderFrameImpl::GetRemoteAssociatedInterfaces() {
return remote_associated_interfaces_.get();
}
+bool RenderFrameImpl::IsFTPDirectoryListing() {
+ return frame_->GetDocumentLoader()->IsListingFtpDirectory();
+}
+
void RenderFrameImpl::SetSelectedText(const std::u16string& selection_text,
size_t offset,
const gfx::Range& range) {
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index b32fbb5887cb2..626bbdd3b1e5d 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -400,6 +400,7 @@ class CONTENT_EXPORT RenderFrameImpl
mojo::ScopedMessagePipeHandle interface_pipe) override;
blink::AssociatedInterfaceRegistry* GetAssociatedInterfaceRegistry() override;
blink::AssociatedInterfaceProvider* GetRemoteAssociatedInterfaces() override;
+ bool IsFTPDirectoryListing() override;
void SetSelectedText(const std::u16string& selection_text,
size_t offset,
const gfx::Range& range) override;
diff --git a/ios/chrome/browser/profile/model/off_the_record_profile_ios_io_data.mm b/ios/chrome/browser/profile/model/off_the_record_profile_ios_io_data.mm
index 00516bebc6fa3..62b2eceb8e48a 100644
--- a/ios/chrome/browser/profile/model/off_the_record_profile_ios_io_data.mm
+++ b/ios/chrome/browser/profile/model/off_the_record_profile_ios_io_data.mm
@@ -26,6 +26,7 @@
#import "ios/web/public/thread/web_thread.h"
#import "net/cookies/cookie_store.h"
#import "net/disk_cache/disk_cache.h"
+#import "net/ftp/ftp_network_layer.h"
#import "net/http/http_cache.h"
#import "net/http/http_network_session.h"
#import "net/http/http_server_properties.h"
index e229ec4c97bb6..dd33fcf48c550 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -95,6 +95,7 @@ buildflag_header("buildflags") {
flags = [
"POSIX_BYPASS_MMAP=$posix_bypass_mmap",
"DISABLE_FILE_SUPPORT=$disable_file_support",
+ "DISABLE_FTP_SUPPORT=$disable_ftp_support",
"ENABLE_MDNS=$enable_mdns",
"ENABLE_REPORTING=$enable_reporting",
"ENABLE_WEBSOCKETS=$enable_websockets",
@@ -1587,6 +1588,41 @@ component("net") {
]
}
+ if (!disable_ftp_support) {
+ sources += [
+ "ftp/ftp_auth_cache.cc",
+ "ftp/ftp_auth_cache.h",
+ "ftp/ftp_ctrl_response_buffer.cc",
+ "ftp/ftp_ctrl_response_buffer.h",
+ "ftp/ftp_directory_listing_parser.cc",
+ "ftp/ftp_directory_listing_parser.h",
+ "ftp/ftp_directory_listing_parser_ls.cc",
+ "ftp/ftp_directory_listing_parser_ls.h",
+ "ftp/ftp_directory_listing_parser_vms.cc",
+ "ftp/ftp_directory_listing_parser_vms.h",
+ "ftp/ftp_directory_listing_parser_windows.cc",
+ "ftp/ftp_directory_listing_parser_windows.h",
+ "ftp/ftp_network_layer.cc",
+ "ftp/ftp_network_layer.h",
+ "ftp/ftp_network_session.cc",
+ "ftp/ftp_network_session.h",
+ "ftp/ftp_network_transaction.cc",
+ "ftp/ftp_network_transaction.h",
+ "ftp/ftp_request_info.h",
+ "ftp/ftp_response_info.cc",
+ "ftp/ftp_response_info.h",
+ "ftp/ftp_server_type.h",
+ "ftp/ftp_transaction.h",
+ "ftp/ftp_transaction_factory.h",
+ "ftp/ftp_util.cc",
+ "ftp/ftp_util.h",
+ "url_request/ftp_protocol_handler.cc",
+ "url_request/ftp_protocol_handler.h",
+ "url_request/url_request_ftp_job.cc",
+ "url_request/url_request_ftp_job.h",
+ ]
+ }
+
if (enable_websockets) {
sources += [
"server/http_connection.cc",
@@ -3203,6 +3239,21 @@ target(_test_target_type, "net_unittests") {
]
}
+ if (!disable_ftp_support) {
+ sources += [
+ "ftp/ftp_auth_cache_unittest.cc",
+ "ftp/ftp_ctrl_response_buffer_unittest.cc",
+ "ftp/ftp_directory_listing_parser_ls_unittest.cc",
+ "ftp/ftp_directory_listing_parser_unittest.cc",
+ "ftp/ftp_directory_listing_parser_unittest.h",
+ "ftp/ftp_directory_listing_parser_vms_unittest.cc",
+ "ftp/ftp_directory_listing_parser_windows_unittest.cc",
+ "ftp/ftp_network_transaction_unittest.cc",
+ "ftp/ftp_util_unittest.cc",
+ "url_request/url_request_ftp_job_unittest.cc",
+ ]
+ }
+
if (enable_built_in_dns) {
sources += [ "url_request/http_with_dns_over_https_unittest.cc" ]
}
@@ -3720,6 +3771,46 @@ fuzzer_test("net_spdy_headers_to_http_response_headers_fuzzer") {
libfuzzer_options = [ "max_len = 512" ]
}
+if (!disable_ftp_support) {
+ fuzzer_test("net_ftp_ctrl_response_fuzzer") {
+ sources = [ "ftp/ftp_ctrl_response_fuzzer.cc" ]
+ deps = [
+ ":net_fuzzer_test_support",
+ "//base",
+ "//net",
+ ]
+ }
+
+ fuzzer_test("net_ftp_directory_listing_fuzzer") {
+ sources = [ "ftp/ftp_directory_listing_fuzzer.cc" ]
+ deps = [
+ ":net_fuzzer_test_support",
+ "//base",
+ "//net",
+ ]
+
+ # TODO(https://crbug.com/921297): Re-enable once source of timeout is
+ # understood (probably just needs to restrict maximum input size).
+ additional_configs = [ "//testing/libfuzzer:no_clusterfuzz" ]
+ }
+
+ fuzzer_test("net_url_request_ftp_fuzzer") {
+ sources = [ "url_request/url_request_ftp_fuzzer.cc" ]
+ deps = [
+ ":net_fuzzer_test_support",
+ ":test_support",
+ "//base",
+ "//net",
+ ]
+ dict = "data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict"
+ seed_corpus = "data/fuzzer_data/net_url_request_ftp_fuzzer/"
+
+ # TODO(https://crbug.com/962087): Re-enable once source of timeout is
+ # understood (probably just needs to restrict maximum input size).
+ additional_configs = [ "//testing/libfuzzer:no_clusterfuzz" ]
+ }
+}
+
fuzzer_test("net_unescape_url_component_fuzzer") {
sources = [ "base/unescape_url_component_fuzzer.cc" ]
deps = [
diff --git a/net/DEPS b/net/DEPS
index 82956220ad000..d016bd9e7d052 100644
--- a/net/DEPS
+++ b/net/DEPS
@@ -45,6 +45,14 @@ specific_include_rules = {
"+base/i18n",
],
+ "ftp_util\.cc": [
+ "+base/i18n",
+ "+third_party/icu",
+ ],
+ "ftp_directory_listing_parser\.cc": [
+ "+base/i18n",
+ ],
+
"brotli_source_stream\.cc": [
"+third_party/brotli",
],
diff --git a/net/base/auth.h b/net/base/auth.h
index e80006ee9ac79..c5ddb3f652b4e 100644
--- a/net/base/auth.h
+++ b/net/base/auth.h
@@ -31,8 +31,8 @@ class NET_EXPORT AuthChallengeInfo {
// The service issuing the challenge.
url::SchemeHostPort challenger;
- // The authentication scheme used, such as "basic" or "digest". The encoding
- // is ASCII.
+ // The authentication scheme used, such as "basic" or "digest". If the
+ // |source| is FTP_SERVER, this is an empty string. The encoding is ASCII.
std::string scheme;
// The realm of the challenge. May be empty. The encoding is UTF-8.
diff --git a/net/base/dir_header.html b/net/base/dir_header.html
index a2421e94b4465..10539e9ee4a0a 100644
--- a/net/base/dir_header.html
+++ b/net/base/dir_header.html
@@ -81,6 +81,13 @@ function onHasParentDirectory() {
link.href = root + "..";
}
+function onListingParsingError() {
+ var box = document.getElementById("listingParsingErrorBox");
+ box.innerHTML = box.innerHTML.replace("LOCATION", encodeURI(document.location)
+ + "?raw");
+ box.style.display = "block";
+}
+
function sortTable(column) {
var theader = document.getElementById("theader");
var oldOrder = theader.cells[column].dataset.order || '1';
@@ -191,6 +198,13 @@ window.addEventListener('DOMContentLoaded', onLoad);
margin-bottom: 10px;
padding-bottom: 10px;
}
+
+ #listingParsingErrorBox {
+ border: 1px solid black;
+ background: #fae691;
+ padding: 10px;
+ display: none;
+ }
</style>
<title id="title"></title>
@@ -199,6 +213,8 @@ window.addEventListener('DOMContentLoaded', onLoad);
<body>
+<div id="listingParsingErrorBox">$i18nRaw{listingParsingErrorBoxText}</div>
+
<h1 id="header">$i18n{header}</h1>
<div id="parentDirLinkBox" style="display:none">
diff --git a/net/base/net_error_list.h b/net/base/net_error_list.h
index dff37088d6c11..31fa5e6ad19a2 100644
--- a/net/base/net_error_list.h
+++ b/net/base/net_error_list.h
@@ -18,7 +18,7 @@
// 300-399 HTTP errors
// 400-499 Cache errors
// 500-599 ?
-// 600-699 <Obsolete: FTP errors>
+// 600-699 FTP errors
// 700-799 Certificate manager errors
// 800-899 DNS resolver errors
@@ -913,14 +913,36 @@ NET_ERROR(TRUST_TOKEN_OPERATION_FAILED, -506)
// to a local provider (for "platform-provided" issuance).
NET_ERROR(TRUST_TOKEN_OPERATION_SUCCESS_WITHOUT_SENDING_REQUEST, -507)
-// *** Code -600 is reserved (was FTP_PASV_COMMAND_FAILED). ***
-// *** Code -601 is reserved (was FTP_FAILED). ***
-// *** Code -602 is reserved (was FTP_SERVICE_UNAVAILABLE). ***
-// *** Code -603 is reserved (was FTP_TRANSFER_ABORTED). ***
-// *** Code -604 is reserved (was FTP_FILE_BUSY). ***
-// *** Code -605 is reserved (was FTP_SYNTAX_ERROR). ***
-// *** Code -606 is reserved (was FTP_COMMAND_NOT_SUPPORTED). ***
-// *** Code -607 is reserved (was FTP_BAD_COMMAND_SEQUENCE). ***
+// A generic error for failed FTP control connection command.
+// If possible, please use or add a more specific error code.
+NET_ERROR(FTP_FAILED, -601)
+
+// The server cannot fulfill the request at this point. This is a temporary
+// error.
+// FTP response code 421.
+NET_ERROR(FTP_SERVICE_UNAVAILABLE, -602)
+
+// The server has aborted the transfer.
+// FTP response code 426.
+NET_ERROR(FTP_TRANSFER_ABORTED, -603)
+
+// The file is busy, or some other temporary error condition on opening
+// the file.
+// FTP response code 450.
+NET_ERROR(FTP_FILE_BUSY, -604)
+
+// Server rejected our command because of syntax errors.
+// FTP response codes 500, 501.
+NET_ERROR(FTP_SYNTAX_ERROR, -605)
+
+// Server does not support the command we issued.
+// FTP response codes 502, 504.
+NET_ERROR(FTP_COMMAND_NOT_SUPPORTED, -606)
+
+// Server rejected our command because we didn't issue the commands in right
+// order.
+// FTP response code 503.
+NET_ERROR(FTP_BAD_COMMAND_SEQUENCE, -607)
// PKCS #12 import failed due to incorrect password.
NET_ERROR(PKCS12_IMPORT_BAD_PASSWORD, -701)
diff --git a/net/base/port_util.cc b/net/base/port_util.cc
index a70f9e9b21a33..5f3d1364d8c62 100644
--- a/net/base/port_util.cc
+++ b/net/base/port_util.cc
@@ -139,6 +139,11 @@ bool IsPortAllowedForScheme(int port, std::string_view url_scheme) {
if (g_explicitly_allowed_ports.Get().count(port) > 0)
return true;
+ // FTP requests are permitted to use port 21.
+ if ((base::ToLowerASCII(url_scheme) == url::kFtpScheme) && port == 21) {
+ return true;
+ }
+
// Finally check against the generic list of restricted ports for all
// schemes.
for (int restricted_port : kRestrictedPorts) {
diff --git a/net/data/ftp/dir-listing-ls-1 b/net/data/ftp/dir-listing-ls-1
new file mode 100644
index 0000000000000..e99088e773075
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 .message
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README
+-rw-r--r-- 1 ftp ftp 560 Sep 28 2007 index.html
+drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-1-utf8 b/net/data/ftp/dir-listing-ls-1-utf8
new file mode 100644
index 0000000000000..7f9a33339aadd
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1-utf8
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 .message
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README
+-rw-r--r-- 1 ftp ftp 560 Sep 28 2007 こんにちは.html
+drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-1-utf8.expected b/net/data/ftp/dir-listing-ls-1-utf8.expected
new file mode 100644
index 0000000000000..8e4508f6e0a72
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1-utf8.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+5
+15
+18
+11
+
+d
+..
+-1
+1994
+5
+15
+18
+11
+
+-
+.message
+528
+2007
+11
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+こんにちは.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+8
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-1.expected b/net/data/ftp/dir-listing-ls-1.expected
new file mode 100644
index 0000000000000..40375902c4c8a
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+5
+15
+18
+11
+
+d
+..
+-1
+1994
+5
+15
+18
+11
+
+-
+.message
+528
+2007
+11
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+index.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+8
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-10 b/net/data/ftp/dir-listing-ls-10
new file mode 100644
index 0000000000000..deff68fc29264
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-10
@@ -0,0 +1,10 @@
+Gesamt 4352
+---------- 1 1 1 0 Okt 25 1999 .notar
+lrwxrwxrwx 1 1 1 7 Okt 23 2007 bin -> usr/bin
+d--x--x--x 1 2 2 512 Apr 23 2002 dev
+d--x--x--x 1 2 2 512 Apr 1 2004 etc
+drwx------ 1 7 root 1536 Aug 14 13:49 lost+found
+drwxr-xr-x 1 3 1 512 M<>r 10 2003 private
+drwxrwsr-x 1 25 1260 1024 Aug 10 2006 pub
+-rw------- 1 1 1 2211496 Okt 23 2007 restoresymtable
+d--x--x--x 1 6 2 512 Apr 23 2002 usr
diff --git a/net/data/ftp/dir-listing-ls-10.expected b/net/data/ftp/dir-listing-ls-10.expected
new file mode 100644
index 0000000000000..d8f7a60941813
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-10.expected
@@ -0,0 +1,80 @@
+-
+.notar
+0
+1999
+10
+25
+0
+0
+
+l
+bin
+-1
+2007
+10
+23
+0
+0
+
+d
+dev
+-1
+2002
+4
+23
+0
+0
+
+d
+etc
+-1
+2004
+4
+1
+0
+0
+
+d
+lost+found
+-1
+1994
+8
+14
+13
+49
+
+d
+private
+-1
+2003
+3
+10
+0
+0
+
+d
+pub
+-1
+2006
+8
+10
+0
+0
+
+-
+restoresymtable
+2211496
+2007
+10
+23
+0
+0
+
+d
+usr
+-1
+2002
+4
+23
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-11 b/net/data/ftp/dir-listing-ls-11
new file mode 100644
index 0000000000000..f81fda61f383d
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-11
@@ -0,0 +1,8 @@
+total 14
+drwxr-xr-x 2 other 512 Feb 25 2009 beid
+lrwxrwxrwx 1 bin 9 May 1 2007 bin -> ./usr/bin
+d--x--x--x 2 sys 512 May 1 2007 dev
+d--x--x--x 5 sys 512 May 1 2007 etc
+drwxr-xr-x 2 sys 512 Mar 27 2009 pub
+drwxr-xr-x 3 other 512 Apr 11 2007 tigerd1
+d--x--x--x 6 sys 512 May 1 2007 usr
diff --git a/net/data/ftp/dir-listing-ls-11.expected b/net/data/ftp/dir-listing-ls-11.expected
new file mode 100644
index 0000000000000..82f1a9ddaa65c
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-11.expected
@@ -0,0 +1,62 @@
+d
+beid
+-1
+2009
+2
+25
+0
+0
+
+l
+bin
+-1
+2007
+5
+1
+0
+0
+
+d
+dev
+-1
+2007
+5
+1
+0
+0
+
+d
+etc
+-1
+2007
+5
+1
+0
+0
+
+d
+pub
+-1
+2009
+3
+27
+0
+0
+
+d
+tigerd1
+-1
+2007
+4
+11
+0
+0
+
+d
+usr
+-1
+2007
+5
+1
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-12 b/net/data/ftp/dir-listing-ls-12
new file mode 100644
index 0000000000000..7eee0bd1f242e
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-12
@@ -0,0 +1,8 @@
+total 14
+lrwxrwxrwx 1 other 7 Sep 1 2005 bin -> usr/bin
+dr-xr-xr-x 2 other 512 Aug 9 2004 dev
+dr-xr-xr-x 2 other 512 Sep 28 2006 etc
+drwxr-xr-x 2 other 512 Sep 28 2006 msgs
+drwxr-xr-x 53 other 1024 Jun 30 09:52 pub
+dr-xr-xr-x 5 other 512 Aug 9 2004 usr
+drwxr-xr-x 2 other 512 Aug 9 2004 var
diff --git a/net/data/ftp/dir-listing-ls-12.expected b/net/data/ftp/dir-listing-ls-12.expected
new file mode 100644
index 0000000000000..c2f323207b8f2
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-12.expected
@@ -0,0 +1,62 @@
+l
+bin
+-1
+2005
+9
+1
+0
+0
+
+d
+dev
+-1
+2004
+8
+9
+0
+0
+
+d
+etc
+-1
+2006
+9
+28
+0
+0
+
+d
+msgs
+-1
+2006
+9
+28
+0
+0
+
+d
+pub
+-1
+1994
+6
+30
+9
+52
+
+d
+usr
+-1
+2004
+8
+9
+0
+0
+
+d
+var
+-1
+2004
+8
+9
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-13 b/net/data/ftp/dir-listing-ls-13
new file mode 100644
index 0000000000000..b2b7bff5a24fd
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-13
@@ -0,0 +1,3 @@
+-r--r--r-- 1 ftp -A--- 93064 Dec 28 2007 test.jpg
+dr--r--r-- 1 ftp ----- 0 Nov 17 17:08 kernels
+-r--r--r-- 1 ftp -A--- 13274 Mar 1 2006 UpTime.exe
diff --git a/net/data/ftp/dir-listing-ls-13.expected b/net/data/ftp/dir-listing-ls-13.expected
new file mode 100644
index 0000000000000..049f3bf497179
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-13.expected
@@ -0,0 +1,26 @@
+-
+test.jpg
+93064
+2007
+12
+28
+0
+0
+
+d
+kernels
+-1
+1993
+11
+17
+17
+8
+
+-
+UpTime.exe
+13274
+2006
+3
+1
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-14 b/net/data/ftp/dir-listing-ls-14
new file mode 100644
index 0000000000000..84edee4ef3b44
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-14
@@ -0,0 +1,3 @@
+-rwxr-xr-x 0 214 214 Jun 30 2005 !readme
+drwxr-xr-x folder 0 Jul 17 2006 online
+-rwxr-xr-x 332 7 339 Feb 5 2004 welcome.txt
diff --git a/net/data/ftp/dir-listing-ls-14.expected b/net/data/ftp/dir-listing-ls-14.expected
new file mode 100644
index 0000000000000..05c039897fa61
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-14.expected
@@ -0,0 +1,26 @@
+-
+!readme
+214
+2005
+6
+30
+0
+0
+
+d
+online
+-1
+2006
+7
+17
+0
+0
+
+-
+welcome.txt
+339
+2004
+2
+5
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-15 b/net/data/ftp/dir-listing-ls-15
new file mode 100644
index 0000000000000..ee0de6ff0b1e7
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-15
@@ -0,0 +1,5 @@
+total 30
+---------- 1 root other 0 Mar 26 2004 .notar
+d-wx-wx-wt+ 4 ftp 989 512 Dec 8 15:54 incoming
+-r--r--r-- 2 ftp 989 7196 Aug 22 2007 incoming.README
+drwxr-xr-x+ 16 root sys 512 Sep 25 09:56 pub
diff --git a/net/data/ftp/dir-listing-ls-15.expected b/net/data/ftp/dir-listing-ls-15.expected
new file mode 100644
index 0000000000000..c521d00914429
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-15.expected
@@ -0,0 +1,35 @@
+-
+.notar
+0
+2004
+3
+26
+0
+0
+
+d
+incoming
+-1
+1993
+12
+8
+15
+54
+
+-
+incoming.README
+7196
+2007
+8
+22
+0
+0
+
+d
+pub
+-1
+1994
+9
+25
+9
+56
diff --git a/net/data/ftp/dir-listing-ls-16 b/net/data/ftp/dir-listing-ls-16
new file mode 100644
index 0000000000000..4c2f783c05435
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-16
@@ -0,0 +1,7 @@
+dr-xr-xr-x 1 owner group 0 Nov 28 2008 documentales
+dr-xr-xr-x 1 owner group 0 Nov 28 2008 dosier
+dr-xr-xr-x 1 owner group 0 Dec 1 2008 promos
+dr-xr-xr-x 1 owner group 0 Nov 28 2008 Sue<75>os_futbol
+dr-xr-xr-x 1 owner group 0 Nov 2 15:53 test
+dr-xr-xr-x 1 owner group 0 Nov 25 10:04 tmp
+-r-xr-xr-x 1 owner group 125 Oct 11 2007 Gastronom<6F>a.txt
diff --git a/net/data/ftp/dir-listing-ls-16.expected b/net/data/ftp/dir-listing-ls-16.expected
new file mode 100644
index 0000000000000..90e39e653a78c
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-16.expected
@@ -0,0 +1,62 @@
+d
+documentales
+-1
+2008
+11
+28
+0
+0
+
+d
+dosier
+-1
+2008
+11
+28
+0
+0
+
+d
+promos
+-1
+2008
+12
+1
+0
+0
+
+d
+Sueños_futbol
+-1
+2008
+11
+28
+0
+0
+
+d
+test
+-1
+1994
+11
+2
+15
+53
+
+d
+tmp
+-1
+1993
+11
+25
+10
+4
+
+-
+Gastronomía.txt
+125
+2007
+10
+11
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-17 b/net/data/ftp/dir-listing-ls-17
new file mode 100644
index 0000000000000..c07fbf3e34f1e
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-17
@@ -0,0 +1 @@
+ftpd-BSD: .: Permission denied
diff --git a/net/data/ftp/dir-listing-ls-17.expected b/net/data/ftp/dir-listing-ls-17.expected
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/net/data/ftp/dir-listing-ls-18 b/net/data/ftp/dir-listing-ls-18
new file mode 100644
index 0000000000000..a504074577592
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-18
@@ -0,0 +1,3 @@
+-rw-r--r-- 1 ftpadmin ftpadmin125435904 Apr 9 2008 .pureftpd-upload.47fcbb3c.849.191b.cd40d08a
+-rw-r--r-- 1 ftpadmin ftpadmin153198592 Nov 20 2008 .pureftpd-upload.4925a00d.849.668d.9ea5b3ed
+-rw-r--r-- 1 ftpadmin ftpadmin 8760 Nov 24 2008 .pureftpd-upload.492b0a03.849.12d.bf5d2bc6
diff --git a/net/data/ftp/dir-listing-ls-18.expected b/net/data/ftp/dir-listing-ls-18.expected
new file mode 100644
index 0000000000000..1f54f350fb0d4
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-18.expected
@@ -0,0 +1,26 @@
+-
+.pureftpd-upload.47fcbb3c.849.191b.cd40d08a
+-1
+2008
+4
+9
+0
+0
+
+-
+.pureftpd-upload.4925a00d.849.668d.9ea5b3ed
+-1
+2008
+11
+20
+0
+0
+
+-
+.pureftpd-upload.492b0a03.849.12d.bf5d2bc6
+8760
+2008
+11
+24
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-19 b/net/data/ftp/dir-listing-ls-19
new file mode 100644
index 0000000000000..ac4c907efce4d
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-19
@@ -0,0 +1,2 @@
+drwxr-xr-x 2 0 0 4096 Mar 18 2007
+-rw-r--r-- 1 48 0 4327486 Jun 16 2006 junorelease.zip
diff --git a/net/data/ftp/dir-listing-ls-19.expected b/net/data/ftp/dir-listing-ls-19.expected
new file mode 100644
index 0000000000000..9f0f29744f5b1
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-19.expected
@@ -0,0 +1,8 @@
+-
+junorelease.zip
+4327486
+2006
+6
+16
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-2 b/net/data/ftp/dir-listing-ls-2
new file mode 100644
index 0000000000000..052d63da4c393
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-2
@@ -0,0 +1,7 @@
+drwxr-xr-x 3 0 0 4096 Sep 18 2008 .
+drwxr-xr-x 3 0 0 4096 Sep 18 2008 ..
+lrwxrwxrwx 1 0 509 1 Nov 08 2007 ftp -> .
+lrwxrwxrwx 1 0 0 3 Oct 12 2007 mirror -> pub
+lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub
+lrwxrwxrwx 1 0 0 27 Oct 16 2007 site -> vol/1/.CLUSTER/var_ftp/site
+drwxr-xr-x 7 0 0 4096 Jul 02 2008 vol
diff --git a/net/data/ftp/dir-listing-ls-2.expected b/net/data/ftp/dir-listing-ls-2.expected
new file mode 100644
index 0000000000000..315f0a55fe2b1
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-2.expected
@@ -0,0 +1,62 @@
+d
+.
+-1
+2008
+9
+18
+0
+0
+
+d
+..
+-1
+2008
+9
+18
+0
+0
+
+l
+ftp
+-1
+2007
+11
+8
+0
+0
+
+l
+mirror
+-1
+2007
+10
+12
+0
+0
+
+l
+pub
+-1
+2008
+9
+18
+0
+0
+
+l
+site
+-1
+2007
+10
+16
+0
+0
+
+d
+vol
+-1
+2008
+7
+2
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-20 b/net/data/ftp/dir-listing-ls-20
new file mode 100644
index 0000000000000..b8cb80b29c728
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-20
@@ -0,0 +1,18 @@
+drwxrwxr-x 17 ftp ftp 4096 Nov 01 16:27 .
+drwxr-xr-x 7 ftp ftp 4096 Apr 03 2010 ..
+drwxrwxrwx 5 ftp ftp 4096 May 26 15:51 2012_-_2012-(2009)-[1080p]_[BD]
+-rw-rw-rw- 1 ftp ftp 4931 Jun 08 15:24 _READ_ME.txt
+drwxrwxrwx 5 ftp ftp 4096 Nov 01 16:27 <20><><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>_-_Face_Off-(1997)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 22 2010 <20><><EFBFBD><EFBFBD><EFBFBD>_-_Up-(2009)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 May 27 18:12 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD>_-_Total_Recall-(1990)-[1080p]_[BD]
+drwxrwxrwx 3 ftp ftp 4096 May 21 14:28 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Law_Abiding_Citizen_[Unrated_Edition]-(2009)-[1080p]_[BD_Remux]
+drwxrwxrwx 5 ftp ftp 4096 Jan 21 2010 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Cloverfield-(2008)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 26 13:43 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Pandorum-(2009)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 27 14:18 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Last_Samurai-(2003)-[1080p]_[BD]
+drwxrwxrwx 6 ftp ftp 4096 Jan 27 2010 <20><><EFBFBD><EFBFBD><EFBFBD>_9_-_District_9-(2009)-[1080p]_[BD]
+drwxrwxrwx 7 ftp ftp 4096 Jan 01 2010 <20><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Rocky_The_Undisputed_Collection-(1976_1979_1982_1985_1990)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 31 12:57 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Surrogates-(2009)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 28 2010 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_-_The_Fast_and_the_Furious-Tokyo_Drift-(2006)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 08 2010 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Fast_and_the_Furious-(2001)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 06 2010 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_2_-_2_Fast_2_Furious-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 08 15:26 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_4_-_Fast_&_Furious-(2009)-[1080p]_[BD]
diff --git a/net/data/ftp/dir-listing-ls-20.expected b/net/data/ftp/dir-listing-ls-20.expected
new file mode 100644
index 0000000000000..2b51bd72abe55
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-20.expected
@@ -0,0 +1,161 @@
+d
+.
+-1
+1994
+11
+1
+16
+27
+
+d
+..
+-1
+2010
+4
+3
+0
+0
+
+d
+2012_-_2012-(2009)-[1080p]_[BD]
+-1
+1994
+5
+26
+15
+51
+
+-
+_READ_ME.txt
+4931
+1994
+6
+8
+15
+24
+
+d
ез_лица_-_Face_Off-(1997)-[1080p]_[BD]
+-1
+1994
+11
+1
+16
+27
+
+d
+Вверх_-_Up-(2009)-[1080p]_[BD]
+-1
+2010
+1
+22
+0
+0
+
+d
+Вспомнить_все_-_Total_Recall-(1990)-[1080p]_[BD]
+-1
+1994
+5
+27
+18
+12
+
+d
+Законопослушный_гражданин_[Расширенная_версия]_-_Law_Abiding_Citizen_[Unrated_Edition]-(2009)-[1080p]_[BD_Remux]
+-1
+1994
+5
+21
+14
+28
+
+d
+Монстро_-_Cloverfield-(2008)-[1080p]_[BD]
+-1
+2010
+1
+21
+0
+0
+
+d
андорум_-_Pandorum-(2009)-[1080p]_[BD]
+-1
+1994
+5
+26
+13
+43
+
+d
оследний_самурай_-_The_Last_Samurai-(2003)-[1080p]_[BD]
+-1
+1994
+5
+27
+14
+18
+
+d
+Район_9_-_District_9-(2009)-[1080p]_[BD]
+-1
+2010
+1
+27
+0
+0
+
+d
+Рокки_Антология_-_Rocky_The_Undisputed_Collection-(1976_1979_1982_1985_1990)-[1080p]_[BD]
+-1
+2010
+1
+1
+0
+0
+
+d
+Суррогаты_-_Surrogates-(2009)-[1080p]_[BD]
+-1
+1994
+5
+31
+12
+57
+
+d
+Тройной_Форсаж-Токийский_Дрифт_-_The_Fast_and_the_Furious-Tokyo_Drift-(2006)-[1080p]_[BD]
+-1
+2010
+1
+28
+0
+0
+
+d
орсаж_-_The_Fast_and_the_Furious-(2001)-[1080p]_[BD]
+-1
+2010
+1
+8
+0
+0
+
+d
орсаж_2_-_2_Fast_2_Furious-(2003)-[1080p]_[BD]
+-1
+2010
+1
+6
+0
+0
+
+d
орсаж_4_-_Fast_&_Furious-(2009)-[1080p]_[BD]
+-1
+1994
+6
+8
+15
+26
diff --git a/net/data/ftp/dir-listing-ls-21 b/net/data/ftp/dir-listing-ls-21
new file mode 100644
index 0000000000000..197177df70ace
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-21
@@ -0,0 +1,27 @@
+drwxrwxr-x 26 ftp ftp 4096 Jul 15 2009 .
+drwxr-xr-x 7 ftp ftp 4096 Apr 03 2010 ..
+-rw-rw-rw- 1 ftp ftp 4931 Jun 08 15:24 _READ_ME.txt
+drwxr-xr-x 5 ftp ftp 4096 Apr 27 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Avalon-(2001)-[1080p]_[BD_Remux]
+drwxrwxrwx 5 ftp ftp 4096 Jun 15 2009 <20><><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Bruce_Almighty-(2003)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 15 2009 <20><><EFBFBD><EFBFBD>-<2D>_-_WALL-E-(2008)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 28 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>_007-<2D><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_James_Bond_007-Quantum_of_Solace-(2008)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 15 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_c<5F><63><EFBFBD><EFBFBD><EFBFBD>_-_Dead_Space-Downfall-(2008)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_1_-_Madagascar_1-(2005)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_2_-_Madagascar-Escape_2_Africa-(2008)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 13 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Matrix-Reloaded-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 14 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Matrix-Revolutions-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 12 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Matrix-(1999)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jul 02 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD>_3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD]
+drwxr-xr-x 3 ftp ftp 4096 May 01 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Island-(2005)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_3_-_Transporter_3-(2008)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 02 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>-<2D><>_<EFBFBD><5F><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_-_Pirates_of_the_Caribbean-At_World's_End-(2007)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 03 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Pirates_of_the_Caribbean-The_Curse_of_the_Black_Pearl-(2003)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 02 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Pirates_of_the_Caribbean-Dead_Man's_Chest-(2006)-[1080p]_[BD]
+drwxrwxr-x 3 ftp ftp 4096 May 01 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Ghost_Rider-(2007)-[1080p]_[BD]
+drwxr-xr-x 5 ftp ftp 4096 Apr 29 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Princess_Bride-(1987)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jun 08 2009 <20><><EFBFBD><EFBFBD>_<EFBFBD>_101_<31><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Sex_and_Death_101-(2007)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 May 01 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>_-_Transformers-Bonus_Disk-(2007)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 30 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Transformers-(2007)-[1080p]_[BD]
+drwxrwxrwx 6 ftp ftp 4096 Jun 07 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>_-_The_Thirteenth_Floor-(1999)-[1080p]_[BD]
+drwxrwxr-x 3 ftp ftp 4096 May 04 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD>_-_Street_Fighter-(1994)-[1080p]_[BD_Remux]
+drwxr-xr-x 5 ftp ftp 4096 Mar 15 2009 <20><><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_What_Woman_Want-(2000)-[1080p]_[BD]
diff --git a/net/data/ftp/dir-listing-ls-21.expected b/net/data/ftp/dir-listing-ls-21.expected
new file mode 100644
index 0000000000000..d86a006012fa4
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-21.expected
@@ -0,0 +1,242 @@
+d
+.
+-1
+2009
+7
+15
+0
+0
+
+d
+..
+-1
+2010
+4
+3
+0
+0
+
+-
+_READ_ME.txt
+4931
+1994
+6
+8
+15
+24
+
+d
+Авалон_-_Avalon-(2001)-[1080p]_[BD_Remux]
+-1
+2009
+4
+27
+0
+0
+
+d
рюссемогущий_-_Bruce_Almighty-(2003)-[1080p]_[BD]
+-1
+2009
+6
+15
+0
+0
+
+d
+ВАЛЛ-И_-_WALL-E-(2008)-[1080p]_[BD]
+-1
+2009
+4
+15
+0
+0
+
+d
+Джеймсонд_007-Квант_милосердия_-_James_Bond_007-Quantum_of_Solace-(2008)-[1080p]_[BD]
+-1
+2009
+4
+28
+0
+0
+
+d
+Космос-Территория_cмерти_-_Dead_Space-Downfall-(2008)-[1080p]_[BD]
+-1
+2009
+4
+15
+0
+0
+
+d
+Мадагаскар_1_-_Madagascar_1-(2005)-[1080p]_[BD]
+-1
+2009
+7
+3
+0
+0
+
+d
+Мадагаскар_2_-_Madagascar-Escape_2_Africa-(2008)-[1080p]_[BD]
+-1
+2009
+7
+3
+0
+0
+
+d
+Матрица-Перезагрузка_-_The_Matrix-Reloaded-(2003)-[1080p]_[BD]
+-1
+2009
+6
+13
+0
+0
+
+d
+Матрица-Революция_-_The_Matrix-Revolutions-(2003)-[1080p]_[BD]
+-1
+2009
+6
+14
+0
+0
+
+d
+Матрица_-_The_Matrix-(1999)-[1080p]_[BD]
+-1
+2009
+6
+12
+0
+0
+
+d
+Обитель_зла_3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD]
+-1
+2009
+7
+2
+0
+0
+
+d
+Остров_-_The_Island-(2005)-[1080p]_[BD]
+-1
+2009
+5
+1
+0
+0
+
+d
еревозчик_3_-_Transporter_3-(2008)-[1080p]_[BD]
+-1
+2009
+7
+3
+0
+0
+
+d
+Пираты_Карибскогооря-Нараю_Света_-_Pirates_of_the_Caribbean-At_World's_End-(2007)-[1080p]_[BD]
+-1
+2009
+5
+2
+0
+0
+
+d
+Пираты_Карибскогооря-Проклятиеерной_Жемчужины_-_Pirates_of_the_Caribbean-The_Curse_of_the_Black_Pearl-(2003)-[1080p]_[BD]
+-1
+2009
+5
+3
+0
+0
+
+d
+Пираты_Карибскогооря-Сундук_мертвеца_-_Pirates_of_the_Caribbean-Dead_Man's_Chest-(2006)-[1080p]_[BD]
+-1
+2009
+5
+2
+0
+0
+
+d
ризрачный_гонщик_-_Ghost_Rider-(2007)-[1080p]_[BD]
+-1
+2009
+5
+1
+0
+0
+
+d
+Принцесса-невеста_-_The_Princess_Bride-(1987)-[1080p]_[BD]
+-1
+2009
+4
+29
+0
+0
+
+d
+Секс_и_101_смерть_-_Sex_and_Death_101-(2007)-[1080p]_[BD]
+-1
+2009
+6
+8
+0
+0
+
+d
+Трансформеры-Бонус_диск_-_Transformers-Bonus_Disk-(2007)-[1080p]_[BD]
+-1
+2009
+5
+1
+0
+0
+
+d
+Трансформеры_-_Transformers-(2007)-[1080p]_[BD]
+-1
+2009
+4
+30
+0
+0
+
+d
+Тринадцатый_этаж_-_The_Thirteenth_Floor-(1999)-[1080p]_[BD]
+-1
+2009
+6
+7
+0
+0
+
+d
+Уличный_боец_-_Street_Fighter-(1994)-[1080p]_[BD_Remux]
+-1
+2009
+5
+4
+0
+0
+
+d
его_хотят_женщины_-_What_Woman_Want-(2000)-[1080p]_[BD]
+-1
+2009
+3
+15
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-22 b/net/data/ftp/dir-listing-ls-22
new file mode 100644
index 0000000000000..d8a01fb6b5a8e
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-22
@@ -0,0 +1,32 @@
+drwxrwxr-x 5 ftp ftp 12288 Oct 20 17:04 .
+drwxr-xr-x 7 ftp ftp 4096 Apr 03 2010 ..
+-rw-rw-rw- 1 ftp ftp 4931 Jun 08 15:23 _READ_ME.txt
+drwxrwxrwx 4 ftp ftp 4096 May 31 16:26 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Avatar-(2009)-[1080p]_[BD]
+-rwxrwxr-x 1 ftp ftp 17577705968 Mar 08 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_1_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_American_Pie_1_[Unrated_Edition]-(1999)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 15512934868 Mar 16 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD>_-_Snatch-(2000)-[1080i]_[HDTV].ts
+drwxrwxrwx 2 ftp ftp 4096 Jun 03 19:07 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD>_-_Snatch-(2000)-[1080p]_[BD_Remux]
+-rwxrwxr-x 1 ftp ftp 8900589105 Mar 24 2009 <20><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_-_War_of_the_Worlds-(2005)-[720p]_[HDTV].mkv
+-rwxrwxr-x 1 ftp ftp 27728321654 Mar 09 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_American_Gangster-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 31731782861 Mar 09 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_American_Gangster_[Unrated_Edition]-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 5009104014 Mar 24 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Road_Trip-(2000)-[720p]_[HDTV_Rip].mkv
+-rwxrwxr-x 1 ftp ftp 21410583980 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_2-<2D><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Star_Wars-Episode_2-Attack_of_the_Clones-(2002)-[1080i]_[HDTV].ts
+-rwxrwxr-x 1 ftp ftp 19858181688 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_3-<2D><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Star_Wars-Episode_3-Revenge_of_the_Sith-(2005)-[1080i]_[HDTV].ts
+-rwxrwxr-x 1 ftp ftp 29026065728 Mar 16 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Starship_Troopers-(1997)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 22169179449 Mar 16 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Mirrors_[Unrated_Edition]-(2008)-[1080p]_[BD_remux].mkv
+drwxrwxrwx 4 ftp ftp 4096 Jun 15 14:56 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Ninja_Assassin-(2009)-[1080p]_[BD]
+-rwxrwxr-x 1 ftp ftp 19717173247 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD>_3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 18660904388 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_Pathology-(2008)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 16476154520 Mar 05 2009 <20><><EFBFBD><EFBFBD>_1_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_I_[Director's_Cut]-(2004)-[1080p]_[HDDVD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19917510515 Mar 05 2009 <20><><EFBFBD><EFBFBD>_2_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_II_[Director's_Cut]-(2005)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 18085592265 Mar 05 2009 <20><><EFBFBD><EFBFBD>_3_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_III_[Director's_Cut]-(2006)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 3473582701 Mar 05 2009 <20><><EFBFBD><EFBFBD>_4_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].flac
+-rwxrwxr-x 1 ftp ftp 15263958421 Mar 05 2009 <20><><EFBFBD><EFBFBD>_4_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19944605507 Mar 16 2009 <20><><EFBFBD><EFBFBD>_5_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Saw_V_[Director's_Cut]-(2008)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 3024333064 Mar 24 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>-<2D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>!_-_The_Madagascar_Penguins_in_A_Christmas_Caper-(2005)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 125961 Mar 05 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].srt
+-rwxrwxr-x 1 ftp ftp 19908695408 Mar 05 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 23185439267 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Shawshank_Redemption-(1994)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19567287274 Mar 16 2009 <20><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>_<EFBFBD><5F><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Dumb_and_Dumber_[Unrated_Edition]-(1994)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 14773061093 Mar 16 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_-_The_Hurricane-(1999)-[1080p]_[HDDVD_Rip].mkv
+-rwxrwxr-x 1 ftp ftp 22411268500 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_2_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Hostel_2_[Director's_Cut]-(2007)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 23712519861 Mar 11 2009 <20><><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_[<5B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]_-_Alien_vs_Predator_[Unrated_Edition]-(2004)-[1080p]_[BD_remux].mkv
diff --git a/net/data/ftp/dir-listing-ls-22.expected b/net/data/ftp/dir-listing-ls-22.expected
new file mode 100644
index 0000000000000..2bf724fb94b6b
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-22.expected
@@ -0,0 +1,287 @@
+d
+.
+-1
+1994
+10
+20
+17
+4
+
+d
+..
+-1
+2010
+4
+3
+0
+0
+
+-
+_READ_ME.txt
+4931
+1994
+6
+8
+15
+23
+
+d
+Аватар_-_Avatar-(2009)-[1080p]_[BD]
+-1
+1994
+5
+31
+16
+26
+
+-
+Американский_пирог_1_[Расширенная_версия]_-_American_Pie_1_[Unrated_Edition]-(1999)-[1080p]_[BD_remux].ts
+17577705968
+2009
+3
+8
+0
+0
+
+-
ольшой_куш_-_Snatch-(2000)-[1080i]_[HDTV].ts
+15512934868
+2009
+3
+16
+0
+0
+
+d
ольшой_куш_-_Snatch-(2000)-[1080p]_[BD_Remux]
+-1
+1994
+6
+3
+19
+7
+
+-
+Война_миров_-_War_of_the_Worlds-(2005)-[720p]_[HDTV].mkv
+8900589105
+2009
+3
+24
+0
+0
+
+-
ангстер_-_American_Gangster-(2007)-[1080p]_[BD_remux].mkv
+27728321654
+2009
+3
+9
+0
+0
+
+-
ангстер_[Расширенная_версия]_-_American_Gangster_[Unrated_Edition]-(2007)-[1080p]_[BD_remux].mkv
+31731782861
+2009
+3
+9
+0
+0
+
+-
орожное_приключение_-_Road_Trip-(2000)-[720p]_[HDTV_Rip].mkv
+5009104014
+2009
+3
+24
+0
+0
+
+-
+Звёздныеойны-Эпизод_2-Атака_клонов_-_Star_Wars-Episode_2-Attack_of_the_Clones-(2002)-[1080i]_[HDTV].ts
+21410583980
+2009
+3
+11
+0
+0
+
+-
+Звёздныеойны-Эпизод_3-Месть_Ситхов_-_Star_Wars-Episode_3-Revenge_of_the_Sith-(2005)-[1080i]_[HDTV].ts
+19858181688
+2009
+3
+11
+0
+0
+
+-
+Звёздный_десант_-_Starship_Troopers-(1997)-[1080p]_[BD_remux].mkv
+29026065728
+2009
+3
+16
+0
+0
+
+-
+Зеркала_[Расширенная_версия]_-_Mirrors_[Unrated_Edition]-(2008)-[1080p]_[BD_remux].mkv
+22169179449
+2009
+3
+16
+0
+0
+
+d
+Ниндзя-убийца_-_Ninja_Assassin-(2009)-[1080p]_[BD]
+-1
+1994
+6
+15
+14
+56
+
+-
+Обитель_зла_3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD_remux].mkv
+19717173247
+2009
+3
+11
+0
+0
+
+-
атология_-_Pathology-(2008)-[1080p]_[BD_remux].mkv
+18660904388
+2009
+3
+11
+0
+0
+
+-
+Пила_1_[Режиссёрская_версия]_-_Saw_I_[Director's_Cut]-(2004)-[1080p]_[HDDVD_remux].mkv
+16476154520
+2009
+3
+5
+0
+0
+
+-
+Пила_2_[Режиссёрская_версия]_-_Saw_II_[Director's_Cut]-(2005)-[1080p]_[BD_remux].mkv
+19917510515
+2009
+3
+5
+0
+0
+
+-
+Пила_3_[Режиссёрская_версия]_-_Saw_III_[Director's_Cut]-(2006)-[1080p]_[BD_remux].mkv
+18085592265
+2009
+3
+5
+0
+0
+
+-
+Пила_4_[Режиссёрская_версия]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].flac
+3473582701
+2009
+3
+5
+0
+0
+
+-
+Пила_4_[Режиссёрская_версия]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].mkv
+15263958421
+2009
+3
+5
+0
+0
+
+-
+Пила_5_[Режиссёрская_версия]_-_Saw_V_[Director's_Cut]-(2008)-[1080p]_[BD_remux].mkv
+19944605507
+2009
+3
+16
+0
+0
+
+-
+Пингвины_из_Мадагаскара-Операция_С_Новым_Годом!_-_The_Madagascar_Penguins_in_A_Christmas_Caper-(2005)-[1080p]_[BD_remux].ts
+3024333064
+2009
+3
+24
+0
+0
+
+-
+Плохой_Санта_[Расширенная_версия]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].srt
+125961
+2009
+3
+5
+0
+0
+
+-
+Плохой_Санта_[Расширенная_версия]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].ts
+19908695408
+2009
+3
+5
+0
+0
+
+-
обег_из_Шоушенка_-_The_Shawshank_Redemption-(1994)-[1080p]_[BD_remux].mkv
+23185439267
+2009
+3
+11
+0
+0
+
+-
+Тупой_и_ещеупее_[Расширенная_версия]_-_Dumb_and_Dumber_[Unrated_Edition]-(1994)-[1080p]_[BD_remux].mkv
+19567287274
+2009
+3
+16
+0
+0
+
+-
+Ураган_-_The_Hurricane-(1999)-[1080p]_[HDDVD_Rip].mkv
+14773061093
+2009
+3
+16
+0
+0
+
+-
+Хостел_2_[Режиссёрская_версия]_-_Hostel_2_[Director's_Cut]-(2007)-[1080p]_[BD_remux].ts
+22411268500
+2009
+3
+11
+0
+0
+
+-
ужой_против_Хищника_[Расширенная_версия]_-_Alien_vs_Predator_[Unrated_Edition]-(2004)-[1080p]_[BD_remux].mkv
+23712519861
+2009
+3
+11
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-23 b/net/data/ftp/dir-listing-ls-23
new file mode 100644
index 0000000000000..2b8c49483d0c6
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-23
@@ -0,0 +1,2 @@
+total 0
+ftpd: .: Permission denied
diff --git a/net/data/ftp/dir-listing-ls-23.expected b/net/data/ftp/dir-listing-ls-23.expected
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/net/data/ftp/dir-listing-ls-24 b/net/data/ftp/dir-listing-ls-24
new file mode 100644
index 0000000000000..855559757c53a
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-24
@@ -0,0 +1 @@
+drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 note_empty_line_below
diff --git a/net/data/ftp/dir-listing-ls-24.expected b/net/data/ftp/dir-listing-ls-24.expected
new file mode 100644
index 0000000000000..c46afa27dd5a6
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-24.expected
@@ -0,0 +1,8 @@
+d
+note_empty_line_below
+-1
+2008
+8
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-25 b/net/data/ftp/dir-listing-ls-25
new file mode 100644
index 0000000000000..47e0487b3cf85
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-25
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 15 апр 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 15 июл 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 01 май 2007 .message
+-rw-r--r-- 1 ftp ftp 528 01 ноя 2007 README
+-rw-r--r-- 1 ftp ftp 560 28 сен 2007 index.html
+drwxr-xr-x 33 ftp ftp 4096 12 фев 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-25.expected b/net/data/ftp/dir-listing-ls-25.expected
new file mode 100644
index 0000000000000..3405f86d27901
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-25.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+4
+15
+18
+11
+
+d
+..
+-1
+1994
+7
+15
+18
+11
+
+-
+.message
+528
+2007
+5
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+index.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+2
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-26 b/net/data/ftp/dir-listing-ls-26
new file mode 100644
index 0000000000000..45d7e8a8d22af
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-26
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 15 <20><><EFBFBD> 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 15 <20><><EFBFBD> 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 01 <20><><EFBFBD> 2007 .message
+-rw-r--r-- 1 ftp ftp 528 01 <20><><EFBFBD> 2007 README
+-rw-r--r-- 1 ftp ftp 560 28 <20><><EFBFBD> 2007 index.html
+drwxr-xr-x 33 ftp ftp 4096 12 <20><><EFBFBD> 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-26.expected b/net/data/ftp/dir-listing-ls-26.expected
new file mode 100644
index 0000000000000..3405f86d27901
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-26.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+4
+15
+18
+11
+
+d
+..
+-1
+1994
+7
+15
+18
+11
+
+-
+.message
+528
+2007
+5
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+index.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+2
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-27 b/net/data/ftp/dir-listing-ls-27
new file mode 100644
index 0000000000000..3c0a3040e17a7
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-27
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 15 <20><><EFBFBD> 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 15 <20><><EFBFBD> 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 01 <20><><EFBFBD> 2007 .message
+-rw-r--r-- 1 ftp ftp 528 01 <20><><EFBFBD> 2007 README
+-rw-r--r-- 1 ftp ftp 560 28 <20><><EFBFBD> 2007 index.html
+drwxr-xr-x 33 ftp ftp 4096 12 <20><><EFBFBD> 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-27.expected b/net/data/ftp/dir-listing-ls-27.expected
new file mode 100644
index 0000000000000..3405f86d27901
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-27.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+4
+15
+18
+11
+
+d
+..
+-1
+1994
+7
+15
+18
+11
+
+-
+.message
+528
+2007
+5
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+index.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+2
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-28 b/net/data/ftp/dir-listing-ls-28
new file mode 100644
index 0000000000000..85b387f57025e
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-28
@@ -0,0 +1,5 @@
+drwxr-x 2 10 4096 Dec 10 14:32 pollq
+drwxr-x 4 0 4096 Jul 28 01:44 etc
+drwxrwx 2 10 4096 Jul 28 02:41 tmp
+drwxr-x 2 10 4096 Jul 28 02:00 status
+drwxr-x 2 0 4096 Jul 27 23:21 bin
diff --git a/net/data/ftp/dir-listing-ls-28.expected b/net/data/ftp/dir-listing-ls-28.expected
new file mode 100644
index 0000000000000..d3e4d46f5d6e6
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-28.expected
@@ -0,0 +1,44 @@
+d
+pollq
+-1
+1993
+12
+10
+14
+32
+
+d
+etc
+-1
+1994
+7
+28
+1
+44
+
+d
+tmp
+-1
+1994
+7
+28
+2
+41
+
+d
+status
+-1
+1994
+7
+28
+2
+0
+
+d
+bin
+-1
+1994
+7
+27
+23
+21
diff --git a/net/data/ftp/dir-listing-ls-29 b/net/data/ftp/dir-listing-ls-29
new file mode 100644
index 0000000000000..6c82909f19544
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-29
@@ -0,0 +1,9 @@
+.:
+total 1204984
+drwxrwxrwx 2 sunle sunle 2048 Apr 6 00:36 .
+drwxrwxrwx 9406 sunle sunle 245760 Apr 10 18:00 ..
+-rw-rw---- 1 ftp sunle 110583757 Apr 5 13:43 GRBRATLIN64_4_77_ia64_4.77_S0@874222@03717.rpm.Z
+-rw-rw---- 1 ftp sunle 121591811 Apr 5 13:43 GRBRATLIN_4_77_ia32_4.77_S0@874222@20701.rpm.Z
+-rw-rw---- 1 ftp sunle 110965625 Apr 5 13:43 XENRATLIN64_4_77_ia64_4.77_S0@874222@17785.rpm.Z
+-rw-rw---- 1 ftp sunle 121986153 Apr 5 13:44 XENRATLIN_4_77_ia32_4.77_S0@874222@09635.rpm.Z
+-rw-rw---- 1 ftp sunle 151525255 Apr 5 13:44 XENRATS10_4_77_sun4_10_4.77_S0@874222@21358.dstream.Z
diff --git a/net/data/ftp/dir-listing-ls-29.expected b/net/data/ftp/dir-listing-ls-29.expected
new file mode 100644
index 0000000000000..f90e8b163e2db
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-29.expected
@@ -0,0 +1,62 @@
+d
+.
+-1
+1994
+4
+6
+0
+36
+
+d
+..
+-1
+1994
+4
+10
+18
+0
+
+-
+GRBRATLIN64_4_77_ia64_4.77_S0@874222@03717.rpm.Z
+110583757
+1994
+4
+5
+13
+43
+
+-
+GRBRATLIN_4_77_ia32_4.77_S0@874222@20701.rpm.Z
+121591811
+1994
+4
+5
+13
+43
+
+-
+XENRATLIN64_4_77_ia64_4.77_S0@874222@17785.rpm.Z
+110965625
+1994
+4
+5
+13
+43
+
+-
+XENRATLIN_4_77_ia32_4.77_S0@874222@09635.rpm.Z
+121986153
+1994
+4
+5
+13
+44
+
+-
+XENRATS10_4_77_sun4_10_4.77_S0@874222@21358.dstream.Z
+151525255
+1994
+4
+5
+13
+44
diff --git a/net/data/ftp/dir-listing-ls-3 b/net/data/ftp/dir-listing-ls-3
new file mode 100644
index 0000000000000..8720c06572b06
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-3
@@ -0,0 +1,7 @@
+
+-r-xr-xr-x 1 anonymou 245 90 Mar 1 2003 .welcome
+drwxr-xr-x 1 system 1 512 Feb 26 2005 decus
+dr-xr-xr-x 1 system 1 3072 Dec 2 2006 gnv
+-r-xr-xr-x 1 anonymou 245 158208 Apr 10 2003 unzip.alpha_exe
+-r-xr-xr-x 1 anonymou 245 102400 Apr 10 2003 unzip.vax_exe
+dr-xr-xr-x 1 anonymou 245 1024 Mar 1 2003 vms-freeware
diff --git a/net/data/ftp/dir-listing-ls-3.expected b/net/data/ftp/dir-listing-ls-3.expected
new file mode 100644
index 0000000000000..37bbbafa1b058
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-3.expected
@@ -0,0 +1,53 @@
+-
+.welcome
+90
+2003
+3
+1
+0
+0
+
+d
+decus
+-1
+2005
+2
+26
+0
+0
+
+d
+gnv
+-1
+2006
+12
+2
+0
+0
+
+-
+unzip.alpha_exe
+158208
+2003
+4
+10
+0
+0
+
+-
+unzip.vax_exe
+102400
+2003
+4
+10
+0
+0
+
+d
+vms-freeware
+-1
+2003
+3
+1
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-30 b/net/data/ftp/dir-listing-ls-30
new file mode 100644
index 0000000000000..6c095ccd554b8
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-30
@@ -0,0 +1,2 @@
+total 395136991232
+drwx-wx-wx 10 ftpadmin ftpusers 256 May 27 2010 downloads
diff --git a/net/data/ftp/dir-listing-ls-30.expected b/net/data/ftp/dir-listing-ls-30.expected
new file mode 100644
index 0000000000000..19dcc75bdaf28
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-30.expected
@@ -0,0 +1,8 @@
+d
+downloads
+-1
+2010
+5
+27
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-31 b/net/data/ftp/dir-listing-ls-31
new file mode 100644
index 0000000000000..95f48ac3e9bb9
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-31
@@ -0,0 +1,15 @@
+drwxrwxrwx 6 1000 1000 4096 Jul 23 2011 Alfresco
+drwxrwxrwx 2 1000 1000 4096 Mar 18 2010 DIRECTUM
+-rwxrwxrwx 1 1000 1000 222510 Mar 29 2010 Featurelist 6.30.pdf
+drwxrwxrwx 2 1000 1000 4096 Jul 23 2011 NauDoc_v4.4
+-rwxrwxrwx 1 1000 1000 1564767 Apr 06 2010 RUS_v_01_00_МЕТОД СБОРА И ДОКУМЕНТИРОВАНИЯ
+ ТРЕБОВАНИЙ К ПОРТАЛУ
+.pdf
+drwxrwxrwx 4 1000 1000 4096 Jul 22 2011 Videoconferencing
+drwxrwxrwx 3 1000 1000 4096 Apr 15 2010 Virtualization
+-rwxrwxrwx 1 1000 1000 111726333 Jan 10 2010 electr_docoborot_2010.flv
+-rwxrwxrwx 1 1000 1000 4224387 Mar 31 2010 millenniumbsa.pdf
+drwxrwxrwx 5 1000 1000 4096 Apr 16 2010 Бизнес План
+-rwxrwxrwx 1 1000 1000 138217 Apr 16 2010 Мониторинг в инфраструктуре распределенных приложений .NET.rar
+-rwxrwxrwx 1 1000 1000 4131 Feb 25 2010 О законе О персональных данных.txt
+-rwxrwxrwx 1 1000 1000 3627173 Feb 21 2010 Шеер А.В. -- Бизнес-процессы. Основные понятия..djvu
diff --git a/net/data/ftp/dir-listing-ls-31.expected b/net/data/ftp/dir-listing-ls-31.expected
new file mode 100644
index 0000000000000..99a8474b46a20
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-31.expected
@@ -0,0 +1,118 @@
+d
+Alfresco
+-1
+2011
+7
+23
+0
+0
+
+d
+DIRECTUM
+-1
+2010
+3
+18
+0
+0
+
+-
+Featurelist 6.30.pdf
+222510
+2010
+3
+29
+0
+0
+
+d
+NauDoc_v4.4
+-1
+2011
+7
+23
+0
+0
+
+-
+RUS_v_01_00_МЕТОД СБОРА И ДОКУМЕНТИРОВАНИЯ
+ ТРЕБОВАНИЙ К ПОРТАЛУ
+.pdf
+1564767
+2010
+4
+6
+0
+0
+
+d
+Videoconferencing
+-1
+2011
+7
+22
+0
+0
+
+d
+Virtualization
+-1
+2010
+4
+15
+0
+0
+
+-
+electr_docoborot_2010.flv
+111726333
+2010
+1
+10
+0
+0
+
+-
+millenniumbsa.pdf
+4224387
+2010
+3
+31
+0
+0
+
+d
+Бизнес План
+-1
+2010
+4
+16
+0
+0
+
+-
+Мониторинг в инфраструктуре распределенных приложений .NET.rar
+138217
+2010
+4
+16
+0
+0
+
+-
+О законе О персональных данных.txt
+4131
+2010
+2
+25
+0
+0
+
+-
+Шеер А.В. -- Бизнес-процессы. Основные понятия..djvu
+3627173
+2010
+2
+21
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-32 b/net/data/ftp/dir-listing-ls-32
new file mode 100644
index 0000000000000..aa21f24570c45
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-32
@@ -0,0 +1,13 @@
+
+total 1
+drwxrwxr-x 1 500 244 660 Jan 1 00:0 bin
+drwxr-xr-x 1 0 0 0 Jan 1 00:0 dev
+drwxrwxr-x 1 500 244 272 Jan 1 00:0 etc
+drwxrwxr-x 1 500 244 16 Jan 1 00:0 mnt
+drwxrwxr-x 1 500 244 0 Jan 1 00:0 nfs
+dr-xr-xr-x 50 0 0 0 Jan 1 00:0 proc
+drwxrwxrwx 1 0 0 0 Jan 1 00:0 ram
+drwxrwxr-x 1 500 244 296 Jan 1 00:0 sbin
+lrwxrwxrwx 1 500 244 7 Jan 1 00:0 tmp -> ram/tmp
+drwxrwxr-x 1 500 244 16 Jan 1 00:0 usr
+lrwxrwxrwx 1 500 244 7 Jan 1 00:0 var -> ram/var
diff --git a/net/data/ftp/dir-listing-ls-32.expected b/net/data/ftp/dir-listing-ls-32.expected
new file mode 100644
index 0000000000000..4626652da423c
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-32.expected
@@ -0,0 +1,98 @@
+d
+bin
+-1
+1994
+1
+1
+0
+0
+
+d
+dev
+-1
+1994
+1
+1
+0
+0
+
+d
+etc
+-1
+1994
+1
+1
+0
+0
+
+d
+mnt
+-1
+1994
+1
+1
+0
+0
+
+d
+nfs
+-1
+1994
+1
+1
+0
+0
+
+d
+proc
+-1
+1994
+1
+1
+0
+0
+
+d
+ram
+-1
+1994
+1
+1
+0
+0
+
+d
+sbin
+-1
+1994
+1
+1
+0
+0
+
+l
+tmp
+-1
+1994
+1
+1
+0
+0
+
+d
+usr
+-1
+1994
+1
+1
+0
+0
+
+l
+var
+-1
+1994
+1
+1
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-33 b/net/data/ftp/dir-listing-ls-33
new file mode 100644
index 0000000000000..2a2e6a28f2dab
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-33
@@ -0,0 +1,4 @@
+total -9560322322989056
+-rw-r--r-- 1 stadmin stgroup 4310450 Nov 17 13:12 01643.001.862.TestPermission-DUP0001.zip
+-rw-r--r-- 1 stadmin stgroup 2496430080 Nov 13 14:46 I_Base_01_RSE_R720.iso
+-rw-r--r-- 1 stadmin stgroup 478576612 Nov 13 08:11 I_Base_01_RSE_R720.zip
diff --git a/net/data/ftp/dir-listing-ls-33.expected b/net/data/ftp/dir-listing-ls-33.expected
new file mode 100644
index 0000000000000..42ae5f932ec6d
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-33.expected
@@ -0,0 +1,26 @@
+-
+01643.001.862.TestPermission-DUP0001.zip
+4310450
+1993
+11
+17
+13
+12
+
+-
+I_Base_01_RSE_R720.iso
+2496430080
+1994
+11
+13
+14
+46
+
+-
+I_Base_01_RSE_R720.zip
+478576612
+1994
+11
+13
+8
+11
diff --git a/net/data/ftp/dir-listing-ls-34 b/net/data/ftp/dir-listing-ls-34
new file mode 100644
index 0000000000000..6286ed186f4d5
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-34
@@ -0,0 +1,2 @@
+-rw-rw-r-- 1 ftpuser ftpusers 1761280 Dec 20 2002 controle_embarqu<71>_avec_labview_real_time_et_compactfieldpoint.ppt
+-rw-rw-r-- 1 ftpuser ftpusers 329216 Dec 20 2002 optimisez_l'acquisition_de_donn<6E>es_sous_labview.ppt
diff --git a/net/data/ftp/dir-listing-ls-34.expected b/net/data/ftp/dir-listing-ls-34.expected
new file mode 100644
index 0000000000000..d197cb362c805
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-34.expected
@@ -0,0 +1,17 @@
+-
+controle_embarqu<71>_avec_labview_real_time_et_compactfieldpoint.ppt
+1761280
+2002
+12
+20
+0
+0
+
+-
+optimisez_l'acquisition_de_donns_sous_labview.ppt
+329216
+2002
+12
+20
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-4 b/net/data/ftp/dir-listing-ls-4
new file mode 100644
index 0000000000000..078542f0976dc
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-4
@@ -0,0 +1,10 @@
+
+-rwx---r-x 1 archives 0 472 Jun 28 2004 .welcome
+drwxr-xr-x 1 archives 0 512 Mar 5 1998 contributed-software
+drwxr-xr-x 1 archives 0 512 Nov 11 1997 customer_support
+drwxr-xr-x 1 archives 0 512 Dec 30 1998 docs
+drwxr-xr-x 1 archives 0 512 May 8 1998 faq
+drwxr-xr-x 1 archives 0 512 Nov 11 1997 mail_archives
+drwxr-xr-x 1 archives 0 1024 Nov 11 1997 patches
+drwxr-xr-x 1 archives 0 512 Nov 11 1997 tech-tips
+drwxr-xr-x 1 archives 0 512 Nov 11 1997 white_papers
diff --git a/net/data/ftp/dir-listing-ls-4.expected b/net/data/ftp/dir-listing-ls-4.expected
new file mode 100644
index 0000000000000..53397030473f2
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-4.expected
@@ -0,0 +1,80 @@
+-
+.welcome
+472
+2004
+6
+28
+0
+0
+
+d
+contributed-software
+-1
+1998
+3
+5
+0
+0
+
+d
+customer_support
+-1
+1997
+11
+11
+0
+0
+
+d
+docs
+-1
+1998
+12
+30
+0
+0
+
+d
+faq
+-1
+1998
+5
+8
+0
+0
+
+d
+mail_archives
+-1
+1997
+11
+11
+0
+0
+
+d
+patches
+-1
+1997
+11
+11
+0
+0
+
+d
+tech-tips
+-1
+1997
+11
+11
+0
+0
+
+d
+white_papers
+-1
+1997
+11
+11
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-5 b/net/data/ftp/dir-listing-ls-5
new file mode 100644
index 0000000000000..87c42665ffd51
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-5
@@ -0,0 +1 @@
+drwxrwsr-x 4 501 501 4096 Feb 20 2007 pub
diff --git a/net/data/ftp/dir-listing-ls-5.expected b/net/data/ftp/dir-listing-ls-5.expected
new file mode 100644
index 0000000000000..3acbcf39b2137
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-5.expected
@@ -0,0 +1,8 @@
+d
+pub
+-1
+2007
+2
+20
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-6 b/net/data/ftp/dir-listing-ls-6
new file mode 100644
index 0000000000000..5cc802c8822a4
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-6
@@ -0,0 +1,7 @@
+total 14
+dr-xr-xr-x 7 0 1 512 Jun 19 2006 .
+dr-xr-xr-x 7 0 1 512 Jun 19 2006 ..
+dr-xr-xr-x 2 0 1 512 Mar 24 2003 bin
+dr-xr-xr-x 2 0 1 512 Mar 24 2003 etc
+dr-xr-xr-x 12 0 0 512 Apr 7 2009 pub
+dr-xr-xr-x 3 0 1 512 Mar 24 2003 usr
diff --git a/net/data/ftp/dir-listing-ls-6.expected b/net/data/ftp/dir-listing-ls-6.expected
new file mode 100644
index 0000000000000..80b61dd391fe3
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-6.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+2006
+6
+19
+0
+0
+
+d
+..
+-1
+2006
+6
+19
+0
+0
+
+d
+bin
+-1
+2003
+3
+24
+0
+0
+
+d
+etc
+-1
+2003
+3
+24
+0
+0
+
+d
+pub
+-1
+2009
+4
+7
+0
+0
+
+d
+usr
+-1
+2003
+3
+24
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-7 b/net/data/ftp/dir-listing-ls-7
new file mode 100644
index 0000000000000..93dd8045ca341
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-7
@@ -0,0 +1,6 @@
+-rw-r--r-- 1 0 100 3108 Mar 07 2001 00readme.html
+drwxr-xr-x 3 1164 100 4096 Oct 19 13:45 OCU
+lrwxrwxrwx 1 203 1 10 Jun 15 2006 a<>o2000 -> ./urte2000
+drwxr-xr-x 2 0 0 4096 Mar 07 2001 bin
+drwxr-xr-x 2 0 100 4096 Mar 07 2001 dev
+drwxr-xr-x 3 0 100 4096 Apr 20 2005 doc
diff --git a/net/data/ftp/dir-listing-ls-7.expected b/net/data/ftp/dir-listing-ls-7.expected
new file mode 100644
index 0000000000000..90c13511e6250
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-7.expected
@@ -0,0 +1,53 @@
+-
+00readme.html
+3108
+2001
+3
+7
+0
+0
+
+d
+OCU
+-1
+1994
+10
+19
+13
+45
+
+l
+año2000
+-1
+2006
+6
+15
+0
+0
+
+d
+bin
+-1
+2001
+3
+7
+0
+0
+
+d
+dev
+-1
+2001
+3
+7
+0
+0
+
+d
+doc
+-1
+2005
+4
+20
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-8 b/net/data/ftp/dir-listing-ls-8
new file mode 100644
index 0000000000000..92811047f08ec
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-8
@@ -0,0 +1 @@
+total 0
diff --git a/net/data/ftp/dir-listing-ls-8.expected b/net/data/ftp/dir-listing-ls-8.expected
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/net/data/ftp/dir-listing-ls-9 b/net/data/ftp/dir-listing-ls-9
new file mode 100644
index 0000000000000..89df17cb8ef6f
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-9
@@ -0,0 +1,4 @@
+total 510528
+-rw-r--r-- 1 nobody nogroup 174680068 Jun 4 23:20 Akademia Teatralna spot.mpg
+-rw-r--r-- 1 nobody nogroup 3447432 May 18 2009 Foo - Instrukcja_Obsługi.pdf
+-rw-r--r-- 1 nobody nogroup 23197684 Jun 9 13:36 Zdjecia.zip
diff --git a/net/data/ftp/dir-listing-ls-9.expected b/net/data/ftp/dir-listing-ls-9.expected
new file mode 100644
index 0000000000000..78ecd3f21c34f
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-9.expected
@@ -0,0 +1,26 @@
+-
+Akademia Teatralna spot.mpg
+174680068
+1994
+6
+4
+23
+20
+
+-
+Foo - Instrukcja_Obsługi.pdf
+3447432
+2009
+5
+18
+0
+0
+
+-
+Zdjecia.zip
+23197684
+1994
+6
+9
+13
+36
diff --git a/net/data/ftp/dir-listing-netware-1 b/net/data/ftp/dir-listing-netware-1
new file mode 100644
index 0000000000000..02a5167699291
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-1
@@ -0,0 +1,3 @@
+total 0
+d [RWCEAFMS] ftpadmin 512 Jun 25 2007 pandora
+d [RWCEAFMS] ftpadmin 512 Jan 29 2004 pub
diff --git a/net/data/ftp/dir-listing-netware-1.expected b/net/data/ftp/dir-listing-netware-1.expected
new file mode 100644
index 0000000000000..be3f9b8b20bfc
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-1.expected
@@ -0,0 +1,17 @@
+d
+pandora
+-1
+2007
+6
+25
+0
+0
+
+d
+pub
+-1
+2004
+1
+29
+0
+0
diff --git a/net/data/ftp/dir-listing-netware-2 b/net/data/ftp/dir-listing-netware-2
new file mode 100644
index 0000000000000..13af27b729257
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-2
@@ -0,0 +1,4 @@
+total 0
+- [RWCEAFMS] AK101850 1328 Dec 27 2007 rootcert.der
+d [RWCEAFMS] Admin 512 Nov 13 07:51 Driver
+d [RWCEAFMS] AK101850 512 Nov 16 15:40 temp
diff --git a/net/data/ftp/dir-listing-netware-2.expected b/net/data/ftp/dir-listing-netware-2.expected
new file mode 100644
index 0000000000000..3c78ff0094c84
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-2.expected
@@ -0,0 +1,26 @@
+-
+rootcert.der
+1328
+2007
+12
+27
+0
+0
+
+d
+Driver
+-1
+1994
+11
+13
+7
+51
+
+d
+temp
+-1
+1993
+11
+16
+15
+40
diff --git a/net/data/ftp/dir-listing-netware-3 b/net/data/ftp/dir-listing-netware-3
new file mode 100644
index 0000000000000..1d24bccd574b3
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-3
@@ -0,0 +1,22 @@
+total 0
+- [RWCEAFMS] bodi 1588 Oct 28 22:21 leltar.as
+- [RWCEAFMS] bodi 19440 Oct 28 22:21 leltarVizellatas.as
+- [RWCEAFMS] bodi 38795 Oct 28 22:21 leltarVizelvezetes.as
+- [RWCEAFMS] bodi 227 Jan 06 2010 Olvass_el.txt
+- [RWCEAFMS] dora 19300888 Jan 15 2011 vegleges.pdf
+d [RWCEAFMS] Fulop 512 Nov 10 2011 DHI
+d [RWCEAFMS] bodi 512 Aug 02 11:48 English
+d [RWCEAFMS] clement 512 Jun 24 2011 Floodrisk
+d [RWCEAFMS] bodi 512 Mar 10 14:53 HC_MIR_FD
+d [RWCEAFMS] bodi 512 Oct 08 15:59 HCWP614-11
+d [RWCEAFMS] knolmar 512 Mar 26 15:10 Mike
+d [RWCEAFMS] daniel 512 May 17 2010 NVP anyagok
+d [RWCEAFMS] bodi 512 Sep 15 2010 Oktatas
+d [RWCEAFMS] buzas 512 Oct 09 12:47 processing_modflow
+d [RWCEAFMS] peterbud 512 Mar 24 2010 Prospektushoz
+d [RWCEAFMS] bodi 512 Sep 05 2011 sewcad
+d [RWCEAFMS] bodi 512 May 09 08:50 szakmernok
+d [RWCEAFMS] daniel 512 Aug 03 18:53 tomi
+d [RWCEAFMS] bodi 512 Sep 20 21:10 Virtualis-GEP
+d [RWCEAFMS] clement 512 Sep 26 12:31 Vizrajzi evkonyvek
+d [RWCEAFMS] darabos 512 Jul 09 2011 VKKI-Tanulmanyok
diff --git a/net/data/ftp/dir-listing-netware-3.expected b/net/data/ftp/dir-listing-netware-3.expected
new file mode 100644
index 0000000000000..fe809e38f6fb8
--- /dev/null
+++ b/net/data/ftp/dir-listing-netware-3.expected
@@ -0,0 +1,188 @@
+-
+leltar.as
+1588
+1994
+10
+28
+22
+21
+
+-
+leltarVizellatas.as
+19440
+1994
+10
+28
+22
+21
+
+-
+leltarVizelvezetes.as
+38795
+1994
+10
+28
+22
+21
+
+-
+Olvass_el.txt
+227
+2010
+1
+6
+0
+0
+
+-
+vegleges.pdf
+19300888
+2011
+1
+15
+0
+0
+
+d
+DHI
+-1
+2011
+11
+10
+0
+0
+
+d
+English
+-1
+1994
+8
+2
+11
+48
+
+d
+Floodrisk
+-1
+2011
+6
+24
+0
+0
+
+d
+HC_MIR_FD
+-1
+1994
+3
+10
+14
+53
+
+d
+HCWP614-11
+-1
+1994
+10
+8
+15
+59
+
+d
+Mike
+-1
+1994
+3
+26
+15
+10
+
+d
+NVP anyagok
+-1
+2010
+5
+17
+0
+0
+
+d
+Oktatas
+-1
+2010
+9
+15
+0
+0
+
+d
+processing_modflow
+-1
+1994
+10
+9
+12
+47
+
+d
+Prospektushoz
+-1
+2010
+3
+24
+0
+0
+
+d
+sewcad
+-1
+2011
+9
+5
+0
+0
+
+d
+szakmernok
+-1
+1994
+5
+9
+8
+50
+
+d
+tomi
+-1
+1994
+8
+3
+18
+53
+
+d
+Virtualis-GEP
+-1
+1994
+9
+20
+21
+10
+
+d
+Vizrajzi evkonyvek
+-1
+1994
+9
+26
+12
+31
+
+d
+VKKI-Tanulmanyok
+-1
+2011
+7
+9
+0
+0
diff --git a/net/data/ftp/dir-listing-os2-1 b/net/data/ftp/dir-listing-os2-1
new file mode 100644
index 0000000000000..e286ce1681b17
--- /dev/null
+++ b/net/data/ftp/dir-listing-os2-1
@@ -0,0 +1,9 @@
+ 0 DIR 08-08-11 01:47 Archive
+ 0 DIR 01-13-11 10:18 BootCD
+ 0 DIR 10-29-10 23:20 BootUSB512
+ 65201 A 08-04-11 10:24 csort.exe
+ 0 DIR 08-04-11 16:07 misc
+ 0 DIR 06-24-11 01:18 moveton
+ 1031106 A 08-08-11 01:47 os2krnlSVN3370_unoff.zip
+ 603 A 01-08-11 14:18 SSE_TEST.EXE
+ 91018 A 04-07-11 18:26 TETRIS.ZIP
diff --git a/net/data/ftp/dir-listing-os2-1.expected b/net/data/ftp/dir-listing-os2-1.expected
new file mode 100644
index 0000000000000..72c37a897c64b
--- /dev/null
+++ b/net/data/ftp/dir-listing-os2-1.expected
@@ -0,0 +1,80 @@
+d
+Archive
+-1
+2011
+8
+8
+1
+47
+
+d
+BootCD
+-1
+2011
+1
+13
+10
+18
+
+d
+BootUSB512
+-1
+2010
+10
+29
+23
+20
+
+-
+csort.exe
+65201
+2011
+8
+4
+10
+24
+
+d
+misc
+-1
+2011
+8
+4
+16
+7
+
+d
+moveton
+-1
+2011
+6
+24
+1
+18
+
+-
+os2krnlSVN3370_unoff.zip
+1031106
+2011
+8
+8
+1
+47
+
+-
+SSE_TEST.EXE
+603
+2011
+1
+8
+14
+18
+
+-
+TETRIS.ZIP
+91018
+2011
+4
+7
+18
+26
diff --git a/net/data/ftp/dir-listing-vms-1 b/net/data/ftp/dir-listing-vms-1
new file mode 100644
index 0000000000000..8df617efba86d
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-1
@@ -0,0 +1,19 @@
+
+
+
+Directory ANONYMOUS_ROOT:[000000]
+
+.WELCOME;1 2 13-FEB-2002 23:32:40.47
+DECUS.DIR;1 1 9-MAY-2001 22:18:51.69
+INFORMATION.DIR;1 1 9-MAY-2001 22:23:42.78
+MADGOAT.DIR;1 2 9-MAY-2001 22:23:44.85
+MAIL_ARCHIVES.DIR;1
+ 1 13-DEC-2005 08:45:27.56
+MOZILLA.DIR;1 1 21-JUN-2001 14:57:51.38
+README.TXT;4 2 18-APR-2000 10:40:39.90
+SSH.DIR;1 1 22-JUN-2002 15:11:12.71
+SUPPORT.DIR;1 3 9-MAY-2001 22:29:45.02
+TCPWARE.DIR;1 1 9-MAY-2001 23:34:10.92
+VMS-FREEWARE.DIR;1 2 9-MAY-2001 23:58:31.39
+
+Total of 11 files, 17 blocks.
diff --git a/net/data/ftp/dir-listing-vms-1.expected b/net/data/ftp/dir-listing-vms-1.expected
new file mode 100644
index 0000000000000..5bc2ab5b21a2b
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-1.expected
@@ -0,0 +1,98 @@
+-
+.welcome
+1024
+2002
+2
+13
+23
+32
+
+d
+decus
+-1
+2001
+5
+9
+22
+18
+
+d
+information
+-1
+2001
+5
+9
+22
+23
+
+d
+madgoat
+-1
+2001
+5
+9
+22
+23
+
+d
+mail_archives
+-1
+2005
+12
+13
+8
+45
+
+d
+mozilla
+-1
+2001
+6
+21
+14
+57
+
+-
+readme.txt
+1024
+2000
+4
+18
+10
+40
+
+d
+ssh
+-1
+2002
+6
+22
+15
+11
+
+d
+support
+-1
+2001
+5
+9
+22
+29
+
+d
+tcpware
+-1
+2001
+5
+9
+23
+34
+
+d
+vms-freeware
+-1
+2001
+5
+9
+23
+58
diff --git a/net/data/ftp/dir-listing-vms-2 b/net/data/ftp/dir-listing-vms-2
new file mode 100644
index 0000000000000..7fc20a93f9c89
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-2
@@ -0,0 +1,35 @@
+
+Directory SYS$SYSDEVICE:[ANONYMOUS]
+
+ANNOUNCE.TXT;2 1/16 12-MAR-2005 08:44:57 [SYSTEM] (RWED,RWED,RE,RE)
+BOINC.DIR;1 1/16 29-DEC-2005 21:33:21 [SYSTEM] (RWE,RWE,RE,RE)
+BZIP2.DIR;1 1/16 27-SEP-2005 19:45:39 [SYSTEM] (RWE,RWE,RE,RE)
+CDRTOOLS.DIR;1 3/16 10-MAR-2005 17:31:44 [SYSTEM] (RWE,RWE,RE,RE)
+DIFFUTILS.DIR;1 1/16 23-JUN-2007 23:04:21 [SYSTEM] (RWE,RWE,RE,RE)
+DTSS_NTP.DIR;1 2/16 25-SEP-2000 21:03:28 [SYSTEM] (RWE,RWE,RE,RE)
+FIXREC.DIR;1 1/16 20-DEC-2003 10:57:22 [SYSTEM] (RWE,RWE,RE,RE)
+GNUPG.DIR;1 1/16 9-AUG-2006 02:11:51 [SYSTEM] (RWE,RWE,RE,RE)
+GZIP.DIR;1 1/16 5-JUL-2006 21:59:45 [SYSTEM] (RWE,RWE,RE,RE)
+INFO-ZIP.DIR;1 15/16 20-SEP-2004 21:27:27 [SYSTEM] (RWE,RWE,RE,RE)
+INPUT.DIR;1 1/16 4-MAR-1999 22:14:34 [UCX$NOBO,ANONYMOUS] (RWE,RWE,RWE,RWE)
+KERMIT.DIR;1 1/16 25-FEB-2006 12:22:34 [SYSTEM] (RWE,RWE,RE,RE)
+LOGIN.COM;2 1/16 28-SEP-2006 09:20:32 [SYSTEM] (RWED,RWED,RE,RE)
+MISC.DIR;1 6/16 12-DEC-1999 17:31:56 [SYSTEM] (RWE,RWE,RE,RE)
+MMK.DIR;1 1/16 30-SEP-2009 08:06:26 [SYSTEM] (RWE,RWE,RE,RE)
+MOZ_TEST.DIR;1 1/16 8-APR-2008 17:12:53 [SYSTEM] (RWE,RWE,RE,RE)
+MPACK.DIR;1 1/16 21-AUG-2009 10:28:57 [SYSTEM] (RWE,RWE,RE,RE)
+MTOOLS.DIR;1 1/16 14-MAR-2006 15:05:01 [SYSTEM] (RWE,RWE,RE,RE)
+OPENSSL.DIR;1 1/16 12-JAN-2009 08:42:56 [SYSTEM] (RWE,RWE,RE,RE)
+PGP.DIR;1 1/16 19-SEP-1999 16:39:04 [SYSTEM] (RWE,RWE,RE,RE)
+PICS.DIR;1 No privilege for attempted operation
+QREADCD.DIR;1 1/16 29-SEP-2004 20:32:38 [SYSTEM] (RWE,RWE,RE,RE)
+RZSPINUP.DIR;1 1/16 24-JUL-2004 21:34:12 [SYSTEM] (RWE,RWE,RE,RE)
+TEST.DIR;1 1/16 5-NOV-2008 21:59:10 [SYSTEM] (RWE,RWE,RE,RE)
+VIM.DIR;1 1/16 30-APR-2005 16:32:56 [SYSTEM] (RWE,RWE,RE,RE)
+VMSTAR.DIR;1 1/16 7-JUN-2007 09:36:04 [SYSTEM] (RWE,RWE,RE,RE)
+WELCOME.TXT;3 1/16 12-MAR-2005 08:45:28 [SYSTEM] (RWED,RWED,RE,RE)
+WGET.DIR;1 3/16 17-AUG-1999 20:41:54 [SYSTEM] (RWE,RWE,RE,RE)
+WGET_TEST.DIR;1 1/16 13-JUN-2006 21:29:27 [SYSTEM] (RWE,RWE,RE,RE)
+WPUT.DIR;1 1/16 9-DEC-2004 20:16:46 [SYSTEM] (RWE,RWE,RE,RE)
+
+Total of 30 files, 53/464 blocks
diff --git a/net/data/ftp/dir-listing-vms-2.expected b/net/data/ftp/dir-listing-vms-2.expected
new file mode 100644
index 0000000000000..6b3ca660232d1
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-2.expected
@@ -0,0 +1,260 @@
+-
+announce.txt
+512
+2005
+3
+12
+8
+44
+
+d
+boinc
+-1
+2005
+12
+29
+21
+33
+
+d
+bzip2
+-1
+2005
+9
+27
+19
+45
+
+d
+cdrtools
+-1
+2005
+3
+10
+17
+31
+
+d
+diffutils
+-1
+2007
+6
+23
+23
+4
+
+d
+dtss_ntp
+-1
+2000
+9
+25
+21
+3
+
+d
+fixrec
+-1
+2003
+12
+20
+10
+57
+
+d
+gnupg
+-1
+2006
+8
+9
+2
+11
+
+d
+gzip
+-1
+2006
+7
+5
+21
+59
+
+d
+info-zip
+-1
+2004
+9
+20
+21
+27
+
+d
+input
+-1
+1999
+3
+4
+22
+14
+
+d
+kermit
+-1
+2006
+2
+25
+12
+22
+
+-
+login.com
+512
+2006
+9
+28
+9
+20
+
+d
+misc
+-1
+1999
+12
+12
+17
+31
+
+d
+mmk
+-1
+2009
+9
+30
+8
+6
+
+d
+moz_test
+-1
+2008
+4
+8
+17
+12
+
+d
+mpack
+-1
+2009
+8
+21
+10
+28
+
+d
+mtools
+-1
+2006
+3
+14
+15
+5
+
+d
+openssl
+-1
+2009
+1
+12
+8
+42
+
+d
+pgp
+-1
+1999
+9
+19
+16
+39
+
+d
+qreadcd
+-1
+2004
+9
+29
+20
+32
+
+d
+rzspinup
+-1
+2004
+7
+24
+21
+34
+
+d
+test
+-1
+2008
+11
+5
+21
+59
+
+d
+vim
+-1
+2005
+4
+30
+16
+32
+
+d
+vmstar
+-1
+2007
+6
+7
+9
+36
+
+-
+welcome.txt
+512
+2005
+3
+12
+8
+45
+
+d
+wget
+-1
+1999
+8
+17
+20
+41
+
+d
+wget_test
+-1
+2006
+6
+13
+21
+29
+
+d
+wput
+-1
+2004
+12
+9
+20
+16
diff --git a/net/data/ftp/dir-listing-vms-3 b/net/data/ftp/dir-listing-vms-3
new file mode 100644
index 0000000000000..14d31f1031743
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-3
@@ -0,0 +1,3 @@
+
+
+Total of 0 blocks in 0 files in 0 directories.
diff --git a/net/data/ftp/dir-listing-vms-3.expected b/net/data/ftp/dir-listing-vms-3.expected
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/net/data/ftp/dir-listing-vms-4 b/net/data/ftp/dir-listing-vms-4
new file mode 100644
index 0000000000000..0954441c229b9
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-4
@@ -0,0 +1,15 @@
+
+EISNER$DRA3:[DECUSERVE_USER.JOHNDOE]
+
+$MAIN.TPU$JOURNAL;1 1 4-NOV-2009 05:59 [JOHNDOE] (RWED,RWED,,)
+.WELCOME;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,)
+EXAMPLE.TXT;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,)
+FILE.;1 1 4-NOV-2009 08:59 [JOHNDOE] (RWED,RWED,,)
+FTP_SERVER.LOG;12 0 4-NOV-2009 09:12 [JOHNDOE] (RWED,RWED,,)
+LOGIN.COM;1 2 4-NOV-2009 05:58 [JOHNDOE] (RWED,RWED,,)
+NOTES$NOTEBOOK.NOTE;1
+ 36 4-NOV-2009 05:55 [DECUSERVE] (RWE,RWE,,)
+TEST.DIR;1 1 4-NOV-2009 08:15 [JOHNDOE] (RWE,RWE,,)
+
+
+Total of 43 blocks in 8 files.
diff --git a/net/data/ftp/dir-listing-vms-4.expected b/net/data/ftp/dir-listing-vms-4.expected
new file mode 100644
index 0000000000000..3c89235e27a0f
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-4.expected
@@ -0,0 +1,71 @@
+-
+$main.tpu$journal
+512
+2009
+11
+4
+5
+59
+
+-
+.welcome
+512
+2009
+11
+4
+6
+2
+
+-
+example.txt
+512
+2009
+11
+4
+6
+2
+
+-
+file.
+512
+2009
+11
+4
+8
+59
+
+-
+ftp_server.log
+0
+2009
+11
+4
+9
+12
+
+-
+login.com
+1024
+2009
+11
+4
+5
+58
+
+-
+notes$notebook.note
+18432
+2009
+11
+4
+5
+55
+
+d
+test
+-1
+2009
+11
+4
+8
+15
diff --git a/net/data/ftp/dir-listing-vms-5 b/net/data/ftp/dir-listing-vms-5
new file mode 100644
index 0000000000000..d769993de8ed2
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-5
@@ -0,0 +1,12 @@
+
+SYS:[ANON_LOGS]
+
+FTP_SERVER.LOG;1682 0 27-NOV-2009 14:35 [NET,ANONYMOUS] (RWED,RWED,,)
+FTP_SERVER_LOG.KEEP;11400
+ 2 19-DEC-1994 15:40 [NET,ANONYMOUS] (RWED,RWED,,)
+FTP_SERVER_LOG.SEARCH;1
+ 274 7-DEC-1993 15:54 [NET,ANONYMOUS] (RWED,RWED,,)
+TESTLOG.DAT;1 0 27-APR-1995 13:18 [NET,ANONYMOUS] (RWED,RWED,,)
+
+
+Total of 276 blocks in 4 files.
diff --git a/net/data/ftp/dir-listing-vms-5.expected b/net/data/ftp/dir-listing-vms-5.expected
new file mode 100644
index 0000000000000..7f12b60984116
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-5.expected
@@ -0,0 +1,35 @@
+-
+ftp_server.log
+0
+2009
+11
+27
+14
+35
+
+-
+ftp_server_log.keep
+1024
+1994
+12
+19
+15
+40
+
+-
+ftp_server_log.search
+140288
+1993
+12
+7
+15
+54
+
+-
+testlog.dat
+0
+1995
+4
+27
+13
+18
diff --git a/net/data/ftp/dir-listing-vms-6 b/net/data/ftp/dir-listing-vms-6
new file mode 100644
index 0000000000000..6143c82e14a3a
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-6
@@ -0,0 +1,13 @@
+
+ANONYMOUS:[000000]
+
+ANON_FTP_LOG.LOG;23
+ <%SYSTEM-F-NOPRIV, insufficient privilege or object protection violation>
+EK-SMCPR-UG-A01.PDF;1
+ <%SYSTEM-F-NOPRIV, insufficient privilege or object protection violation>
+INCOMING.DIR;1 1 16-FEB-2009 00:49 [ANONY,ANONYMOUS] (RWE,RWE,RE,RE)
+INPUT.DIR;1 1 25-JAN-2004 07:11 [ANONY,ANONYMOUS] (RWED,RWE,R,R)
+LOGIN.COM;1 8 25-NOV-2003 20:01 [ANONY,ANONYMOUS] (RWED,RE,RE,RE)
+PUB.DIR;1 2 25-JAN-2004 07:11 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+
+Total of 6 Files, 12 Blocks.
diff --git a/net/data/ftp/dir-listing-vms-6.expected b/net/data/ftp/dir-listing-vms-6.expected
new file mode 100644
index 0000000000000..34fd885a2e370
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-6.expected
@@ -0,0 +1,35 @@
+d
+incoming
+-1
+2009
+2
+16
+0
+49
+
+d
+input
+-1
+2004
+1
+25
+7
+11
+
+-
+login.com
+4096
+2003
+11
+25
+20
+1
+
+d
+pub
+-1
+2004
+1
+25
+7
+11
diff --git a/net/data/ftp/dir-listing-vms-7 b/net/data/ftp/dir-listing-vms-7
new file mode 100644
index 0000000000000..d9ad20ba97bc0
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-7
@@ -0,0 +1,4 @@
+
+ANONYMOUS:[INCOMING]
+
+*.*;0 <%RMS-E-FNF, file not found>
diff --git a/net/data/ftp/dir-listing-vms-7.expected b/net/data/ftp/dir-listing-vms-7.expected
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/net/data/ftp/dir-listing-vms-8 b/net/data/ftp/dir-listing-vms-8
new file mode 100644
index 0000000000000..d805ab50b7258
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-8
@@ -0,0 +1,37 @@
+
+ANONYMOUS:[PUB]
+
+ALPHA0721.ISO-GZ;1 383594 14-MAY-2008 08:52 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)
+AXP.DIR;1 1 25-JAN-2004 07:11 [SYSTEM] (RWE,RE,RE,RE)
+BENCHMARKS.DIR;1 1 10-JUN-2009 19:32 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+EULER.DIR;1 1 10-JUN-2009 20:43 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+GAMES.DIR;1 1 5-FEB-2009 18:43 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+HOUSE.DIR;1 1 27-MAR-2008 08:02 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+HTML.DIR;1 1 25-JAN-2004 07:12 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+I64.DIR;1 1 22-JUN-2008 20:07 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+ITANIUM.DIR;1 1 12-JAN-2008 06:56 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+J2VMS.DIR;1 1 17-NOV-2008 23:26 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+MISC.DIR;1 1 25-JAN-2004 07:12 [SYSTEM] (RWE,RE,RE,RE)
+OPENSOURCE.DIR;1 1 8-SEP-2009 19:29 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+PHONE.DIR;1 2 15-DEC-2008 21:03 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+PLIDOCS.DIR;1 1 25-JAN-2004 07:12 [SYSTEM] (RWE,RE,RE,RE)
+PLI_HTML_DOCS-BCK.ZIP;1
+ 2992 25-MAR-2009 18:27 [ANONY,ANONYMOUS] (RWED,RWED,RE,RE)
+PLI_HTML_DOCS.BCK;2
+ 9639 25-MAR-2009 18:23 [VMSUSER,TSNEDDON] (RE,RWED,RE,RE)
+RESET_BACKUP_SAVESET_ATTRIBUTES.COM;1
+ 3 14-JUN-2009 22:46 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)
+RESET_BACKUP_SAVESET_ATTRIBUTES.TXT;1
+ 3 28-AUG-2001 07:54 [SYSTEM] (RE,RE,RE,RE)
+rexx.DIR;1 1 11-AUG-2009 18:06 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+SDLEXT.DIR;1 1 19-DEC-2008 08:35 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+TOOLS.DIR;1 1 25-JAN-2004 07:13 [SYSTEM] (RWE,RE,RE,RE)
+TRU64.DIR;1 1 25-JAN-2004 07:13 [SYSTEM] (RWE,RE,RE,RE)
+VAX.DIR;1 1 25-JAN-2004 07:13 [SYSTEM] (RWE,RE,RE,RE)
+VMS721.ISO;2 ****** 6-MAY-2008 09:29 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)
+WINDOWS.DIR;1 1 26-AUG-2011 05:53 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+XMLRTL.DIR;1 1 3-JUN-2009 17:24 [ANONY,ANONYMOUS] (RWE,RE,RE,RE)
+_SITE.CSS;8 5 19-MAR-2009 23:44 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)
+_VWCMS.CSS;5 14 19-MAR-2009 23:38 [ANONY,ANONYMOUS] (RE,RWED,RE,RE)
+
+Total of 28 Files, 1557541 Blocks.
diff --git a/net/data/ftp/dir-listing-vms-8.expected b/net/data/ftp/dir-listing-vms-8.expected
new file mode 100644
index 0000000000000..75676be74b39a
--- /dev/null
+++ b/net/data/ftp/dir-listing-vms-8.expected
@@ -0,0 +1,251 @@
+-
+alpha0721.iso-gz
+196400128
+2008
+5
+14
+8
+52
+
+d
+axp
+-1
+2004
+1
+25
+7
+11
+
+d
+benchmarks
+-1
+2009
+6
+10
+19
+32
+
+d
+euler
+-1
+2009
+6
+10
+20
+43
+
+d
+games
+-1
+2009
+2
+5
+18
+43
+
+d
+house
+-1
+2008
+3
+27
+8
+2
+
+d
+html
+-1
+2004
+1
+25
+7
+12
+
+d
+i64
+-1
+2008
+6
+22
+20
+7
+
+d
+itanium
+-1
+2008
+1
+12
+6
+56
+
+d
+j2vms
+-1
+2008
+11
+17
+23
+26
+
+d
+misc
+-1
+2004
+1
+25
+7
+12
+
+d
+opensource
+-1
+2009
+9
+8
+19
+29
+
+d
+phone
+-1
+2008
+12
+15
+21
+3
+
+d
+plidocs
+-1
+2004
+1
+25
+7
+12
+
+-
+pli_html_docs-bck.zip
+1531904
+2009
+3
+25
+18
+27
+
+-
+pli_html_docs.bck
+4935168
+2009
+3
+25
+18
+23
+
+-
+reset_backup_saveset_attributes.com
+1536
+2009
+6
+14
+22
+46
+
+-
+reset_backup_saveset_attributes.txt
+1536
+2001
+8
+28
+7
+54
+
+d
+rexx
+-1
+2009
+8
+11
+18
+6
+
+d
+sdlext
+-1
+2008
+12
+19
+8
+35
+
+d
+tools
+-1
+2004
+1
+25
+7
+13
+
+d
+tru64
+-1
+2004
+1
+25
+7
+13
+
+d
+vax
+-1
+2004
+1
+25
+7
+13
+
+-
+vms721.iso
+-1
+2008
+5
+6
+9
+29
+
+d
+windows
+-1
+2011
+8
+26
+5
+53
+
+d
+xmlrtl
+-1
+2009
+6
+3
+17
+24
+
+-
+_site.css
+2560
+2009
+3
+19
+23
+44
+
+-
+_vwcms.css
+7168
+2009
+3
+19
+23
+38
diff --git a/net/data/ftp/dir-listing-windows-1 b/net/data/ftp/dir-listing-windows-1
new file mode 100644
index 0000000000000..a8ff16d73a49b
--- /dev/null
+++ b/net/data/ftp/dir-listing-windows-1
@@ -0,0 +1,2 @@
+11-02-09 05:32PM <DIR> NT
+01-06-09 02:42PM 458 Readme.txt
diff --git a/net/data/ftp/dir-listing-windows-1.expected b/net/data/ftp/dir-listing-windows-1.expected
new file mode 100644
index 0000000000000..8fbbb48fcd621
--- /dev/null
+++ b/net/data/ftp/dir-listing-windows-1.expected
@@ -0,0 +1,17 @@
+d
+NT
+-1
+2009
+11
+2
+17
+32
+
+-
+Readme.txt
+458
+2009
+1
+6
+14
+42
diff --git a/net/data/ftp/dir-listing-windows-2 b/net/data/ftp/dir-listing-windows-2
new file mode 100644
index 0000000000000..f62fea49959e3
--- /dev/null
+++ b/net/data/ftp/dir-listing-windows-2
@@ -0,0 +1,16 @@
+05-18-09 11:07AM <DIR> beta
+01-06-09 04:25PM <DIR> cdrom
+01-06-09 02:38PM 129 checkdownload.html
+08-19-09 11:23AM <DIR> Digital_Media_Player
+12-29-08 10:27PM <DIR> LiveUpdate
+08-20-09 04:34PM <DIR> mb
+01-06-09 02:38PM 83933 Mb.eng
+11-04-09 03:42PM <DIR> misc
+06-12-09 04:20PM 462 Path.idx
+12-30-08 07:41AM <DIR> PDA
+01-06-09 02:38PM 2625 Platform.idx
+12-30-08 07:41AM <DIR> print
+01-06-09 02:42PM 458 Readme.txt
+10-28-09 02:27PM <DIR> server
+12-30-08 08:59AM <DIR> vga
+02-03-09 04:42PM 1951823 VH203_FR.pdf
diff --git a/net/data/ftp/dir-listing-windows-2.expected b/net/data/ftp/dir-listing-windows-2.expected
new file mode 100644
index 0000000000000..81388eef34514
--- /dev/null
+++ b/net/data/ftp/dir-listing-windows-2.expected
@@ -0,0 +1,143 @@
+d
+beta
+-1
+2009
+5
+18
+11
+7
+
+d
+cdrom
+-1
+2009
+1
+6
+16
+25
+
+-
+checkdownload.html
+129
+2009
+1
+6
+14
+38
+
+d
+Digital_Media_Player
+-1
+2009
+8
+19
+11
+23
+
+d
+LiveUpdate
+-1
+2008
+12
+29
+22
+27
+
+d
+mb
+-1
+2009
+8
+20
+16
+34
+
+-
+Mb.eng
+83933
+2009
+1
+6
+14
+38
+
+d
+misc
+-1
+2009
+11
+4
+15
+42
+
+-
+Path.idx
+462
+2009
+6
+12
+16
+20
+
+d
+PDA
+-1
+2008
+12
+30
+7
+41
+
+-
+Platform.idx
+2625
+2009
+1
+6
+14
+38
+
+d
+print
+-1
+2008
+12
+30
+7
+41
+
+-
+Readme.txt
+458
+2009
+1
+6
+14
+42
+
+d
+server
+-1
+2009
+10
+28
+14
+27
+
+d
+vga
+-1
+2008
+12
+30
+8
+59
+
+-
+VH203_FR.pdf
+1951823
+2009
+2
+3
+16
+42
diff --git a/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict b/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict
new file mode 100644
index 0000000000000..eb4acff145d95
--- /dev/null
+++ b/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict
@@ -0,0 +1,59 @@
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Fuzzer dictionary targetting FTP responses.
+
+# Each response is on its own line, so CRLF is generally useful.
+"\x0D\x0A"
+
+# End of string marker, used by FuzzedDataProvider.
+"\\ "
+
+# Suffixes for file types.
+";type=a"
+";type=i"
+";type=d"
+
+# Dashes indicate multi-line responses.
+"-"
+
+# Generic success.
+"200 OK\x0D\x0A\\ "
+
+# Greeting
+"230 Welcome\x0D\x0A\\ "
+
+# SIZE response
+"213 18\x0D\x0A\\ "
+
+# All important PASV/EPSV responses
+"227 Entering PASV mode (1,1,1,1,50,50)\x0D\x0A\\ "
+"227 Entering Passive Mode 127,0,0,1,123,456\x0D\x0A\\ "
+"227 Entering Extended Passive Mode (|||31744|)\x0D\x0A\\ "
+"227 Entering The Twilight Zone\x0D\x0A\\ "
+
+# RETR/LIST response.
+"125-Data connection already open.\x0D\x0A125 Transfer starting.\x0D\x0A226 Transfer complete.\x0D\x0A\\ "
+"125-Data connection already open.\x0D\x0A\\ "
+"125 Transfer starting.\x0D\x0A\\ "
+"226 Transfer complete.\x0D\x0A\\ "
+
+# Some specific success messages, taken from unittests.
+"215 UNIX\x0D\x0A\\ "
+"215 VMS\x0D\x0A\\ "
+"220 host TestFTPd\x0D\x0A\\ "
+"221 Goodbye!\x0D\x0A\\ "
+"257 \"/\" is your current location\x0D\x0A\\ "
+"257 \"ANONYMOUS_ROOT:[000000]\"\x0D\x0A\\ "
+
+# Error messages, taken from unittests.
+"331 Password needed\x0D\x0A\\ "
+"331 User okay, send password\x0D\x0A\\ "
+"451 not a directory\x0D\x0A\\ "
+"500 EPSV command unknown\x0D\x0A\\ "
+"503 Bad sequence of commands\x0D\x0A\\ "
+"530 Login authentication failed\x0D\x0A\\ "
+"550 I can only retrieve regular files\x0D\x0A\\ "
+"550 Not a directory\x0D\x0A\\ "
+"599 I'm sorry, Dave, I'm afraid I can't do that.\x0D\x0A\\ "
diff --git a/net/docs/proxy.md b/net/docs/proxy.md
index a0ea9f52d1779..589b499e2d4c4 100644
--- a/net/docs/proxy.md
+++ b/net/docs/proxy.md
@@ -109,6 +109,7 @@ about an HTTP proxy.
When using an HTTP proxy in Chrome, name resolution is always deferred to the
proxy. HTTP proxies can proxy `http://`, `https://`, `ws://` and `wss://` URLs.
+(Chromium's FTP support is deprecated, and HTTP proxies cannot proxy `ftp://` anymore)
Communication to HTTP proxy servers is insecure, meaning proxied `http://`
requests are sent in the clear. When proxying `https://` requests through an
diff --git a/net/features.gni b/net/features.gni
index 026a38c0baab1..1d7d13a7c409f 100644
--- a/net/features.gni
+++ b/net/features.gni
@@ -14,6 +14,9 @@ declare_args() {
# and are optional in cronet.
enable_websockets = use_blink
+ # Disable FTP support.
+ disable_ftp_support = is_ios
+
# Enable Kerberos authentication. It is disabled by default on iOS, Fuchsia
# and Chromecast, at least for now. This feature needs configuration
# (krb5.conf and so on).
diff --git a/net/ftp/DIR_METADATA b/net/ftp/DIR_METADATA
new file mode 100644
index 0000000000000..c611fb72b48b1
--- /dev/null
+++ b/net/ftp/DIR_METADATA
@@ -0,0 +1,11 @@
+# Metadata information for this directory.
+#
+# For more information on DIR_METADATA files, see:
+# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/README.md
+#
+# For the schema of this file, see Metadata message:
+# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/proto/dir_metadata.proto
+
+monorail {
+ component: "Internals>Network>FTP"
+}
\ No newline at end of file
diff --git a/net/ftp/OWNERS b/net/ftp/OWNERS
new file mode 100644
index 0000000000000..53e68eb7787a3
--- /dev/null
+++ b/net/ftp/OWNERS
@@ -0,0 +1 @@
+mmenke@chromium.org
diff --git a/net/ftp/ftp_auth_cache.cc b/net/ftp/ftp_auth_cache.cc
new file mode 100644
index 0000000000000..359d5460db859
--- /dev/null
+++ b/net/ftp/ftp_auth_cache.cc
@@ -0,0 +1,62 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/check_op.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() = default;
+
+FtpAuthCache::FtpAuthCache() = default;
+
+FtpAuthCache::~FtpAuthCache() = default;
+
+FtpAuthCache::Entry* FtpAuthCache::Lookup(const GURL& origin) {
+ for (auto it = entries_.begin(); it != entries_.end(); ++it) {
+ if (it->origin == origin)
+ return &(*it);
+ }
+ return nullptr;
+}
+
+void FtpAuthCache::Add(const GURL& origin, const AuthCredentials& credentials) {
+ DCHECK(origin.SchemeIs("ftp"));
+ DCHECK_EQ(origin.DeprecatedGetOriginAsURL(), 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 (auto 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/net/ftp/ftp_auth_cache.h b/net/ftp/ftp_auth_cache.h
new file mode 100644
index 0000000000000..e7f3057e153bf
--- /dev/null
+++ b/net/ftp/ftp_auth_cache.h
@@ -0,0 +1,63 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <stddef.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/net/ftp/ftp_auth_cache_unittest.cc b/net/ftp/ftp_auth_cache_unittest.cc
new file mode 100644
index 0000000000000..9c5dea5303895
--- /dev/null
+++ b/net/ftp/ftp_auth_cache_unittest.cc
@@ -0,0 +1,161 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 base::ASCIIToUTF16;
+
+namespace net {
+
+namespace {
+
+const std::u16string kBogus(u"bogus");
+const std::u16string kOthername(u"othername");
+const std::u16string kOtherword(u"otherword");
+const std::u16string kPassword(u"password");
+const std::u16string kPassword1(u"password1");
+const std::u16string kPassword2(u"password2");
+const std::u16string kPassword3(u"password3");
+const std::u16string kUsername(u"username");
+const std::u16string kUsername1(u"username1");
+const std::u16string kUsername2(u"username2");
+const std::u16string kUsername3(u"username3");
+
+} // namespace
+
+TEST(FtpAuthCacheTest, LookupAddRemove) {
+ FtpAuthCache cache;
+
+ GURL origin1("ftp://foo1");
+ GURL origin2("ftp://foo2");
+
+ // Lookup non-existent entry.
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+
+ // Add entry for origin1.
+ cache.Add(origin1, 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, 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, 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, AuthCredentials(kUsername3, kPassword3));
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+
+ // Remove non-existent entry.
+ cache.Remove(origin1, AuthCredentials(kUsername3, kPassword3));
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+}
+
+// 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, AuthCredentials(kUsername, kPassword));
+ cache.Add(origin2, 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"), 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"), 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"), AuthCredentials(kOthername, kOtherword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == nullptr);
+}
+
+TEST(FtpAuthCacheTest, OnlyRemoveMatching) {
+ FtpAuthCache cache;
+
+ cache.Add(GURL("ftp://host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")));
+
+ // Auth data doesn't match, shouldn't remove.
+ cache.Remove(GURL("ftp://host"), AuthCredentials(kBogus, kBogus));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")));
+
+ // Auth data matches, should remove.
+ cache.Remove(GURL("ftp://host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == nullptr);
+}
+
+TEST(FtpAuthCacheTest, EvictOldEntries) {
+ FtpAuthCache cache;
+
+ for (size_t i = 0; i < FtpAuthCache::kMaxEntries; i++) {
+ cache.Add(GURL("ftp://host" + base::NumberToString(i)),
+ 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::NumberToString(i))));
+ }
+
+ // Adding one entry should cause eviction of the first entry.
+ cache.Add(GURL("ftp://last_host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host0")) == nullptr);
+
+ // Remaining entries should not get evicted.
+ for (size_t i = 1; i < FtpAuthCache::kMaxEntries; i++) {
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host" + base::NumberToString(i))));
+ }
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://last_host")));
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_ctrl_response_buffer.cc b/net/ftp/ftp_ctrl_response_buffer.cc
new file mode 100644
index 0000000000000..9bd4059d5fbe4
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer.cc
@@ -0,0 +1,159 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <utility>
+
+#include "base/check_op.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "net/base/net_errors.h"
+#include "net/base/parse_number.h"
+#include "net/log/net_log_event_type.h"
+#include "net/log/net_log_values.h"
+
+namespace net {
+
+// static
+const int FtpCtrlResponse::kInvalidStatusCode = -1;
+
+FtpCtrlResponse::FtpCtrlResponse() : status_code(kInvalidStatusCode) {}
+
+FtpCtrlResponse::FtpCtrlResponse(const FtpCtrlResponse& other) = default;
+
+FtpCtrlResponse::~FtpCtrlResponse() = default;
+
+FtpCtrlResponseBuffer::FtpCtrlResponseBuffer(const NetLogWithSource& net_log)
+ : multiline_(false), net_log_(net_log) {}
+
+FtpCtrlResponseBuffer::~FtpCtrlResponseBuffer() = default;
+
+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::Dict NetLogFtpCtrlResponseParams(const FtpCtrlResponse* response) {
+ base::Value::List lines;
+ for (const auto& line : response->lines)
+ lines.Append(NetLogStringValue(line));
+
+ base::Value::Dict dict;
+ dict.Set("status_code", response->status_code);
+ dict.Set("lines", std::move(lines));
+ return dict;
+}
+
+} // namespace
+
+FtpCtrlResponse FtpCtrlResponseBuffer::PopResponse() {
+ FtpCtrlResponse result = responses_.front();
+ responses_.pop();
+
+ net_log_.AddEvent(NetLogEventType::FTP_CONTROL_RESPONSE,
+ [&] { return NetLogFtpCtrlResponseParams(&result); });
+
+ return result;
+}
+
+FtpCtrlResponseBuffer::ParsedLine::ParsedLine()
+ : has_status_code(false),
+ is_multiline(false),
+ is_complete(false),
+ status_code(FtpCtrlResponse::kInvalidStatusCode) {
+}
+
+FtpCtrlResponseBuffer::ParsedLine::ParsedLine(const ParsedLine& other) =
+ default;
+
+// static
+FtpCtrlResponseBuffer::ParsedLine FtpCtrlResponseBuffer::ParseLine(
+ const std::string& line) {
+ ParsedLine result;
+
+ if (line.length() >= 3) {
+ if (ParseInt32(base::MakeStringPiece(line.begin(), line.begin() + 3),
+ ParseIntFormat::NON_NEGATIVE, &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/net/ftp/ftp_ctrl_response_buffer.h b/net/ftp/ftp_ctrl_response_buffer.h
new file mode 100644
index 0000000000000..149097185dafc
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer.h
@@ -0,0 +1,101 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <string>
+#include <vector>
+
+#include "base/containers/queue.h"
+#include "net/base/net_export.h"
+#include "net/log/net_log_with_source.h"
+
+namespace net {
+
+struct NET_EXPORT_PRIVATE FtpCtrlResponse {
+ static const int kInvalidStatusCode;
+
+ FtpCtrlResponse();
+ FtpCtrlResponse(const FtpCtrlResponse& other);
+ ~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 NetLogWithSource& net_log);
+ ~FtpCtrlResponseBuffer();
+ FtpCtrlResponseBuffer(const FtpCtrlResponseBuffer&) = delete;
+ FtpCtrlResponseBuffer& operator=(const FtpCtrlResponseBuffer&) =
+ delete;
+ // 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();
+ ParsedLine(const ParsedLine& other);
+
+ // 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_;
+
+ base::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.
+ base::queue<FtpCtrlResponse> responses_;
+
+ NetLogWithSource net_log_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
diff --git a/net/ftp/ftp_ctrl_response_buffer_unittest.cc b/net/ftp/ftp_ctrl_response_buffer_unittest.cc
new file mode 100644
index 0000000000000..a133aa441d7fc
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer_unittest.cc
@@ -0,0 +1,184 @@
+// Copyright (c) 2009 The Chromium Authors
+// 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 "net/test/gtest_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using net::test::IsError;
+using net::test::IsOk;
+
+namespace net {
+
+namespace {
+
+class FtpCtrlResponseBufferTest : public testing::Test {
+ public:
+ FtpCtrlResponseBufferTest() : buffer_(NetLogWithSource()) {}
+
+ protected:
+ int PushDataToBuffer(const char* data) {
+ return buffer_.ConsumeData(data, strlen(data));
+ }
+
+ FtpCtrlResponseBuffer buffer_;
+};
+
+TEST_F(FtpCtrlResponseBufferTest, Basic) {
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("200 Status Text\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("20"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("0 Status"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer(" Text"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("\r"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230-SecondLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230-SecondLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("215 Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("230-\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("example.com\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ 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_THAT(PushDataToBuffer(" 230 Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 TrueLastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("300-Continuation\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("300 Still continuation\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 Real End\r\n"), IsOk());
+ ASSERT_TRUE(buffer_.ResponseAvailable());
+
+ 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_THAT(PushDataToBuffer("Non-numeric\r\n"),
+ IsError(ERR_INVALID_RESPONSE));
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+}
+
+TEST_F(FtpCtrlResponseBufferTest, OutOfRangeResponse) {
+ EXPECT_THAT(PushDataToBuffer("777 OK?\r\n"), IsError(ERR_INVALID_RESPONSE));
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_ctrl_response_fuzzer.cc b/net/ftp/ftp_ctrl_response_fuzzer.cc
new file mode 100644
index 0000000000000..f3a9ee3c6dfbb
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_fuzzer.cc
@@ -0,0 +1,21 @@
+// Copyright 2015 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+
+// Entry point for LibFuzzer.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ const net::NetLogWithSource log;
+ net::FtpCtrlResponseBuffer buffer(log);
+ if (!buffer.ConsumeData(reinterpret_cast<const char*>(data), size)) {
+ return 0;
+ }
+ while (buffer.ResponseAvailable()) {
+ (void)buffer.PopResponse();
+ }
+ return 0;
+}
diff --git a/net/ftp/ftp_directory_listing_fuzzer.cc b/net/ftp/ftp_directory_listing_fuzzer.cc
new file mode 100644
index 0000000000000..459379e3eef89
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_fuzzer.cc
@@ -0,0 +1,20 @@
+// Copyright 2015 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "base/time/time.h"
+#include "net/ftp/ftp_directory_listing_parser.h"
+
+// Entry point for LibFuzzer.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ std::string buffer(reinterpret_cast<const char*>(data), size);
+ std::vector<net::FtpDirectoryListingEntry> entries;
+ net::ParseFtpDirectoryListing(buffer, base::Time::Now(), &entries);
+ return 0;
+}
diff --git a/net/ftp/ftp_directory_listing_parser.cc b/net/ftp/ftp_directory_listing_parser.cc
new file mode 100644
index 0000000000000..57013a78c6e72
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser.cc
@@ -0,0 +1,131 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_directory_listing_parser.h"
+
+#include "base/functional/callback.h"
+#include "base/functional/bind.h"
+#include "base/i18n/encoding_detection.h"
+#include "base/i18n/icu_string_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.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_vms.h"
+#include "net/ftp/ftp_directory_listing_parser_windows.h"
+#include "net/ftp/ftp_server_type.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::SUBSTITUTE,
+ &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 std::u16string& text,
+ const std::u16string& newline_separator,
+ const std::string& encoding,
+ const base::Time& current_time,
+ std::vector<FtpDirectoryListingEntry>* entries,
+ FtpServerType* server_type) {
+ std::vector<std::u16string> lines = base::SplitStringUsingSubstr(
+ text, newline_separator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ struct {
+ base::OnceCallback<bool(void)> callback;
+ FtpServerType server_type;
+ } parsers[] = {
+ {
+ base::BindOnce(&ParseFtpDirectoryListingLs, lines, current_time, entries),
+ SERVER_LS
+ },
+ {
+ base::BindOnce(&ParseFtpDirectoryListingWindows, lines, entries),
+ SERVER_WINDOWS
+ },
+ {
+ base::BindOnce(&ParseFtpDirectoryListingVms, lines, entries),
+ SERVER_VMS
+ },
+ };
+
+ for (size_t i = 0; i < std::ranges::size(parsers); i++) {
+ entries->clear();
+ if (std::move(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) {
+ std::string encoding;
+ if (!base::DetectEncoding(text, &encoding))
+ return ERR_ENCODING_DETECTION_FAILED;
+ const char* encoding_name = encoding.c_str();
+
+ std::u16string converted_text;
+ if (base::CodepageToUTF16(text, encoding_name,
+ base::OnStringConversionError::SUBSTITUTE,
+ &converted_text)) {
+ const char* const kNewlineSeparators[] = {"\n", "\r\n"};
+
+ for (size_t j = 0; j < std::ranges::size(kNewlineSeparators); j++) {
+ int rv = ParseListing(converted_text,
+ base::ASCIIToUTF16(kNewlineSeparators[j]),
+ encoding_name, 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);
+ return rv;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser.h b/net/ftp/ftp_directory_listing_parser.h
new file mode 100644
index 0000000000000..f813bb04e5209
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <stdint.h>
+
+#include <string>
+#include <vector>
+
+#include "base/time/time.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+struct FtpDirectoryListingEntry {
+ enum Type {
+ UNKNOWN,
+ FILE,
+ DIRECTORY,
+ SYMLINK,
+ };
+
+ FtpDirectoryListingEntry();
+
+ Type type;
+ std::u16string name; // Name (UTF-16-encoded).
+ std::string raw_name; // Name in original character encoding.
+ int64_t 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/net/ftp/ftp_directory_listing_parser_ls.cc b/net/ftp/ftp_directory_listing_parser_ls.cc
new file mode 100644
index 0000000000000..5388146beff3c
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls.cc
@@ -0,0 +1,227 @@
+// Copyright (c) 2012 The Chromium Authors
+// 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 net {
+
+namespace {
+
+bool TwoColumnDateListingToTime(const std::u16string& date,
+ const std::u16string& time,
+ base::Time* result) {
+ base::Time::Exploded time_exploded = { 0 };
+
+ // Date should be in format YYYY-MM-DD.
+ std::vector<std::u16string> date_parts = base::SplitString(
+ date, u"-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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<std::u16string> time_parts = base::SplitString(
+ time, u":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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 UTC.
+ return base::Time::FromUTCExploded(time_exploded, result);
+}
+
+// Returns the column index of the end of the date listing and detected
+// last modification time.
+bool DetectColumnOffsetSizeAndModificationTime(
+ const std::vector<std::u16string>& columns,
+ const base::Time& current_time,
+ size_t* offset,
+ std::u16string* 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 (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 (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
+
+bool ParseFtpDirectoryListingLs(
+ const std::vector<std::u16string>& 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<std::u16string> columns =
+ base::SplitString(base::CollapseWhitespace(lines[i], false), u" ",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // 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;
+
+ // Some FTP servers incorrectly return a negative integer for "n". Since
+ // this value is ignored anyway, just check any valid integer was
+ // provided.
+ int64_t total_number;
+ if (!base::StringToInt64(columns[1], &total_number))
+ return false;
+
+ continue;
+ }
+
+ FtpDirectoryListingEntry entry;
+
+ size_t column_offset;
+ std::u16string 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(u".:") != std::u16string::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.
+ entry.size = -1;
+ }
+ 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) {
+ std::u16string::size_type pos = entry.name.rfind(u" -> ");
+
+ // We don't require the " -> " to be present. Some FTP servers don't send
+ // the symlink target, possibly for security reasons.
+ if (pos != std::u16string::npos)
+ entry.name = entry.name.substr(0, pos);
+ }
+
+ entries->push_back(entry);
+ }
+
+ return true;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_ls.h b/net/ftp/ftp_directory_listing_parser_ls.h
new file mode 100644
index 0000000000000..42ec4fc6a3a27
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <string>
+#include <vector>
+
+#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<std::u16string>& lines,
+ const base::Time& current_time,
+ std::vector<FtpDirectoryListingEntry>* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_ls_unittest.cc b/net/ftp/ftp_directory_listing_parser_ls_unittest.cc
new file mode 100644
index 0000000000000..d64a564b413e0
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls_unittest.cc
@@ -0,0 +1,222 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/cxx17_backports.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", -1,
+ 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 },
+
+ { "drwxrwxr-x 1 500 244 660 Jan 1 00:0 bin",
+ FtpDirectoryListingEntry::DIRECTORY, "bin", -1,
+ 1994, 1, 1, 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 < base::size(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* const 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 < base::size(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* const 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",
+
+ // Invalid month value (30).
+ "drwxrwxrwx 2 root root 4096 2012-30-07 00:31 notas_servico",
+ };
+ for (size_t i = 0; i < base::size(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/net/ftp/ftp_directory_listing_parser_unittest.cc b/net/ftp/ftp_directory_listing_parser_unittest.cc
new file mode 100644
index 0000000000000..de08f99478562
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_unittest.cc
@@ -0,0 +1,186 @@
+// Copyright (c) 2012 The Chromium Authors
+// 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/files/file_util.h"
+#include "base/format_macros.h"
+#include "base/path_service.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.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 {
+
+struct FtpTestParam {
+ const char* name;
+ Error expected_result;
+};
+
+std::string TestName(testing::TestParamInfo<FtpTestParam> info) {
+ std::string result;
+ base::ReplaceChars(info.param.name, "-", "_", &result);
+ return result;
+}
+
+class FtpDirectoryListingParserTest
+ : public testing::TestWithParam<FtpTestParam> {};
+
+TEST_P(FtpDirectoryListingParserTest, Parse) {
+ FtpTestParam param = GetParam();
+ base::FilePath test_dir;
+ base::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;
+ EXPECT_TRUE(base::Time::FromUTCExploded(mock_current_time_exploded,
+ &mock_current_time));
+
+ SCOPED_TRACE(base::StringPrintf("Test case: %s", param.name));
+
+ std::string test_listing;
+ EXPECT_TRUE(
+ base::ReadFileToString(test_dir.AppendASCII(param.name), &test_listing));
+
+ std::vector<FtpDirectoryListingEntry> entries;
+ EXPECT_EQ(
+ param.expected_result,
+ ParseFtpDirectoryListing(test_listing, mock_current_time, &entries));
+ if (param.expected_result != OK)
+ return;
+
+ std::string expected_listing;
+ ASSERT_TRUE(base::ReadFileToString(
+ test_dir.AppendASCII(std::string(param.name) + ".expected"),
+ &expected_listing));
+
+ std::vector<std::string> lines = base::SplitStringUsingSubstr(
+ expected_listing, "\r\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // 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_t 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(base::UTF8ToUTF16(name), entry.name);
+ EXPECT_EQ(size, entry.size);
+
+ base::Time::Exploded time_exploded;
+ entry.last_modified.UTCExplode(&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 FtpTestParam kTestParams[] = {
+ {"dir-listing-ls-1", OK},
+ {"dir-listing-ls-1-utf8", OK},
+ {"dir-listing-ls-2", OK},
+ {"dir-listing-ls-3", OK},
+ {"dir-listing-ls-4", OK},
+ {"dir-listing-ls-5", OK},
+ {"dir-listing-ls-6", OK},
+ {"dir-listing-ls-7", OK},
+ {"dir-listing-ls-8", OK},
+ {"dir-listing-ls-9", OK},
+ {"dir-listing-ls-10", OK},
+ {"dir-listing-ls-11", OK},
+ {"dir-listing-ls-12", OK},
+ {"dir-listing-ls-13", OK},
+ {"dir-listing-ls-14", OK},
+ {"dir-listing-ls-15", OK},
+ {"dir-listing-ls-16", OK},
+ {"dir-listing-ls-17", OK},
+ {"dir-listing-ls-18", OK},
+ {"dir-listing-ls-19", OK},
+ {"dir-listing-ls-20", OK},
+ {"dir-listing-ls-21", OK},
+ {"dir-listing-ls-22", OK},
+ {"dir-listing-ls-23", OK},
+ {"dir-listing-ls-24", OK},
+
+ // Tests for Russian listings. The only difference between those
+ // files is character encoding:
+ {"dir-listing-ls-25", OK}, // UTF-8
+ {"dir-listing-ls-26", OK}, // KOI8-R
+ {"dir-listing-ls-27", OK}, // windows-1251
+
+ {"dir-listing-ls-28", OK}, // Hylafax FTP server
+ {"dir-listing-ls-29", OK},
+ {"dir-listing-ls-30", OK},
+ {"dir-listing-ls-31", OK},
+ {"dir-listing-ls-32", OK}, // busybox
+ {"dir-listing-ls-33", OK},
+ {"dir-listing-ls-34", OK}, // Broken encoding. Should not fail.
+
+ {"dir-listing-netware-1", OK},
+ {"dir-listing-netware-2", OK},
+ {"dir-listing-netware-3", OK},
+ {"dir-listing-os2-1", ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT},
+ {"dir-listing-vms-1", OK},
+ {"dir-listing-vms-2", OK},
+ {"dir-listing-vms-3", OK},
+ {"dir-listing-vms-4", OK},
+ {"dir-listing-vms-5", OK},
+ {"dir-listing-vms-6", OK},
+ {"dir-listing-vms-7", OK},
+ {"dir-listing-vms-8", OK},
+ {"dir-listing-windows-1", OK},
+ {"dir-listing-windows-2", OK},
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+ FtpDirectoryListingParserTest,
+ testing::ValuesIn(kTestParams),
+ TestName);
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_unittest.h b/net/ftp/ftp_directory_listing_parser_unittest.h
new file mode 100644
index 0000000000000..9dfc44acfd999
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_unittest.h
@@ -0,0 +1,81 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <stdint.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_t size;
+ int year;
+ int month;
+ int day_of_month;
+ int hour;
+ int minute;
+ };
+
+ protected:
+ FtpDirectoryListingParserTest() {}
+
+ std::vector<std::u16string> GetSingleLineTestCase(const std::string& text) {
+ std::vector<std::u16string> lines;
+ lines.push_back(base::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(base::UTF8ToUTF16(test_case.filename), entry.name);
+ EXPECT_EQ(test_case.size, entry.size);
+
+ base::Time::Exploded time_exploded;
+ entry.last_modified.UTCExplode(&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;
+
+ base::Time out_time;
+ EXPECT_TRUE(
+ base::Time::FromUTCExploded(mock_current_time_exploded, &out_time));
+ return out_time;
+ }
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
diff --git a/net/ftp/ftp_directory_listing_parser_vms.cc b/net/ftp/ftp_directory_listing_parser_vms.cc
new file mode 100644
index 0000000000000..7d0ca2a045f75
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms.cc
@@ -0,0 +1,313 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_directory_listing_parser_vms.h"
+
+#include <vector>
+
+#include "base/numerics/safe_math.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/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 std::u16string& raw_filename,
+ std::u16string* 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<std::u16string> listing_parts = base::SplitString(
+ raw_filename, u";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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<std::u16string> filename_parts = base::SplitString(
+ listing_parts[0], u".", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (filename_parts.size() != 2)
+ return false;
+ if (base::EqualsASCII(filename_parts[1], "DIR")) {
+ *parsed_filename = base::ToLowerASCII(filename_parts[0]);
+ *type = FtpDirectoryListingEntry::DIRECTORY;
+ } else {
+ *parsed_filename = base::ToLowerASCII(listing_parts[0]);
+ *type = FtpDirectoryListingEntry::FILE;
+ }
+ return true;
+}
+
+// VMS's directory listing gives file size in blocks. The exact file size is
+// unknown both because it is measured in blocks, but also because the block
+// size is unknown (but assumed to be 512 bytes).
+bool ApproximateFilesizeFromBlockCount(int64_t num_blocks, int64_t* out_size) {
+ if (num_blocks < 0)
+ return false;
+
+ const int kBlockSize = 512;
+ base::CheckedNumeric<int64_t> num_bytes = num_blocks;
+ num_bytes *= kBlockSize;
+
+ if (!num_bytes.IsValid())
+ return false; // Block count is too large.
+
+ *out_size = num_bytes.ValueOrDie();
+ return true;
+}
+
+bool ParseVmsFilesize(const std::u16string& input, int64_t* size) {
+ if (base::ContainsOnlyChars(input, u"*")) {
+ // Response consisting of asterisks means unknown size.
+ *size = -1;
+ return true;
+ }
+
+ int64_t num_blocks;
+ if (base::StringToInt64(input, &num_blocks))
+ return ApproximateFilesizeFromBlockCount(num_blocks, size);
+
+ std::vector<std::u16string_view> parts = base::SplitStringPiece(
+ input, u"/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (parts.size() != 2)
+ return false;
+
+ int64_t 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;
+
+ return ApproximateFilesizeFromBlockCount(blocks_used, size);
+}
+
+bool LooksLikeVmsFileProtectionListingPart(const std::u16string& 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");
+ std::u16string 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 std::u16string& input) {
+ if (input.length() < 2)
+ return false;
+ if (input.front() != '(' || input.back() != ')')
+ return false;
+
+ // We expect four parts of the file protection listing: for System, Owner,
+ // Group, and World.
+ std::vector<std::u16string> parts = base::SplitString(
+ std::u16string_view(input).substr(1, input.length() - 2), u",",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (parts.size() != 4)
+ return false;
+
+ return LooksLikeVmsFileProtectionListingPart(parts[0]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[1]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[2]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[3]);
+}
+
+bool LooksLikeVmsUserIdentificationCode(const std::u16string& input) {
+ if (input.length() < 2)
+ return false;
+ return input.front() == '[' && input.back() == ']';
+}
+
+bool LooksLikeVMSError(const std::u16string& text) {
+ static const char* const kPermissionDeniedMessages[] = {
+ "%RMS-E-FNF", // File not found.
+ "%RMS-E-PRV", // Access denied.
+ "%SYSTEM-F-NOPRIV",
+ "privilege",
+ };
+
+ for (size_t i = 0; i < std::ranges::size(kPermissionDeniedMessages); i++) {
+ if (text.find(base::ASCIIToUTF16(kPermissionDeniedMessages[i])) !=
+ std::u16string::npos)
+ return true;
+ }
+
+ return false;
+}
+
+bool VmsDateListingToTime(const std::vector<std::u16string>& 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<std::u16string_view> date_parts = base::SplitStringPiece(
+ columns[2], u"-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (date_parts.size() != 3)
+ return false;
+ if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month))
+ return false;
+ if (!FtpUtil::AbbreviatedMonthToNumber(std::u16string(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.
+ std::u16string 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<std::u16string_view> time_parts = base::SplitStringPiece(
+ time_column, u":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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 UTC.
+ return base::Time::FromUTCExploded(time_exploded, time);
+}
+
+} // namespace
+
+bool ParseFtpDirectoryListingVms(
+ const std::vector<std::u16string>& 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;
+
+ std::u16string total_of = u"Total of ";
+ char16_t space[2] = {' ', 0};
+ for (size_t i = 0; i < lines.size(); i++) {
+ if (lines[i].empty())
+ continue;
+
+ if (base::StartsWith(lines[i], total_of, base::CompareCase::SENSITIVE)) {
+ // 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<std::u16string> columns =
+ base::SplitString(base::CollapseWhitespace(lines[i], false), space,
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ 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.
+ columns = base::SplitString(
+ base::CollapseWhitespace(
+ lines[i - 1] + space + lines[i], false),
+ space, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ }
+
+ if (columns.empty())
+ return false;
+
+ 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/net/ftp/ftp_directory_listing_parser_vms.h b/net/ftp/ftp_directory_listing_parser_vms.h
new file mode 100644
index 0000000000000..e59a679303e23
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <string>
+#include <vector>
+
+#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<std::u16string>& lines,
+ std::vector<FtpDirectoryListingEntry>* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_vms_unittest.cc b/net/ftp/ftp_directory_listing_parser_vms_unittest.cc
new file mode 100644
index 0000000000000..88deedd5e2d28
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms_unittest.cc
@@ -0,0 +1,197 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/cxx17_backports.h"
+#include "base/format_macros.h"
+#include "base/strings/string_split.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_vms.h"
+
+using base::ASCIIToUTF16;
+
+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},
+ // This has an unusually large allocated block size (INT64_MAX), but
+ // shouldn't matter as it is not used.
+ {"ANNOUNCE.TXT;2 1/9223372036854775807 12-MAR-2005 08:44:57 [SYSTEM] "
+ "(RWED,RWED,RE,RE)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ };
+ for (size_t i = 0; i < base::size(good_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i,
+ good_cases[i].input));
+
+ std::vector<std::u16string> lines(
+ GetSingleLineTestCase(good_cases[i].input));
+
+ // The parser requires a directory header before accepting regular input.
+ lines.insert(lines.begin(), u"Directory ANONYMOUS_ROOT:[000000]");
+
+ // A valid listing must also have a "Total" line at the end.
+ lines.insert(lines.end(), u"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* const 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)",
+
+ // Block size (INT64_MAX) is too large -- will overflow when
+ // multiplying by 512 to calculate the file size in bytes.
+ "README.TXT;1 9223372036854775807 18-APR-2000 10:40:39.90",
+ "README.TXT;1 9223372036854775807/9223372036854775807 18-APR-2000 "
+ "10:40:39.90",
+
+ // Block size (larger than INT64_MAX) is too large -- will fail to
+ // parse to an int64_t
+ "README.TXT;1 19223372036854775807 18-APR-2000 10:40:39.90",
+ "README.TXT;1 19223372036854775807/19223372036854775807 18-APR-2000 "
+ "10:40:39.90",
+ };
+ for (size_t i = 0; i < base::size(bad_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, bad_cases[i]));
+
+ std::vector<std::u16string> lines(GetSingleLineTestCase(bad_cases[i]));
+
+ // The parser requires a directory header before accepting regular input.
+ lines.insert(lines.begin(), u"Directory ANONYMOUS_ROOT:[000000]");
+
+ // A valid listing must also have a "Total" line at the end.
+ lines.insert(lines.end(), u"Total of 1 file, 2 blocks.");
+
+ std::vector<FtpDirectoryListingEntry> entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines,
+ &entries));
+ }
+}
+
+TEST_F(FtpDirectoryListingParserVmsTest, BadDataAfterFooter) {
+ const char* const bad_cases[] = {
+ "garbage",
+ "Total of 1 file, 2 blocks.",
+ "Directory ANYNYMOUS_ROOT:[000000]",
+ };
+ for (size_t i = 0; i < base::size(bad_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i, bad_cases[i]));
+
+ std::vector<std::u16string> 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(), u"Directory ANONYMOUS_ROOT:[000000]");
+
+ // A valid listing must also have a "Total" line at the end.
+ lines.insert(lines.end(), u"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));
+ }
+ }
+}
+
+TEST_F(FtpDirectoryListingParserVmsTest, EmptyColumnZero) {
+ std::vector<std::u16string> lines;
+
+ // The parser requires a directory header before accepting regular input.
+ lines.push_back(u"garbage");
+
+ char16_t data[] = {0x0};
+ lines.push_back(std::u16string(data, 1));
+
+ std::vector<FtpDirectoryListingEntry> entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, &entries));
+}
+
+TEST_F(FtpDirectoryListingParserVmsTest, EmptyColumnWhitespace) {
+ std::vector<std::u16string> lines;
+
+ // The parser requires a directory header before accepting regular input.
+ lines.push_back(u"garbage");
+
+ lines.push_back(u" ");
+
+ std::vector<FtpDirectoryListingEntry> entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, &entries));
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_windows.cc b/net/ftp/ftp_directory_listing_parser_windows.cc
new file mode 100644
index 0000000000000..0d7906f5d68db
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows.cc
@@ -0,0 +1,74 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/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 {
+
+bool ParseFtpDirectoryListingWindows(
+ const std::vector<std::u16string>& lines,
+ std::vector<FtpDirectoryListingEntry>* entries) {
+ for (size_t i = 0; i < lines.size(); i++) {
+ if (lines[i].empty())
+ continue;
+
+ std::vector<std::u16string> columns =
+ base::SplitString(base::CollapseWhitespace(lines[i], false), u" ",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // 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 (base::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/net/ftp/ftp_directory_listing_parser_windows.h b/net/ftp/ftp_directory_listing_parser_windows.h
new file mode 100644
index 0000000000000..39aefe2929328
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <string>
+#include <vector>
+
+#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<std::u16string>& lines,
+ std::vector<FtpDirectoryListingEntry>* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_windows_unittest.cc b/net/ftp/ftp_directory_listing_parser_windows_unittest.cc
new file mode 100644
index 0000000000000..08b01b88f6017
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows_unittest.cc
@@ -0,0 +1,128 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/cxx17_backports.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_CHROMEOS) && !defined(OS_ANDROID)
+ // TODO(phajdan.jr): https://crbug.com/28792: 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_CHROMEOS) && !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 < base::size(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* const 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 < base::size(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* const 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 < base::size(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/net/ftp/ftp_network_layer.cc b/net/ftp/ftp_network_layer.cc
new file mode 100644
index 0000000000000..8a467f8896487
--- /dev/null
+++ b/net/ftp/ftp_network_layer.cc
@@ -0,0 +1,34 @@
+// Copyright (c) 2008 The Chromium Authors
+// 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 "base/check.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) {
+ DCHECK(host_resolver);
+}
+
+FtpNetworkLayer::~FtpNetworkLayer() = default;
+
+std::unique_ptr<FtpTransaction> FtpNetworkLayer::CreateTransaction() {
+ if (suspended_)
+ return nullptr;
+
+ return std::make_unique<FtpNetworkTransaction>(
+ session_->host_resolver(), ClientSocketFactory::GetDefaultFactory());
+}
+
+void FtpNetworkLayer::Suspend(bool suspend) {
+ suspended_ = suspend;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_network_layer.h b/net/ftp/ftp_network_layer.h
new file mode 100644
index 0000000000000..59c4d2b04bdf3
--- /dev/null
+++ b/net/ftp/ftp_network_layer.h
@@ -0,0 +1,38 @@
+// Copyright (c) 2012 The Chromium Authors
+// 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 <memory>
+
+#include "base/compiler_specific.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);
+ ~FtpNetworkLayer() override;
+ FtpNetworkLayer(const FtpNetworkLayer&) = delete;
+ FtpNetworkLayer& operator=(const FtpNetworkLayer&) =
+ delete;
+
+ // FtpTransactionFactory methods:
+ std::unique_ptr<FtpTransaction> CreateTransaction() override;
+ void Suspend(bool suspend) override;
+
+ private:
+ std::unique_ptr<FtpNetworkSession> session_;
+ bool suspended_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_NETWORK_LAYER_H_
diff --git a/net/ftp/ftp_network_session.cc b/net/ftp/ftp_network_session.cc
new file mode 100644
index 0000000000000..9de9796b08bf3
--- /dev/null
+++ b/net/ftp/ftp_network_session.cc
@@ -0,0 +1,14 @@
+// Copyright (c) 2010 The Chromium Authors
+// 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() = default;
+
+} // namespace net
diff --git a/net/ftp/ftp_network_session.h b/net/ftp/ftp_network_session.h
new file mode 100644
index 0000000000000..0ef5b781eec4e
--- /dev/null
+++ b/net/ftp/ftp_network_session.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/raw_ptr.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:
+ explicit FtpNetworkSession(HostResolver* host_resolver);
+ virtual ~FtpNetworkSession();
+
+ HostResolver* host_resolver() { return host_resolver_; }
+
+ private:
+ raw_ptr<HostResolver> const host_resolver_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_NETWORK_SESSION_H_
diff --git a/net/ftp/ftp_network_transaction.cc b/net/ftp/ftp_network_transaction.cc
new file mode 100644
index 0000000000000..85053f403f224
--- /dev/null
+++ b/net/ftp/ftp_network_transaction.cc
@@ -0,0 +1,1298 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_network_transaction.h"
+
+#include <vector>
+
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/compiler_specific.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/strings/escape.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/values.h"
+#include "net/base/address_list.h"
+#include "net/base/completion_once_callback.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/net_errors.h"
+#include "net/base/network_isolation_key.h"
+#include "net/base/parse_number.h"
+#include "net/base/port_util.h"
+#include "net/base/url_util.h"
+#include "net/ftp/ftp_request_info.h"
+#include "net/ftp/ftp_util.h"
+#include "net/log/net_log.h"
+#include "net/log/net_log_event_type.h"
+#include "net/log/net_log_source.h"
+#include "net/socket/client_socket_factory.h"
+#include "net/socket/stream_socket.h"
+#include "url/scheme_host_port.h"
+#include "url/url_constants.h"
+
+namespace net {
+
+namespace {
+
+const char kCRLF[] = "\r\n";
+
+const int kCtrlBufLen = 1024;
+
+// Returns true if |input| can be safely used as a part of an FTP command.
+bool IsValidFTPCommandSubstring(const std::string& input) {
+ // RFC 959 only allows ASCII strings, but at least Firefox can send non-ASCII
+ // characters in the command if the request path contains them. To be
+ // compatible, we do the same and allow non-ASCII characters in a command.
+
+ // Protect agains newline injection attack.
+ if (input.find_first_of("\r\n") != std::string::npos)
+ return false;
+
+ return true;
+}
+
+enum ErrorClass {
+ // The requested action was initiated. The client should expect another
+ // reply before issuing the next command.
+ ERROR_CLASS_INITIATED,
+
+ // The requested action has been successfully completed.
+ ERROR_CLASS_OK,
+
+ // The command has been accepted, but to complete the operation, more
+ // information must be sent by the client.
+ ERROR_CLASS_INFO_NEEDED,
+
+ // The command was not accepted and the requested action did not take place.
+ // This condition is temporary, and the client is encouraged to restart the
+ // command sequence.
+ ERROR_CLASS_TRANSIENT_ERROR,
+
+ // The command was not accepted and the requested action did not take place.
+ // This condition is rather permanent, and the client is discouraged from
+ // repeating the exact request.
+ ERROR_CLASS_PERMANENT_ERROR,
+};
+
+// Returns the error class for given response code. Caller should ensure
+// that |response_code| is in range 100-599.
+ErrorClass GetErrorClass(int response_code) {
+ if (response_code >= 100 && response_code <= 199)
+ return ERROR_CLASS_INITIATED;
+
+ if (response_code >= 200 && response_code <= 299)
+ return ERROR_CLASS_OK;
+
+ if (response_code >= 300 && response_code <= 399)
+ return ERROR_CLASS_INFO_NEEDED;
+
+ if (response_code >= 400 && response_code <= 499)
+ return ERROR_CLASS_TRANSIENT_ERROR;
+
+ if (response_code >= 500 && response_code <= 599)
+ return ERROR_CLASS_PERMANENT_ERROR;
+
+ // We should not be called on invalid error codes.
+ NOTREACHED() << response_code;
+}
+
+// Returns network error code for received FTP |response_code|.
+int GetNetErrorCodeForFtpResponseCode(int response_code) {
+ switch (response_code) {
+ case 421:
+ return ERR_FTP_SERVICE_UNAVAILABLE;
+ case 426:
+ return ERR_FTP_TRANSFER_ABORTED;
+ case 450:
+ return ERR_FTP_FILE_BUSY;
+ case 500:
+ case 501:
+ return ERR_FTP_SYNTAX_ERROR;
+ case 502:
+ case 504:
+ return ERR_FTP_COMMAND_NOT_SUPPORTED;
+ case 503:
+ return ERR_FTP_BAD_COMMAND_SEQUENCE;
+ default:
+ return ERR_FTP_FAILED;
+ }
+}
+
+// From RFC 2428 Section 3:
+// The text returned in response to the EPSV command MUST be:
+// <some text> (<d><d><d><tcp-port><d>)
+// <d> is a delimiter character, ideally to be |
+bool ExtractPortFromEPSVResponse(const FtpCtrlResponse& response, int* port) {
+ if (response.lines.size() != 1)
+ return false;
+
+ std::string_view epsv_line(response.lines[0]);
+ size_t start = epsv_line.find('(');
+ // If the line doesn't have a '(' or doesn't have enough characters after the
+ // first '(', fail.
+ if (start == std::string_view::npos || epsv_line.length() - start < 7)
+ return false;
+
+ char separator = epsv_line[start + 1];
+
+ // Make sure we have "(<d><d><d>...", where <d> is not a number.
+ if ((separator >= '0' && separator <= '9') ||
+ epsv_line[start + 2] != separator || epsv_line[start + 3] != separator) {
+ return false;
+ }
+
+ // Skip over those characters.
+ start += 4;
+
+ // Make sure there's a terminal <d>.
+ size_t end = epsv_line.find(separator, start);
+ if (end == std::string_view::npos)
+ return false;
+
+ return ParseInt32(epsv_line.substr(start, end - start),
+ ParseIntFormat::NON_NEGATIVE, port);
+}
+
+// There are two way we can receive IP address and port.
+// (127,0,0,1,23,21) IP address and port encapsulated in ().
+// 127,0,0,1,23,21 IP address and port without ().
+//
+// See RFC 959, Section 4.1.2
+bool ExtractPortFromPASVResponse(const FtpCtrlResponse& response, int* port) {
+ if (response.lines.size() != 1)
+ return false;
+
+ std::string line(response.lines[0]);
+ if (!base::IsStringASCII(line))
+ return false;
+ if (line.length() < 2)
+ return false;
+
+ size_t paren_pos = line.find('(');
+ if (paren_pos == std::string::npos) {
+ // Find the first comma and use it to locate the beginning
+ // of the response data.
+ size_t comma_pos = line.find(',');
+ if (comma_pos == std::string::npos)
+ return false;
+
+ size_t space_pos = line.rfind(' ', comma_pos);
+ if (space_pos != std::string::npos)
+ line = line.substr(space_pos + 1);
+ } else {
+ // Remove the parentheses and use the text inside them.
+ size_t closing_paren_pos = line.rfind(')');
+ if (closing_paren_pos == std::string::npos)
+ return false;
+ if (closing_paren_pos <= paren_pos)
+ return false;
+
+ line = line.substr(paren_pos + 1, closing_paren_pos - paren_pos - 1);
+ }
+
+ // Split the line into comma-separated pieces and extract
+ // the last two.
+ std::vector<std::string_view> pieces = base::SplitStringPiece(
+ line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (pieces.size() != 6)
+ return false;
+
+ // Ignore the IP address supplied in the response. We are always going
+ // to connect back to the same server to prevent FTP PASV port scanning.
+ uint32_t p0, p1;
+ if (!ParseUint32(pieces[4], ParseIntFormat::NON_NEGATIVE, &p0))
+ return false;
+ if (!ParseUint32(pieces[5], ParseIntFormat::NON_NEGATIVE, &p1))
+ return false;
+ if (p0 > 0xFF || p1 > 0xFF)
+ return false;
+
+ *port = (p0 << 8) + p1;
+ return true;
+}
+
+} // namespace
+
+FtpNetworkTransaction::FtpNetworkTransaction(
+ HostResolver* resolver,
+ ClientSocketFactory* socket_factory)
+ : command_sent_(COMMAND_NONE),
+ io_callback_(base::BindRepeating(&FtpNetworkTransaction::OnIOComplete,
+ base::Unretained(this))),
+ request_(nullptr),
+ resolver_(resolver),
+ read_ctrl_buf_(base::MakeRefCounted<IOBufferWithSize>(kCtrlBufLen)),
+ read_data_buf_len_(0),
+ last_error_(OK),
+ system_type_(SYSTEM_TYPE_UNKNOWN),
+ // Use image (binary) transfer by default. It should always work,
+ // whereas the ascii transfer may damage binary data.
+ data_type_(DATA_TYPE_IMAGE),
+ resource_type_(RESOURCE_TYPE_UNKNOWN),
+ use_epsv_(true),
+ data_connection_port_(0),
+ socket_factory_(socket_factory),
+ next_state_(STATE_NONE),
+ state_after_data_connect_complete_(STATE_NONE) {}
+
+FtpNetworkTransaction::~FtpNetworkTransaction() = default;
+
+int FtpNetworkTransaction::Stop(int error) {
+ if (command_sent_ == COMMAND_QUIT) {
+ if (error != ERR_EMPTY_RESPONSE)
+ return error;
+
+ // For empty responses, if this is propagating an error, then it will return
+ // the error. If the error occurred during a QUIT command, then this will
+ // return OK since there was no previous error. Some FTP servers are lazy
+ // and do not bother responding to QUIT commands.
+ // See https://crbug.com/633841
+ return last_error_;
+ }
+
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ last_error_ = error;
+ return OK;
+}
+
+int FtpNetworkTransaction::Start(
+ raw_ptr <FtpRequestInfo> request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) {
+ net_log_ = net_log;
+ request_ = request_info;
+ traffic_annotation_ = MutableNetworkTrafficAnnotationTag(traffic_annotation);
+
+ ctrl_response_buffer_ = std::make_unique<FtpCtrlResponseBuffer>(net_log_);
+
+ if (request_->url.has_username()) {
+ std::u16string username;
+ std::u16string password;
+ GetIdentityFromURL(request_->url, &username, &password);
+ credentials_.Set(username, password);
+ } else {
+ credentials_.Set(u"anonymous", u"chrome@example.com");
+ }
+
+ DetectTypecode();
+
+ 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);
+
+ // This may unescape to non-ASCII characters, but we allow that. See the
+ // comment for IsValidFTPCommandSubstring.
+ if (!base::UnescapeBinaryURLComponentSafe(
+ gurl_path, true /* fail_on_path_separators*/, &unescaped_path_)) {
+ return ERR_INVALID_URL;
+ }
+ }
+
+ next_state_ = STATE_CTRL_RESOLVE_HOST;
+ int rv = DoLoop(OK);
+ if (rv == ERR_IO_PENDING)
+ user_callback_ = std::move(callback);
+ return rv;
+}
+
+int FtpNetworkTransaction::RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback callback) {
+ ResetStateForRestart();
+
+ credentials_ = credentials;
+
+ next_state_ = STATE_CTRL_RESOLVE_HOST;
+ int rv = DoLoop(OK);
+ if (rv == ERR_IO_PENDING)
+ user_callback_ = std::move(callback);
+ return rv;
+}
+
+int FtpNetworkTransaction::Read(IOBuffer* buf,
+ int buf_len,
+ CompletionOnceCallback callback) {
+ DCHECK(buf);
+ DCHECK_GT(buf_len, 0);
+
+ read_data_buf_ = buf;
+ read_data_buf_len_ = buf_len;
+
+ next_state_ = STATE_DATA_READ;
+ int rv = DoLoop(OK);
+ if (rv == ERR_IO_PENDING)
+ user_callback_ = std::move(callback);
+ return rv;
+}
+
+const FtpResponseInfo* FtpNetworkTransaction::GetResponseInfo() const {
+ return &response_;
+}
+
+LoadState FtpNetworkTransaction::GetLoadState() const {
+ if (next_state_ == STATE_CTRL_RESOLVE_HOST_COMPLETE)
+ return LOAD_STATE_RESOLVING_HOST;
+
+ if (next_state_ == STATE_CTRL_CONNECT_COMPLETE ||
+ next_state_ == STATE_DATA_CONNECT_COMPLETE)
+ return LOAD_STATE_CONNECTING;
+
+ if (next_state_ == STATE_DATA_READ_COMPLETE)
+ return LOAD_STATE_READING_RESPONSE;
+
+ if (command_sent_ == COMMAND_RETR && read_data_buf_.get())
+ return LOAD_STATE_READING_RESPONSE;
+
+ if (command_sent_ == COMMAND_QUIT)
+ return LOAD_STATE_IDLE;
+
+ if (command_sent_ != COMMAND_NONE)
+ return LOAD_STATE_SENDING_REQUEST;
+
+ return LOAD_STATE_IDLE;
+}
+
+uint64_t FtpNetworkTransaction::GetUploadProgress() const {
+ return 0;
+}
+
+void FtpNetworkTransaction::ResetStateForRestart() {
+ command_sent_ = COMMAND_NONE;
+ user_callback_.Reset();
+ response_ = FtpResponseInfo();
+ read_ctrl_buf_ = base::MakeRefCounted<IOBufferWithSize>(kCtrlBufLen);
+ ctrl_response_buffer_ = std::make_unique<FtpCtrlResponseBuffer>(net_log_);
+ read_data_buf_ = nullptr;
+ read_data_buf_len_ = 0;
+ if (write_buf_.get())
+ write_buf_->SetOffset(0);
+ last_error_ = OK;
+ data_connection_port_ = 0;
+ ctrl_socket_.reset();
+ data_socket_.reset();
+ next_state_ = STATE_NONE;
+ state_after_data_connect_complete_ = STATE_NONE;
+}
+
+void FtpNetworkTransaction::EstablishDataConnection(State state_after_connect) {
+ DCHECK(state_after_connect == STATE_CTRL_WRITE_RETR ||
+ state_after_connect == STATE_CTRL_WRITE_LIST);
+ state_after_data_connect_complete_ = state_after_connect;
+ next_state_ = use_epsv_ ? STATE_CTRL_WRITE_EPSV : STATE_CTRL_WRITE_PASV;
+}
+
+void FtpNetworkTransaction::DoCallback(int rv) {
+ DCHECK(rv != ERR_IO_PENDING);
+
+ std::move(user_callback_).Run(rv);
+}
+
+void FtpNetworkTransaction::OnIOComplete(int result) {
+ int rv = DoLoop(result);
+ if (rv != ERR_IO_PENDING)
+ DoCallback(rv);
+}
+
+int FtpNetworkTransaction::ProcessCtrlResponse() {
+ FtpCtrlResponse response = ctrl_response_buffer_->PopResponse();
+
+ int rv = OK;
+ switch (command_sent_) {
+ case COMMAND_NONE:
+ // TODO(phajdan.jr): https://crbug.com/526721: Check for errors in the
+ // welcome message.
+ next_state_ = STATE_CTRL_WRITE_USER;
+ break;
+ case COMMAND_USER:
+ rv = ProcessResponseUSER(response);
+ break;
+ case COMMAND_PASS:
+ rv = ProcessResponsePASS(response);
+ break;
+ case COMMAND_SYST:
+ rv = ProcessResponseSYST(response);
+ break;
+ case COMMAND_PWD:
+ rv = ProcessResponsePWD(response);
+ break;
+ case COMMAND_TYPE:
+ rv = ProcessResponseTYPE(response);
+ break;
+ case COMMAND_EPSV:
+ rv = ProcessResponseEPSV(response);
+ break;
+ case COMMAND_PASV:
+ rv = ProcessResponsePASV(response);
+ break;
+ case COMMAND_SIZE:
+ rv = ProcessResponseSIZE(response);
+ break;
+ case COMMAND_RETR:
+ rv = ProcessResponseRETR(response);
+ break;
+ case COMMAND_CWD:
+ rv = ProcessResponseCWD(response);
+ break;
+ case COMMAND_LIST:
+ rv = ProcessResponseLIST(response);
+ break;
+ case COMMAND_QUIT:
+ rv = ProcessResponseQUIT(response);
+ break;
+ default:
+ LOG(DFATAL) << "Unexpected value of command_sent_: " << command_sent_;
+ return ERR_UNEXPECTED;
+ }
+
+ // We may get multiple responses for some commands,
+ // see http://crbug.com/18036. Consume all responses, regardless of whether
+ // they make sense. On unexpected responses, SendFtpCommand expects all data
+ // to have already been consumed, even when sending the QUIT command.
+ while (ctrl_response_buffer_->ResponseAvailable() && rv == OK) {
+ response = ctrl_response_buffer_->PopResponse();
+
+ switch (command_sent_) {
+ case COMMAND_RETR:
+ rv = ProcessResponseRETR(response);
+ break;
+ case COMMAND_LIST:
+ rv = ProcessResponseLIST(response);
+ break;
+ default:
+ // Multiple responses for other commands are invalid.
+ rv = Stop(ERR_INVALID_RESPONSE);
+ break;
+ }
+ }
+
+ return rv;
+}
+
+// Used to prepare and send FTP command.
+int FtpNetworkTransaction::SendFtpCommand(const std::string& command,
+ const std::string& command_for_log,
+ Command cmd) {
+ // If we send a new command when we still have unprocessed responses
+ // for previous commands, the response receiving code will have no way to know
+ // which responses are for which command.
+ DCHECK(!ctrl_response_buffer_->ResponseAvailable());
+
+ DCHECK(!write_command_buf_.get());
+ DCHECK(!write_buf_.get());
+
+ if (!IsValidFTPCommandSubstring(command)) {
+ // Callers should validate the command themselves and return a more specific
+ // error code.
+ NOTREACHED();
+ }
+
+ command_sent_ = cmd;
+
+ write_command_buf_ =
+ base::MakeRefCounted<IOBufferWithSize>(command.length() + 2);
+ write_buf_ = base::MakeRefCounted<DrainableIOBuffer>(
+ write_command_buf_, write_command_buf_->size());
+ memcpy(write_command_buf_->data(), command.data(), command.length());
+ memcpy(write_command_buf_->data() + command.length(), kCRLF, 2);
+
+ net_log_.AddEventWithStringParams(NetLogEventType::FTP_COMMAND_SENT,
+ "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_ + unescaped_path_);
+
+ // Make sure that if the path is expected to be a file, it won't end
+ // with a trailing slash.
+ if (!is_directory && path.length() > 1 && path.back() == '/')
+ path.erase(path.length() - 1);
+
+ if (system_type_ == SYSTEM_TYPE_VMS) {
+ if (is_directory)
+ path = FtpUtil::UnixDirectoryPathToVMS(path);
+ else
+ path = FtpUtil::UnixFilePathToVMS(path);
+ }
+
+ DCHECK(IsValidFTPCommandSubstring(path));
+ return path;
+}
+
+void FtpNetworkTransaction::DetectTypecode() {
+ if (!request_->url.has_path())
+ return;
+ std::string gurl_path(request_->url.path());
+
+ // Extract the typecode, see RFC 1738 section 3.2.2. FTP url-path.
+ std::string::size_type pos = gurl_path.rfind(';');
+ if (pos == std::string::npos)
+ return;
+ std::string typecode_string(gurl_path.substr(pos));
+ if (typecode_string == ";type=a") {
+ data_type_ = DATA_TYPE_ASCII;
+ resource_type_ = RESOURCE_TYPE_FILE;
+ } else if (typecode_string == ";type=i") {
+ data_type_ = DATA_TYPE_IMAGE;
+ resource_type_ = RESOURCE_TYPE_FILE;
+ } else if (typecode_string == ";type=d") {
+ resource_type_ = RESOURCE_TYPE_DIRECTORY;
+ }
+}
+
+int FtpNetworkTransaction::DoLoop(int result) {
+ DCHECK(next_state_ != STATE_NONE);
+
+ int rv = result;
+ do {
+ State state = next_state_;
+ next_state_ = STATE_NONE;
+ switch (state) {
+ case STATE_CTRL_RESOLVE_HOST:
+ DCHECK(rv == OK);
+ rv = DoCtrlResolveHost();
+ break;
+ case STATE_CTRL_RESOLVE_HOST_COMPLETE:
+ rv = DoCtrlResolveHostComplete(rv);
+ break;
+ case STATE_CTRL_CONNECT:
+ DCHECK(rv == OK);
+ rv = DoCtrlConnect();
+ break;
+ case STATE_CTRL_CONNECT_COMPLETE:
+ rv = DoCtrlConnectComplete(rv);
+ break;
+ case STATE_CTRL_READ:
+ DCHECK(rv == OK);
+ rv = DoCtrlRead();
+ break;
+ case STATE_CTRL_READ_COMPLETE:
+ rv = DoCtrlReadComplete(rv);
+ break;
+ case STATE_CTRL_WRITE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWrite();
+ break;
+ case STATE_CTRL_WRITE_COMPLETE:
+ rv = DoCtrlWriteComplete(rv);
+ break;
+ case STATE_CTRL_WRITE_USER:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteUSER();
+ break;
+ case STATE_CTRL_WRITE_PASS:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePASS();
+ break;
+ case STATE_CTRL_WRITE_SYST:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteSYST();
+ break;
+ case STATE_CTRL_WRITE_PWD:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePWD();
+ break;
+ case STATE_CTRL_WRITE_TYPE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteTYPE();
+ break;
+ case STATE_CTRL_WRITE_EPSV:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteEPSV();
+ break;
+ case STATE_CTRL_WRITE_PASV:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePASV();
+ break;
+ case STATE_CTRL_WRITE_RETR:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteRETR();
+ break;
+ case STATE_CTRL_WRITE_SIZE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteSIZE();
+ break;
+ case STATE_CTRL_WRITE_CWD:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteCWD();
+ break;
+ case STATE_CTRL_WRITE_LIST:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteLIST();
+ break;
+ case STATE_CTRL_WRITE_QUIT:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteQUIT();
+ break;
+ case STATE_DATA_CONNECT:
+ DCHECK(rv == OK);
+ rv = DoDataConnect();
+ break;
+ case STATE_DATA_CONNECT_COMPLETE:
+ rv = DoDataConnectComplete(rv);
+ break;
+ case STATE_DATA_READ:
+ DCHECK(rv == OK);
+ rv = DoDataRead();
+ break;
+ case STATE_DATA_READ_COMPLETE:
+ rv = DoDataReadComplete(rv);
+ break;
+ default:
+ NOTREACHED() << "bad state";
+ }
+ } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
+ return rv;
+}
+
+int FtpNetworkTransaction::DoCtrlResolveHost() {
+ next_state_ = STATE_CTRL_RESOLVE_HOST_COMPLETE;
+
+ resolve_request_ =
+ resolver_->CreateRequest(url::SchemeHostPort(request_->url),
+ NetworkAnonymizationKey(), net_log_, std::nullopt);
+ return resolve_request_->Start(base::BindOnce(
+ &FtpNetworkTransaction::OnIOComplete, base::Unretained(this)));
+}
+
+int FtpNetworkTransaction::DoCtrlResolveHostComplete(int result) {
+ if (result == OK)
+ next_state_ = STATE_CTRL_CONNECT;
+ return result;
+}
+
+int FtpNetworkTransaction::DoCtrlConnect() {
+ next_state_ = STATE_CTRL_CONNECT_COMPLETE;
+ // TODO(https://crbug.com/1123197): Pass a non-null NetworkQualityEstimator.
+ NetworkQualityEstimator* network_quality_estimator = nullptr;
+
+ DCHECK(resolve_request_ && resolve_request_->GetAddressResults());
+ ctrl_socket_ = socket_factory_->CreateTransportClientSocket(
+ AddressList(*resolve_request_->GetAddressResults()), nullptr,
+ network_quality_estimator, net_log_.net_log(), net_log_.source());
+ net_log_.AddEventReferencingSource(NetLogEventType::FTP_CONTROL_CONNECTION,
+ ctrl_socket_->NetLog().source());
+ 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_.remote_endpoint = 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 blocks IPv4 not to use EPSV instead of allowing
+ // 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() == u"anonymous") {
+ response_.needs_auth = true;
+ }
+ return Stop(ERR_EMPTY_RESPONSE);
+ }
+ if (result < 0)
+ return Stop(result);
+
+ ctrl_response_buffer_->ConsumeData(read_ctrl_buf_->data(), result);
+
+ if (!ctrl_response_buffer_->ResponseAvailable()) {
+ // Read more data from the control socket.
+ next_state_ = STATE_CTRL_READ;
+ return OK;
+ }
+
+ return ProcessCtrlResponse();
+}
+
+int FtpNetworkTransaction::DoCtrlWrite() {
+ next_state_ = STATE_CTRL_WRITE_COMPLETE;
+
+ return ctrl_socket_->Write(write_buf_.get(), write_buf_->BytesRemaining(),
+ io_callback_,
+ NetworkTrafficAnnotationTag(traffic_annotation_));
+}
+
+int FtpNetworkTransaction::DoCtrlWriteComplete(int result) {
+ if (result < 0)
+ return result;
+
+ write_buf_->DidConsume(result);
+ if (write_buf_->BytesRemaining() == 0) {
+ // Clear the write buffer.
+ write_buf_ = nullptr;
+ write_command_buf_ = nullptr;
+
+ next_state_ = STATE_CTRL_READ;
+ } else {
+ next_state_ = STATE_CTRL_WRITE;
+ }
+ return OK;
+}
+
+// FTP Commands and responses
+
+// USER Command.
+int FtpNetworkTransaction::DoCtrlWriteUSER() {
+ std::string command = "USER " + base::UTF16ToUTF8(credentials_.username());
+
+ if (!IsValidFTPCommandSubstring(command))
+ return Stop(ERR_MALFORMED_IDENTITY);
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, "USER ***", COMMAND_USER);
+}
+
+int FtpNetworkTransaction::ProcessResponseUSER(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SYST;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ next_state_ = STATE_CTRL_WRITE_PASS;
+ break;
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ case ERROR_CLASS_PERMANENT_ERROR:
+ response_.needs_auth = true;
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// PASS command.
+int FtpNetworkTransaction::DoCtrlWritePASS() {
+ std::string command = "PASS " + base::UTF16ToUTF8(credentials_.password());
+
+ if (!IsValidFTPCommandSubstring(command))
+ return Stop(ERR_MALFORMED_IDENTITY);
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, "PASS ***", COMMAND_PASS);
+}
+
+int FtpNetworkTransaction::ProcessResponsePASS(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SYST;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ case ERROR_CLASS_PERMANENT_ERROR:
+ response_.needs_auth = true;
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// SYST command.
+int FtpNetworkTransaction::DoCtrlWriteSYST() {
+ std::string command = "SYST";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_SYST);
+}
+
+int FtpNetworkTransaction::ProcessResponseSYST(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ // All important info should be on the first line.
+ std::string line = response.lines[0];
+ // The response should be ASCII, which allows us to do case-insensitive
+ // comparisons easily. If it is not ASCII, we leave the system type
+ // as unknown.
+ if (base::IsStringASCII(line)) {
+ line = base::ToLowerASCII(line);
+
+ // Remove all whitespace, to correctly handle cases like fancy "V M S"
+ // response instead of "VMS".
+ base::RemoveChars(line, base::kWhitespaceASCII, &line);
+
+ // The "magic" strings we test for below have been gathered by an
+ // empirical study. VMS needs to come first because some VMS systems
+ // also respond with "UNIX emulation", which is not perfect. It is much
+ // more reliable to talk to these servers in their native language.
+ if (line.find("vms") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_VMS;
+ } else if (line.find("l8") != std::string::npos ||
+ line.find("unix") != std::string::npos ||
+ line.find("bsd") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_UNIX;
+ } else if (line.find("win32") != std::string::npos ||
+ line.find("windows") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_WINDOWS;
+ } else if (line.find("os/2") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_OS2;
+ }
+ }
+ next_state_ = STATE_CTRL_WRITE_PWD;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ // Server does not recognize the SYST command so proceed.
+ next_state_ = STATE_CTRL_WRITE_PWD;
+ break;
+ }
+ return OK;
+}
+
+// PWD command.
+int FtpNetworkTransaction::DoCtrlWritePWD() {
+ std::string command = "PWD";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_PWD);
+}
+
+int FtpNetworkTransaction::ProcessResponsePWD(const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ // The info we look for should be on the first line.
+ std::string line = response.lines[0];
+ if (line.empty())
+ return Stop(ERR_INVALID_RESPONSE);
+ std::string::size_type quote_pos = line.find('"');
+ if (quote_pos != std::string::npos) {
+ line = line.substr(quote_pos + 1);
+ quote_pos = line.find('"');
+ if (quote_pos == std::string::npos)
+ return Stop(ERR_INVALID_RESPONSE);
+ line = line.substr(0, quote_pos);
+ }
+ if (system_type_ == SYSTEM_TYPE_VMS)
+ line = FtpUtil::VMSPathToUnix(line);
+ if (!line.empty() && line.back() == '/')
+ line.erase(line.length() - 1);
+ // Fail if the "path" contains characters not allowed in commands.
+ // This does mean that files with CRs or LFs in their names aren't
+ // handled, but that's probably for the best.
+ if (!IsValidFTPCommandSubstring(line))
+ return Stop(ERR_INVALID_RESPONSE);
+ current_remote_directory_ = line;
+ next_state_ = STATE_CTRL_WRITE_TYPE;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// TYPE command.
+int FtpNetworkTransaction::DoCtrlWriteTYPE() {
+ std::string command = "TYPE ";
+ if (data_type_ == DATA_TYPE_ASCII) {
+ command += "A";
+ } else if (data_type_ == DATA_TYPE_IMAGE) {
+ command += "I";
+ } else {
+ NOTREACHED();
+ }
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_TYPE);
+}
+
+int FtpNetworkTransaction::ProcessResponseTYPE(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SIZE;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// EPSV command
+int FtpNetworkTransaction::DoCtrlWriteEPSV() {
+ const std::string command = "EPSV";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_EPSV);
+}
+
+int FtpNetworkTransaction::ProcessResponseEPSV(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ int port;
+ if (!ExtractPortFromEPSVResponse(response, &port))
+ return Stop(ERR_INVALID_RESPONSE);
+ if (IsWellKnownPort(port) ||
+ !IsPortAllowedForScheme(port, url::kFtpScheme)) {
+ return Stop(ERR_UNSAFE_PORT);
+ }
+ data_connection_port_ = static_cast<uint16_t>(port);
+ next_state_ = STATE_DATA_CONNECT;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ case ERROR_CLASS_PERMANENT_ERROR:
+ use_epsv_ = false;
+ next_state_ = STATE_CTRL_WRITE_PASV;
+ return OK;
+ }
+ return OK;
+}
+
+// PASV command
+int FtpNetworkTransaction::DoCtrlWritePASV() {
+ std::string command = "PASV";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_PASV);
+}
+
+int FtpNetworkTransaction::ProcessResponsePASV(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ int port;
+ if (!ExtractPortFromPASVResponse(response, &port))
+ return Stop(ERR_INVALID_RESPONSE);
+ if (IsWellKnownPort(port) ||
+ !IsPortAllowedForScheme(port, url::kFtpScheme)) {
+ return Stop(ERR_UNSAFE_PORT);
+ }
+ data_connection_port_ = static_cast<uint16_t>(port);
+ next_state_ = STATE_DATA_CONNECT;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// RETR command
+int FtpNetworkTransaction::DoCtrlWriteRETR() {
+ std::string command = "RETR " + GetRequestPathForFtpCommand(false);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_RETR);
+}
+
+int FtpNetworkTransaction::ProcessResponseRETR(
+ const FtpCtrlResponse& response) {
+ // Resource type should be either filled in by DetectTypecode() or
+ // detected with CWD. RETR is sent only when the resource is a file.
+ DCHECK_EQ(RESOURCE_TYPE_FILE, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ // We want the client to start reading the response at this point.
+ // It got here either through Start or RestartWithAuth. We want that
+ // method to complete. Not setting next state here will make DoLoop exit
+ // and in turn make Start/RestartWithAuth complete.
+ break;
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+
+ return OK;
+}
+
+// SIZE command
+int FtpNetworkTransaction::DoCtrlWriteSIZE() {
+ std::string command = "SIZE " + GetRequestPathForFtpCommand(false);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_SIZE);
+}
+
+int FtpNetworkTransaction::ProcessResponseSIZE(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ break;
+ case ERROR_CLASS_OK:
+ if (response.lines.size() != 1)
+ return Stop(ERR_INVALID_RESPONSE);
+ int64_t size;
+ if (!base::StringToInt64(response.lines[0], &size))
+ return Stop(ERR_INVALID_RESPONSE);
+ if (size < 0)
+ return Stop(ERR_INVALID_RESPONSE);
+
+ // A successful response to SIZE does not mean the resource is a file.
+ // Some FTP servers (for example, the qnx one) send a SIZE even for
+ // directories.
+ response_.expected_content_size = size;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ break;
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ break;
+ case ERROR_CLASS_PERMANENT_ERROR:
+ // It's possible that SIZE failed because the path is a directory.
+ // TODO(xunjieli): https://crbug.com/526724: Add a test for this case.
+ if (resource_type_ == RESOURCE_TYPE_UNKNOWN &&
+ response.status_code != 550) {
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ break;
+ }
+
+ // If the resource is known beforehand to be a file, RETR should be issued,
+ // otherwise do CWD which will detect the resource type.
+ if (resource_type_ == RESOURCE_TYPE_FILE)
+ EstablishDataConnection(STATE_CTRL_WRITE_RETR);
+ else
+ next_state_ = STATE_CTRL_WRITE_CWD;
+ return OK;
+}
+
+// CWD command
+int FtpNetworkTransaction::DoCtrlWriteCWD() {
+ std::string command = "CWD " + GetRequestPathForFtpCommand(true);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_CWD);
+}
+
+int FtpNetworkTransaction::ProcessResponseCWD(const FtpCtrlResponse& response) {
+ // CWD should be invoked only when the resource is not a file.
+ DCHECK_NE(RESOURCE_TYPE_FILE, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ resource_type_ = RESOURCE_TYPE_DIRECTORY;
+ EstablishDataConnection(STATE_CTRL_WRITE_LIST);
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ // Some FTP servers send response 451 (not a valid CWD response according
+ // to RFC 959) instead of 550.
+ if (response.status_code == 451)
+ return ProcessResponseCWDNotADirectory();
+
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ if (response.status_code == 550)
+ return ProcessResponseCWDNotADirectory();
+
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+
+ return OK;
+}
+
+int FtpNetworkTransaction::ProcessResponseCWDNotADirectory() {
+ if (resource_type_ == RESOURCE_TYPE_DIRECTORY) {
+ // We're assuming that the resource is a directory, but the server
+ // says it's not true. The most probable interpretation is that it
+ // doesn't exist (with FTP we can't be sure).
+ return Stop(ERR_FILE_NOT_FOUND);
+ }
+
+ // If it is not a directory, it is probably a file.
+ resource_type_ = RESOURCE_TYPE_FILE;
+
+ EstablishDataConnection(STATE_CTRL_WRITE_RETR);
+ return OK;
+}
+
+// LIST command
+int FtpNetworkTransaction::DoCtrlWriteLIST() {
+ // Use the -l option for mod_ftp configured in LISTIsNLST mode: the option
+ // forces LIST output instead of NLST (which would be ambiguous for us
+ // to parse).
+ std::string command("LIST -l");
+ if (system_type_ == SYSTEM_TYPE_VMS)
+ command = "LIST *.*;0";
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_LIST);
+}
+
+int FtpNetworkTransaction::ProcessResponseLIST(
+ const FtpCtrlResponse& response) {
+ // Resource type should be either filled in by DetectTypecode() or
+ // detected with CWD. LIST is sent only when the resource is a directory.
+ DCHECK_EQ(RESOURCE_TYPE_DIRECTORY, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ // We want the client to start reading the response at this point.
+ // It got here either through Start or RestartWithAuth. We want that
+ // method to complete. Not setting next state here will make DoLoop exit
+ // and in turn make Start/RestartWithAuth complete.
+ response_.is_directory_listing = true;
+ break;
+ case ERROR_CLASS_OK:
+ response_.is_directory_listing = true;
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// QUIT command
+int FtpNetworkTransaction::DoCtrlWriteQUIT() {
+ std::string command = "QUIT";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_QUIT);
+}
+
+int FtpNetworkTransaction::ProcessResponseQUIT(
+ const FtpCtrlResponse& response) {
+ ctrl_socket_->Disconnect();
+ return last_error_;
+}
+
+// Data Connection
+
+int FtpNetworkTransaction::DoDataConnect() {
+ next_state_ = STATE_DATA_CONNECT_COMPLETE;
+ IPEndPoint ip_endpoint;
+ AddressList data_address;
+ // Connect to the same host as the control socket to prevent PASV port
+ // scanning attacks.
+ int rv = ctrl_socket_->GetPeerAddress(&ip_endpoint);
+ if (rv != OK)
+ return Stop(rv);
+ data_address = AddressList::CreateFromIPAddress(
+ ip_endpoint.address(), data_connection_port_);
+ // TODO(https://crbug.com/1123197): Pass a non-null NetworkQualityEstimator.
+ NetworkQualityEstimator* network_quality_estimator = nullptr;
+
+ data_socket_ = socket_factory_->CreateTransportClientSocket(
+ data_address, nullptr, network_quality_estimator, net_log_.net_log(),
+ net_log_.source());
+ net_log_.AddEventReferencingSource(NetLogEventType::FTP_DATA_CONNECTION,
+ data_socket_->NetLog().source());
+ return data_socket_->Connect(io_callback_);
+}
+
+int FtpNetworkTransaction::DoDataConnectComplete(int result) {
+ if (result != OK && use_epsv_) {
+ // It's possible we hit a broken server, sadly. They can break in different
+ // ways. Some time out, some reset a connection. Fall back to PASV.
+ // TODO(phajdan.jr): https://crbug.com/526723: remember it for future
+ // transactions with this server.
+ use_epsv_ = false;
+ next_state_ = STATE_CTRL_WRITE_PASV;
+ return OK;
+ }
+
+ if (result != OK)
+ return Stop(result);
+
+ next_state_ = state_after_data_connect_complete_;
+ return OK;
+}
+
+int FtpNetworkTransaction::DoDataRead() {
+ DCHECK(read_data_buf_.get());
+ DCHECK_GT(read_data_buf_len_, 0);
+
+ if (!data_socket_ || !data_socket_->IsConnected()) {
+ // If we don't destroy the data socket completely, some servers will wait
+ // for us (http://crbug.com/21127). The half-closed TCP connection needs
+ // to be closed on our side too.
+ data_socket_.reset();
+
+ if (ctrl_socket_->IsConnected()) {
+ // Wait for the server's response, we should get it before sending QUIT.
+ next_state_ = STATE_CTRL_READ;
+ return OK;
+ }
+
+ // We are no longer connected to the server, so just finish the transaction.
+ return Stop(OK);
+ }
+
+ next_state_ = STATE_DATA_READ_COMPLETE;
+ read_data_buf_->data()[0] = 0;
+ return data_socket_->Read(
+ read_data_buf_.get(), read_data_buf_len_, io_callback_);
+}
+
+int FtpNetworkTransaction::DoDataReadComplete(int result) {
+ return result;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_network_transaction.h b/net/ftp/ftp_network_transaction.h
new file mode 100644
index 0000000000000..c7cb8753f8acf
--- /dev/null
+++ b/net/ftp/ftp_network_transaction.h
@@ -0,0 +1,268 @@
+// Copyright (c) 2012 The Chromium Authors
+// 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 <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "net/base/auth.h"
+#include "net/base/completion_once_callback.h"
+#include "net/base/completion_repeating_callback.h"
+#include "net/base/net_export.h"
+#include "net/dns/host_resolver.h"
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+#include "net/ftp/ftp_response_info.h"
+#include "net/ftp/ftp_transaction.h"
+#include "net/log/net_log_with_source.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+
+namespace net {
+
+class ClientSocketFactory;
+class StreamSocket;
+
+class NET_EXPORT_PRIVATE FtpNetworkTransaction : public FtpTransaction {
+ public:
+ FtpNetworkTransaction(HostResolver* resolver,
+ ClientSocketFactory* socket_factory);
+ ~FtpNetworkTransaction() override;
+
+ int Stop(int error);
+
+ // FtpTransaction methods:
+ int Start(raw_ptr <FtpRequestInfo> request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) override;
+ int RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback callback) override;
+ int Read(IOBuffer* buf,
+ int buf_len,
+ CompletionOnceCallback callback) override;
+ const FtpResponseInfo* GetResponseInfo() const override;
+ LoadState GetLoadState() const override;
+ uint64_t 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();
+
+ // Establishes the data connection and switches to |state_after_connect|.
+ // |state_after_connect| should only be RETR or LIST.
+ void EstablishDataConnection(State state_after_connect);
+
+ 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);
+
+ Command command_sent_;
+
+ CompletionRepeatingCallback io_callback_;
+ CompletionOnceCallback user_callback_;
+
+ NetLogWithSource net_log_;
+ raw_ptr<FtpRequestInfo> request_;
+ MutableNetworkTrafficAnnotationTag traffic_annotation_;
+ FtpResponseInfo response_;
+
+ raw_ptr<HostResolver> resolver_;
+ // Cancels the outstanding request on destruction.
+ std::unique_ptr<HostResolver::ResolveHostRequest> resolve_request_;
+
+ // User buffer passed to the Read method for control socket.
+ scoped_refptr<IOBuffer> read_ctrl_buf_;
+
+ std::unique_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_;
+
+ uint16_t data_connection_port_;
+
+ raw_ptr<ClientSocketFactory> const socket_factory_;
+
+ std::string unescaped_path_;
+
+ std::unique_ptr<StreamSocket> ctrl_socket_;
+ std::unique_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/net/ftp/ftp_network_transaction_unittest.cc b/net/ftp/ftp_network_transaction_unittest.cc
new file mode 100644
index 0000000000000..fb2facdd3c653
--- /dev/null
+++ b/net/ftp/ftp_network_transaction_unittest.cc
@@ -0,0 +1,1722 @@
+// Copyright (c) 2012 The Chromium Authors
+// 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/containers/circular_deque.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/io_buffer.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/sys_addrinfo.h"
+#include "net/base/test_completion_callback.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/ftp/ftp_request_info.h"
+#include "net/socket/socket_test_util.h"
+#include "net/test/gtest_util.h"
+#include "net/test/test_with_task_environment.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+using net::test::IsError;
+using net::test::IsOk;
+
+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 SocketDataProvider {
+ public:
+ enum State {
+ NONE,
+ PRE_USER,
+ PRE_PASSWD,
+ PRE_SYST,
+ PRE_PWD,
+ PRE_TYPE,
+ PRE_SIZE,
+ PRE_LIST_EPSV,
+ PRE_LIST_PASV,
+ PRE_LIST,
+ PRE_RETR,
+ PRE_RETR_EPSV,
+ PRE_RETR_PASV,
+ PRE_CWD,
+ PRE_QUIT,
+ PRE_NOPASV,
+ QUIT
+ };
+
+ FtpSocketDataProvider()
+ : short_read_limit_(0),
+ allow_unconsumed_reads_(false),
+ failure_injection_state_(NONE),
+ multiline_welcome_(false),
+ use_epsv_(true),
+ data_type_('I') {
+ Init();
+ }
+ ~FtpSocketDataProvider() override = default;
+
+ // SocketDataProvider implementation.
+ MockRead OnRead() override {
+ if (reads_.empty())
+ return MockRead(SYNCHRONOUS, ERR_UNEXPECTED);
+ MockRead result = reads_.front();
+ if (short_read_limit_ == 0 || result.data_len <= short_read_limit_) {
+ reads_.pop_front();
+ } else {
+ result.data_len = short_read_limit_;
+ reads_.front().data += result.data_len;
+ reads_.front().data_len -= result.data_len;
+ }
+ return result;
+ }
+
+ 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:
+ {
+ static const char response_one[] = "230 Welcome\r\n";
+ static 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,
+ PRE_SIZE, "200 TYPE set successfully\r\n");
+ case PRE_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST,
+ "227 Entering Extended Passive Mode (|||31744|)\r\n");
+ case PRE_LIST_PASV:
+ return Verify("PASV\r\n", data, PRE_LIST,
+ "227 Entering Passive Mode 127,0,0,1,123,123\r\n");
+ case PRE_RETR_EPSV:
+ return Verify("EPSV\r\n", data, PRE_RETR,
+ "227 Entering Extended Passive Mode (|||31744|)\r\n");
+ case PRE_RETR_PASV:
+ return Verify("PASV\r\n", data, PRE_RETR,
+ "227 Entering Passive Mode 127,0,0,1,123,123\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_;
+ }
+
+ void Reset() override {
+ reads_.clear();
+ Init();
+ }
+
+ bool AllReadDataConsumed() const override { return state_ == QUIT; }
+
+ bool AllWriteDataConsumed() const override { return state_ == QUIT; }
+
+ 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; }
+
+ int short_read_limit() const { return short_read_limit_; }
+ void set_short_read_limit(int limit) { short_read_limit_ = limit; }
+
+ void set_allow_unconsumed_reads(bool allow) {
+ allow_unconsumed_reads_ = allow;
+ }
+
+ 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));
+ }
+
+ // The next time there is a read from this socket, it will return |data|.
+ // Before calling SimulateRead next time, the previous data must be consumed.
+ void SimulateRead(const char* data, size_t length) {
+ if (!allow_unconsumed_reads_) {
+ EXPECT_TRUE(reads_.empty()) << "Unconsumed read: " << reads_.front().data;
+ }
+ reads_.push_back(MockRead(ASYNC, data, length));
+ }
+ void SimulateRead(const char* data) { SimulateRead(data, std::strlen(data)); }
+
+ private:
+ // List of reads to be consumed.
+ base::circular_deque<MockRead> reads_;
+
+ // Max number of bytes we will read at a time. 0 means no limit.
+ int short_read_limit_;
+
+ // If true, we'll not require the client to consume all data before we
+ // mock the next read.
+ bool allow_unconsumed_reads_;
+
+ 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() = default;
+ ~FtpSocketDataProviderDirectoryListing() override = default;
+
+ 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,
+ "550 I can only retrieve regular files\r\n");
+ case PRE_CWD:
+ return Verify("CWD /\r\n", data,
+ use_epsv() ? PRE_LIST_EPSV : PRE_LIST_PASV, "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() = default;
+ ~FtpSocketDataProviderDirectoryListingWithPasvFallback() override = default;
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST_PASV,
+ "500 no EPSV for you\r\n");
+ case PRE_SIZE:
+ return Verify("SIZE /\r\n", data, PRE_CWD,
+ "550 I can only retrieve regular files\r\n");
+ default:
+ return FtpSocketDataProviderDirectoryListing::OnWrite(data);
+ }
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(
+ FtpSocketDataProviderDirectoryListingWithPasvFallback);
+};
+
+class FtpSocketDataProviderVMSDirectoryListing : public FtpSocketDataProvider {
+ public:
+ FtpSocketDataProviderVMSDirectoryListing() = default;
+ ~FtpSocketDataProviderVMSDirectoryListing() override = default;
+
+ 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_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST_PASV,
+ "500 Invalid command\r\n");
+ case PRE_SIZE:
+ return Verify("SIZE ANONYMOUS_ROOT:[000000]dir\r\n", data, PRE_CWD,
+ "550 I can only retrieve regular files\r\n");
+ case PRE_CWD:
+ return Verify("CWD ANONYMOUS_ROOT:[dir]\r\n", data,
+ use_epsv() ? PRE_LIST_EPSV : PRE_LIST_PASV, "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() = default;
+ ~FtpSocketDataProviderVMSDirectoryListingRootDirectory() override = default;
+
+ 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_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST_PASV,
+ "500 EPSV command unknown\r\n");
+ case PRE_SIZE:
+ return Verify("SIZE ANONYMOUS_ROOT\r\n", data, PRE_CWD,
+ "550 I can only retrieve regular files\r\n");
+ case PRE_CWD:
+ return Verify("CWD ANONYMOUS_ROOT:[000000]\r\n", data,
+ use_epsv() ? PRE_LIST_EPSV : PRE_LIST_PASV, "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() = default;
+ ~FtpSocketDataProviderFileDownloadWithFileTypecode() override = default;
+
+ 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_RETR_EPSV : PRE_RETR_PASV, "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() = default;
+ ~FtpSocketDataProviderFileDownload() override = default;
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_SIZE:
+ return Verify(base::StringPrintf("SIZE %s\r\n", file_path_.c_str()),
+ data, PRE_CWD, "213 18\r\n");
+ case PRE_CWD:
+ return Verify(base::StringPrintf("CWD %s\r\n", file_path_.c_str()),
+ data, use_epsv() ? PRE_RETR_EPSV : PRE_RETR_PASV,
+ "550 Not a directory\r\n");
+ case PRE_RETR:
+ return Verify(base::StringPrintf("RETR %s\r\n", file_path_.c_str()),
+ data, PRE_QUIT, "200 OK\r\n");
+ default:
+ return FtpSocketDataProvider::OnWrite(data);
+ }
+ }
+
+ void set_file_path(const std::string& file_path) { file_path_ = file_path; }
+
+ private:
+ std::string file_path_ = "/file";
+
+ DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderFileDownload);
+};
+
+class FtpSocketDataProviderFileNotFound : public FtpSocketDataProvider {
+ public:
+ FtpSocketDataProviderFileNotFound() = default;
+ ~FtpSocketDataProviderFileNotFound() override = default;
+
+ 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,
+ "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() = default;
+ ~FtpSocketDataProviderFileDownloadWithPasvFallback() override = default;
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_RETR_EPSV:
+ return Verify("EPSV\r\n", data, PRE_RETR_PASV, "500 No can do\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(FtpSocketDataProviderFileDownloadWithPasvFallback);
+};
+
+class FtpSocketDataProviderFileDownloadZeroSize
+ : public FtpSocketDataProviderFileDownload {
+ public:
+ FtpSocketDataProviderFileDownloadZeroSize() = default;
+ ~FtpSocketDataProviderFileDownloadZeroSize() override = default;
+
+ 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() = default;
+ ~FtpSocketDataProviderFileDownloadCWD451() override = default;
+
+ 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() = default;
+ ~FtpSocketDataProviderVMSFileDownload() override = default;
+
+ 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_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST_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,
+ use_epsv() ? PRE_RETR_EPSV : 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 FtpSocketDataProviderFileDownloadInvalidResponse
+ : public FtpSocketDataProviderFileDownload {
+ public:
+ FtpSocketDataProviderFileDownloadInvalidResponse() = default;
+ ~FtpSocketDataProviderFileDownloadInvalidResponse() override = default;
+
+ 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) {}
+
+ ~FtpSocketDataProviderEvilEpsv() override = default;
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_RETR_EPSV:
+ return Verify("EPSV\r\n", data, expected_state_,
+ epsv_response_, epsv_response_length_);
+ default:
+ return FtpSocketDataProviderFileDownload::OnWrite(data);
+ }
+ }
+
+ private:
+ const char* const 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) {
+ }
+ ~FtpSocketDataProviderEvilPasv() override = default;
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_RETR_PASV:
+ return Verify("PASV\r\n", data, expected_state_, pasv_response_);
+ default:
+ return FtpSocketDataProviderFileDownloadWithPasvFallback::OnWrite(data);
+ }
+ }
+
+ private:
+ const char* const 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) {
+ }
+ ~FtpSocketDataProviderEvilSize() override = default;
+
+ 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* const 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) {
+ }
+ ~FtpSocketDataProviderEvilLogin() override = default;
+
+ 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* const expected_user_;
+ const char* const expected_password_;
+
+ DISALLOW_COPY_AND_ASSIGN(FtpSocketDataProviderEvilLogin);
+};
+
+class FtpNetworkTransactionTest : public PlatformTest,
+ public ::testing::WithParamInterface<int>,
+ public WithTaskEnvironment {
+ public:
+ FtpNetworkTransactionTest() : host_resolver_(new MockHostResolver) {
+ SetUpTransaction();
+
+ scoped_refptr<RuleBasedHostResolverProc> rules(
+ new RuleBasedHostResolverProc(nullptr));
+ 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());
+ }
+ ~FtpNetworkTransactionTest() override = default;
+
+ // Sets up an FtpNetworkTransaction and MocketClientSocketFactory, replacing
+ // the default one. Only needs to be called if a test runs multiple
+ // transactions.
+ void SetUpTransaction() {
+ mock_socket_factory_ = std::make_unique<MockClientSocketFactory>();
+ transaction_ = std::make_unique<FtpNetworkTransaction>(
+ host_resolver_.get(), mock_socket_factory_.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 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()),
+ };
+
+ std::unique_ptr<StaticSocketDataProvider> data_socket =
+ std::make_unique<StaticSocketDataProvider>(data_reads,
+ base::span<MockWrite>());
+ mock_socket_factory_->AddSocketDataProvider(data_socket.get());
+ FtpRequestInfo request_info = GetRequestInfo(request);
+ EXPECT_EQ(LOAD_STATE_IDLE, transaction_->GetLoadState());
+ ASSERT_EQ(
+ ERR_IO_PENDING,
+ transaction_->Start(&request_info, callback_.callback(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ EXPECT_NE(LOAD_STATE_IDLE, transaction_->GetLoadState());
+ ASSERT_EQ(expected_result, callback_.WaitForResult());
+ if (expected_result == OK) {
+ scoped_refptr<IOBuffer> io_buffer =
+ base::MakeRefCounted<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, expected_result);
+ }
+
+ std::unique_ptr<MockHostResolver> host_resolver_;
+ std::unique_ptr<MockClientSocketFactory> mock_socket_factory_;
+ std::unique_ptr<FtpNetworkTransaction> transaction_;
+ TestCompletionCallback callback_;
+};
+
+TEST_P(FtpNetworkTransactionTest, FailedLookup) {
+ FtpRequestInfo request_info = GetRequestInfo("ftp://badhost");
+ scoped_refptr<RuleBasedHostResolverProc> rules(
+ new RuleBasedHostResolverProc(nullptr));
+ 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(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsError(ERR_NAME_NOT_RESOLVED));
+ 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", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransaction) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host", 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()->remote_endpoint.ToStringWithoutPort());
+ EXPECT_EQ(21, transaction_->GetResponseInfo()->remote_endpoint.port());
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionWithPasvFallback) {
+ FtpSocketDataProviderDirectoryListingWithPasvFallback ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host", 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", 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", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionShortReads2) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.set_short_read_limit(2);
+ ExecuteTransaction(&ctrl_socket, "ftp://host", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionShortReads5) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.set_short_read_limit(5);
+ ExecuteTransaction(&ctrl_socket, "ftp://host", 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.set_allow_unconsumed_reads(true);
+ ctrl_socket.set_multiline_welcome(true);
+ ctrl_socket.set_short_read_limit(5);
+ ExecuteTransaction(&ctrl_socket, "ftp://host", OK);
+}
+
+// Regression test for http://crbug.com/60555.
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionZeroSize) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_SIZE,
+ FtpSocketDataProvider::PRE_CWD, "213 0\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionVMS) {
+ FtpSocketDataProviderVMSDirectoryListing ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/dir", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionVMSRootDirectory) {
+ FtpSocketDataProviderVMSDirectoryListingRootDirectory ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionTransferStarting) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_LIST,
+ FtpSocketDataProvider::PRE_QUIT,
+ "125-Data connection already open.\r\n"
+ "125 Transfer starting.\r\n"
+ "226 Transfer complete.\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransaction) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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()->remote_endpoint.ToStringWithoutPort());
+ EXPECT_EQ(21, transaction_->GetResponseInfo()->remote_endpoint.port());
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionWithPasvFallback) {
+ FtpSocketDataProviderFileDownloadWithPasvFallback ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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", 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", 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", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionShortReads2) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.set_short_read_limit(2);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionShortReads5) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.set_short_read_limit(5);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionZeroSize) {
+ FtpSocketDataProviderFileDownloadZeroSize ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionCWD451) {
+ FtpSocketDataProviderFileDownloadCWD451 ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionVMS) {
+ FtpSocketDataProviderVMSFileDownload ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionTransferStarting) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_RETR,
+ FtpSocketDataProvider::PRE_QUIT,
+ "125-Data connection already open.\r\n"
+ "125 Transfer starting.\r\n"
+ "226 Transfer complete.\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionInvalidResponse) {
+ FtpSocketDataProviderFileDownloadInvalidResponse ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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", 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", 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", 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", 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", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvInvalidPort1) {
+ // Unsafe. 8 * 256 + 1 = 2049, which is used by nfs.
+ FtpSocketDataProviderEvilPasv ctrl_socket(
+ "227 Portscan (127,0,0,1,256,100)\r\n", FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvInvalidPort2) {
+ // Unsafe. 8 * 256 + 1 = 2049, which is used by nfs.
+ FtpSocketDataProviderEvilPasv ctrl_socket(
+ "227 Portscan (127,0,0,1,100,256)\r\n", FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvInvalidPort3) {
+ // Unsafe. 8 * 256 + 1 = 2049, which is used by nfs.
+ FtpSocketDataProviderEvilPasv ctrl_socket(
+ "227 Portscan (127,0,0,1,-100,100)\r\n", FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvInvalidPort4) {
+ // Unsafe. 8 * 256 + 1 = 2049, which is used by nfs.
+ FtpSocketDataProviderEvilPasv ctrl_socket(
+ "227 Portscan (127,0,0,1,100,-100)\r\n", FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilPasvUnsafeHost) {
+ FtpSocketDataProviderEvilPasv ctrl_socket(
+ "227 Portscan (10,1,2,3,123,123)\r\n", FtpSocketDataProvider::PRE_RETR);
+ 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, base::span<MockWrite>());
+ 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(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsOk());
+
+ // 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 (auto 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", 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", 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", 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", 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;
+
+ // Breaking the string in the next line prevents MSVC warning C4125.
+ const char response[] = "227 Portscan (\0\0\031" "773\0)\r\n";
+ FtpSocketDataProviderEvilEpsv ctrl_socket(response, sizeof(response)-1,
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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", 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", 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", 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", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvInvalidPort) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||4294973296|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+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_RETR);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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", 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", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadUsername) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("hello%0Aworld", "test");
+ ExecuteTransaction(&ctrl_socket, "ftp://hello%0Aworld:test@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadPassword) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello%0Dworld");
+ ExecuteTransaction(&ctrl_socket, "ftp://test:hello%0Dworld@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInLogin) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("hello world", "test");
+ ExecuteTransaction(&ctrl_socket, "ftp://hello%20world:test@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInPassword) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello world");
+ ExecuteTransaction(&ctrl_socket, "ftp://test:hello%20world@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, FailOnInvalidUrls) {
+ const char* const kBadUrls[]{
+ // Make sure FtpNetworkTransaction doesn't request paths like
+ // "/foo/../bar". Doing so wouldn't be a security issue, client side, but
+ // just doesn't seem like a good idea.
+ "ftp://host/foo%2f..%2fbar%5c",
+
+ // LF
+ "ftp://host/foo%10.txt",
+ // CR
+ "ftp://host/foo%13.txt",
+
+ "ftp://host/foo%00.txt",
+ };
+
+ for (const char* bad_url : kBadUrls) {
+ SCOPED_TRACE(bad_url);
+
+ SetUpTransaction();
+ FtpRequestInfo request_info = GetRequestInfo(bad_url);
+ ASSERT_EQ(
+ ERR_INVALID_URL,
+ transaction_->Start(&request_info, callback_.callback(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ }
+}
+
+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(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsError(ERR_FTP_FAILED));
+
+ 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, ctrl_writes);
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket2);
+ ASSERT_EQ(ERR_IO_PENDING, transaction_->RestartWithAuth(
+ AuthCredentials(u"foo\nownz0red", u"innocent"),
+ callback_.callback()));
+ EXPECT_THAT(callback_.WaitForResult(), IsError(ERR_MALFORMED_IDENTITY));
+}
+
+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(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsError(ERR_FTP_FAILED));
+
+ 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, ctrl_writes);
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket2);
+ ASSERT_EQ(ERR_IO_PENDING, transaction_->RestartWithAuth(
+ AuthCredentials(u"innocent", u"foo\nownz0red"),
+ callback_.callback()));
+ EXPECT_THAT(callback_.WaitForResult(), IsError(ERR_MALFORMED_IDENTITY));
+}
+
+TEST_P(FtpNetworkTransactionTest, Escaping) {
+ const struct TestCase {
+ const char* url;
+ const char* expected_path;
+ } kTestCases[] = {
+ {"ftp://host/%20%21%22%23%24%25%79%80%81", "/ !\"#$%y\200\201"},
+ // This is no allowed to be unescaped by UnescapeURLComponent, since it's
+ // a lock icon. But it has no special meaning or security concern in the
+ // context of making FTP requests.
+ {"ftp://host/%F0%9F%94%92", "/\xF0\x9F\x94\x92"},
+ // Invalid UTF-8 character, which again has no special meaning over FTP.
+ {"ftp://host/%81", "/\x81"},
+ };
+
+ for (const auto& test_case : kTestCases) {
+ SCOPED_TRACE(test_case.url);
+
+ SetUpTransaction();
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.set_file_path(test_case.expected_path);
+ ExecuteTransaction(&ctrl_socket, test_case.url, OK);
+ }
+}
+
+// Test for http://crbug.com/23794.
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilSize) {
+ // Try to overflow int64_t in the response.
+ FtpSocketDataProviderEvilSize ctrl_socket(
+ "213 99999999999999999999999999999999\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", 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", OK);
+ EXPECT_EQ(3204427776LL,
+ transaction_->GetResponseInfo()->expected_content_size);
+}
+
+// Regression test for http://crbug.com/25023.
+TEST_P(FtpNetworkTransactionTest, CloseConnection) {
+ FtpSocketDataProvider ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_USER,
+ FtpSocketDataProvider::PRE_QUIT, "");
+ ExecuteTransaction(&ctrl_socket, "ftp://host", 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_LIST_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_RETR_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", 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);
+}
+
+TEST_P(FtpNetworkTransactionTest, UnexpectedInitiatedResponseForDirectory) {
+ // The states for a directory listing where an initiated response will cause
+ // an error. Includes all commands sent on the directory listing path, except
+ // CWD, SIZE, LIST, and QUIT commands.
+ FtpSocketDataProvider::State kFailingStates[] = {
+ FtpSocketDataProvider::PRE_USER, FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_SYST, FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_TYPE,
+ GetFamily() != AF_INET ? FtpSocketDataProvider::PRE_LIST_EPSV
+ : FtpSocketDataProvider::PRE_LIST_PASV,
+ FtpSocketDataProvider::PRE_CWD,
+ };
+
+ for (FtpSocketDataProvider::State state : kFailingStates) {
+ SetUpTransaction();
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(state, FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+ }
+}
+
+TEST_P(FtpNetworkTransactionTest, UnexpectedInitiatedResponseForFile) {
+ // The states for a download where an initiated response will cause an error.
+ // Includes all commands sent on the file download path, except CWD, SIZE, and
+ // QUIT commands.
+ const FtpSocketDataProvider::State kFailingStates[] = {
+ FtpSocketDataProvider::PRE_USER, FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_SYST, FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_TYPE,
+ GetFamily() != AF_INET ? FtpSocketDataProvider::PRE_RETR_EPSV
+ : FtpSocketDataProvider::PRE_RETR_PASV,
+ FtpSocketDataProvider::PRE_CWD};
+
+ for (FtpSocketDataProvider::State state : kFailingStates) {
+ LOG(ERROR) << "??: " << state;
+ SetUpTransaction();
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.InjectFailure(state, FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+ }
+}
+
+// Make sure that receiving extra unexpected responses correctly results in
+// sending a QUIT message, without triggering a DCHECK.
+TEST_P(FtpNetworkTransactionTest, ExtraResponses) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_TYPE,
+ FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n"
+ "157 Bar\r\n"
+ "157 Trombones\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+}
+
+// Make sure that receiving extra unexpected responses to a QUIT message
+// correctly results in ending the transaction with an error, without triggering
+// a DCHECK.
+TEST_P(FtpNetworkTransactionTest, ExtraQuitResponses) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_QUIT,
+ FtpSocketDataProvider::QUIT,
+ "221 Foo\r\n"
+ "221 Bar\r\n"
+ "221 Trombones\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+}
+
+// Test case for https://crbug.com/633841 - similar to the ExtraQuitResponses
+// test case, but with an empty response.
+TEST_P(FtpNetworkTransactionTest, EmptyQuitResponse) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_QUIT,
+ FtpSocketDataProvider::QUIT, "");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, InvalidRemoteDirectory) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host/file", FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "257 \"foo\rbar\" is your current location\r\n", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, InvalidRemoteDirectory2) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host/file", FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "257 \"foo\nbar\" is your current location\r\n", ERR_INVALID_RESPONSE);
+}
+
+INSTANTIATE_TEST_SUITE_P(Ftp,
+ FtpNetworkTransactionTest,
+ ::testing::Values(AF_INET, AF_INET6));
+
+} // namespace net
diff --git a/net/ftp/ftp_request_info.h b/net/ftp/ftp_request_info.h
new file mode 100644
index 0000000000000..be7702fd3d9c0
--- /dev/null
+++ b/net/ftp/ftp_request_info.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2010 The Chromium Authors
+// 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/net/ftp/ftp_response_info.cc b/net/ftp/ftp_response_info.cc
new file mode 100644
index 0000000000000..51b3c0e3aa367
--- /dev/null
+++ b/net/ftp/ftp_response_info.cc
@@ -0,0 +1,17 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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() = default;
+
+} // namespace net
diff --git a/net/ftp/ftp_response_info.h b/net/ftp/ftp_response_info.h
new file mode 100644
index 0000000000000..576ef2501b465
--- /dev/null
+++ b/net/ftp/ftp_response_info.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2010 The Chromium Authors
+// 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 <stdint.h>
+
+#include "base/time/time.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+class NET_EXPORT_PRIVATE 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_t 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.
+ IPEndPoint remote_endpoint;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_RESPONSE_INFO_H_
diff --git a/net/ftp/ftp_server_type.h b/net/ftp/ftp_server_type.h
new file mode 100644
index 0000000000000..18b0595bd9500
--- /dev/null
+++ b/net/ftp/ftp_server_type.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2009 The Chromium Authors
+// 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_H_
+#define NET_FTP_FTP_SERVER_TYPE_H_
+
+namespace net {
+
+enum FtpServerType {
+ // 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, // OBSOLETE. Server using Netware listing style.
+ SERVER_OS2 = 5, // OBSOLETE. Server using OS/2 listing style.
+
+ NUM_OF_SERVER_TYPES
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_SERVER_TYPE_H_
diff --git a/net/ftp/ftp_transaction.h b/net/ftp/ftp_transaction.h
new file mode 100644
index 0000000000000..dcb67243f9dae
--- /dev/null
+++ b/net/ftp/ftp_transaction.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <stdint.h>
+
+#include "net/base/completion_once_callback.h"
+#include "net/base/io_buffer.h"
+#include "net/base/load_states.h"
+#include "net/base/net_export.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+
+namespace net {
+
+class AuthCredentials;
+class FtpResponseInfo;
+class FtpRequestInfo;
+class NetLogWithSource;
+
+// 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 |callback| 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(raw_ptr <FtpRequestInfo> request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) = 0;
+
+ // Restarts the FTP transaction with authentication credentials.
+ virtual int RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback 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. |callback| 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, |callback| 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,
+ CompletionOnceCallback 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_t GetUploadProgress() const = 0;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_TRANSACTION_H_
diff --git a/net/ftp/ftp_transaction_factory.h b/net/ftp/ftp_transaction_factory.h
new file mode 100644
index 0000000000000..5f4270f25ca7d
--- /dev/null
+++ b/net/ftp/ftp_transaction_factory.h
@@ -0,0 +1,31 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 <memory>
+
+#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 std::unique_ptr<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/net/ftp/ftp_util.cc b/net/ftp/ftp_util.cc
new file mode 100644
index 0000000000000..5d06d8d81d459
--- /dev/null
+++ b/net/ftp/ftp_util.cc
@@ -0,0 +1,378 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_util.h"
+
+#include <map>
+#include <vector>
+
+#include "base/check_op.h"
+#include "base/i18n/case_conversion.h"
+#include "base/i18n/char_iterator.h"
+#include "base/i18n/unicodestring.h"
+#include "base/memory/singleton.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_number_conversions.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::ASCIIToUTF16;
+using std::u16string_view;
+
+// 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_view> tokens;
+ while (tokenizer.GetNext())
+ tokens.push_back(tokenizer.token_piece());
+
+ if (unix_path[0] == '/') {
+ // It's an absolute path.
+
+ if (tokens.empty()) {
+ // It's just "/" or a series of slashes, which all mean the same thing.
+ return "[]";
+ }
+
+ if (tokens.size() == 1)
+ return std::string(tokens.front()); // Return without leading slashes.
+
+ std::string result = base::StrCat({tokens[0], ":["});
+ if (tokens.size() == 2) {
+ // Don't ask why, it just works that way on VMS.
+ result.append("000000");
+ } else {
+ base::StrAppend(&result, {tokens[1]});
+ for (size_t i = 2; i < tokens.size() - 1; i++)
+ base::StrAppend(&result, {".", tokens[i]});
+ }
+ base::StrAppend(&result, {"]", tokens.back()});
+ return result;
+ }
+
+ if (tokens.size() == 1)
+ return unix_path;
+
+ std::string result("[");
+ for (size_t i = 0; i < tokens.size() - 1; i++)
+ base::StrAppend(&result, {".", tokens[i]});
+ base::StrAppend(&result, {"]", tokens.back()});
+ 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.back() != '/')
+ 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.
+ base::ReplaceFirstSubstringAfterOffset(
+ &result, 0, "[.", std::string_view());
+ } else {
+ // It's an absolute path.
+ result.insert(0, "/");
+ base::ReplaceSubstringsAfterOffset(&result, 0, ":[000000]", "/");
+ base::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.empty() && result.back() == '/')
+ result = result.substr(0, result.length() - 1);
+
+ return result;
+}
+
+namespace {
+
+// Lazy-initialized map of abbreviated month names.
+class AbbreviatedMonthsMap {
+ public:
+ static AbbreviatedMonthsMap* GetInstance() {
+ return base::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 std::u16string& text, int* number) {
+ // Ignore the case of the month names. The simplest way to handle that
+ // is to make everything lowercase.
+ std::u16string text_lower(base::i18n::ToLower(text));
+
+ if (map_.find(text_lower) == map_.end())
+ return false;
+
+ *number = map_[text_lower];
+ return true;
+ }
+
+ private:
+ friend struct base::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++) {
+ std::u16string month_name(
+ base::i18n::UnicodeStringToString16(months[month]));
+
+ // 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_[u"jan"]);
+ CHECK_EQ(2, map_[u"feb"]);
+ CHECK_EQ(3, map_[u"mar"]);
+ CHECK_EQ(4, map_[u"apr"]);
+ CHECK_EQ(5, map_[u"may"]);
+ CHECK_EQ(6, map_[u"jun"]);
+ CHECK_EQ(7, map_[u"jul"]);
+ CHECK_EQ(8, map_[u"aug"]);
+ CHECK_EQ(9, map_[u"sep"]);
+ CHECK_EQ(10, map_[u"oct"]);
+ CHECK_EQ(11, map_[u"nov"]);
+ CHECK_EQ(12, map_[u"dec"]);
+ }
+ AbbreviatedMonthsMap(const AbbreviatedMonthsMap&) = delete;
+ AbbreviatedMonthsMap& operator=(const AbbreviatedMonthsMap&) =
+ delete;
+ // Maps lowercase month names to numbers in range 1-12.
+ std::map<std::u16string, int> map_;
+};
+
+} // namespace
+
+// static
+bool FtpUtil::AbbreviatedMonthToNumber(const std::u16string& text,
+ int* number) {
+ return AbbreviatedMonthsMap::GetInstance()->GetMonthNumber(text, number);
+}
+
+// static
+bool FtpUtil::LsDateListingToTime(const std::u16string& month,
+ const std::u16string& day,
+ const std::u16string& 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? Note that it can be any of
+ // "HH:MM", "H:MM", "HH:M" or maybe even "H:M".
+ if (rest.length() > 5)
+ return false;
+
+ size_t colon_pos = rest.find(':');
+ if (colon_pos == std::u16string::npos)
+ return false;
+ if (colon_pos > 2)
+ return false;
+
+ if (!base::StringToInt(
+ base::MakeStringPiece16(rest.begin(), rest.begin() + colon_pos),
+ &time_exploded.hour)) {
+ return false;
+ }
+ if (!base::StringToInt(
+ base::MakeStringPiece16(rest.begin() + colon_pos + 1, rest.end()),
+ &time_exploded.minute)) {
+ return false;
+ }
+
+ // Guess the year.
+ base::Time::Exploded current_exploded;
+ current_time.UTCExplode(&current_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 UTC.
+ return base::Time::FromUTCExploded(time_exploded, result);
+}
+
+// static
+bool FtpUtil::WindowsDateListingToTime(const std::u16string& date,
+ const std::u16string& time,
+ base::Time* result) {
+ base::Time::Exploded time_exploded = { 0 };
+
+ // Date should be in format MM-DD-YY[YY].
+ std::vector<std::u16string_view> date_parts = base::SplitStringPiece(
+ date, u"-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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<std::u16string_view> time_parts =
+ base::SplitStringPiece(std::u16string_view(time).substr(0, 5), u":",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ 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;
+ std::u16string am_or_pm(time.substr(5, 2));
+ if (base::EqualsASCII(am_or_pm, "PM")) {
+ if (time_exploded.hour < 12)
+ time_exploded.hour += 12;
+ } else if (base::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 UTC.
+ return base::Time::FromUTCExploded(time_exploded, result);
+}
+
+// static
+std::u16string FtpUtil::GetStringPartAfterColumns(const std::u16string& text,
+ int columns) {
+ base::i18n::UTF16CharIterator iter(text);
+
+ 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();
+ }
+
+ std::u16string result(text.substr(iter.array_pos()));
+ base::TrimWhitespace(result, base::TRIM_ALL, &result);
+ return result;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_util.h b/net/ftp/ftp_util.h
new file mode 100644
index 0000000000000..6647df1569862
--- /dev/null
+++ b/net/ftp/ftp_util.h
@@ -0,0 +1,57 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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 "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 std::u16string& 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 std::u16string& month,
+ const std::u16string& day,
+ const std::u16string& rest,
+ const base::Time& current_time,
+ base::Time* result);
+
+ // Converts a Windows date listing to time. Returns true on success.
+ static bool WindowsDateListingToTime(const std::u16string& date,
+ const std::u16string& time,
+ base::Time* result);
+
+ // Skips |columns| columns from |text| (whitespace-delimited), and returns the
+ // remaining part, without leading/trailing whitespace.
+ static std::u16string GetStringPartAfterColumns(const std::u16string& text,
+ int columns);
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_UTIL_H_
diff --git a/net/ftp/ftp_util_unittest.cc b/net/ftp/ftp_util_unittest.cc
new file mode 100644
index 0000000000000..ffeaaa541d7d8
--- /dev/null
+++ b/net/ftp/ftp_util_unittest.cc
@@ -0,0 +1,268 @@
+// Copyright (c) 2011 The Chromium Authors
+// 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/cxx17_backports.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"
+
+using base::ASCIIToUTF16;
+using base::UTF8ToUTF16;
+
+namespace net {
+
+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" },
+ // Extra slashes shouldn't matter.
+ { "/////", "[]" },
+ { "/////a", "a" },
+ { "//a//b///c", "a:[b]c" },
+ { "a//b///c", "[.a.b]c" },
+ };
+ for (size_t i = 0; i < base::size(kTestCases); i++) {
+ EXPECT_EQ(kTestCases[i].expected_output,
+ 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]" },
+ // Extra slashes shouldn't matter.
+ { "/////", "" },
+ { "//a//b///c//", "a:[b.c]" },
+ { "a//b///c//", "[.a.b.c]" },
+ };
+ for (size_t i = 0; i < base::size(kTestCases); i++) {
+ EXPECT_EQ(kTestCases[i].expected_output,
+ 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 < base::size(kTestCases); i++) {
+ EXPECT_EQ(kTestCases[i].expected_output,
+ 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 < base::size(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(FtpUtil::LsDateListingToTime(
+ UTF8ToUTF16(kTestCases[i].month), UTF8ToUTF16(kTestCases[i].day),
+ UTF8ToUTF16(kTestCases[i].rest), mock_current_time, &time));
+
+ base::Time::Exploded time_exploded;
+ time.UTCExplode(&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 < base::size(kTestCases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s %s", i,
+ kTestCases[i].date, kTestCases[i].time));
+
+ base::Time time;
+ ASSERT_TRUE(FtpUtil::WindowsDateListingToTime(
+ UTF8ToUTF16(kTestCases[i].date), UTF8ToUTF16(kTestCases[i].time),
+ &time));
+
+ base::Time::Exploded time_exploded;
+ time.UTCExplode(&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 < base::size(kTestCases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s %d", i,
+ kTestCases[i].text, kTestCases[i].column));
+
+ EXPECT_EQ(ASCIIToUTF16(kTestCases[i].expected_result),
+ FtpUtil::GetStringPartAfterColumns(
+ ASCIIToUTF16(kTestCases[i].text), kTestCases[i].column));
+ }
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/proxy_resolution/pac_file_fetcher_impl.cc b/net/proxy_resolution/pac_file_fetcher_impl.cc
index 02ef9a79878bb..ee62937fdecbd 100644
--- a/net/proxy_resolution/pac_file_fetcher_impl.cc
+++ b/net/proxy_resolution/pac_file_fetcher_impl.cc
@@ -324,8 +324,8 @@ PacFileFetcherImpl::PacFileFetcherImpl(URLRequestContext* url_request_context)
}
bool PacFileFetcherImpl::IsUrlSchemeAllowed(const GURL& url) const {
- // Always allow http://, https://, and data:.
- if (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs("data"))
+ // Always allow http://, https://, data:, and ftp://.
+ if (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs("ftp") || url.SchemeIs("data"))
return true;
// Disallow any other URL scheme.
diff --git a/net/url_request/ftp_protocol_handler.cc b/net/url_request/ftp_protocol_handler.cc
new file mode 100644
index 0000000000000..03783e50cfc0b
--- /dev/null
+++ b/net/url_request/ftp_protocol_handler.cc
@@ -0,0 +1,58 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/url_request/ftp_protocol_handler.h"
+
+#include "base/check_op.h"
+#include "base/memory/ptr_util.h"
+#include "net/base/net_errors.h"
+#include "net/base/port_util.h"
+#include "net/ftp/ftp_auth_cache.h"
+#include "net/ftp/ftp_network_layer.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_error_job.h"
+#include "net/url_request/url_request_ftp_job.h"
+#include "url/gurl.h"
+
+namespace net {
+
+std::unique_ptr<FtpProtocolHandler> FtpProtocolHandler::Create(
+ HostResolver* host_resolver,
+ FtpAuthCache* auth_cache) {
+ DCHECK(auth_cache);
+ return base::WrapUnique(new FtpProtocolHandler(
+ base::WrapUnique(new FtpNetworkLayer(host_resolver)), auth_cache));
+}
+
+std::unique_ptr<FtpProtocolHandler> FtpProtocolHandler::CreateForTesting(
+ std::unique_ptr<FtpTransactionFactory> ftp_transaction_factory,
+ FtpAuthCache* auth_cache) {
+ return base::WrapUnique(
+ new FtpProtocolHandler(std::move(ftp_transaction_factory), auth_cache));
+}
+
+FtpProtocolHandler::~FtpProtocolHandler() = default;
+
+std::unique_ptr<URLRequestJob> FtpProtocolHandler::CreateJob(
+ URLRequest* request) const {
+ DCHECK_EQ("ftp", request->url().scheme());
+
+ if (!IsPortAllowedForScheme(request->url().EffectiveIntPort(),
+ request->url().scheme_piece())) {
+ return std::make_unique<URLRequestErrorJob>(request, ERR_UNSAFE_PORT);
+ }
+
+ return std::make_unique<URLRequestFtpJob>(
+ request, ftp_transaction_factory_.get(), ftp_auth_cache_);
+}
+
+FtpProtocolHandler::FtpProtocolHandler(
+ std::unique_ptr<FtpTransactionFactory> ftp_transaction_factory,
+ FtpAuthCache* auth_cache)
+ : ftp_transaction_factory_(std::move(ftp_transaction_factory)),
+ ftp_auth_cache_(auth_cache) {
+ DCHECK(ftp_transaction_factory_);
+}
+
+} // namespace net
diff --git a/net/url_request/ftp_protocol_handler.h b/net/url_request/ftp_protocol_handler.h
new file mode 100644
index 0000000000000..3c7e0faebc2bb
--- /dev/null
+++ b/net/url_request/ftp_protocol_handler.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_URL_REQUEST_FTP_PROTOCOL_HANDLER_H_
+#define NET_URL_REQUEST_FTP_PROTOCOL_HANDLER_H_
+
+#include <memory>
+
+#include "base/memory/raw_ptr.h"
+#include "base/compiler_specific.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "net/base/net_export.h"
+#include "net/url_request/url_request_job_factory.h"
+
+namespace net {
+
+class FtpAuthCache;
+class FtpTransactionFactory;
+class HostResolver;
+class URLRequestJob;
+
+// Implements a ProtocolHandler for FTP.
+class NET_EXPORT FtpProtocolHandler :
+ public URLRequestJobFactory::ProtocolHandler {
+ public:
+ ~FtpProtocolHandler() override;
+ FtpProtocolHandler(const FtpProtocolHandler&) = delete;
+ FtpProtocolHandler& operator=(const FtpProtocolHandler&) =
+ delete;
+
+ // Creates an FtpProtocolHandler using the specified HostResolver and
+ // FtpAuthCache. |auth_cache| cannot be null.
+ static std::unique_ptr<FtpProtocolHandler> Create(HostResolver* host_resolver,
+ FtpAuthCache* auth_cache);
+
+ // Creates an FtpProtocolHandler using the specified FtpTransactionFactory, to
+ // allow a mock to be used for testing.
+ static std::unique_ptr<FtpProtocolHandler> CreateForTesting(
+ std::unique_ptr<FtpTransactionFactory> ftp_transaction_factory,
+ FtpAuthCache* auth_cache);
+
+ std::unique_ptr<URLRequestJob> CreateJob(URLRequest* request) const override;
+
+ private:
+ friend class FtpTestURLRequestContext;
+
+ explicit FtpProtocolHandler(
+ std::unique_ptr<FtpTransactionFactory> ftp_transaction_factory,
+ FtpAuthCache* auth_cache);
+
+ std::unique_ptr<FtpTransactionFactory> ftp_transaction_factory_;
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
+};
+
+} // namespace net
+
+#endif // NET_URL_REQUEST_FTP_PROTOCOL_HANDLER_H_
diff --git a/net/url_request/url_request.h b/net/url_request/url_request.h
index 859dcb00e8153..d0791fdace3c5 100644
--- a/net/url_request/url_request.h
+++ b/net/url_request/url_request.h
@@ -749,7 +749,8 @@ class NET_EXPORT URLRequest : public base::SupportsUserData {
}
// The number of bytes in the raw response body (before any decompression,
- // etc.). This is only available after the final Read completes.
+ // etc.). This is only available after the final Read completes. Not available
+ // for FTP responses.
int64_t received_response_content_length() const {
return received_response_content_length_;
}
diff --git a/net/url_request/url_request_context.h b/net/url_request/url_request_context.h
index 5618d02062989..e71e085486f2f 100644
--- a/net/url_request/url_request_context.h
+++ b/net/url_request/url_request_context.h
@@ -55,6 +55,10 @@ class URLRequest;
class URLRequestJobFactory;
class URLRequestContextBuilder;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+class FtpAuthCache;
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
#if BUILDFLAG(ENABLE_REPORTING)
class NetworkErrorLoggingService;
class PersistentReportingAndNelStore;
@@ -235,6 +239,13 @@ class NET_EXPORT URLRequestContext final {
// context has been bound to.
handles::NetworkHandle bound_network() const { return bound_network_; }
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ void set_ftp_auth_cache(FtpAuthCache* auth_cache) {
+ ftp_auth_cache_ = auth_cache;
+ }
+ FtpAuthCache* ftp_auth_cache() { return ftp_auth_cache_; }
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
void AssertCalledOnValidThread() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}
@@ -363,6 +374,10 @@ class NET_EXPORT URLRequestContext final {
std::unique_ptr<TransportSecurityPersister> transport_security_persister_;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
std::unique_ptr<std::set<raw_ptr<const URLRequest, SetExperimental>>>
url_requests_;
diff --git a/net/url_request/url_request_context_builder.cc b/net/url_request/url_request_context_builder.cc
index c4ba7328234bf..4f5bb37448ccd 100644
--- a/net/url_request/url_request_context_builder.cc
+++ b/net/url_request/url_request_context_builder.cc
@@ -53,6 +53,12 @@
#include "net/url_request/url_request_job_factory.h"
#include "url/url_constants.h"
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+#include "net/ftp/ftp_auth_cache.h" // nogncheck
+#include "net/ftp/ftp_network_layer.h" // nogncheck
+#include "net/url_request/ftp_protocol_handler.h" // nogncheck
+#endif
+
#if BUILDFLAG(ENABLE_REPORTING)
#include "net/network_error_logging/network_error_logging_service.h"
#include "net/network_error_logging/persistent_reporting_and_nel_store.h"
@@ -592,12 +598,23 @@ std::unique_ptr<URLRequestContext> URLRequestContextBuilder::Build() {
std::unique_ptr<URLRequestJobFactory> job_factory =
std::make_unique<URLRequestJobFactory>();
+ // Adds caller-provided protocol handlers first so that these handlers are
+ // used over the ftp handler below.
for (auto& scheme_handler : protocol_handlers_) {
job_factory->SetProtocolHandler(scheme_handler.first,
std::move(scheme_handler.second));
}
protocol_handlers_.clear();
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ if (ftp_enabled_) {
+ context->set_ftp_auth_cache(new FtpAuthCache());
+ job_factory->SetProtocolHandler(
+ url::kFtpScheme, FtpProtocolHandler::Create(context->host_resolver(),
+ context->ftp_auth_cache()));
+ }
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
context->set_job_factory(std::move(job_factory));
if (cookie_deprecation_label_.has_value()) {
diff --git a/net/url_request/url_request_context_builder.h b/net/url_request/url_request_context_builder.h
index 66fd979ebae17..996f83526a69c 100644
--- a/net/url_request/url_request_context_builder.h
+++ b/net/url_request/url_request_context_builder.h
@@ -217,6 +217,11 @@ class NET_EXPORT URLRequestContextBuilder {
void set_http_user_agent_settings(
std::unique_ptr<HttpUserAgentSettings> http_user_agent_settings);
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ // Control support for ftp:// requests. By default it's disabled.
+ void set_ftp_enabled(bool enable) { ftp_enabled_ = enable; }
+#endif
+
// Sets a valid ProtocolHandler for a scheme.
// A ProtocolHandler already exists for |scheme| will be overwritten.
void SetProtocolHandler(
@@ -441,6 +446,11 @@ class NET_EXPORT URLRequestContextBuilder {
std::optional<std::string> cookie_deprecation_label_;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ // Include support for ftp:// requests.
+ bool ftp_enabled_ = false;
+#endif
+
bool http_cache_enabled_ = true;
bool cookie_store_set_by_client_ = false;
bool suppress_setting_socket_performance_watcher_factory_for_testing_ = false;
diff --git a/net/url_request/url_request_ftp_fuzzer.cc b/net/url_request/url_request_ftp_fuzzer.cc
new file mode 100644
index 0000000000000..e589091fb92e4
--- /dev/null
+++ b/net/url_request/url_request_ftp_fuzzer.cc
@@ -0,0 +1,92 @@
+// Copyright 2016 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <fuzzer/FuzzedDataProvider.h>
+
+#include "base/macros.h"
+#include "base/run_loop.h"
+#include "net/base/request_priority.h"
+#include "net/dns/context_host_resolver.h"
+#include "net/dns/fuzzed_host_resolver_util.h"
+#include "net/ftp/ftp_auth_cache.h"
+#include "net/ftp/ftp_network_transaction.h"
+#include "net/ftp/ftp_transaction_factory.h"
+#include "net/socket/client_socket_factory.h"
+#include "net/socket/fuzzed_socket_factory.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/ftp_protocol_handler.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_job_factory.h"
+#include "net/url_request/url_request_test_util.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Returns FtpNetworkTransactions using the specified HostResolver
+// and ClientSocketFactory.
+class FuzzedFtpTransactionFactory : public net::FtpTransactionFactory {
+ public:
+ FuzzedFtpTransactionFactory(net::HostResolver* host_resolver,
+ net::ClientSocketFactory* client_socket_factory)
+ : host_resolver_(host_resolver),
+ client_socket_factory_(client_socket_factory) {}
+
+ // FtpTransactionFactory:
+ std::unique_ptr<net::FtpTransaction> CreateTransaction() override {
+ return std::make_unique<net::FtpNetworkTransaction>(host_resolver_,
+ client_socket_factory_);
+ }
+
+ void Suspend(bool suspend) override { NOTREACHED(); }
+
+ private:
+ net::HostResolver* host_resolver_;
+ net::ClientSocketFactory* client_socket_factory_;
+
+ DISALLOW_COPY_AND_ASSIGN(FuzzedFtpTransactionFactory);
+};
+
+} // namespace
+
+// Integration fuzzer for URLRequestFtpJob.
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider data_provider(data, size);
+ net::TestURLRequestContext url_request_context(true);
+ net::FuzzedSocketFactory fuzzed_socket_factory(&data_provider);
+ url_request_context.set_client_socket_factory(&fuzzed_socket_factory);
+
+ // Need to fuzz the HostResolver to select between IPv4 and IPv6.
+ std::unique_ptr<net::ContextHostResolver> host_resolver =
+ net::CreateFuzzedContextHostResolver(net::HostResolver::ManagerOptions(),
+ nullptr, &data_provider,
+ true /* enable_caching */);
+ url_request_context.set_host_resolver(host_resolver.get());
+
+ net::URLRequestJobFactory job_factory;
+ net::FtpAuthCache auth_cache;
+ job_factory.SetProtocolHandler(
+ "ftp", net::FtpProtocolHandler::CreateForTesting(
+ std::make_unique<FuzzedFtpTransactionFactory>(
+ host_resolver.get(), &fuzzed_socket_factory),
+ &auth_cache));
+ url_request_context.set_job_factory(&job_factory);
+
+ url_request_context.Init();
+
+ net::TestDelegate delegate;
+
+ std::unique_ptr<net::URLRequest> url_request(
+ url_request_context.CreateRequest(
+ GURL("ftp://foo/" + data_provider.ConsumeRandomLengthString(1000)),
+ net::DEFAULT_PRIORITY, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+ url_request->Start();
+ // TestDelegate quits the message loop on completion.
+ base::RunLoop().Run();
+
+ return 0;
+}
diff --git a/net/url_request/url_request_ftp_job.cc b/net/url_request/url_request_ftp_job.cc
new file mode 100644
index 0000000000000..1e8bacb3d9ac9
--- /dev/null
+++ b/net/url_request/url_request_ftp_job.cc
@@ -0,0 +1,324 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/url_request/url_request_ftp_job.h"
+
+#include "base/functional/bind.h"
+#include "base/compiler_specific.h"
+#include "base/location.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task/single_thread_task_runner.h"
+#include "net/base/auth.h"
+#include "net/base/host_port_pair.h"
+#include "net/base/load_flags.h"
+#include "net/base/net_errors.h"
+#include "net/ftp/ftp_auth_cache.h"
+#include "net/ftp/ftp_network_transaction.h"
+#include "net/ftp/ftp_response_info.h"
+#include "net/ftp/ftp_transaction_factory.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_transaction_factory.h"
+#include "net/proxy_resolution/proxy_resolution_request.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_error_job.h"
+
+namespace net {
+
+class URLRequestFtpJob::AuthData {
+ public:
+ AuthState state; // Whether we need, have, or gave up on authentication.
+ AuthCredentials credentials; // The credentials to use for auth.
+
+ AuthData();
+ ~AuthData();
+};
+
+URLRequestFtpJob::AuthData::AuthData() : state(AUTH_STATE_NEED_AUTH) {}
+
+URLRequestFtpJob::AuthData::~AuthData() = default;
+
+URLRequestFtpJob::URLRequestFtpJob(
+ URLRequest* request,
+ FtpTransactionFactory* ftp_transaction_factory,
+ FtpAuthCache* ftp_auth_cache)
+ : URLRequestJob(request),
+ proxy_resolution_service_(
+ request_->context()->proxy_resolution_service()),
+ read_in_progress_(false),
+ ftp_transaction_factory_(ftp_transaction_factory),
+ ftp_auth_cache_(ftp_auth_cache) {
+ DCHECK(proxy_resolution_service_);
+ DCHECK(ftp_transaction_factory);
+ DCHECK(ftp_auth_cache);
+}
+
+URLRequestFtpJob::~URLRequestFtpJob() {
+ Kill();
+}
+
+bool URLRequestFtpJob::IsSafeRedirect(const GURL& location) {
+ // Disallow all redirects.
+ return false;
+}
+
+bool URLRequestFtpJob::GetMimeType(std::string* mime_type) const {
+ // When auth has been cancelled, return a blank text/plain page instead of
+ // triggering a download.
+ if (auth_data_ && auth_data_->state == AUTH_STATE_CANCELED) {
+ *mime_type = "text/plain";
+ return true;
+ }
+
+ if (ftp_transaction_->GetResponseInfo()->is_directory_listing) {
+ *mime_type = "text/vnd.chromium.ftp-dir";
+ return true;
+ }
+
+ // FTP resources other than directory listings ought to be handled as raw
+ // binary data, not sniffed into HTML or etc.
+ *mime_type = "application/octet-stream";
+ return true;
+}
+
+IPEndPoint URLRequestFtpJob::GetResponseRemoteEndpoint() const {
+ if (!ftp_transaction_)
+ return IPEndPoint();
+ return ftp_transaction_->GetResponseInfo()->remote_endpoint;
+}
+
+void URLRequestFtpJob::Start() {
+ DCHECK(!proxy_resolve_request_);
+ DCHECK(!ftp_transaction_);
+
+ int rv = OK;
+ if (request_->load_flags() & LOAD_BYPASS_PROXY) {
+ proxy_info_.UseDirect();
+ } else {
+ DCHECK_EQ(request_->context()->proxy_resolution_service(),
+ proxy_resolution_service_);
+ rv = proxy_resolution_service_->ResolveProxy(
+ request_->url(), "GET", NetworkAnonymizationKey(), &proxy_info_,
+ base::BindOnce(&URLRequestFtpJob::OnResolveProxyComplete,
+ base::Unretained(this)),
+ &proxy_resolve_request_, request_->net_log());
+
+ if (rv == ERR_IO_PENDING)
+ return;
+ }
+ OnResolveProxyComplete(rv);
+}
+
+void URLRequestFtpJob::Kill() {
+ if (proxy_resolve_request_) {
+ proxy_resolve_request_.reset();
+ }
+ if (ftp_transaction_)
+ ftp_transaction_.reset();
+ URLRequestJob::Kill();
+ weak_factory_.InvalidateWeakPtrs();
+}
+
+void URLRequestFtpJob::GetResponseInfo(HttpResponseInfo* info) {
+ // Don't expose the challenge if it has already been successfully
+ // authenticated.
+ if (!auth_data_ || auth_data_->state == AUTH_STATE_HAVE_AUTH)
+ return;
+
+ std::unique_ptr<AuthChallengeInfo> challenge = GetAuthChallengeInfo();
+ if (challenge)
+ info->auth_challenge = *challenge;
+}
+
+void URLRequestFtpJob::OnResolveProxyComplete(int result) {
+ proxy_resolve_request_ = nullptr;
+
+ if (result != OK) {
+ OnStartCompletedAsync(result);
+ return;
+ }
+
+ // Remove unsupported proxies from the list.
+ proxy_info_.RemoveProxiesWithoutScheme(ProxyServer::SCHEME_QUIC);
+
+ if (proxy_info_.is_direct()) {
+ StartFtpTransaction();
+ } else {
+ OnStartCompletedAsync(ERR_NO_SUPPORTED_PROXIES);
+ }
+}
+
+void URLRequestFtpJob::StartFtpTransaction() {
+ // Create a transaction.
+ DCHECK(!ftp_transaction_);
+
+ ftp_request_info_.url = request_->url();
+ ftp_transaction_ = ftp_transaction_factory_->CreateTransaction();
+
+ int rv;
+ if (ftp_transaction_) {
+ rv = ftp_transaction_->Start(
+ &ftp_request_info_,
+ base::BindOnce(&URLRequestFtpJob::OnStartCompleted,
+ base::Unretained(this)),
+ request_->net_log(), request_->traffic_annotation());
+ if (rv == ERR_IO_PENDING)
+ return;
+ } else {
+ rv = ERR_FAILED;
+ }
+ // The transaction started synchronously, but we need to notify the
+ // URLRequest delegate via the message loop.
+ OnStartCompletedAsync(rv);
+}
+
+void URLRequestFtpJob::OnStartCompleted(int result) {
+ if (result == OK) {
+ DCHECK(ftp_transaction_);
+
+ // FTP obviously doesn't have HTTP Content-Length header. We have to pass
+ // the content size information manually.
+ set_expected_content_size(
+ ftp_transaction_->GetResponseInfo()->expected_content_size);
+
+ if (auth_data_ && auth_data_->state == AUTH_STATE_HAVE_AUTH) {
+ LogFtpStartResult(FTPStartResult::kSuccessAuth);
+ } else {
+ LogFtpStartResult(FTPStartResult::kSuccessNoAuth);
+ }
+
+ NotifyHeadersComplete();
+ } else if (ftp_transaction_ /* May be null if creation fails. */ &&
+ ftp_transaction_->GetResponseInfo()->needs_auth) {
+ HandleAuthNeededResponse();
+ } else {
+ LogFtpStartResult(FTPStartResult::kFailed);
+ NotifyStartError(result);
+ }
+}
+
+void URLRequestFtpJob::OnStartCompletedAsync(int result) {
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(&URLRequestFtpJob::OnStartCompleted,
+ weak_factory_.GetWeakPtr(), result));
+}
+
+void URLRequestFtpJob::OnReadCompleted(int result) {
+ read_in_progress_ = false;
+ ReadRawDataComplete(result);
+}
+
+void URLRequestFtpJob::RestartTransactionWithAuth() {
+ DCHECK(auth_data_.get() && auth_data_->state == AUTH_STATE_HAVE_AUTH);
+
+ int rv = ftp_transaction_->RestartWithAuth(
+ auth_data_->credentials,
+ base::BindOnce(&URLRequestFtpJob::OnStartCompleted,
+ base::Unretained(this)));
+ if (rv == ERR_IO_PENDING)
+ return;
+
+ OnStartCompletedAsync(rv);
+}
+
+LoadState URLRequestFtpJob::GetLoadState() const {
+ if (proxy_resolve_request_)
+ return proxy_resolve_request_->GetLoadState();
+ return ftp_transaction_ ? ftp_transaction_->GetLoadState() : LOAD_STATE_IDLE;
+}
+
+bool URLRequestFtpJob::NeedsAuth() {
+ return auth_data_.get() && auth_data_->state == AUTH_STATE_NEED_AUTH;
+}
+
+std::unique_ptr<AuthChallengeInfo> URLRequestFtpJob::GetAuthChallengeInfo() {
+ std::unique_ptr<AuthChallengeInfo> result =
+ std::make_unique<AuthChallengeInfo>();
+ result->is_proxy = false;
+ result->challenger = url::SchemeHostPort(request_->url());
+ // scheme, realm, path, and challenge are kept empty.
+ DCHECK(result->scheme.empty());
+ DCHECK(result->realm.empty());
+ DCHECK(result->challenge.empty());
+ DCHECK(result->path.empty());
+ return result;
+}
+
+void URLRequestFtpJob::SetAuth(const AuthCredentials& credentials) {
+ DCHECK(ftp_transaction_);
+ DCHECK(NeedsAuth());
+
+ auth_data_->state = AUTH_STATE_HAVE_AUTH;
+ auth_data_->credentials = credentials;
+
+ ftp_auth_cache_->Add(request_->url().DeprecatedGetOriginAsURL(), auth_data_->credentials);
+
+ RestartTransactionWithAuth();
+}
+
+void URLRequestFtpJob::CancelAuth() {
+ DCHECK(ftp_transaction_);
+ DCHECK(NeedsAuth());
+
+ auth_data_->state = AUTH_STATE_CANCELED;
+
+ ftp_transaction_.reset();
+ NotifyHeadersComplete();
+}
+
+int URLRequestFtpJob::ReadRawData(IOBuffer* buf, int buf_size) {
+ DCHECK_NE(buf_size, 0);
+ DCHECK(!read_in_progress_);
+
+ if (!ftp_transaction_)
+ return 0;
+
+ int rv =
+ ftp_transaction_->Read(buf, buf_size,
+ base::BindOnce(&URLRequestFtpJob::OnReadCompleted,
+ base::Unretained(this)));
+
+ if (rv == ERR_IO_PENDING)
+ read_in_progress_ = true;
+ return rv;
+}
+
+void URLRequestFtpJob::HandleAuthNeededResponse() {
+ GURL origin = request_->url().DeprecatedGetOriginAsURL();
+
+ if (auth_data_.get()) {
+ if (auth_data_->state == AUTH_STATE_CANCELED) {
+ NotifyHeadersComplete();
+ return;
+ }
+
+ if (ftp_transaction_ && auth_data_->state == AUTH_STATE_HAVE_AUTH) {
+ ftp_auth_cache_->Remove(origin, auth_data_->credentials);
+
+ // The user entered invalid auth
+ LogFtpStartResult(FTPStartResult::kFailed);
+ }
+ } else {
+ auth_data_ = std::make_unique<AuthData>();
+ }
+ auth_data_->state = AUTH_STATE_NEED_AUTH;
+
+ FtpAuthCache::Entry* cached_auth = nullptr;
+ if (ftp_transaction_ && ftp_transaction_->GetResponseInfo()->needs_auth)
+ cached_auth = ftp_auth_cache_->Lookup(origin);
+ if (cached_auth) {
+ // Retry using cached auth data.
+ SetAuth(cached_auth->credentials);
+ } else {
+ // Prompt for a username/password.
+ NotifyHeadersComplete();
+ }
+}
+
+void URLRequestFtpJob::LogFtpStartResult(FTPStartResult result) {
+ UMA_HISTOGRAM_ENUMERATION("Net.FTP.StartResult", result);
+}
+
+} // namespace net
diff --git a/net/url_request/url_request_ftp_job.h b/net/url_request/url_request_ftp_job.h
new file mode 100644
index 0000000000000..fda29bf6cb24c
--- /dev/null
+++ b/net/url_request/url_request_ftp_job.h
@@ -0,0 +1,102 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_URL_REQUEST_URL_REQUEST_FTP_JOB_H_
+#define NET_URL_REQUEST_URL_REQUEST_FTP_JOB_H_
+
+#include <memory>
+#include <string>
+
+#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "net/base/auth.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/net_export.h"
+#include "net/ftp/ftp_request_info.h"
+#include "net/ftp/ftp_transaction.h"
+#include "net/proxy_resolution/proxy_info.h"
+#include "net/proxy_resolution/proxy_resolution_service.h"
+#include "net/url_request/url_request_job.h"
+
+namespace net {
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class FTPStartResult : int {
+ kSuccessNoAuth = 0,
+ kSuccessAuth = 1,
+ kFailed = 2,
+ kMaxValue = kFailed
+};
+
+class FtpAuthCache;
+class FtpTransactionFactory;
+class ProxyResolutionRequest;
+
+// A URLRequestJob subclass that is built on top of FtpTransaction. It
+// provides an implementation for FTP.
+class NET_EXPORT_PRIVATE URLRequestFtpJob : public URLRequestJob {
+ public:
+ URLRequestFtpJob(URLRequest* request,
+ FtpTransactionFactory* ftp_transaction_factory,
+ FtpAuthCache* ftp_auth_cache);
+ ~URLRequestFtpJob() override;
+ void Start() override;
+ URLRequestFtpJob(const URLRequestFtpJob&) = delete;
+ URLRequestFtpJob& operator=(const URLRequestFtpJob&) =
+ delete;
+ protected:
+ // Overridden from URLRequestJob:
+ bool IsSafeRedirect(const GURL& location) override;
+ bool GetMimeType(std::string* mime_type) const override;
+ IPEndPoint GetResponseRemoteEndpoint() const override;
+ void Kill() override;
+ void GetResponseInfo(HttpResponseInfo* info) override;
+
+ private:
+ class AuthData;
+
+ void OnResolveProxyComplete(int result);
+
+ void StartFtpTransaction();
+
+ void OnStartCompleted(int result);
+ void OnStartCompletedAsync(int result);
+ void OnReadCompleted(int result);
+
+ void RestartTransactionWithAuth();
+
+ // Overridden from URLRequestJob:
+ LoadState GetLoadState() const override;
+ bool NeedsAuth() override;
+ std::unique_ptr<AuthChallengeInfo> GetAuthChallengeInfo() override;
+ void SetAuth(const AuthCredentials& credentials) override;
+ void CancelAuth() override;
+
+ int ReadRawData(IOBuffer* buf, int buf_size) override;
+
+ void HandleAuthNeededResponse();
+
+ void LogFtpStartResult(FTPStartResult result);
+
+ raw_ptr<ProxyResolutionService> proxy_resolution_service_;
+ ProxyInfo proxy_info_;
+ std::unique_ptr<ProxyResolutionRequest> proxy_resolve_request_;
+
+ FtpRequestInfo ftp_request_info_;
+ std::unique_ptr<FtpTransaction> ftp_transaction_;
+
+ bool read_in_progress_;
+
+ std::unique_ptr<AuthData> auth_data_;
+
+ raw_ptr<FtpTransactionFactory> ftp_transaction_factory_;
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
+
+ base::WeakPtrFactory<URLRequestFtpJob> weak_factory_{this};
+};
+
+} // namespace net
+
+#endif // NET_URL_REQUEST_URL_REQUEST_FTP_JOB_H_
diff --git a/net/url_request/url_request_ftp_job_unittest.cc b/net/url_request/url_request_ftp_job_unittest.cc
new file mode 100644
index 0000000000000..62e4719adc72d
--- /dev/null
+++ b/net/url_request/url_request_ftp_job_unittest.cc
@@ -0,0 +1,245 @@
+// Copyright (c) 2019 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/url_request/url_request_ftp_job.h"
+
+#include <memory>
+
+#include "base/test/metrics/histogram_tester.h"
+#include "net/base/auth.h"
+#include "net/base/load_states.h"
+#include "net/base/net_errors.h"
+#include "net/base/request_priority.h"
+#include "net/ftp/ftp_auth_cache.h"
+#include "net/ftp/ftp_response_info.h"
+#include "net/ftp/ftp_transaction.h"
+#include "net/ftp/ftp_transaction_factory.h"
+#include "net/test/test_with_task_environment.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "net/url_request/url_request_job_factory.h"
+#include "net/url_request/url_request_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+namespace {
+
+class MockFtpTransaction : public FtpTransaction {
+ public:
+ MockFtpTransaction(int start_return_value,
+ int read_return_value,
+ bool needs_auth,
+ std::vector<int> restart_return_values)
+ : start_return_value_(start_return_value),
+ read_return_value_(read_return_value),
+ restart_return_values_(restart_return_values),
+ restart_index_(0) {
+ response_.needs_auth = needs_auth;
+ }
+ ~MockFtpTransaction() override {}
+
+ int Start(const FtpRequestInfo* request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) override {
+ return start_return_value_;
+ }
+
+ int RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback callback) override {
+ CHECK(restart_index_ < restart_return_values_.size());
+ return restart_return_values_[restart_index_++];
+ }
+
+ int Read(IOBuffer* buf,
+ int buf_len,
+ CompletionOnceCallback callback) override {
+ return read_return_value_;
+ }
+
+ const FtpResponseInfo* GetResponseInfo() const override { return &response_; }
+
+ LoadState GetLoadState() const override { return LOAD_STATE_IDLE; }
+
+ uint64_t GetUploadProgress() const override { return 0; }
+
+ private:
+ FtpResponseInfo response_;
+ int start_return_value_;
+ int read_return_value_;
+ std::vector<int> restart_return_values_;
+ unsigned int restart_index_;
+
+ DISALLOW_COPY_AND_ASSIGN(MockFtpTransaction);
+};
+
+class MockFtpTransactionFactory : public FtpTransactionFactory {
+ public:
+ MockFtpTransactionFactory(int start_return_value,
+ int read_return_value,
+ bool needs_auth,
+ std::vector<int> restart_return_values)
+ : start_return_value_(start_return_value),
+ read_return_value_(read_return_value),
+ needs_auth_(needs_auth),
+ restart_return_values_(restart_return_values) {}
+
+ ~MockFtpTransactionFactory() override {}
+
+ std::unique_ptr<FtpTransaction> CreateTransaction() override {
+ return std::make_unique<MockFtpTransaction>(start_return_value_,
+ read_return_value_, needs_auth_,
+ restart_return_values_);
+ }
+
+ void Suspend(bool suspend) override {}
+
+ private:
+ int start_return_value_;
+ int read_return_value_;
+ bool needs_auth_;
+ std::vector<int> restart_return_values_;
+
+ DISALLOW_COPY_AND_ASSIGN(MockFtpTransactionFactory);
+};
+
+class MockURLRequestFtpJobFactory : public URLRequestJobFactory {
+ public:
+ MockURLRequestFtpJobFactory(int start_return_value,
+ int read_return_value,
+ bool needs_auth,
+ std::vector<int> restart_return_values)
+ : auth_cache(new FtpAuthCache()),
+ factory(new MockFtpTransactionFactory(start_return_value,
+ read_return_value,
+ needs_auth,
+ restart_return_values)) {}
+
+ ~MockURLRequestFtpJobFactory() override {
+ delete auth_cache;
+ delete factory;
+ }
+
+ std::unique_ptr<URLRequestJob> CreateJob(URLRequest* request) const override {
+ return std::make_unique<URLRequestFtpJob>(request, factory, auth_cache);
+ }
+
+ bool IsSafeRedirectTarget(const GURL& location) const override {
+ return true;
+ }
+
+ private:
+ FtpAuthCache* auth_cache;
+ MockFtpTransactionFactory* factory;
+
+ DISALLOW_COPY_AND_ASSIGN(MockURLRequestFtpJobFactory);
+};
+
+using UrlRequestFtpJobTest = TestWithTaskEnvironment;
+
+TEST_F(UrlRequestFtpJobTest, HistogramLogSuccessNoAuth) {
+ base::HistogramTester histograms;
+ MockURLRequestFtpJobFactory url_request_ftp_job_factory(OK, OK, false, {OK});
+ TestNetworkDelegate network_delegate;
+ TestURLRequestContext context(true);
+ context.set_network_delegate(&network_delegate);
+ context.set_job_factory(&url_request_ftp_job_factory);
+ context.Init();
+
+ TestDelegate test_delegate;
+ std::unique_ptr<URLRequest> r(context.CreateRequest(
+ GURL("ftp://example.test/"), RequestPriority::DEFAULT_PRIORITY,
+ &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ test_delegate.RunUntilComplete();
+
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessNoAuth, 1);
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessAuth, 0);
+ histograms.ExpectBucketCount("Net.FTP.StartResult", FTPStartResult::kFailed,
+ 0);
+}
+
+TEST_F(UrlRequestFtpJobTest, HistogramLogSuccessAuth) {
+ base::HistogramTester histograms;
+ MockURLRequestFtpJobFactory url_request_ftp_job_factory(
+ ERR_FAILED, ERR_FAILED, true, {OK});
+ TestNetworkDelegate network_delegate;
+ TestURLRequestContext context(true);
+ context.set_network_delegate(&network_delegate);
+ context.set_job_factory(&url_request_ftp_job_factory);
+ context.Init();
+
+ TestDelegate test_delegate;
+ test_delegate.set_credentials(AuthCredentials(u"user", u"pass"));
+ std::unique_ptr<URLRequest> r(context.CreateRequest(
+ GURL("ftp://example.test/"), RequestPriority::DEFAULT_PRIORITY,
+ &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ test_delegate.RunUntilComplete();
+
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessNoAuth, 0);
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessAuth, 1);
+ histograms.ExpectBucketCount("Net.FTP.StartResult", FTPStartResult::kFailed,
+ 0);
+}
+
+TEST_F(UrlRequestFtpJobTest, HistogramLogFailed) {
+ base::HistogramTester histograms;
+ MockURLRequestFtpJobFactory url_request_ftp_job_factory(
+ ERR_FAILED, ERR_FAILED, false, {ERR_FAILED});
+ TestNetworkDelegate network_delegate;
+ TestURLRequestContext context(true);
+ context.set_network_delegate(&network_delegate);
+ context.set_job_factory(&url_request_ftp_job_factory);
+ context.Init();
+
+ TestDelegate test_delegate;
+ std::unique_ptr<URLRequest> r(context.CreateRequest(
+ GURL("ftp://example.test/"), RequestPriority::DEFAULT_PRIORITY,
+ &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ test_delegate.RunUntilComplete();
+
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessNoAuth, 0);
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessAuth, 0);
+ histograms.ExpectBucketCount("Net.FTP.StartResult", FTPStartResult::kFailed,
+ 1);
+}
+
+TEST_F(UrlRequestFtpJobTest, HistogramLogFailedInvalidAuthThenSucceed) {
+ base::HistogramTester histograms;
+ MockURLRequestFtpJobFactory url_request_ftp_job_factory(
+ ERR_FAILED, ERR_FAILED, true, {ERR_ACCESS_DENIED, OK});
+ TestNetworkDelegate network_delegate;
+ TestURLRequestContext context(true);
+ context.set_network_delegate(&network_delegate);
+ context.set_job_factory(&url_request_ftp_job_factory);
+ context.Init();
+
+ TestDelegate test_delegate;
+ test_delegate.set_credentials(AuthCredentials(u"user", u"pass"));
+ std::unique_ptr<URLRequest> r(context.CreateRequest(
+ GURL("ftp://example.test/"), RequestPriority::DEFAULT_PRIORITY,
+ &test_delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+
+ r->Start();
+ test_delegate.RunUntilComplete();
+
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessNoAuth, 0);
+ histograms.ExpectBucketCount("Net.FTP.StartResult",
+ FTPStartResult::kSuccessAuth, 1);
+ histograms.ExpectBucketCount("Net.FTP.StartResult", FTPStartResult::kFailed,
+ 1);
+}
+} // namespace
+} // namespace net
diff --git a/net/url_request/url_request_job_factory.cc b/net/url_request/url_request_job_factory.cc
index 68b4dae4b2e71..32c72a06c53bf 100644
--- a/net/url_request/url_request_job_factory.cc
+++ b/net/url_request/url_request_job_factory.cc
@@ -20,9 +20,9 @@ namespace {
URLRequestInterceptor* g_interceptor_for_testing = nullptr;
-// TODO(mmenke): Look into removing this class and
-// URLRequestJobFactory::ProtocolHandlers completely. The only other subclass
-// is iOS-only.
+// TODO(mmenke): Once FTP support is removed, look into removing this class, and
+// URLRequestJobFactory::ProtocolHandlers completely. The only other subclass is
+// iOS-only.
class HttpProtocolHandler : public URLRequestJobFactory::ProtocolHandler {
public:
// URLRequest::is_for_websockets() must match `is_for_websockets`, or requests
diff --git a/net/url_request/url_request_test_util.h b/net/url_request/url_request_test_util.h
index 575a510b7e188..2ada1ead4c8f8 100644
--- a/net/url_request/url_request_test_util.h
+++ b/net/url_request/url_request_test_util.h
@@ -35,6 +35,7 @@
#include "net/cookies/cookie_util.h"
#include "net/disk_cache/disk_cache.h"
#include "net/first_party_sets/first_party_set_metadata.h"
+#include "net/ftp/ftp_network_layer.h"
#include "net/http/http_auth_handler_factory.h"
#include "net/http/http_cache.h"
#include "net/http/http_network_layer.h"
diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc
index 18b16b9d46f01..24c0efb4f2cef 100644
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
@@ -173,10 +173,15 @@
#include <shlobj.h>
#include <wrl/client.h>
-
#include "base/win/scoped_com_initializer.h"
#endif
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT) && !BUILDFLAG(IS_ANDROID)
+#include "net/ftp/ftp_auth_cache.h"
+#include "net/ftp/ftp_network_layer.h"
+#include "net/url_request/ftp_protocol_handler.h"
+#endif
+
#if BUILDFLAG(IS_APPLE)
#include "base/mac/mac_util.h"
#endif
@@ -218,6 +223,12 @@ const std::u16string kUser(u"user");
const base::FilePath::CharType kTestFilePath[] =
FILE_PATH_LITERAL("net/data/url_request_unittest");
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT) && !defined(OS_ANDROID) && \
+ !defined(OS_FUCHSIA)
+// Test file used in most FTP tests.
+const char kFtpTestFile[] = "BullRunSpeech.txt";
+#endif
+
// Tests load timing information in the case a fresh connection was used, with
// no proxy.
void TestLoadTimingNotReused(const LoadTimingInfo& load_timing_info,
@@ -319,6 +330,29 @@ void TestLoadTimingCacheHitNoNetwork(const LoadTimingInfo& load_timing_info) {
EXPECT_TRUE(load_timing_info.proxy_resolve_end.is_null());
}
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT) && !defined(OS_ANDROID) && \
+ !defined(OS_FUCHSIA)
+// Tests load timing in the case that there is no HTTP response. This can be
+// used to test in the case of errors or non-HTTP requests.
+void TestLoadTimingNoHttpResponse(const LoadTimingInfo& load_timing_info) {
+ EXPECT_FALSE(load_timing_info.socket_reused);
+ EXPECT_EQ(NetLogSource::kInvalidId, load_timing_info.socket_log_id);
+
+ // Only the request times should be non-null.
+ EXPECT_FALSE(load_timing_info.request_start_time.is_null());
+ EXPECT_FALSE(load_timing_info.request_start.is_null());
+
+ ExpectConnectTimingHasNoTimes(load_timing_info.connect_timing);
+
+ EXPECT_TRUE(load_timing_info.proxy_resolve_start.is_null());
+ EXPECT_TRUE(load_timing_info.proxy_resolve_end.is_null());
+ EXPECT_TRUE(load_timing_info.send_start.is_null());
+ EXPECT_TRUE(load_timing_info.send_end.is_null());
+ EXPECT_TRUE(load_timing_info.receive_headers_start.is_null());
+ EXPECT_TRUE(load_timing_info.receive_headers_end.is_null());
+}
+#endif
+
// Job that allows monitoring of its priority.
class PriorityMonitoringURLRequestJob : public URLRequestTestJob {
public:
@@ -11822,6 +11856,404 @@ TEST_F(HTTPSLocalCRLSetTest, InterceptionBlockedAllowOverrideOnHSTS) {
}
#endif // !BUILDFLAG(IS_IOS)
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT) && !defined(OS_ANDROID) && \
+ !defined(OS_FUCHSIA)
+// FTP uses a second TCP connection with the port number allocated dynamically
+// on the server side, so it would be hard to make RemoteTestServer proxy FTP
+// connections reliably. FTP tests are disabled on platforms that use
+// RemoteTestServer. See http://crbug.com/495220
+class URLRequestTestFTP : public URLRequestTest {
+ public:
+ URLRequestTestFTP()
+ : ftp_test_server_(SpawnedTestServer::TYPE_FTP,
+ base::FilePath(kTestFilePath)) {
+ // Can't use |default_context_|'s HostResolver to set up the
+ // FTPTransactionFactory because it hasn't been created yet.
+ default_context().set_host_resolver(&host_resolver_);
+ }
+
+ // URLRequestTest interface:
+ void SetUpFactory() override {
+ // Add FTP support to the default URLRequestContext.
+ job_factory_->SetProtocolHandler(
+ "ftp", FtpProtocolHandler::Create(&host_resolver_, &ftp_auth_cache_));
+ }
+
+ std::string GetTestFileContents() {
+ base::FilePath path;
+ EXPECT_TRUE(base::PathService::Get(base::DIR_SOURCE_ROOT, &path));
+ path = path.Append(kTestFilePath);
+ path = path.AppendASCII(kFtpTestFile);
+ std::string contents;
+ EXPECT_TRUE(base::ReadFileToString(path, &contents));
+ return contents;
+ }
+
+ protected:
+ MockHostResolver host_resolver_;
+ FtpAuthCache ftp_auth_cache_;
+
+ SpawnedTestServer ftp_test_server_;
+};
+
+// Make sure an FTP request using an unsafe ports fails.
+TEST_F(URLRequestTestFTP, UnsafePort) {
+ GURL url("ftp://127.0.0.1:7");
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(ERR_UNSAFE_PORT, d.request_status());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPDirectoryListing) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURL("/"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_LT(0, d.bytes_received());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().host(),
+ r->GetResponseRemoteEndpoint().ToStringWithoutPort());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().port(),
+ r->GetResponseRemoteEndpoint().port());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPGetTestAnonymous) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURL(kFtpTestFile), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d.data_received());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().host(),
+ r->GetResponseRemoteEndpoint().ToStringWithoutPort());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().port(),
+ r->GetResponseRemoteEndpoint().port());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPMimeType) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ struct {
+ const char* path;
+ const char* mime;
+ } test_cases[] = {
+ {"/", "text/vnd.chromium.ftp-dir"},
+ {kFtpTestFile, "application/octet-stream"},
+ };
+
+ for (const auto test : test_cases) {
+ TestDelegate d;
+
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURL(test.path), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ std::string mime;
+ r->GetMimeType(&mime);
+ EXPECT_EQ(test.mime, mime);
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPGetTest) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "chrome",
+ "chrome"),
+ DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d.data_received());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().host(),
+ r->GetResponseRemoteEndpoint().ToStringWithoutPort());
+ EXPECT_EQ(ftp_test_server_.host_port_pair().port(),
+ r->GetResponseRemoteEndpoint().port());
+
+ LoadTimingInfo load_timing_info;
+ r->GetLoadTimingInfo(&load_timing_info);
+ TestLoadTimingNoHttpResponse(load_timing_info);
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCheckWrongPassword) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "chrome",
+ "wrong_password"),
+ DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(d.bytes_received(), 0);
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCheckWrongPasswordRestart) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ // Set correct login credentials. The delegate will be asked for them when
+ // the initial login with wrong credentials will fail.
+ d.set_credentials(AuthCredentials(kChrome, kChrome));
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "chrome",
+ "wrong_password"),
+ DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d.data_received());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCheckWrongUser) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "wrong_user",
+ "chrome"),
+ DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(0, d.bytes_received());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCheckWrongUserRestart) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ // Set correct login credentials. The delegate will be asked for them when
+ // the initial login with wrong credentials will fail.
+ d.set_credentials(AuthCredentials(kChrome, kChrome));
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "wrong_user",
+ "chrome"),
+ DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d.RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d.response_started_count());
+ EXPECT_FALSE(d.received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d.data_received());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCacheURLCredentials) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ std::unique_ptr<TestDelegate> d(new TestDelegate);
+ {
+ // Pass correct login identity in the URL.
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "chrome",
+ "chrome"),
+ DEFAULT_PRIORITY, d.get(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d->RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d->response_started_count());
+ EXPECT_FALSE(d->received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d->data_received());
+ }
+
+ d = std::make_unique<TestDelegate>();
+ {
+ // This request should use cached identity from previous request.
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURL(kFtpTestFile), DEFAULT_PRIORITY, d.get(),
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d->RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d->response_started_count());
+ EXPECT_FALSE(d->received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d->data_received());
+ }
+}
+
+TEST_F(URLRequestTestFTP, FTPCacheLoginBoxCredentials) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ std::unique_ptr<TestDelegate> d(new TestDelegate);
+ // Set correct login credentials. The delegate will be asked for them when
+ // the initial login with wrong credentials will fail.
+ d->set_credentials(AuthCredentials(kChrome, kChrome));
+ {
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURLWithUserAndPassword(kFtpTestFile, "chrome",
+ "wrong_password"),
+ DEFAULT_PRIORITY, d.get(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d->RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d->response_started_count());
+ EXPECT_FALSE(d->received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d->data_received());
+ }
+
+ // Use a new delegate without explicit credentials. The cached ones should be
+ // used.
+ d = std::make_unique<TestDelegate>();
+ {
+ // Don't pass wrong credentials in the URL, they would override valid cached
+ // ones.
+ std::unique_ptr<URLRequest> r(default_context().CreateRequest(
+ ftp_test_server_.GetURL(kFtpTestFile), DEFAULT_PRIORITY, d.get(),
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ r->Start();
+ EXPECT_TRUE(r->is_pending());
+
+ d->RunUntilComplete();
+
+ EXPECT_FALSE(r->is_pending());
+ EXPECT_EQ(1, d->response_started_count());
+ EXPECT_FALSE(d->received_data_before_response());
+ EXPECT_EQ(GetTestFileContents(), d->data_received());
+ }
+}
+
+TEST_F(URLRequestTestFTP, RawBodyBytes) {
+ ASSERT_TRUE(ftp_test_server_.Start());
+
+ TestDelegate d;
+ std::unique_ptr<URLRequest> req(default_context().CreateRequest(
+ ftp_test_server_.GetURL("simple.html"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ req->Start();
+ d.RunUntilComplete();
+
+ EXPECT_EQ(6, req->GetRawBodyBytes());
+}
+
+TEST_F(URLRequestTestFTP, FtpAuthCancellation) {
+ ftp_test_server_.set_no_anonymous_ftp_user(true);
+ ASSERT_TRUE(ftp_test_server_.Start());
+ TestDelegate d;
+ std::unique_ptr<URLRequest> req(default_context().CreateRequest(
+ ftp_test_server_.GetURL("simple.html"), DEFAULT_PRIORITY, &d,
+ TRAFFIC_ANNOTATION_FOR_TESTS));
+ req->Start();
+ d.RunUntilComplete();
+
+ ASSERT_TRUE(d.auth_required_called());
+ EXPECT_EQ(OK, d.request_status());
+ EXPECT_TRUE(req->auth_challenge_info());
+ std::string mime_type;
+ req->GetMimeType(&mime_type);
+ EXPECT_EQ("text/plain", mime_type);
+ EXPECT_EQ("", d.data_received());
+ EXPECT_EQ(-1, req->GetExpectedContentSize());
+}
+
+class URLRequestTestFTPOverHttpProxy : public URLRequestTestFTP {
+ public:
+ // Test interface:
+ void SetUp() override {
+ proxy_resolution_service_ = ConfiguredProxyResolutionService::CreateFixed(
+ "localhost", TRAFFIC_ANNOTATION_FOR_TESTS);
+ default_context_->set_proxy_resolution_service(
+ proxy_resolution_service_.get());
+ URLRequestTestFTP::SetUp();
+ }
+
+ private:
+ std::unique_ptr<ProxyResolutionService> proxy_resolution_service_;
+};
+
+// Check that FTP is not supported over an HTTP proxy.
+TEST_F(URLRequestTestFTPOverHttpProxy, Fails) {
+ TestDelegate delegate;
+ std::unique_ptr<URLRequest> request(
+ default_context_->CreateRequest(GURL("ftp://foo.test/"), DEFAULT_PRIORITY,
+ &delegate, TRAFFIC_ANNOTATION_FOR_TESTS));
+ request->Start();
+ delegate.RunUntilComplete();
+
+ EXPECT_THAT(delegate.request_status(), IsError(ERR_NO_SUPPORTED_PROXIES));
+}
+
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
TEST_F(URLRequestTest, NetworkAccessedSetOnHostResolutionFailure) {
auto context_builder = CreateTestURLRequestContextBuilder();
auto host_resolver = std::make_unique<MockHostResolver>();
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index 16e8cf435e4b0..2d06e601e9376 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -164,6 +164,10 @@
#include "services/network/sct_auditing/sct_auditing_handler.h"
#endif // BUILDFLAG(IS_CT_SUPPORTED)
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+#include "net/ftp/ftp_auth_cache.h"
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
#if BUILDFLAG(IS_CHROMEOS)
#include "services/network/cert_verifier_with_trust_anchors.h"
#endif // BUILDFLAG(IS_CHROMEOS)
@@ -2313,6 +2317,14 @@ void NetworkContext::AddAuthCacheEntry(
const net::NetworkAnonymizationKey& network_anonymization_key,
const net::AuthCredentials& credentials,
AddAuthCacheEntryCallback callback) {
+ if (challenge.challenger.scheme() == url::kFtpScheme) {
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ net::FtpAuthCache* auth_cache = url_request_context_->ftp_auth_cache();
+ auth_cache->Add(challenge.challenger.GetURL(), credentials);
+#else
+ NOTREACHED();
+#endif // BUILDFLAG(DISABLE_FTP_SUPPORT)
+ } else {
net::HttpAuthCache* http_auth_cache =
url_request_context_->http_transaction_factory()
->GetSession()
@@ -2324,6 +2336,7 @@ void NetworkContext::AddAuthCacheEntry(
net::HttpAuth::StringToScheme(challenge.scheme),
network_anonymization_key, challenge.challenge,
credentials, challenge.path);
+ }
std::move(callback).Run();
}
@@ -2734,6 +2747,12 @@ URLRequestContextOwner NetworkContext::MakeURLRequestContext(
}
builder.set_hsts_policy_bypass_list(params_->hsts_policy_bypass_list);
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ builder.set_ftp_enabled(params_->enable_ftp_url_support);
+#else // BUILDFLAG(DISABLE_FTP_SUPPORT)
+ DCHECK(!params_->enable_ftp_url_support);
+#endif
+
#if BUILDFLAG(ENABLE_REPORTING)
bool reporting_enabled = base::FeatureList::IsEnabled(features::kReporting);
if (reporting_enabled) {
diff --git a/services/network/network_context_unittest.cc b/services/network/network_context_unittest.cc
index 6d3382b4de9ff..a4d5cdd555ad8 100644
--- a/services/network/network_context_unittest.cc
+++ b/services/network/network_context_unittest.cc
@@ -180,6 +180,10 @@
#include "url/scheme_host_port.h"
#include "url/url_constants.h"
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+#include "net/ftp/ftp_auth_cache.h"
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
#if BUILDFLAG(ENABLE_REPORTING)
#include "net/network_error_logging/network_error_logging_service.h"
#include "net/reporting/reporting_cache.h"
@@ -927,7 +931,8 @@ TEST_F(NetworkContextTest, UnhandedProtocols) {
GURL("file:///not/a/path/that/leads/anywhere/but/it/should/not/matter/"
"anyways"),
- // FTP is not supported natively by Chrome.
+ // FTP is handled by the network service on some platforms, but support
+ // for it is not enabled by default.
GURL("ftp://foo.test/"),
};
@@ -7438,6 +7443,37 @@ TEST_F(NetworkContextTest, BlockAllCookies) {
EXPECT_EQ("None", response_body);
}
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+TEST_F(NetworkContextTest, AddFtpAuthCacheEntry) {
+ GURL url("ftp://example.test/");
+ const char16_t kUsername[] = u"test_user";
+ const char16_t kPassword[] = u"test_pass";
+ mojom::NetworkContextParamsPtr params =
+ CreateNetworkContextParamsForTesting();
+ params->enable_ftp_url_support = true;
+ std::unique_ptr<NetworkContext> network_context =
+ CreateContextWithParams(std::move(params));
+ net::AuthChallengeInfo challenge;
+ challenge.is_proxy = false;
+ challenge.challenger = url::Origin::Create(url);
+
+ ASSERT_TRUE(network_context->url_request_context()->ftp_auth_cache());
+ ASSERT_FALSE(
+ network_context->url_request_context()->ftp_auth_cache()->Lookup(url));
+ base::RunLoop run_loop;
+ network_context->AddAuthCacheEntry(challenge, net::NetworkIsolationKey(),
+ net::AuthCredentials(kUsername, kPassword),
+ run_loop.QuitClosure());
+ run_loop.Run();
+ net::FtpAuthCache::Entry* entry =
+ network_context->url_request_context()->ftp_auth_cache()->Lookup(url);
+ ASSERT_TRUE(entry);
+ EXPECT_EQ(url, entry->origin);
+ EXPECT_EQ(kUsername, entry->credentials.username());
+ EXPECT_EQ(kPassword, entry->credentials.password());
+}
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
TEST_F(NetworkContextTest, AddHttpAuthCacheEntry) {
std::unique_ptr<NetworkContext> network_context =
CreateContextWithParams(CreateNetworkContextParamsForTesting());
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index d38116aca078f..18b3daef09f8e 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -225,6 +225,11 @@ BASE_FEATURE(kAcceptCHFrame, "AcceptCHFrame", base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kGetCookiesStringUma,
"GetCookiesStringUma",
base::FEATURE_ENABLED_BY_DEFAULT);
+// Enables support for FTP URLs. When disabled FTP URLs will behave the same as
+// any other URL scheme that's unknown to the UA. See https://crbug.com/333943
+BASE_FEATURE(kFtpProtocol,
+ "FtpProtocol",
+ base::FEATURE_ENABLED_BY_DEFAULT);
namespace {
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 66221223e2c55..31dc4e14bfe10 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -92,6 +92,8 @@ extern uint32_t GetDataPipeDefaultAllocationSize(
COMPONENT_EXPORT(NETWORK_CPP)
extern size_t GetNetAdapterMaxBufSize();
+COMPONENT_EXPORT(NETWORK_CPP) BASE_DECLARE_FEATURE(kFtpProtocol);
+
COMPONENT_EXPORT(NETWORK_CPP)
extern size_t GetLoaderChunkSize();
diff --git a/services/network/public/cpp/simple_url_loader.cc b/services/network/public/cpp/simple_url_loader.cc
index 517777afcfeae..9b2469197b537 100644
--- a/services/network/public/cpp/simple_url_loader.cc
+++ b/services/network/public/cpp/simple_url_loader.cc
@@ -1848,7 +1848,7 @@ void SimpleURLLoaderImpl::OnReceiveResponse(
// Assume a 200 response unless headers were received indicating otherwise.
// No headers indicates this was not a real HTTP response (Could be a file
- // URL, chrome URL, response could have been provided by something else, etc).
+ // URL, chrome URL, FTP, response could have been provided by something else, etc).
int response_code = 200;
if (response_head->headers)
response_code = response_head->headers->response_code();
diff --git a/services/network/public/cpp/url_util.cc b/services/network/public/cpp/url_util.cc
index bb80e3ebd42f8..dbf99f662b65d 100644
--- a/services/network/public/cpp/url_util.cc
+++ b/services/network/public/cpp/url_util.cc
@@ -4,12 +4,25 @@
#include "services/network/public/cpp/url_util.h"
+#include "base/feature_list.h"
+#include "build/build_config.h"
+#include "net/net_buildflags.h"
+#include "services/network/public/cpp/features.h"
+#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "url/gurl.h"
+#include "url/url_constants.h"
namespace network {
bool IsURLHandledByNetworkService(const GURL& url) {
- return (url.SchemeIsHTTPOrHTTPS() || url.SchemeIsWSOrWSS());
+ if (url.SchemeIsHTTPOrHTTPS() || url.SchemeIsWSOrWSS())
+ return true;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ if (url.SchemeIs(url::kFtpScheme) &&
+ base::FeatureList::IsEnabled(features::kFtpProtocol))
+ return true;
+#endif
+ return false;
}
} // namespace network
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index c774dfca15ae1..e972bac69d2e5 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -374,6 +374,10 @@ struct NetworkContextParams {
// used to fetch PAC scripts. Note that currently data URLs are always enabled
// and file URLs are always disabled.
+ // True if ftp URLs should be supported.
+ // Must be false if built without FTP support.
+ bool enable_ftp_url_support = false;
+
// Whether or not to check the Android platform's cleartext policy for
// requests. Under some conditions, Android may advise us to block cleartext
// traffic.
@@ -1620,12 +1624,14 @@ interface NetworkContext {
LoadHttpAuthCacheProxyEntries(mojo_base.mojom.UnguessableToken cache_key)
=> ();
- // Adds an entry to the HttpAuthCache. `network_anonymization_key` is the
- // NetworkAnonymizationKey to restrict the credentials to, and is only
- // respected for server (not proxy) HTTP auth and only when the NetworkService
- // was configured to split the auth cache by NetworkAnonymizationKey.
- // `challenge` may not necessarily contain a stateful challenge that requires
- // a persistent connection, allowing the cache to be pre-populated.
+ // Adds an entry to the HttpAuthCache or FtpAuthCache (determined by whether
+ // the |challenger| field within |challenge| is an ftp:// URL).
+ // |network_isolation_key| is the NetworkIsolationKey to restrict the
+ // credentials to, and is only respected for server (not proxy) HTTP auth
+ // and only when the NetworkService was configured to split the auth cache by
+ // NetworkIsolationKey. |challenge| may not necessarily contain a stateful
+ // challenge that requires a persistent connection, allowing the cache to be
+ // pre-populated.
AddAuthCacheEntry(AuthChallengeInfo challenge,
NetworkAnonymizationKey network_anonymization_key,
AuthCredentials credentials) => ();
diff --git a/third_party/blink/public/web/web_document_loader.h b/third_party/blink/public/web/web_document_loader.h
index f9d3219990dcf..d2b95cce4c380 100644
--- a/third_party/blink/public/web/web_document_loader.h
+++ b/third_party/blink/public/web/web_document_loader.h
@@ -135,6 +135,9 @@ class BLINK_EXPORT WebDocumentLoader {
// committed in this WebDocumentLoader had transient activation.
virtual bool LastNavigationHadTransientUserActivation() const = 0;
+ // Returns true when the document is a FTP directory.
+ virtual bool IsListingFtpDirectory() const = 0;
+
// Sets the CodeCacheHosts for this loader.
virtual void SetCodeCacheHost(
CrossVariantMojoRemote<mojom::CodeCacheHostInterfaceBase> code_cache_host,
diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index 15c696731f42b..61659b43bc89a 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -160,6 +160,7 @@
#include "third_party/blink/renderer/platform/loader/fetch/resource_timing_utils.h"
#include "third_party/blink/renderer/platform/loader/fetch/unique_identifier.h"
#include "third_party/blink/renderer/platform/loader/fetch/url_loader/navigation_body_loader.h"
+#include "third_party/blink/renderer/platform/loader/ftp_directory_listing.h"
#include "third_party/blink/renderer/platform/loader/static_data_navigation_body_loader.h"
#include "third_party/blink/renderer/platform/mhtml/archive_resource.h"
#include "third_party/blink/renderer/platform/mhtml/mhtml_archive.h"
@@ -309,6 +310,7 @@ struct SameSizeAsDocumentLoader
bool is_same_origin_navigation;
bool has_text_fragment_token;
bool was_discarded;
+ bool listing_ftp_directory;
bool loading_main_document_from_mhtml_archive;
bool loading_srcdoc;
KURL fallback_base_url;
@@ -1279,7 +1281,7 @@ void DocumentLoader::BodyDataReceivedImpl(BodyData& data) {
DCHECK(!frame_->GetPage()->Paused());
time_of_last_data_received_ = clock_->NowTicks();
- if (loading_main_document_from_mhtml_archive_) {
+ if (listing_ftp_directory_ || loading_main_document_from_mhtml_archive_) {
// 1) Ftp directory listings accumulate data buffer and transform it later
// to the actual document content.
// 2) Mhtml archives accumulate data buffer and parse it as mhtml later
@@ -1402,6 +1404,12 @@ void DocumentLoader::FinishedLoading(base::TimeTicks finish_time) {
MainThreadDebugger::Instance(frame_->DomWindow()->GetIsolate())
->IsPaused());
+ if (listing_ftp_directory_) {
+ data_buffer_ = GenerateFtpDirectoryListingHtml(
+ response_.CurrentRequestUrl(), data_buffer_.get());
+ ProcessDataBuffer();
+ }
+
if (loading_main_document_from_mhtml_archive_ && state_ < kCommitted) {
// The browser process should block any navigation to an MHTML archive
// inside iframes. See NavigationRequest::OnResponseStarted().
@@ -1542,6 +1550,17 @@ DocumentPolicy::ParsedDocumentPolicy DocumentLoader::CreateDocumentPolicy() {
void DocumentLoader::HandleResponse() {
DCHECK(frame_);
+ if (response_.CurrentRequestUrl().ProtocolIs("ftp") &&
+ response_.MimeType() == "text/vnd.chromium.ftp-dir") {
+ if (response_.CurrentRequestUrl().Query() == "raw") {
+ // Interpret the FTP LIST command result as text.
+ response_.SetMimeType(AtomicString("text/plain"));
+ } else {
+ // FTP directory listing: Make up an HTML for the entries.
+ listing_ftp_directory_ = true;
+ response_.SetMimeType(AtomicString("text/html"));
+ }
+ }
if (response_.IsHTTP() &&
!network::IsSuccessfulStatus(response_.HttpStatusCode())) {
DCHECK(!IsA<HTMLObjectElement>(frame_->Owner()));
diff --git a/third_party/blink/renderer/core/loader/document_loader.h b/third_party/blink/renderer/core/loader/document_loader.h
index 4b0cc3793ecb9..337cd1eea6a70 100644
--- a/third_party/blink/renderer/core/loader/document_loader.h
+++ b/third_party/blink/renderer/core/loader/document_loader.h
@@ -359,6 +359,8 @@ class CORE_EXPORT DocumentLoader : public GarbageCollected<DocumentLoader>,
return devtools_navigation_token_;
}
+ bool IsListingFtpDirectory() const override { return listing_ftp_directory_; }
+
UseCounterImpl& GetUseCounter() { return use_counter_; }
PrefetchedSignedExchangeManager* GetPrefetchedSignedExchangeManager() const;
@@ -778,6 +780,8 @@ class CORE_EXPORT DocumentLoader : public GarbageCollected<DocumentLoader>,
// See WebNavigationParams for definition.
const bool was_discarded_ = false;
+ bool listing_ftp_directory_ = false;
+
// True when loading the main document from the MHTML archive. It implies an
// |archive_| to be created. Nested documents will also inherit from the same
// |archive_|, but won't have |loading_main_document_from_mhtml_archive_| set.
diff --git a/third_party/blink/renderer/platform/loader/BUILD.gn b/third_party/blink/renderer/platform/loader/BUILD.gn
index da5070066fd47..39fc54f713ec4 100644
--- a/third_party/blink/renderer/platform/loader/BUILD.gn
+++ b/third_party/blink/renderer/platform/loader/BUILD.gn
@@ -161,6 +161,8 @@ blink_platform_sources("loader") {
"fetch/worker_resource_timing_notifier.h",
"internet_disconnected_url_loader.cc",
"internet_disconnected_url_loader.h",
+ "ftp_directory_listing.cc",
+ "ftp_directory_listing.h",
"link_header.cc",
"link_header.h",
"mixed_content.cc",
@@ -244,6 +246,7 @@ source_set("unit_tests") {
"fetch/url_loader/sync_load_context_unittest.cc",
"fetch/url_loader/url_loader_unittest.cc",
"fetch/url_loader/worker_main_script_loader_unittest.cc",
+ "ftp_directory_listing_test.cc",
"link_header_test.cc",
"static_data_navigation_body_loader_test.cc",
"subresource_integrity_test.cc",
diff --git a/third_party/blink/renderer/platform/loader/ftp_directory_listing.cc b/third_party/blink/renderer/platform/loader/ftp_directory_listing.cc
new file mode 100644
index 0000000000000..fbee10b1875a7
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/ftp_directory_listing.cc
@@ -0,0 +1,109 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/loader/ftp_directory_listing.h"
+
+#include <string>
+#include <vector>
+
+#include "base/i18n/encoding_detection.h"
+#include "base/i18n/icu_string_conversions.h"
+#include "base/strings/escape.h"
+#include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/time/time.h"
+#include "net/base/directory_listing.h"
+#include "net/base/net_errors.h"
+#include "net/ftp/ftp_directory_listing_parser.h"
+#include "net/net_buildflags.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_error.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
+#include "url/gurl.h"
+
+namespace blink {
+
+namespace {
+
+std::u16string ConvertPathToUTF16(const std::string& path) {
+ // Per RFC 2640, FTP servers should use UTF-8 or its proper subset ASCII,
+ // but many old FTP servers use legacy encodings. Try UTF-8 first.
+ if (base::IsStringUTF8(path))
+ return base::UTF8ToUTF16(path);
+
+ // Try detecting the encoding. The sample is rather small though, so it may
+ // fail.
+ std::string encoding;
+ if (base::DetectEncoding(path, &encoding) && encoding != "US-ASCII") {
+ std::u16string path_utf16;
+ if (base::CodepageToUTF16(path, encoding.c_str(),
+ base::OnStringConversionError::SUBSTITUTE,
+ &path_utf16)) {
+ return path_utf16;
+ }
+ }
+
+ // Use system native encoding as the last resort.
+ return base::WideToUTF16(base::SysNativeMBToWide(path));
+}
+
+} // namespace
+
+scoped_refptr<SharedBuffer> GenerateFtpDirectoryListingHtml(
+ const KURL& url,
+ const SharedBuffer* input) {
+ const GURL gurl = GURL(url);
+ scoped_refptr<SharedBuffer> output = SharedBuffer::Create();
+ base::UnescapeRule::Type unescape_rules =
+ base::UnescapeRule::SPACES |
+ base::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS;
+ std::string unescaped_path =
+ base::UnescapeURLComponent(gurl.path(), unescape_rules);
+ const std::string header =
+ net::GetDirectoryListingHeader(ConvertPathToUTF16(unescaped_path));
+ output->Append(header.c_str(), header.size());
+
+ // If this isn't top level directory (i.e. the path isn't "/",)
+ // add a link to the parent directory.
+ if (gurl.path().length() > 1) {
+ const std::string link = net::GetParentDirectoryLink();
+ output->Append(link.c_str(), link.size());
+ }
+
+ std::string flatten;
+ for (const auto& span : *input) {
+ flatten.append(span.data(), span.size());
+ }
+
+ std::vector<net::FtpDirectoryListingEntry> entries;
+ int rv = net::ERR_NOT_IMPLEMENTED;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+ rv = net::ParseFtpDirectoryListing(flatten, base::Time::Now(), &entries);
+#endif
+ if (rv != net::OK) {
+ const std::string script = "<script>onListingParsingError();</script>\n";
+ output->Append(script.c_str(), script.size());
+ return output;
+ }
+ for (const net::FtpDirectoryListingEntry& entry : entries) {
+ // Skip the current and parent directory entries in the listing.
+ // net::GetParentDirectoryLink() takes care of them.
+ if (base::EqualsASCII(entry.name, ".") ||
+ base::EqualsASCII(entry.name, ".."))
+ continue;
+
+ bool is_directory =
+ (entry.type == net::FtpDirectoryListingEntry::DIRECTORY);
+ int64_t size =
+ entry.type == net::FtpDirectoryListingEntry::FILE ? entry.size : 0;
+ std::string entry_string = net::GetDirectoryListingEntry(
+ entry.name, entry.raw_name, is_directory, size, entry.last_modified);
+ output->Append(entry_string.c_str(), entry_string.size());
+ }
+
+ return output;
+}
+
+} // namespace blink
diff --git a/third_party/blink/renderer/platform/loader/ftp_directory_listing.h b/third_party/blink/renderer/platform/loader/ftp_directory_listing.h
new file mode 100644
index 0000000000000..c5f9bda28b5c0
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/ftp_directory_listing.h
@@ -0,0 +1,24 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FTP_DIRECTORY_LISTING_H_
+#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FTP_DIRECTORY_LISTING_H_
+
+#include "base/memory/scoped_refptr.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
+#include "third_party/blink/renderer/platform/wtf/forward.h"
+
+namespace blink {
+
+class KURL;
+
+// Translates |input|, an FTP LISTING result, to an HTML and returns it. When
+// an error happens that is written in the result HTML.
+PLATFORM_EXPORT scoped_refptr<SharedBuffer> GenerateFtpDirectoryListingHtml(
+ const KURL& url,
+ const SharedBuffer* input);
+
+} // namespace blink
+
+#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_LOADER_FTP_DIRECTORY_LISTING_H_
diff --git a/third_party/blink/renderer/platform/loader/ftp_directory_listing_test.cc b/third_party/blink/renderer/platform/loader/ftp_directory_listing_test.cc
new file mode 100644
index 0000000000000..9f52cba0c1222
--- /dev/null
+++ b/third_party/blink/renderer/platform/loader/ftp_directory_listing_test.cc
@@ -0,0 +1,114 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/loader/ftp_directory_listing.h"
+
+#include <string>
+
+#include "base/test/icu_test_util.h"
+#include "net/net_buildflags.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/loader/fetch/resource_error.h"
+#include "third_party/blink/renderer/platform/weborigin/kurl.h"
+#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
+#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
+#include "third_party/icu/source/i18n/unicode/timezone.h"
+
+namespace blink {
+namespace {
+
+class ScopedRestoreDefaultTimezone {
+ STACK_ALLOCATED();
+
+ public:
+ explicit ScopedRestoreDefaultTimezone(const char* zoneid) {
+ original_zone_.reset(icu::TimeZone::createDefault());
+ icu::TimeZone::adoptDefault(icu::TimeZone::createTimeZone(zoneid));
+ }
+ ~ScopedRestoreDefaultTimezone() {
+ icu::TimeZone::adoptDefault(original_zone_.release());
+ }
+
+ ScopedRestoreDefaultTimezone(const ScopedRestoreDefaultTimezone&) = delete;
+ ScopedRestoreDefaultTimezone& operator=(const ScopedRestoreDefaultTimezone&) =
+ delete;
+
+ private:
+ std::unique_ptr<icu::TimeZone> original_zone_;
+};
+
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+TEST(FtpDirectoryListingTest, Top) {
+ base::test::ScopedRestoreICUDefaultLocale locale("en_US");
+ ScopedRestoreDefaultTimezone timezone("Asia/Tokyo");
+
+ const KURL url("ftp://ftp.example.com/");
+
+ const std::string input = "drwxr-xr-x 1 ftp ftp 17 Feb 15 2016 top\r\n";
+ // Referring to code in net/base/dir_header.html, but the code itself
+ // is not included in the expectation due to unittest configuration.
+ std::string expected = R"JS(<script>start("/");</script>
+<script>addRow("top","top",1,0,"0 B",1455494400,"2/15/16, 9:00:00 AM");</script>
+)JS";
+ auto input_buffer = SharedBuffer::Create();
+ input_buffer->Append(input.data(), input.size());
+
+ auto output = GenerateFtpDirectoryListingHtml(url, input_buffer.get());
+ std::string flatten_output;
+ for (const auto span : *output) {
+ flatten_output.append(span.data(), span.size());
+ }
+
+ EXPECT_EQ(expected, flatten_output);
+}
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+TEST(FtpDirectoryListingTest, NonTop) {
+ base::test::ScopedRestoreICUDefaultLocale locale("en_US");
+ ScopedRestoreDefaultTimezone timezone("Asia/Tokyo");
+ const KURL url("ftp://ftp.example.com/foo/");
+
+ const std::string input = "drwxr-xr-x 1 ftp ftp 17 Feb 15 2016 dir\r\n";
+ // Referring to code in net/base/dir_header.html, but the code itself
+ // is not included in the expectation due to unittest configuration.
+ std::string expected = R"JS(<script>start("/foo/");</script>
+<script>onHasParentDirectory();</script>
+<script>addRow("dir","dir",1,0,"0 B",1455494400,"2/15/16, 9:00:00 AM");</script>
+)JS";
+
+ auto input_buffer = SharedBuffer::Create();
+ input_buffer->Append(input.data(), input.size());
+
+ auto output = GenerateFtpDirectoryListingHtml(url, input_buffer.get());
+ std::string flatten_output;
+ for (const auto span : *output) {
+ flatten_output.append(span.data(), span.size());
+ }
+
+ EXPECT_EQ(expected, flatten_output);
+}
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
+TEST(FtpDirectoryListingTest, Fail) {
+ base::test::ScopedRestoreICUDefaultLocale locale("en_US");
+ ScopedRestoreDefaultTimezone timezone("Asia/Tokyo");
+ const KURL url("ftp://ftp.example.com/");
+ auto input = SharedBuffer::Create();
+ input->Append("bogus", 5u);
+ std::string expected = R"JS(<script>start("/");</script>
+<script>onListingParsingError();</script>
+)JS";
+ auto output = GenerateFtpDirectoryListingHtml(url, input.get());
+ std::string flatten_output;
+ for (const auto span : *output) {
+ flatten_output.append(span.data(), span.size());
+ }
+
+ EXPECT_EQ(expected, flatten_output);
+}
+
+} // namespace
+
+} // namespace blink