thorium-mirror/other/ftp-support-thorium.patch

14183 lines
422 KiB
Diff
Raw Normal View History

2024-05-14 01:58:07 -04:00
diff --git a/android_webview/browser/aw_browser_context.cc b/android_webview/browser/aw_browser_context.cc
2024-05-14 03:02:35 -04:00
index 6167ef5ed42af..6af9e477c4c8a 100644
2024-05-14 01:58:07 -04:00
--- a/android_webview/browser/aw_browser_context.cc
+++ b/android_webview/browser/aw_browser_context.cc
2024-11-18 07:45:40 -03:00
@@ -574,6 +574,9 @@ void AwBrowserContext::ConfigureNetworkContextParams(
2024-05-14 01:58:07 -04:00
// (http://crbug.com/921750).
context_params->enforce_chrome_ct_policy = false;
+ // WebView does not support ftp yet.
+ context_params->enable_ftp_url_support = false;
+
2024-11-18 07:45:40 -03:00
context_params->enable_brotli = true;
2024-05-14 01:58:07 -04:00
context_params->enable_zstd =
2024-11-18 07:45:40 -03:00
base::FeatureList::IsEnabled(net::features::kZstdContentEncoding);
2024-11-19 13:58:44 -03:00
diff --git a/build/config/unsafe_buffers_paths.txt b/build/config/unsafe_buffers_paths.txt
index 99e985eacda99..15dea18f1fceb 100644
--- a/build/config/unsafe_buffers_paths.txt
+++ b/build/config/unsafe_buffers_paths.txt
@@ -112,13 +112,13 @@
-ios_internal/
-media/
-native_client/
+-net/ftp/
-net/third_party/
-printing/
-remoting/host/
-remoting/client/input/
-testing/iossim/
-third_party/
-+third_party/blink/
-tools/
-ui/base/clipboard/
-ui/base/ime/ash/
2024-05-14 01:58:07 -04:00
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
2024-07-29 18:06:20 -04:00
@@ -233,6 +233,14 @@
2024-05-14 01:58:07 -04:00
<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
2024-05-14 03:02:35 -04:00
index ceabbced0f67e..c298005d46bc3 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
2024-11-18 07:45:40 -03:00
@@ -13571,6 +13571,9 @@ This can include information about installed software, files, your browser, and
2024-05-14 01:58:07 -04:00
<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.">
2024-08-17 12:57:19 -04:00
+ 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>.
2024-05-14 01:58:07 -04:00
+ </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
2024-05-14 03:02:35 -04:00
index c9e74052c7afb..d271ccc0dca71 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -8542,6 +8542,12 @@ const FeatureEntry kFeatureEntries[] = {
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index e2a95fa3aadaa..5b608e9272838 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
2024-11-18 07:45:40 -03:00
@@ -355,6 +355,13 @@ const char kEnableDrDcDescription[] =
2024-05-14 03:02:35 -04:00
"(raster, webgl, video) "
" continues using the gpu main thread.";
2024-05-14 01:58:07 -04:00
+const char kEnableFtpName[] = "Enable support for FTP URLs";
+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.";
+
2024-07-29 18:06:20 -04:00
const char kTextBasedAudioDescriptionName[] = "Enable audio descriptions.";
const char kTextBasedAudioDescriptionDescription[] =
"When enabled, HTML5 video elements with a 'descriptions' WebVTT track "
2024-05-14 01:58:07 -04:00
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
2024-05-14 03:02:35 -04:00
index 4ef2b219f1d74..69f9c4f7d9e1a 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
2024-11-18 07:45:40 -03:00
@@ -255,6 +255,9 @@ extern const char
2024-07-29 18:06:20 -04:00
extern const char
2024-05-14 01:58:07 -04:00
kEnableExtensionsPermissionsForSupervisedUsersOnDesktopDescription[];
+extern const char kEnableFtpName[];
+extern const char kEnableFtpDescription[];
+
2024-07-29 18:06:20 -04:00
extern const char
kEnableSupervisedUserSkipParentApprovalToInstallExtensionsName[];
extern const char
2024-05-14 01:58:07 -04:00
diff --git a/chrome/browser/net/profile_network_context_service.cc b/chrome/browser/net/profile_network_context_service.cc
2024-05-14 03:02:35 -04:00
index e50641649a959..0f0ef6bd2b0fd 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/net/profile_network_context_service.cc
+++ b/chrome/browser/net/profile_network_context_service.cc
2024-11-18 07:45:40 -03:00
@@ -1190,6 +1190,15 @@ void ProfileNetworkContextService::ConfigureNetworkContextParamsInternal(
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 22a62ad698fdb..ec30208a38bf0 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/net/system_network_context_manager.cc
+++ b/chrome/browser/net/system_network_context_manager.cc
2024-11-18 07:45:40 -03:00
@@ -1042,6 +1042,12 @@ SystemNetworkContextManager::CreateNetworkContextParams() {
2024-05-14 01:58:07 -04:00
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
2024-11-18 07:45:40 -03:00
index 39f96f45bca3a..123e400761e0f 100644
2024-05-14 01:58:07 -04:00
--- a/chrome/browser/profiles/profile_io_data.cc
+++ b/chrome/browser/profiles/profile_io_data.cc
2024-10-03 16:40:37 -03:00
@@ -15,6 +15,7 @@
2024-05-14 01:58:07 -04:00
#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"
2024-11-18 07:45:40 -03:00
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
2024-10-03 22:50:47 -03:00
@@ -54,6 +55,9 @@ bool ProfileIOData::IsHandledProtocol(const std::string& scheme) {
2024-10-03 16:40:37 -03:00
#if !BUILDFLAG(IS_ANDROID)
chrome::kIsolatedAppScheme,
#endif // !BUILDFLAG(IS_ANDROID)
2024-05-14 01:58:07 -04:00
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
2024-10-03 22:50:47 -03:00
+ url::kFtpScheme,
2024-05-14 01:58:07 -04:00
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
2024-10-03 16:40:37 -03:00
});
2024-05-14 01:58:07 -04:00
2024-10-03 16:40:37 -03:00
return kProtocolList.contains(scheme);
2024-05-14 01:58:07 -04:00
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(
2024-10-03 13:28:28 -03:00
diff --git a/chrome/installer/util/shell_util.cc b/chrome/installer/util/shell_util.cc
2024-11-18 07:45:40 -03:00
index c087d1c02bca4..aa5d721c5ea93 100644
2024-10-03 13:28:28 -03:00
--- 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.
//
2024-11-18 07:45:40 -03:00
@@ -1545,10 +1545,10 @@ const wchar_t* ShellUtil::kDefaultFileAssociations[] = {
2024-10-03 13:28:28 -03:00
const wchar_t* ShellUtil::kPotentialFileAssociations[] = {
2024-11-18 07:45:40 -03:00
L".htm", L".html", L".mhtml", L".pdf", L".shtml",
L".svg", L".xht", L".xhtml", L".webp", nullptr};
2024-10-03 13:28:28 -03:00
-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";
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 37852c47b90d4..8be654c440fc9 100644
2024-05-14 01:58:07 -04:00
--- 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
2024-11-18 07:45:40 -03:00
index 3158ee1460b5b..8f62b65b2ee3e 100644
2024-05-14 01:58:07 -04:00
--- a/content/browser/browser_url_handler_impl.cc
+++ b/content/browser/browser_url_handler_impl.cc
2024-11-18 07:45:40 -03:00
@@ -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,
2024-05-14 01:58:07 -04:00
diff --git a/content/public/common/content_switches.cc b/content/public/common/content_switches.cc
2024-05-14 03:02:35 -04:00
index 30f63bfc1750f..a38a1839d7207 100644
2024-05-14 01:58:07 -04:00
--- a/content/public/common/content_switches.cc
+++ b/content/public/common/content_switches.cc
2024-11-18 07:45:40 -03:00
@@ -329,6 +329,9 @@ const char kEnableFakeNoAllocDirectCallForTesting[] =
2024-05-14 01:58:07 -04:00
// 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
2024-05-14 03:02:35 -04:00
index b6f3b21a2b1ff..d01af10dcec28 100644
2024-05-14 01:58:07 -04:00
--- a/content/public/common/content_switches.h
+++ b/content/public/common/content_switches.h
2024-10-03 16:40:37 -03:00
@@ -106,6 +106,7 @@ CONTENT_EXPORT extern const char kEnableExperimentalWebAssemblyFeatures[];
2024-07-29 18:06:20 -04:00
CONTENT_EXPORT extern const char kEnableExperimentalWebAssemblyFeatures[];
2024-05-14 01:58:07 -04:00
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
2024-11-18 07:45:40 -03:00
@@ -179,6 +179,9 @@ class CONTENT_EXPORT RenderFrame :
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index f362ee7c23241..d34f9fbe475b2 100644
2024-05-14 01:58:07 -04:00
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
2024-11-18 07:45:40 -03:00
@@ -2500,6 +2500,10 @@ RenderFrameImpl::GetRemoteAssociatedInterfaces() {
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index b32fbb5887cb2..626bbdd3b1e5d 100644
2024-05-14 01:58:07 -04:00
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
2024-11-18 07:45:40 -03:00
@@ -400,6 +400,7 @@ class CONTENT_EXPORT RenderFrameImpl
2024-05-14 01:58:07 -04:00
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;
2024-11-18 07:45:40 -03:00
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
2024-05-14 01:58:07 -04:00
@@ -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"
2024-11-18 07:45:40 -03:00
index e229ec4c97bb6..dd33fcf48c550 100644
2024-05-14 01:58:07 -04:00
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
2024-11-18 07:45:40 -03:00
@@ -95,6 +95,7 @@ buildflag_header("buildflags") {
2024-05-14 01:58:07 -04:00
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",
2024-11-18 07:45:40 -03:00
@@ -1587,6 +1588,41 @@ component("net") {
2024-05-14 01:58:07 -04:00
]
}
+ 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 += [
2024-11-18 07:45:40 -03:00
"server/http_connection.cc",
@@ -3203,6 +3239,21 @@ target(_test_target_type, "net_unittests") {
2024-05-14 01:58:07 -04:00
]
}
+ 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" ]
}
2024-11-18 07:45:40 -03:00
@@ -3720,6 +3771,46 @@ fuzzer_test("net_spdy_headers_to_http_response_headers_fuzzer") {
2024-05-14 01:58:07 -04:00
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
2024-11-18 07:45:40 -03:00
@@ -45,6 +45,14 @@ specific_include_rules = {
2024-05-14 01:58:07 -04:00
"+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">
2024-05-14 09:06:17 -04:00
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
2024-10-03 16:40:37 -03:00
@@ -913,14 +913,36 @@ NET_ERROR(TRUST_TOKEN_OPERATION_FAILED, -506)
2024-05-14 09:06:17 -04:00
// 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)
2024-05-14 01:58:07 -04:00
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. All rights reserved.
+# 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
2024-05-14 03:02:35 -04:00
index a0ea9f52d1779..589b499e2d4c4 100644
2024-05-14 01:58:07 -04:00
--- a/net/docs/proxy.md
+++ b/net/docs/proxy.md
2024-11-18 07:45:40 -03:00
@@ -109,6 +109,7 @@ about an HTTP proxy.
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 026a38c0baab1..1d7d13a7c409f 100644
2024-05-14 01:58:07 -04:00
--- 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_auth_cache.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_AUTH_CACHE_H_
+#define NET_FTP_FTP_AUTH_CACHE_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_auth_cache.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/auth.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+using 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
2024-11-19 13:58:44 -03:00
@@ -0,0 +1,159 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
+#define NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+
+#include <string.h>
+
+#include "net/base/net_errors.h"
+#include "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. All rights reserved.
+// 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. All rights reserved.
+// 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..0e51c8dd4e99c
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser.cc
@@ -0,0 +1,126 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser.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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_ls.h"
+
+#include <vector>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/time/time.h"
+#include "net/ftp/ftp_directory_listing_parser.h"
+#include "net/ftp/ftp_util.h"
+
+namespace 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
+
+#include <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..86e34e661ae0a
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms.cc
@@ -0,0 +1,308 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_vms.h"
+
+#include <vector>
+
+#include "base/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);
+
2024-10-03 22:50:47 -03:00
+ std::vector<std::u16string_view> parts = base::SplitStringPiece(
2024-05-14 01:58:07 -04:00
+ 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(
2024-10-03 22:50:47 -03:00
+ std::u16string_view(input).substr(1, input.length() - 2), u",",
2024-05-14 01:58:07 -04:00
+ 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.
2024-10-03 22:50:47 -03:00
+ std::vector<std::u16string_view> date_parts = base::SplitStringPiece(
2024-05-14 01:58:07 -04:00
+ 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;
2024-10-03 22:50:47 -03:00
+ std::vector<std::u16string_view> time_parts = base::SplitStringPiece(
2024-05-14 01:58:07 -04:00
+ 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_windows.h"
+
+#include <vector>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_layer.h"
+
+#include "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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_LAYER_H_
+#define NET_FTP_FTP_NETWORK_LAYER_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_session.h"
+
+namespace net {
+
+FtpNetworkSession::FtpNetworkSession(HostResolver* host_resolver)
+ : host_resolver_(host_resolver) {}
+
+FtpNetworkSession::~FtpNetworkSession() = 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
2024-11-12 23:55:35 -03:00
@@ -0,0 +1,29 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_SESSION_H_
+#define NET_FTP_FTP_NETWORK_SESSION_H_
+
2024-11-12 23:55:35 -03:00
+#include "base/memory/raw_ptr.h"
2024-05-14 01:58:07 -04:00
+#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:
2024-11-12 23:55:35 -03:00
+ raw_ptr<HostResolver> const host_resolver_;
2024-05-14 01:58:07 -04:00
+};
+
+} // 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
2024-11-19 13:58:44 -03:00
index 0000000000000..4fdad5392e0a7
2024-05-14 01:58:07 -04:00
--- /dev/null
+++ b/net/ftp/ftp_network_transaction.cc
2024-11-19 13:58:44 -03:00
@@ -0,0 +1,1293 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_transaction.h"
+
+#include <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;
+
2024-10-03 22:50:47 -03:00
+ std::string_view epsv_line(response.lines[0]);
2024-05-14 01:58:07 -04:00
+ size_t start = epsv_line.find('(');
+ // If the line doesn't have a '(' or doesn't have enough characters after the
+ // first '(', fail.
2024-10-03 22:50:47 -03:00
+ if (start == std::string_view::npos || epsv_line.length() - start < 7)
2024-05-14 01:58:07 -04:00
+ 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);
2024-10-03 22:50:47 -03:00
+ if (end == std::string_view::npos)
2024-05-14 01:58:07 -04:00
+ 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.
2024-10-03 22:50:47 -03:00
+ std::vector<std::string_view> pieces = base::SplitStringPiece(
2024-05-14 01:58:07 -04:00
+ 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(
2024-11-12 23:55:35 -03:00
+ raw_ptr <FtpRequestInfo> request_info,
2024-05-14 01:58:07 -04:00
+ 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),
2024-05-14 06:35:50 -04:00
+ NetworkAnonymizationKey(), net_log_, std::nullopt);
2024-05-14 01:58:07 -04:00
+ 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
2024-11-12 23:55:35 -03:00
@@ -0,0 +1,268 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_TRANSACTION_H_
+#define NET_FTP_FTP_NETWORK_TRANSACTION_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
2024-11-12 23:55:35 -03:00
+#include "base/memory/raw_ptr.h"
2024-05-14 01:58:07 -04:00
+#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:
2024-11-12 23:55:35 -03:00
+ int Start(raw_ptr <FtpRequestInfo> request_info,
2024-05-14 01:58:07 -04:00
+ 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_;
2024-11-12 23:55:35 -03:00
+ raw_ptr<FtpRequestInfo> request_;
2024-05-14 01:58:07 -04:00
+ MutableNetworkTrafficAnnotationTag traffic_annotation_;
+ FtpResponseInfo response_;
+
2024-11-12 23:55:35 -03:00
+ raw_ptr<HostResolver> resolver_;
2024-05-14 01:58:07 -04:00
+ // 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_;
+
2024-11-12 23:55:35 -03:00
+ raw_ptr<ClientSocketFactory> const socket_factory_;
2024-05-14 01:58:07 -04:00
+
+ 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_transaction.h"
+
+#include "base/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_REQUEST_INFO_H_
+#define NET_FTP_FTP_REQUEST_INFO_H_
+
+#include "url/gurl.h"
+
+namespace net {
+
+class FtpRequestInfo {
+ public:
+ // The requested URL.
+ GURL url;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_REQUEST_INFO_H_
diff --git a/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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_response_info.h"
+
+namespace net {
+
+FtpResponseInfo::FtpResponseInfo()
+ : needs_auth(false),
+ expected_content_size(-1),
+ is_directory_listing(false) {
+}
+
+FtpResponseInfo::~FtpResponseInfo() = 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_RESPONSE_INFO_H_
+#define NET_FTP_FTP_RESPONSE_INFO_H_
+
+#include <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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_SERVER_TYPE_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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_TRANSACTION_H_
+#define NET_FTP_FTP_TRANSACTION_H_
+
+#include <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.
2024-11-12 23:55:35 -03:00
+ virtual int Start(raw_ptr <FtpRequestInfo> request_info,
2024-05-14 01:58:07 -04:00
+ 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_TRANSACTION_FACTORY_H_
+#define NET_FTP_FTP_TRANSACTION_FACTORY_H_
+
+#include <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..fe7e81c2e8ad7
--- /dev/null
+++ b/net/ftp/ftp_util.cc
2024-11-19 13:58:44 -03:00
@@ -0,0 +1,373 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_util.h"
+
+#include <map>
+#include <vector>
+
+#include "base/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;
2024-10-03 22:50:47 -03:00
+using std::u16string_view;
2024-05-14 01:58:07 -04:00
+
+// 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, "/");
2024-10-03 22:50:47 -03:00
+ std::vector<std::string_view> tokens;
2024-05-14 01:58:07 -04:00
+ 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(
2024-10-03 22:50:47 -03:00
+ &result, 0, "[.", std::string_view());
2024-05-14 01:58:07 -04:00
+ } 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].
2024-10-03 22:50:47 -03:00
+ std::vector<std::u16string_view> date_parts = base::SplitStringPiece(
2024-05-14 01:58:07 -04:00
+ 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;
+
2024-10-03 22:50:47 -03:00
+ std::vector<std::u16string_view> time_parts =
+ base::SplitStringPiece(std::u16string_view(time).substr(0, 5), u":",
2024-05-14 01:58:07 -04:00
+ 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_UTIL_H_
+#define NET_FTP_FTP_UTIL_H_
+
+#include <string>
+
+#include "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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_util.h"
+
+#include "base/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
2024-07-29 18:06:20 -04:00
@@ -324,8 +324,8 @@ PacFileFetcherImpl::PacFileFetcherImpl(URLRequestContext* url_request_context)
2024-05-14 01:58:07 -04:00
}
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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/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
2024-11-12 23:55:35 -03:00
@@ -0,0 +1,59 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_URL_REQUEST_FTP_PROTOCOL_HANDLER_H_
+#define NET_URL_REQUEST_FTP_PROTOCOL_HANDLER_H_
+
+#include <memory>
+
+#include "base/memory/raw_ptr.h"
2024-05-14 01:58:07 -04:00
+#include "base/compiler_specific.h"
2024-11-12 23:55:35 -03:00
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
2024-05-14 01:58:07 -04:00
+#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_;
2024-08-02 11:45:16 -04:00
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
2024-05-14 01:58:07 -04:00
+};
+
+} // 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
2024-05-14 03:02:35 -04:00
index 859dcb00e8153..d0791fdace3c5 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request.h
+++ b/net/url_request/url_request.h
2024-10-03 16:40:37 -03:00
@@ -749,7 +749,8 @@ class NET_EXPORT URLRequest : public base::SupportsUserData {
2024-05-14 01:58:07 -04:00
}
// 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
2024-05-14 03:02:35 -04:00
index 5618d02062989..e71e085486f2f 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request_context.h
+++ b/net/url_request/url_request_context.h
2024-05-14 03:02:35 -04:00
@@ -55,6 +55,10 @@ class URLRequest;
class URLRequestJobFactory;
2024-05-14 01:58:07 -04:00
class URLRequestContextBuilder;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
+class FtpAuthCache;
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
#if BUILDFLAG(ENABLE_REPORTING)
class NetworkErrorLoggingService;
class PersistentReportingAndNelStore;
2024-10-03 16:40:37 -03:00
@@ -235,6 +239,13 @@ class NET_EXPORT URLRequestContext final {
2024-05-14 01:58:07 -04:00
// 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_);
}
2024-10-03 16:40:37 -03:00
@@ -363,6 +374,10 @@ class NET_EXPORT URLRequestContext final {
2024-05-14 01:58:07 -04:00
std::unique_ptr<TransportSecurityPersister> transport_security_persister_;
+#if !BUILDFLAG(DISABLE_FTP_SUPPORT)
2024-08-02 11:45:16 -04:00
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
2024-05-14 01:58:07 -04:00
+#endif // !BUILDFLAG(DISABLE_FTP_SUPPORT)
+
2024-05-14 03:02:35 -04:00
std::unique_ptr<std::set<raw_ptr<const URLRequest, SetExperimental>>>
url_requests_;
2024-05-14 01:58:07 -04:00
diff --git a/net/url_request/url_request_context_builder.cc b/net/url_request/url_request_context_builder.cc
2024-05-14 03:02:35 -04:00
index c4ba7328234bf..4f5bb37448ccd 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request_context_builder.cc
+++ b/net/url_request/url_request_context_builder.cc
2024-10-03 16:40:37 -03:00
@@ -53,6 +53,12 @@
2024-05-14 03:02:35 -04:00
#include "net/url_request/url_request_job_factory.h"
2024-05-14 01:58:07 -04:00
#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"
2024-11-18 07:45:40 -03:00
@@ -592,12 +598,23 @@ std::unique_ptr<URLRequestContext> URLRequestContextBuilder::Build() {
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 66fd979ebae17..996f83526a69c 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request_context_builder.h
+++ b/net/url_request/url_request_context_builder.h
2024-11-18 07:45:40 -03:00
@@ -217,6 +217,11 @@ class NET_EXPORT URLRequestContextBuilder {
2024-05-14 01:58:07 -04:00
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(
2024-11-18 07:45:40 -03:00
@@ -441,6 +446,11 @@ class NET_EXPORT URLRequestContextBuilder {
2024-05-14 01:58:07 -04:00
2024-05-14 03:02:35 -04:00
std::optional<std::string> cookie_deprecation_label_;
2024-05-14 01:58:07 -04:00
+#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;
2024-05-14 03:02:35 -04:00
bool suppress_setting_socket_performance_watcher_factory_for_testing_ = false;
2024-05-14 01:58:07 -04:00
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. All rights reserved.
+// 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/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
2024-11-12 23:55:35 -03:00
@@ -0,0 +1,102 @@
2024-05-14 01:58:07 -04:00
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_URL_REQUEST_URL_REQUEST_FTP_JOB_H_
+#define NET_URL_REQUEST_URL_REQUEST_FTP_JOB_H_
+
+#include <memory>
+#include <string>
+
2024-11-12 23:55:35 -03:00
+#include "base/memory/raw_ptr.h"
2024-05-14 01:58:07 -04:00
+#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);
+
2024-11-12 23:55:35 -03:00
+ raw_ptr<ProxyResolutionService> proxy_resolution_service_;
2024-05-14 01:58:07 -04:00
+ 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_;
+
2024-11-12 23:55:35 -03:00
+ raw_ptr<FtpTransactionFactory> ftp_transaction_factory_;
2024-08-02 11:45:16 -04:00
+ raw_ptr<FtpAuthCache> ftp_auth_cache_;
2024-05-14 01:58:07 -04:00
+
+ 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. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/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
2024-05-14 03:02:35 -04:00
index 575a510b7e188..2ada1ead4c8f8 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request_test_util.h
+++ b/net/url_request/url_request_test_util.h
2024-10-03 16:40:37 -03:00
@@ -35,6 +35,7 @@
#include "net/cookies/cookie_util.h"
2024-05-14 01:58:07 -04:00
#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
2024-05-14 03:02:35 -04:00
index 18b16b9d46f01..24c0efb4f2cef 100644
2024-05-14 01:58:07 -04:00
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
2024-11-18 07:45:40 -03:00
@@ -173,10 +173,15 @@
2024-07-29 18:06:20 -04:00
2024-05-14 01:58:07 -04:00
#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
2024-11-18 07:45:40 -03:00
@@ -218,6 +223,12 @@ const std::u16string kUser(u"user");
2024-05-14 01:58:07 -04:00
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,
2024-11-18 07:45:40 -03:00
@@ -319,6 +330,29 @@ void TestLoadTimingCacheHitNoNetwork(const LoadTimingInfo& load_timing_info) {
2024-05-14 01:58:07 -04:00
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:
2024-11-18 07:45:40 -03:00
@@ -11822,6 +11856,404 @@ TEST_F(HTTPSLocalCRLSetTest, InterceptionBlockedAllowOverrideOnHSTS) {
2024-05-14 01:58:07 -04:00
}
#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
2024-05-14 03:02:35 -04:00
index 16e8cf435e4b0..2d06e601e9376 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
2024-11-18 07:45:40 -03:00
@@ -164,6 +164,10 @@
2024-05-14 01:58:07 -04:00
#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)
2024-11-18 07:45:40 -03:00
@@ -2313,6 +2317,14 @@ void NetworkContext::AddAuthCacheEntry(
2024-05-14 01:58:07 -04:00
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()
2024-11-18 07:45:40 -03:00
@@ -2324,6 +2336,7 @@ void NetworkContext::AddAuthCacheEntry(
2024-05-14 01:58:07 -04:00
net::HttpAuth::StringToScheme(challenge.scheme),
network_anonymization_key, challenge.challenge,
credentials, challenge.path);
+ }
std::move(callback).Run();
}
2024-11-18 07:45:40 -03:00
@@ -2734,6 +2747,12 @@ URLRequestContextOwner NetworkContext::MakeURLRequestContext(
2024-05-14 01:58:07 -04:00
}
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
2024-05-14 03:02:35 -04:00
index 6d3382b4de9ff..a4d5cdd555ad8 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/network_context_unittest.cc
+++ b/services/network/network_context_unittest.cc
2024-11-18 07:45:40 -03:00
@@ -180,6 +180,10 @@
2024-05-14 01:58:07 -04:00
#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"
2024-11-18 07:45:40 -03:00
@@ -927,7 +931,8 @@ TEST_F(NetworkContextTest, UnhandedProtocols) {
2024-05-14 01:58:07 -04:00
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/"),
};
2024-11-18 07:45:40 -03:00
@@ -7438,6 +7443,37 @@ TEST_F(NetworkContextTest, BlockAllCookies) {
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index d38116aca078f..18b3daef09f8e 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
2024-10-03 16:40:37 -03:00
@@ -225,6 +225,11 @@ BASE_FEATURE(kAcceptCHFrame, "AcceptCHFrame", base::FEATURE_ENABLED_BY_DEFAULT);
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 66221223e2c55..31dc4e14bfe10 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
2024-10-03 16:40:37 -03:00
@@ -92,6 +92,8 @@ extern uint32_t GetDataPipeDefaultAllocationSize(
2024-05-14 01:58:07 -04:00
COMPONENT_EXPORT(NETWORK_CPP)
2024-07-29 18:06:20 -04:00
extern size_t GetNetAdapterMaxBufSize();
2024-05-14 01:58:07 -04:00
+COMPONENT_EXPORT(NETWORK_CPP) BASE_DECLARE_FEATURE(kFtpProtocol);
+
COMPONENT_EXPORT(NETWORK_CPP)
2024-07-29 18:06:20 -04:00
extern size_t GetLoaderChunkSize();
2024-05-14 01:58:07 -04:00
diff --git a/services/network/public/cpp/simple_url_loader.cc b/services/network/public/cpp/simple_url_loader.cc
2024-05-14 03:02:35 -04:00
index 517777afcfeae..9b2469197b537 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/public/cpp/simple_url_loader.cc
+++ b/services/network/public/cpp/simple_url_loader.cc
2024-11-18 07:45:40 -03:00
@@ -1848,7 +1848,7 @@ void SimpleURLLoaderImpl::OnReceiveResponse(
2024-05-14 01:58:07 -04:00
// 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
2024-05-14 03:02:35 -04:00
index c774dfca15ae1..e972bac69d2e5 100644
2024-05-14 01:58:07 -04:00
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
2024-05-14 03:02:35 -04:00
@@ -374,6 +374,10 @@ struct NetworkContextParams {
2024-05-14 01:58:07 -04:00
// 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.
2024-11-18 07:45:40 -03:00
@@ -1620,12 +1624,14 @@ interface NetworkContext {
2024-05-14 01:58:07 -04:00
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
2024-11-18 07:45:40 -03:00
@@ -135,6 +135,9 @@ class BLINK_EXPORT WebDocumentLoader {
2024-05-14 01:58:07 -04:00
// 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
2024-05-14 03:02:35 -04:00
index 15c696731f42b..61659b43bc89a 100644
2024-05-14 01:58:07 -04:00
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
2024-11-18 07:45:40 -03:00
@@ -160,6 +160,7 @@
2024-05-14 01:58:07 -04:00
#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"
2024-11-18 07:45:40 -03:00
@@ -309,6 +310,7 @@ struct SameSizeAsDocumentLoader
2024-05-14 01:58:07 -04:00
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;
2024-11-18 07:45:40 -03:00
@@ -1279,7 +1281,7 @@ void DocumentLoader::BodyDataReceivedImpl(BodyData& data) {
2024-05-14 01:58:07 -04:00
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
2024-11-18 07:45:40 -03:00
@@ -1402,6 +1404,12 @@ void DocumentLoader::FinishedLoading(base::TimeTicks finish_time) {
2024-05-14 01:58:07 -04:00
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().
2024-11-18 07:45:40 -03:00
@@ -1542,6 +1550,17 @@ DocumentPolicy::ParsedDocumentPolicy DocumentLoader::CreateDocumentPolicy() {
2024-05-14 01:58:07 -04:00
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
2024-05-14 03:02:35 -04:00
index 4b0cc3793ecb9..337cd1eea6a70 100644
2024-05-14 01:58:07 -04:00
--- a/third_party/blink/renderer/core/loader/document_loader.h
+++ b/third_party/blink/renderer/core/loader/document_loader.h
2024-11-18 07:45:40 -03:00
@@ -359,6 +359,8 @@ class CORE_EXPORT DocumentLoader : public GarbageCollected<DocumentLoader>,
2024-05-14 01:58:07 -04:00
return devtools_navigation_token_;
}
+ bool IsListingFtpDirectory() const override { return listing_ftp_directory_; }
+
UseCounterImpl& GetUseCounter() { return use_counter_; }
PrefetchedSignedExchangeManager* GetPrefetchedSignedExchangeManager() const;
2024-11-18 07:45:40 -03:00
@@ -778,6 +780,8 @@ class CORE_EXPORT DocumentLoader : public GarbageCollected<DocumentLoader>,
2024-05-14 01:58:07 -04:00
// 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
2024-05-14 03:02:35 -04:00
index da5070066fd47..39fc54f713ec4 100644
2024-05-14 01:58:07 -04:00
--- a/third_party/blink/renderer/platform/loader/BUILD.gn
+++ b/third_party/blink/renderer/platform/loader/BUILD.gn
2024-10-03 16:40:37 -03:00
@@ -161,6 +161,8 @@ blink_platform_sources("loader") {
2024-05-14 01:58:07 -04:00
"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",
2024-10-03 16:40:37 -03:00
@@ -244,6 +246,7 @@ source_set("unit_tests") {
2024-05-14 01:58:07 -04:00
"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. All rights reserved.
+// 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. All rights reserved.
+// 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. All rights reserved.
+// 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