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
+// 600-699 FTP errors
// 700-799 Certificate manager errors
// 800-899 DNS resolver errors
@@ -913,14 +913,36 @@ NET_ERROR(TRUST_TOKEN_OPERATION_FAILED, -506)
// to a local provider (for "platform-provided" issuance).
NET_ERROR(TRUST_TOKEN_OPERATION_SUCCESS_WITHOUT_SENDING_REQUEST, -507)
-// *** Code -600 is reserved (was FTP_PASV_COMMAND_FAILED). ***
-// *** Code -601 is reserved (was FTP_FAILED). ***
-// *** Code -602 is reserved (was FTP_SERVICE_UNAVAILABLE). ***
-// *** Code -603 is reserved (was FTP_TRANSFER_ABORTED). ***
-// *** Code -604 is reserved (was FTP_FILE_BUSY). ***
-// *** Code -605 is reserved (was FTP_SYNTAX_ERROR). ***
-// *** Code -606 is reserved (was FTP_COMMAND_NOT_SUPPORTED). ***
-// *** Code -607 is reserved (was FTP_BAD_COMMAND_SEQUENCE). ***
+// A generic error for failed FTP control connection command.
+// If possible, please use or add a more specific error code.
+NET_ERROR(FTP_FAILED, -601)
+
+// The server cannot fulfill the request at this point. This is a temporary
+// error.
+// FTP response code 421.
+NET_ERROR(FTP_SERVICE_UNAVAILABLE, -602)
+
+// The server has aborted the transfer.
+// FTP response code 426.
+NET_ERROR(FTP_TRANSFER_ABORTED, -603)
+
+// The file is busy, or some other temporary error condition on opening
+// the file.
+// FTP response code 450.
+NET_ERROR(FTP_FILE_BUSY, -604)
+
+// Server rejected our command because of syntax errors.
+// FTP response codes 500, 501.
+NET_ERROR(FTP_SYNTAX_ERROR, -605)
+
+// Server does not support the command we issued.
+// FTP response codes 502, 504.
+NET_ERROR(FTP_COMMAND_NOT_SUPPORTED, -606)
+
+// Server rejected our command because we didn't issue the commands in right
+// order.
+// FTP response code 503.
+NET_ERROR(FTP_BAD_COMMAND_SEQUENCE, -607)
// PKCS #12 import failed due to incorrect password.
NET_ERROR(PKCS12_IMPORT_BAD_PASSWORD, -701)
diff --git a/net/base/port_util.cc b/net/base/port_util.cc
index a70f9e9b21a33..5f3d1364d8c62 100644
--- a/net/base/port_util.cc
+++ b/net/base/port_util.cc
@@ -139,6 +139,11 @@ bool IsPortAllowedForScheme(int port, std::string_view url_scheme) {
if (g_explicitly_allowed_ports.Get().count(port) > 0)
return true;
+ // FTP requests are permitted to use port 21.
+ if ((base::ToLowerASCII(url_scheme) == url::kFtpScheme) && port == 21) {
+ return true;
+ }
+
// Finally check against the generic list of restricted ports for all
// schemes.
for (int restricted_port : kRestrictedPorts) {
diff --git a/net/data/ftp/dir-listing-ls-1 b/net/data/ftp/dir-listing-ls-1
new file mode 100644
index 0000000000000..e99088e773075
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 .message
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README
+-rw-r--r-- 1 ftp ftp 560 Sep 28 2007 index.html
+drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-1-utf8 b/net/data/ftp/dir-listing-ls-1-utf8
new file mode 100644
index 0000000000000..7f9a33339aadd
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1-utf8
@@ -0,0 +1,6 @@
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 .
+drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 ..
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 .message
+-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README!
+-rw-r--r-- 1 ftp ftp 560 Sep 28 2007 こんにちは.html
+drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 pub
diff --git a/net/data/ftp/dir-listing-ls-1-utf8.expected b/net/data/ftp/dir-listing-ls-1-utf8.expected
new file mode 100644
index 0000000000000..8e4508f6e0a72
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1-utf8.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+5
+15
+18
+11
+
+d
+..
+-1
+1994
+5
+15
+18
+11
+
+-
+.message
+528
+2007
+11
+1
+0
+0
+
+-
+README!
+528
+2007
+11
+1
+0
+0
+
+-
+こんにちは.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+8
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-1.expected b/net/data/ftp/dir-listing-ls-1.expected
new file mode 100644
index 0000000000000..40375902c4c8a
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-1.expected
@@ -0,0 +1,53 @@
+d
+.
+-1
+1994
+5
+15
+18
+11
+
+d
+..
+-1
+1994
+5
+15
+18
+11
+
+-
+.message
+528
+2007
+11
+1
+0
+0
+
+-
+README
+528
+2007
+11
+1
+0
+0
+
+-
+index.html
+560
+2007
+9
+28
+0
+0
+
+d
+pub
+-1
+2008
+8
+12
+0
+0
diff --git a/net/data/ftp/dir-listing-ls-10 b/net/data/ftp/dir-listing-ls-10
new file mode 100644
index 0000000000000..deff68fc29264
--- /dev/null
+++ b/net/data/ftp/dir-listing-ls-10
@@ -0,0 +1,10 @@
+Gesamt 4352
+---------- 1 1 1 0 Okt 25 1999 .notar
+lrwxrwxrwx 1 1 1 7 Okt 23 2007 bin -> usr/bin
+d--x--x--x 1 2 2 512 Apr 23 2002 dev
+d--x--x--x 1 2 2 512 Apr 1 2004 etc
+drwx------ 1 7 root 1536 Aug 14 13:49 lost+found
+drwxr-xr-x 1 3 1 512 Mr 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 Sueos_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 Gastronoma.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 __-_Face_Off-(1997)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 22 2010 _-_Up-(2009)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 May 27 18:12 __-_Total_Recall-(1990)-[1080p]_[BD]
+drwxrwxrwx 3 ftp ftp 4096 May 21 14:28 __[_]_-_Law_Abiding_Citizen_[Unrated_Edition]-(2009)-[1080p]_[BD_Remux]
+drwxrwxrwx 5 ftp ftp 4096 Jan 21 2010 _-_Cloverfield-(2008)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 26 13:43 _-_Pandorum-(2009)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 27 14:18 __-_The_Last_Samurai-(2003)-[1080p]_[BD]
+drwxrwxrwx 6 ftp ftp 4096 Jan 27 2010 _9_-_District_9-(2009)-[1080p]_[BD]
+drwxrwxrwx 7 ftp ftp 4096 Jan 01 2010 __-_Rocky_The_Undisputed_Collection-(1976_1979_1982_1985_1990)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 May 31 12:57 _-_Surrogates-(2009)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 28 2010 _-__-_The_Fast_and_the_Furious-Tokyo_Drift-(2006)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 08 2010 _-_The_Fast_and_the_Furious-(2001)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jan 06 2010 _2_-_2_Fast_2_Furious-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 08 15:26 _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 _-_Avalon-(2001)-[1080p]_[BD_Remux]
+drwxrwxrwx 5 ftp ftp 4096 Jun 15 2009 __-_Bruce_Almighty-(2003)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 15 2009 -_-_WALL-E-(2008)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 28 2009 __007-__-_James_Bond_007-Quantum_of_Solace-(2008)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 15 2009 -_c_-_Dead_Space-Downfall-(2008)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 _1_-_Madagascar_1-(2005)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 _2_-_Madagascar-Escape_2_Africa-(2008)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 13 2009 -_-_The_Matrix-Reloaded-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 14 2009 -_-_The_Matrix-Revolutions-(2003)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jun 12 2009 _-_The_Matrix-(1999)-[1080p]_[BD]
+drwxrwxrwx 4 ftp ftp 4096 Jul 02 2009 __3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD]
+drwxr-xr-x 3 ftp ftp 4096 May 01 2009 _-_The_Island-(2005)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jul 03 2009 _3_-_Transporter_3-(2008)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 02 2009 __-___-_Pirates_of_the_Caribbean-At_World's_End-(2007)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 03 2009 __-___-_Pirates_of_the_Caribbean-The_Curse_of_the_Black_Pearl-(2003)-[1080p]_[BD]
+drwxrwxr-x 5 ftp ftp 4096 May 02 2009 __-__-_Pirates_of_the_Caribbean-Dead_Man's_Chest-(2006)-[1080p]_[BD]
+drwxrwxr-x 3 ftp ftp 4096 May 01 2009 __-_Ghost_Rider-(2007)-[1080p]_[BD]
+drwxr-xr-x 5 ftp ftp 4096 Apr 29 2009 -_-_The_Princess_Bride-(1987)-[1080p]_[BD]
+drwxrwxrwx 5 ftp ftp 4096 Jun 08 2009 __101__-_Sex_and_Death_101-(2007)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 May 01 2009 -__-_Transformers-Bonus_Disk-(2007)-[1080p]_[BD]
+drwxr-xr-x 4 ftp ftp 4096 Apr 30 2009 _-_Transformers-(2007)-[1080p]_[BD]
+drwxrwxrwx 6 ftp ftp 4096 Jun 07 2009 __-_The_Thirteenth_Floor-(1999)-[1080p]_[BD]
+drwxrwxr-x 3 ftp ftp 4096 May 04 2009 __-_Street_Fighter-(1994)-[1080p]_[BD_Remux]
+drwxr-xr-x 5 ftp ftp 4096 Mar 15 2009 ___-_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 _-_Avatar-(2009)-[1080p]_[BD]
+-rwxrwxr-x 1 ftp ftp 17577705968 Mar 08 2009 __1_[_]_-_American_Pie_1_[Unrated_Edition]-(1999)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 15512934868 Mar 16 2009 __-_Snatch-(2000)-[1080i]_[HDTV].ts
+drwxrwxrwx 2 ftp ftp 4096 Jun 03 19:07 __-_Snatch-(2000)-[1080p]_[BD_Remux]
+-rwxrwxr-x 1 ftp ftp 8900589105 Mar 24 2009 __-_War_of_the_Worlds-(2005)-[720p]_[HDTV].mkv
+-rwxrwxr-x 1 ftp ftp 27728321654 Mar 09 2009 _-_American_Gangster-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 31731782861 Mar 09 2009 _[_]_-_American_Gangster_[Unrated_Edition]-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 5009104014 Mar 24 2009 __-_Road_Trip-(2000)-[720p]_[HDTV_Rip].mkv
+-rwxrwxr-x 1 ftp ftp 21410583980 Mar 11 2009 _-_2-__-_Star_Wars-Episode_2-Attack_of_the_Clones-(2002)-[1080i]_[HDTV].ts
+-rwxrwxr-x 1 ftp ftp 19858181688 Mar 11 2009 _-_3-__-_Star_Wars-Episode_3-Revenge_of_the_Sith-(2005)-[1080i]_[HDTV].ts
+-rwxrwxr-x 1 ftp ftp 29026065728 Mar 16 2009 __-_Starship_Troopers-(1997)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 22169179449 Mar 16 2009 _[_]_-_Mirrors_[Unrated_Edition]-(2008)-[1080p]_[BD_remux].mkv
+drwxrwxrwx 4 ftp ftp 4096 Jun 15 14:56 -_-_Ninja_Assassin-(2009)-[1080p]_[BD]
+-rwxrwxr-x 1 ftp ftp 19717173247 Mar 11 2009 __3_-_Resident_Evil-Extinction-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 18660904388 Mar 11 2009 _-_Pathology-(2008)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 16476154520 Mar 05 2009 _1_[_]_-_Saw_I_[Director's_Cut]-(2004)-[1080p]_[HDDVD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19917510515 Mar 05 2009 _2_[_]_-_Saw_II_[Director's_Cut]-(2005)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 18085592265 Mar 05 2009 _3_[_]_-_Saw_III_[Director's_Cut]-(2006)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 3473582701 Mar 05 2009 _4_[_]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].flac
+-rwxrwxr-x 1 ftp ftp 15263958421 Mar 05 2009 _4_[_]_-_Saw_IV_[Director's_Cut]-(2007)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19944605507 Mar 16 2009 _5_[_]_-_Saw_V_[Director's_Cut]-(2008)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 3024333064 Mar 24 2009 __-___!_-_The_Madagascar_Penguins_in_A_Christmas_Caper-(2005)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 125961 Mar 05 2009 __[_]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].srt
+-rwxrwxr-x 1 ftp ftp 19908695408 Mar 05 2009 __[_]_-_Bad_Santa_[Unrated_Edition]-(2003)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 23185439267 Mar 11 2009 ___-_The_Shawshank_Redemption-(1994)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 19567287274 Mar 16 2009 ____[_]_-_Dumb_and_Dumber_[Unrated_Edition]-(1994)-[1080p]_[BD_remux].mkv
+-rwxrwxr-x 1 ftp ftp 14773061093 Mar 16 2009 _-_The_Hurricane-(1999)-[1080p]_[HDDVD_Rip].mkv
+-rwxrwxr-x 1 ftp ftp 22411268500 Mar 11 2009 _2_[_]_-_Hostel_2_[Director's_Cut]-(2007)-[1080p]_[BD_remux].ts
+-rwxrwxr-x 1 ftp ftp 23712519861 Mar 11 2009 ___[_]_-_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 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-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 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-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_avec_labview_real_time_et_compactfieldpoint.ppt
+-rw-rw-r-- 1 ftpuser ftpusers 329216 Dec 20 2002 optimisez_l'acquisition_de_donnes_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�_avec_labview_real_time_et_compactfieldpoint.ppt
+1761280
+2002
+12
+20
+0
+0
+
+-
+optimisez_l'acquisition_de_donnFs_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 ao2000 -> ./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 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 beta
+01-06-09 04:25PM cdrom
+01-06-09 02:38PM 129 checkdownload.html
+08-19-09 11:23AM Digital_Media_Player
+12-29-08 10:27PM LiveUpdate
+08-20-09 04:34PM mb
+01-06-09 02:38PM 83933 Mb.eng
+11-04-09 03:42PM misc
+06-12-09 04:20PM 462 Path.idx
+12-30-08 07:41AM PDA
+01-06-09 02:38PM 2625 Platform.idx
+12-30-08 07:41AM print
+01-06-09 02:42PM 458 Readme.txt
+10-28-09 02:27PM server
+12-30-08 08:59AM vga
+02-03-09 04:42PM 1951823 VH203_FR.pdf
diff --git a/net/data/ftp/dir-listing-windows-2.expected b/net/data/ftp/dir-listing-windows-2.expected
new file mode 100644
index 0000000000000..81388eef34514
--- /dev/null
+++ b/net/data/ftp/dir-listing-windows-2.expected
@@ -0,0 +1,143 @@
+d
+beta
+-1
+2009
+5
+18
+11
+7
+
+d
+cdrom
+-1
+2009
+1
+6
+16
+25
+
+-
+checkdownload.html
+129
+2009
+1
+6
+14
+38
+
+d
+Digital_Media_Player
+-1
+2009
+8
+19
+11
+23
+
+d
+LiveUpdate
+-1
+2008
+12
+29
+22
+27
+
+d
+mb
+-1
+2009
+8
+20
+16
+34
+
+-
+Mb.eng
+83933
+2009
+1
+6
+14
+38
+
+d
+misc
+-1
+2009
+11
+4
+15
+42
+
+-
+Path.idx
+462
+2009
+6
+12
+16
+20
+
+d
+PDA
+-1
+2008
+12
+30
+7
+41
+
+-
+Platform.idx
+2625
+2009
+1
+6
+14
+38
+
+d
+print
+-1
+2008
+12
+30
+7
+41
+
+-
+Readme.txt
+458
+2009
+1
+6
+14
+42
+
+d
+server
+-1
+2009
+10
+28
+14
+27
+
+d
+vga
+-1
+2008
+12
+30
+8
+59
+
+-
+VH203_FR.pdf
+1951823
+2009
+2
+3
+16
+42
diff --git a/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict b/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict
new file mode 100644
index 0000000000000..eb4acff145d95
--- /dev/null
+++ b/net/data/fuzzer_dictionaries/net_url_request_ftp_fuzzer.dict
@@ -0,0 +1,59 @@
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Fuzzer dictionary targetting FTP responses.
+
+# Each response is on its own line, so CRLF is generally useful.
+"\x0D\x0A"
+
+# End of string marker, used by FuzzedDataProvider.
+"\\ "
+
+# Suffixes for file types.
+";type=a"
+";type=i"
+";type=d"
+
+# Dashes indicate multi-line responses.
+"-"
+
+# Generic success.
+"200 OK\x0D\x0A\\ "
+
+# Greeting
+"230 Welcome\x0D\x0A\\ "
+
+# SIZE response
+"213 18\x0D\x0A\\ "
+
+# All important PASV/EPSV responses
+"227 Entering PASV mode (1,1,1,1,50,50)\x0D\x0A\\ "
+"227 Entering Passive Mode 127,0,0,1,123,456\x0D\x0A\\ "
+"227 Entering Extended Passive Mode (|||31744|)\x0D\x0A\\ "
+"227 Entering The Twilight Zone\x0D\x0A\\ "
+
+# RETR/LIST response.
+"125-Data connection already open.\x0D\x0A125 Transfer starting.\x0D\x0A226 Transfer complete.\x0D\x0A\\ "
+"125-Data connection already open.\x0D\x0A\\ "
+"125 Transfer starting.\x0D\x0A\\ "
+"226 Transfer complete.\x0D\x0A\\ "
+
+# Some specific success messages, taken from unittests.
+"215 UNIX\x0D\x0A\\ "
+"215 VMS\x0D\x0A\\ "
+"220 host TestFTPd\x0D\x0A\\ "
+"221 Goodbye!\x0D\x0A\\ "
+"257 \"/\" is your current location\x0D\x0A\\ "
+"257 \"ANONYMOUS_ROOT:[000000]\"\x0D\x0A\\ "
+
+# Error messages, taken from unittests.
+"331 Password needed\x0D\x0A\\ "
+"331 User okay, send password\x0D\x0A\\ "
+"451 not a directory\x0D\x0A\\ "
+"500 EPSV command unknown\x0D\x0A\\ "
+"503 Bad sequence of commands\x0D\x0A\\ "
+"530 Login authentication failed\x0D\x0A\\ "
+"550 I can only retrieve regular files\x0D\x0A\\ "
+"550 Not a directory\x0D\x0A\\ "
+"599 I'm sorry, Dave, I'm afraid I can't do that.\x0D\x0A\\ "
diff --git a/net/docs/proxy.md b/net/docs/proxy.md
index a0ea9f52d1779..589b499e2d4c4 100644
--- a/net/docs/proxy.md
+++ b/net/docs/proxy.md
@@ -109,6 +109,7 @@ about an HTTP proxy.
When using an HTTP proxy in Chrome, name resolution is always deferred to the
proxy. HTTP proxies can proxy `http://`, `https://`, `ws://` and `wss://` URLs.
+(Chromium's FTP support is deprecated, and HTTP proxies cannot proxy `ftp://` anymore)
Communication to HTTP proxy servers is insecure, meaning proxied `http://`
requests are sent in the clear. When proxying `https://` requests through an
diff --git a/net/features.gni b/net/features.gni
index 026a38c0baab1..1d7d13a7c409f 100644
--- a/net/features.gni
+++ b/net/features.gni
@@ -14,6 +14,9 @@ declare_args() {
# and are optional in cronet.
enable_websockets = use_blink
+ # Disable FTP support.
+ disable_ftp_support = is_ios
+
# Enable Kerberos authentication. It is disabled by default on iOS, Fuchsia
# and Chromecast, at least for now. This feature needs configuration
# (krb5.conf and so on).
diff --git a/net/ftp/DIR_METADATA b/net/ftp/DIR_METADATA
new file mode 100644
index 0000000000000..c611fb72b48b1
--- /dev/null
+++ b/net/ftp/DIR_METADATA
@@ -0,0 +1,11 @@
+# Metadata information for this directory.
+#
+# For more information on DIR_METADATA files, see:
+# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/README.md
+#
+# For the schema of this file, see Metadata message:
+# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/proto/dir_metadata.proto
+
+monorail {
+ component: "Internals>Network>FTP"
+}
\ No newline at end of file
diff --git a/net/ftp/OWNERS b/net/ftp/OWNERS
new file mode 100644
index 0000000000000..53e68eb7787a3
--- /dev/null
+++ b/net/ftp/OWNERS
@@ -0,0 +1 @@
+mmenke@chromium.org
diff --git a/net/ftp/ftp_auth_cache.cc b/net/ftp/ftp_auth_cache.cc
new file mode 100644
index 0000000000000..359d5460db859
--- /dev/null
+++ b/net/ftp/ftp_auth_cache.cc
@@ -0,0 +1,62 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_auth_cache.h"
+
+#include "base/check_op.h"
+#include "url/gurl.h"
+
+namespace net {
+
+// static
+const size_t FtpAuthCache::kMaxEntries = 10;
+
+FtpAuthCache::Entry::Entry(const GURL& origin,
+ const AuthCredentials& credentials)
+ : origin(origin),
+ credentials(credentials) {
+}
+
+FtpAuthCache::Entry::~Entry() = default;
+
+FtpAuthCache::FtpAuthCache() = default;
+
+FtpAuthCache::~FtpAuthCache() = default;
+
+FtpAuthCache::Entry* FtpAuthCache::Lookup(const GURL& origin) {
+ for (auto it = entries_.begin(); it != entries_.end(); ++it) {
+ if (it->origin == origin)
+ return &(*it);
+ }
+ return nullptr;
+}
+
+void FtpAuthCache::Add(const GURL& origin, const AuthCredentials& credentials) {
+ DCHECK(origin.SchemeIs("ftp"));
+ DCHECK_EQ(origin.DeprecatedGetOriginAsURL(), origin);
+
+ Entry* entry = Lookup(origin);
+ if (entry) {
+ entry->credentials = credentials;
+ } else {
+ entries_.push_front(Entry(origin, credentials));
+
+ // Prevent unbound memory growth of the cache.
+ if (entries_.size() > kMaxEntries)
+ entries_.pop_back();
+ }
+}
+
+void FtpAuthCache::Remove(const GURL& origin,
+ const AuthCredentials& credentials) {
+ for (auto it = entries_.begin(); it != entries_.end(); ++it) {
+ if (it->origin == origin && it->credentials.Equals(credentials)) {
+ entries_.erase(it);
+ DCHECK(!Lookup(origin));
+ return;
+ }
+ }
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_auth_cache.h b/net/ftp/ftp_auth_cache.h
new file mode 100644
index 0000000000000..e7f3057e153bf
--- /dev/null
+++ b/net/ftp/ftp_auth_cache.h
@@ -0,0 +1,63 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_AUTH_CACHE_H_
+#define NET_FTP_FTP_AUTH_CACHE_H_
+
+#include
+
+#include
+
+#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 EntryList;
+
+ // Internal representation of cache, an STL list. This makes lookups O(n),
+ // but we expect n to be very low.
+ EntryList entries_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_AUTH_CACHE_H_
diff --git a/net/ftp/ftp_auth_cache_unittest.cc b/net/ftp/ftp_auth_cache_unittest.cc
new file mode 100644
index 0000000000000..9c5dea5303895
--- /dev/null
+++ b/net/ftp/ftp_auth_cache_unittest.cc
@@ -0,0 +1,161 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_auth_cache.h"
+
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/auth.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+using base::ASCIIToUTF16;
+
+namespace net {
+
+namespace {
+
+const std::u16string kBogus(u"bogus");
+const std::u16string kOthername(u"othername");
+const std::u16string kOtherword(u"otherword");
+const std::u16string kPassword(u"password");
+const std::u16string kPassword1(u"password1");
+const std::u16string kPassword2(u"password2");
+const std::u16string kPassword3(u"password3");
+const std::u16string kUsername(u"username");
+const std::u16string kUsername1(u"username1");
+const std::u16string kUsername2(u"username2");
+const std::u16string kUsername3(u"username3");
+
+} // namespace
+
+TEST(FtpAuthCacheTest, LookupAddRemove) {
+ FtpAuthCache cache;
+
+ GURL origin1("ftp://foo1");
+ GURL origin2("ftp://foo2");
+
+ // Lookup non-existent entry.
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+
+ // Add entry for origin1.
+ cache.Add(origin1, AuthCredentials(kUsername1, kPassword1));
+ FtpAuthCache::Entry* entry1 = cache.Lookup(origin1);
+ ASSERT_TRUE(entry1);
+ EXPECT_EQ(origin1, entry1->origin);
+ EXPECT_EQ(kUsername1, entry1->credentials.username());
+ EXPECT_EQ(kPassword1, entry1->credentials.password());
+
+ // Add an entry for origin2.
+ cache.Add(origin2, AuthCredentials(kUsername2, kPassword2));
+ FtpAuthCache::Entry* entry2 = cache.Lookup(origin2);
+ ASSERT_TRUE(entry2);
+ EXPECT_EQ(origin2, entry2->origin);
+ EXPECT_EQ(kUsername2, entry2->credentials.username());
+ EXPECT_EQ(kPassword2, entry2->credentials.password());
+
+ // The original entry1 should still be there.
+ EXPECT_EQ(entry1, cache.Lookup(origin1));
+
+ // Overwrite the entry for origin1.
+ cache.Add(origin1, AuthCredentials(kUsername3, kPassword3));
+ FtpAuthCache::Entry* entry3 = cache.Lookup(origin1);
+ ASSERT_TRUE(entry3);
+ EXPECT_EQ(origin1, entry3->origin);
+ EXPECT_EQ(kUsername3, entry3->credentials.username());
+ EXPECT_EQ(kPassword3, entry3->credentials.password());
+
+ // Remove entry of origin1.
+ cache.Remove(origin1, AuthCredentials(kUsername3, kPassword3));
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+
+ // Remove non-existent entry.
+ cache.Remove(origin1, AuthCredentials(kUsername3, kPassword3));
+ EXPECT_TRUE(cache.Lookup(origin1) == nullptr);
+}
+
+// Check that if the origin differs only by port number, it is considered
+// a separate origin.
+TEST(FtpAuthCacheTest, LookupWithPort) {
+ FtpAuthCache cache;
+
+ GURL origin1("ftp://foo:80");
+ GURL origin2("ftp://foo:21");
+
+ cache.Add(origin1, AuthCredentials(kUsername, kPassword));
+ cache.Add(origin2, AuthCredentials(kUsername, kPassword));
+
+ EXPECT_NE(cache.Lookup(origin1), cache.Lookup(origin2));
+}
+
+TEST(FtpAuthCacheTest, NormalizedKey) {
+ // GURL is automatically canonicalized. Hence the following variations in
+ // url format should all map to the same entry (case insensitive host,
+ // default port of 21).
+
+ FtpAuthCache cache;
+
+ // Add.
+ cache.Add(GURL("ftp://HoSt:21"), AuthCredentials(kUsername, kPassword));
+
+ // Lookup.
+ FtpAuthCache::Entry* entry1 = cache.Lookup(GURL("ftp://HoSt:21"));
+ ASSERT_TRUE(entry1);
+ EXPECT_EQ(entry1, cache.Lookup(GURL("ftp://host:21")));
+ EXPECT_EQ(entry1, cache.Lookup(GURL("ftp://host")));
+
+ // Overwrite.
+ cache.Add(GURL("ftp://host"), AuthCredentials(kOthername, kOtherword));
+ FtpAuthCache::Entry* entry2 = cache.Lookup(GURL("ftp://HoSt:21"));
+ ASSERT_TRUE(entry2);
+ EXPECT_EQ(GURL("ftp://host"), entry2->origin);
+ EXPECT_EQ(kOthername, entry2->credentials.username());
+ EXPECT_EQ(kOtherword, entry2->credentials.password());
+
+ // Remove
+ cache.Remove(GURL("ftp://HOsT"), AuthCredentials(kOthername, kOtherword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == nullptr);
+}
+
+TEST(FtpAuthCacheTest, OnlyRemoveMatching) {
+ FtpAuthCache cache;
+
+ cache.Add(GURL("ftp://host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")));
+
+ // Auth data doesn't match, shouldn't remove.
+ cache.Remove(GURL("ftp://host"), AuthCredentials(kBogus, kBogus));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")));
+
+ // Auth data matches, should remove.
+ cache.Remove(GURL("ftp://host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host")) == nullptr);
+}
+
+TEST(FtpAuthCacheTest, EvictOldEntries) {
+ FtpAuthCache cache;
+
+ for (size_t i = 0; i < FtpAuthCache::kMaxEntries; i++) {
+ cache.Add(GURL("ftp://host" + base::NumberToString(i)),
+ AuthCredentials(kUsername, kPassword));
+ }
+
+ // No entries should be evicted before reaching the limit.
+ for (size_t i = 0; i < FtpAuthCache::kMaxEntries; i++) {
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host" + base::NumberToString(i))));
+ }
+
+ // Adding one entry should cause eviction of the first entry.
+ cache.Add(GURL("ftp://last_host"), AuthCredentials(kUsername, kPassword));
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host0")) == nullptr);
+
+ // Remaining entries should not get evicted.
+ for (size_t i = 1; i < FtpAuthCache::kMaxEntries; i++) {
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://host" + base::NumberToString(i))));
+ }
+ EXPECT_TRUE(cache.Lookup(GURL("ftp://last_host")));
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_ctrl_response_buffer.cc b/net/ftp/ftp_ctrl_response_buffer.cc
new file mode 100644
index 0000000000000..9bd4059d5fbe4
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer.cc
@@ -0,0 +1,159 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+
+#include
+
+#include "base/check_op.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "net/base/net_errors.h"
+#include "net/base/parse_number.h"
+#include "net/log/net_log_event_type.h"
+#include "net/log/net_log_values.h"
+
+namespace net {
+
+// static
+const int FtpCtrlResponse::kInvalidStatusCode = -1;
+
+FtpCtrlResponse::FtpCtrlResponse() : status_code(kInvalidStatusCode) {}
+
+FtpCtrlResponse::FtpCtrlResponse(const FtpCtrlResponse& other) = default;
+
+FtpCtrlResponse::~FtpCtrlResponse() = default;
+
+FtpCtrlResponseBuffer::FtpCtrlResponseBuffer(const NetLogWithSource& net_log)
+ : multiline_(false), net_log_(net_log) {}
+
+FtpCtrlResponseBuffer::~FtpCtrlResponseBuffer() = default;
+
+int FtpCtrlResponseBuffer::ConsumeData(const char* data, int data_length) {
+ buffer_.append(data, data_length);
+ ExtractFullLinesFromBuffer();
+
+ while (!lines_.empty()) {
+ ParsedLine line = lines_.front();
+ lines_.pop();
+
+ if (multiline_) {
+ if (!line.is_complete || line.status_code != response_buf_.status_code) {
+ line_buf_.append(line.raw_text);
+ continue;
+ }
+
+ response_buf_.lines.push_back(line_buf_);
+
+ line_buf_ = line.status_text;
+ DCHECK_EQ(line.status_code, response_buf_.status_code);
+
+ if (!line.is_multiline) {
+ response_buf_.lines.push_back(line_buf_);
+ responses_.push(response_buf_);
+
+ // Prepare to handle following lines.
+ response_buf_ = FtpCtrlResponse();
+ line_buf_.clear();
+ multiline_ = false;
+ }
+ } else {
+ if (!line.is_complete)
+ return ERR_INVALID_RESPONSE;
+
+ response_buf_.status_code = line.status_code;
+ if (line.is_multiline) {
+ line_buf_ = line.status_text;
+ multiline_ = true;
+ } else {
+ response_buf_.lines.push_back(line.status_text);
+ responses_.push(response_buf_);
+
+ // Prepare to handle following lines.
+ response_buf_ = FtpCtrlResponse();
+ line_buf_.clear();
+ }
+ }
+ }
+
+ return OK;
+}
+
+namespace {
+
+base::Value::Dict NetLogFtpCtrlResponseParams(const FtpCtrlResponse* response) {
+ base::Value::List lines;
+ for (const auto& line : response->lines)
+ lines.Append(NetLogStringValue(line));
+
+ base::Value::Dict dict;
+ dict.Set("status_code", response->status_code);
+ dict.Set("lines", std::move(lines));
+ return dict;
+}
+
+} // namespace
+
+FtpCtrlResponse FtpCtrlResponseBuffer::PopResponse() {
+ FtpCtrlResponse result = responses_.front();
+ responses_.pop();
+
+ net_log_.AddEvent(NetLogEventType::FTP_CONTROL_RESPONSE,
+ [&] { return NetLogFtpCtrlResponseParams(&result); });
+
+ return result;
+}
+
+FtpCtrlResponseBuffer::ParsedLine::ParsedLine()
+ : has_status_code(false),
+ is_multiline(false),
+ is_complete(false),
+ status_code(FtpCtrlResponse::kInvalidStatusCode) {
+}
+
+FtpCtrlResponseBuffer::ParsedLine::ParsedLine(const ParsedLine& other) =
+ default;
+
+// static
+FtpCtrlResponseBuffer::ParsedLine FtpCtrlResponseBuffer::ParseLine(
+ const std::string& line) {
+ ParsedLine result;
+
+ if (line.length() >= 3) {
+ if (ParseInt32(base::MakeStringPiece(line.begin(), line.begin() + 3),
+ ParseIntFormat::NON_NEGATIVE, &result.status_code)) {
+ result.has_status_code =
+ (100 <= result.status_code && result.status_code <= 599);
+ }
+ if (result.has_status_code && line.length() >= 4 && line[3] == ' ') {
+ result.is_complete = true;
+ } else if (result.has_status_code && line.length() >= 4 && line[3] == '-') {
+ result.is_complete = true;
+ result.is_multiline = true;
+ }
+ }
+
+ if (result.is_complete) {
+ result.status_text = line.substr(4);
+ } else {
+ result.status_text = line;
+ }
+
+ result.raw_text = line;
+
+ return result;
+}
+
+void FtpCtrlResponseBuffer::ExtractFullLinesFromBuffer() {
+ int cut_pos = 0;
+ for (size_t i = 0; i < buffer_.length(); i++) {
+ if (i >= 1 && buffer_[i - 1] == '\r' && buffer_[i] == '\n') {
+ lines_.push(ParseLine(buffer_.substr(cut_pos, i - cut_pos - 1)));
+ cut_pos = i + 1;
+ }
+ }
+ buffer_.erase(0, cut_pos);
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_ctrl_response_buffer.h b/net/ftp/ftp_ctrl_response_buffer.h
new file mode 100644
index 0000000000000..149097185dafc
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer.h
@@ -0,0 +1,101 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
+#define NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
+
+#include
+#include
+
+#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 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 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 responses_;
+
+ NetLogWithSource net_log_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_CTRL_RESPONSE_BUFFER_H_
diff --git a/net/ftp/ftp_ctrl_response_buffer_unittest.cc b/net/ftp/ftp_ctrl_response_buffer_unittest.cc
new file mode 100644
index 0000000000000..a133aa441d7fc
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_buffer_unittest.cc
@@ -0,0 +1,184 @@
+// Copyright (c) 2009 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+
+#include
+
+#include "net/base/net_errors.h"
+#include "net/test/gtest_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using net::test::IsError;
+using net::test::IsOk;
+
+namespace net {
+
+namespace {
+
+class FtpCtrlResponseBufferTest : public testing::Test {
+ public:
+ FtpCtrlResponseBufferTest() : buffer_(NetLogWithSource()) {}
+
+ protected:
+ int PushDataToBuffer(const char* data) {
+ return buffer_.ConsumeData(data, strlen(data));
+ }
+
+ FtpCtrlResponseBuffer buffer_;
+};
+
+TEST_F(FtpCtrlResponseBufferTest, Basic) {
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("200 Status Text\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(200, response.status_code);
+ ASSERT_EQ(1U, response.lines.size());
+ EXPECT_EQ("Status Text", response.lines[0]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, Chunks) {
+ EXPECT_THAT(PushDataToBuffer("20"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("0 Status"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer(" Text"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("\r"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_THAT(PushDataToBuffer("\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(200, response.status_code);
+ ASSERT_EQ(1U, response.lines.size());
+ EXPECT_EQ("Status Text", response.lines[0]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, Continuation) {
+ EXPECT_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230-SecondLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(230, response.status_code);
+ ASSERT_EQ(3U, response.lines.size());
+ EXPECT_EQ("FirstLine", response.lines[0]);
+ EXPECT_EQ("SecondLine", response.lines[1]);
+ EXPECT_EQ("LastLine", response.lines[2]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, MultilineContinuation) {
+ EXPECT_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230-SecondLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("215 Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(230, response.status_code);
+ ASSERT_EQ(3U, response.lines.size());
+ EXPECT_EQ("FirstLineContinued", response.lines[0]);
+ EXPECT_EQ("SecondLine215 Continued", response.lines[1]);
+ EXPECT_EQ("LastLine", response.lines[2]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, MultilineContinuationZeroLength) {
+ // For the corner case from bug 29322.
+ EXPECT_THAT(PushDataToBuffer("230-\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("example.com\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 LastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(230, response.status_code);
+ ASSERT_EQ(2U, response.lines.size());
+ EXPECT_EQ("example.com", response.lines[0]);
+ EXPECT_EQ("LastLine", response.lines[1]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, SimilarContinuation) {
+ EXPECT_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ // Notice the space at the start of the line. It should be recognized
+ // as a continuation, and not the last line.
+ EXPECT_THAT(PushDataToBuffer(" 230 Continued\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 TrueLastLine\r\n"), IsOk());
+ EXPECT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(230, response.status_code);
+ ASSERT_EQ(2U, response.lines.size());
+ EXPECT_EQ("FirstLine 230 Continued", response.lines[0]);
+ EXPECT_EQ("TrueLastLine", response.lines[1]);
+}
+
+// The nesting of multi-line responses is not allowed.
+TEST_F(FtpCtrlResponseBufferTest, NoNesting) {
+ EXPECT_THAT(PushDataToBuffer("230-FirstLine\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("300-Continuation\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("300 Still continuation\r\n"), IsOk());
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+
+ EXPECT_THAT(PushDataToBuffer("230 Real End\r\n"), IsOk());
+ ASSERT_TRUE(buffer_.ResponseAvailable());
+
+ FtpCtrlResponse response = buffer_.PopResponse();
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+ EXPECT_EQ(230, response.status_code);
+ ASSERT_EQ(2U, response.lines.size());
+ EXPECT_EQ("FirstLine300-Continuation300 Still continuation",
+ response.lines[0]);
+ EXPECT_EQ("Real End", response.lines[1]);
+}
+
+TEST_F(FtpCtrlResponseBufferTest, NonNumericResponse) {
+ EXPECT_THAT(PushDataToBuffer("Non-numeric\r\n"),
+ IsError(ERR_INVALID_RESPONSE));
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+}
+
+TEST_F(FtpCtrlResponseBufferTest, OutOfRangeResponse) {
+ EXPECT_THAT(PushDataToBuffer("777 OK?\r\n"), IsError(ERR_INVALID_RESPONSE));
+ EXPECT_FALSE(buffer_.ResponseAvailable());
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_ctrl_response_fuzzer.cc b/net/ftp/ftp_ctrl_response_fuzzer.cc
new file mode 100644
index 0000000000000..f3a9ee3c6dfbb
--- /dev/null
+++ b/net/ftp/ftp_ctrl_response_fuzzer.cc
@@ -0,0 +1,21 @@
+// Copyright 2015 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include
+#include
+
+#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(data), size)) {
+ return 0;
+ }
+ while (buffer.ResponseAvailable()) {
+ (void)buffer.PopResponse();
+ }
+ return 0;
+}
diff --git a/net/ftp/ftp_directory_listing_fuzzer.cc b/net/ftp/ftp_directory_listing_fuzzer.cc
new file mode 100644
index 0000000000000..459379e3eef89
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_fuzzer.cc
@@ -0,0 +1,20 @@
+// Copyright 2015 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include
+#include
+
+#include
+#include
+
+#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(data), size);
+ std::vector entries;
+ net::ParseFtpDirectoryListing(buffer, base::Time::Now(), &entries);
+ return 0;
+}
diff --git a/net/ftp/ftp_directory_listing_parser.cc b/net/ftp/ftp_directory_listing_parser.cc
new file mode 100644
index 0000000000000..57013a78c6e72
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser.cc
@@ -0,0 +1,131 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_directory_listing_parser.h"
+
+#include "base/functional/callback.h"
+#include "base/functional/bind.h"
+#include "base/i18n/encoding_detection.h"
+#include "base/i18n/icu_string_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/net_errors.h"
+#include "net/ftp/ftp_directory_listing_parser_ls.h"
+#include "net/ftp/ftp_directory_listing_parser_vms.h"
+#include "net/ftp/ftp_directory_listing_parser_windows.h"
+#include "net/ftp/ftp_server_type.h"
+
+namespace net {
+
+namespace {
+
+// Fills in |raw_name| for all |entries| using |encoding|. Returns network
+// error code.
+int FillInRawName(const std::string& encoding,
+ std::vector* 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* entries,
+ FtpServerType* server_type) {
+ std::vector lines = base::SplitStringUsingSubstr(
+ text, newline_separator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ struct {
+ base::OnceCallback 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* 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* entries) {
+ FtpServerType server_type = SERVER_UNKNOWN;
+ int rv = DecodeAndParse(text, current_time, entries, &server_type);
+ return rv;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser.h b/net/ftp/ftp_directory_listing_parser.h
new file mode 100644
index 0000000000000..f813bb04e5209
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_
+
+#include
+
+#include
+#include
+
+#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* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_H_
diff --git a/net/ftp/ftp_directory_listing_parser_ls.cc b/net/ftp/ftp_directory_listing_parser_ls.cc
new file mode 100644
index 0000000000000..5388146beff3c
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls.cc
@@ -0,0 +1,227 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_ls.h"
+
+#include
+
+#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 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 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& 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& lines,
+ const base::Time& current_time,
+ std::vector* 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 columns =
+ base::SplitString(base::CollapseWhitespace(lines[i], false), u" ",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // Some FTP servers put a "total n" line at the beginning of the listing
+ // (n is an integer). Allow such a line, but only once, and only if it's
+ // the first non-empty line. Do not match the word exactly, because it may
+ // be in different languages (at least English and German have been seen
+ // in the field).
+ if (columns.size() == 2 && !received_total_line) {
+ received_total_line = true;
+
+ // Some FTP servers incorrectly return a negative integer for "n". Since
+ // this value is ignored anyway, just check any valid integer was
+ // provided.
+ int64_t total_number;
+ if (!base::StringToInt64(columns[1], &total_number))
+ return false;
+
+ continue;
+ }
+
+ FtpDirectoryListingEntry entry;
+
+ size_t column_offset;
+ std::u16string size;
+ if (!DetectColumnOffsetSizeAndModificationTime(columns,
+ current_time,
+ &column_offset,
+ &size,
+ &entry.last_modified)) {
+ // Some servers send a message in one of the first few lines.
+ // All those messages have in common is the string ".:",
+ // where "." means the current directory, and ":" separates it
+ // from the rest of the message, which may be empty.
+ if (lines[i].find(u".:") != std::u16string::npos)
+ continue;
+
+ return false;
+ }
+
+ // Do not check "validity" of the permission listing. It's quirky,
+ // and some servers send garbage here while other parts of the line are OK.
+
+ if (!columns[0].empty() && columns[0][0] == 'l') {
+ entry.type = FtpDirectoryListingEntry::SYMLINK;
+ } else if (!columns[0].empty() && columns[0][0] == 'd') {
+ entry.type = FtpDirectoryListingEntry::DIRECTORY;
+ } else {
+ entry.type = FtpDirectoryListingEntry::FILE;
+ }
+
+ if (!base::StringToInt64(size, &entry.size)) {
+ // Some FTP servers do not separate owning group name from file size,
+ // like "group1234". We still want to display the file name for that
+ // entry, but can't really get the size (What if the group is named
+ // "group1", and the size is in fact 234? We can't distinguish between
+ // that and "group" with size 1234). Use a dummy value for the size.
+ entry.size = -1;
+ }
+ if (entry.size < 0) {
+ // Some FTP servers have bugs that cause them to display the file size
+ // as negative. They're most likely big files like DVD ISO images.
+ // We still want to display them, so just say the real file size
+ // is unknown.
+ entry.size = -1;
+ }
+ if (entry.type != FtpDirectoryListingEntry::FILE)
+ entry.size = -1;
+
+ if (column_offset == columns.size() - 1) {
+ // If the end of the date listing is the last column, there is no file
+ // name. Some FTP servers send listing entries with empty names.
+ // It's not obvious how to display such an entry, so we ignore them.
+ // We don't want to make the parsing fail at this point though.
+ // Other entries can still be useful.
+ continue;
+ }
+
+ entry.name = FtpUtil::GetStringPartAfterColumns(lines[i],
+ column_offset + 1);
+
+ if (entry.type == FtpDirectoryListingEntry::SYMLINK) {
+ std::u16string::size_type pos = entry.name.rfind(u" -> ");
+
+ // We don't require the " -> " to be present. Some FTP servers don't send
+ // the symlink target, possibly for security reasons.
+ if (pos != std::u16string::npos)
+ entry.name = entry.name.substr(0, pos);
+ }
+
+ entries->push_back(entry);
+ }
+
+ return true;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_ls.h b/net/ftp/ftp_directory_listing_parser_ls.h
new file mode 100644
index 0000000000000..42ec4fc6a3a27
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
+
+#include
+#include
+
+#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& lines,
+ const base::Time& current_time,
+ std::vector* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_LS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_ls_unittest.cc b/net/ftp/ftp_directory_listing_parser_ls_unittest.cc
new file mode 100644
index 0000000000000..d64a564b413e0
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_ls_unittest.cc
@@ -0,0 +1,222 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/cxx17_backports.h"
+#include "base/format_macros.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "net/ftp/ftp_directory_listing_parser_ls.h"
+
+namespace net {
+
+namespace {
+
+typedef FtpDirectoryListingParserTest FtpDirectoryListingParserLsTest;
+
+TEST_F(FtpDirectoryListingParserLsTest, Good) {
+ const struct SingleLineTestData good_cases[] = {
+ { "-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README",
+ FtpDirectoryListingEntry::FILE, "README", 528,
+ 2007, 11, 1, 0, 0 },
+ { "drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 directory",
+ FtpDirectoryListingEntry::DIRECTORY, "directory", -1,
+ 1994, 5, 15, 18, 11 },
+ { "lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub",
+ FtpDirectoryListingEntry::SYMLINK, "pub", -1,
+ 2008, 9, 18, 0, 0 },
+ { "lrwxrwxrwx 1 0 0 3 Oct 12 13:37 mirror -> pub",
+ FtpDirectoryListingEntry::SYMLINK, "mirror", -1,
+ 1994, 10, 12, 13, 37 },
+ { "drwxrwsr-x 4 501 501 4096 Feb 20 2007 pub",
+ FtpDirectoryListingEntry::DIRECTORY, "pub", -1,
+ 2007, 2, 20, 0, 0 },
+ { "drwxr-xr-x 4 (?) (?) 4096 Apr 8 2007 jigdo",
+ FtpDirectoryListingEntry::DIRECTORY, "jigdo", -1,
+ 2007, 4, 8, 0, 0 },
+ { "drwx-wx-wt 2 root wheel 512 Jul 1 02:15 incoming",
+ FtpDirectoryListingEntry::DIRECTORY, "incoming", -1,
+ 1994, 7, 1, 2, 15 },
+ { "-rw-r--r-- 1 2 3 3447432 May 18 2009 Foo - Manual.pdf",
+ FtpDirectoryListingEntry::FILE, "Foo - Manual.pdf", 3447432,
+ 2009, 5, 18, 0, 0 },
+ { "d-wx-wx-wt+ 4 ftp 989 512 Dec 8 15:54 incoming",
+ FtpDirectoryListingEntry::DIRECTORY, "incoming", -1,
+ 1993, 12, 8, 15, 54 },
+ { "drwxrwxrwx 1 owner group 1024 Sep 13 0:30 audio",
+ FtpDirectoryListingEntry::DIRECTORY, "audio", -1,
+ 1994, 9, 13, 0, 30 },
+ { "lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub",
+ FtpDirectoryListingEntry::SYMLINK, "pub", -1,
+ 2008, 9, 18, 0, 0 },
+ { "-rw-r--r-- 1 ftp ftp -528 Nov 01 2007 README",
+ FtpDirectoryListingEntry::FILE, "README", -1,
+ 2007, 11, 1, 0, 0 },
+
+ // Tests for the wu-ftpd variant:
+ { "drwxr-xr-x 2 sys 512 Mar 27 2009 pub",
+ FtpDirectoryListingEntry::DIRECTORY, "pub", -1,
+ 2009, 3, 27, 0, 0 },
+ { "lrwxrwxrwx 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub",
+ FtpDirectoryListingEntry::SYMLINK, "pub", -1,
+ 2008, 9, 18, 0, 0 },
+ { "drwxr-xr-x (?) (?) 4096 Apr 8 2007 jigdo",
+ FtpDirectoryListingEntry::DIRECTORY, "jigdo", -1,
+ 2007, 4, 8, 0, 0 },
+ { "-rw-r--r-- 2 3 3447432 May 18 2009 Foo - Manual.pdf",
+ FtpDirectoryListingEntry::FILE, "Foo - Manual.pdf", 3447432,
+ 2009, 5, 18, 0, 0 },
+
+ // Tests for "ls -l" style listings sent by an OS/2 server (FtpServer):
+ { "-r--r--r-- 1 ftp -A--- 13274 Mar 1 2006 UpTime.exe",
+ FtpDirectoryListingEntry::FILE, "UpTime.exe", 13274,
+ 2006, 3, 1, 0, 0 },
+ { "dr--r--r-- 1 ftp ----- 0 Nov 17 17:08 kernels",
+ FtpDirectoryListingEntry::DIRECTORY, "kernels", -1,
+ 1993, 11, 17, 17, 8 },
+
+ // Tests for "ls -l" style listing sent by Xplain FTP Server.
+ { "drwxr-xr-x folder 0 Jul 17 2006 online",
+ FtpDirectoryListingEntry::DIRECTORY, "online", -1,
+ 2006, 7, 17, 0, 0 },
+
+ // Tests for "ls -l" style listing with owning group name
+ // not separated from file size (http://crbug.com/58963).
+ { "-rw-r--r-- 1 ftpadmin ftpadmin125435904 Apr 9 2008 .pureftpd-upload",
+ FtpDirectoryListingEntry::FILE, ".pureftpd-upload", -1,
+ 2008, 4, 9, 0, 0 },
+
+ // Tests for "ls -l" style listing with number of links
+ // not separated from permission listing (http://crbug.com/70394).
+ { "drwxr-xr-x1732 266 111 90112 Jun 21 2001 .rda_2",
+ FtpDirectoryListingEntry::DIRECTORY, ".rda_2", -1,
+ 2001, 6, 21, 0, 0 },
+
+ // Tests for "ls -l" style listing with group name containing spaces.
+ { "drwxrwxr-x 3 %%%% Domain Users 4096 Dec 9 2009 %%%%%",
+ FtpDirectoryListingEntry::DIRECTORY, "%%%%%", -1,
+ 2009, 12, 9, 0, 0 },
+
+ // Tests for "ls -l" style listing in Russian locale (note the swapped
+ // parts order: the day of month is the first, before month).
+ { "-rwxrwxr-x 1 ftp ftp 123 23 \xd0\xbc\xd0\xb0\xd0\xb9 2011 test",
+ FtpDirectoryListingEntry::FILE, "test", 123,
+ 2011, 5, 23, 0, 0 },
+ { "drwxrwxr-x 1 ftp ftp 4096 19 \xd0\xbe\xd0\xba\xd1\x82 2011 dir",
+ FtpDirectoryListingEntry::DIRECTORY, "dir", -1,
+ 2011, 10, 19, 0, 0 },
+
+ // Plan9 sends entry type "a" for append-only files.
+ { "ar-xr-xr-x 2 none none 512 Apr 26 17:52 plan9",
+ FtpDirectoryListingEntry::FILE, "plan9", 512,
+ 1994, 4, 26, 17, 52 },
+
+ // Hylafax sends a shorter permission listing.
+ { "drwxrwx 2 10 4096 Jul 28 02:41 tmp",
+ FtpDirectoryListingEntry::DIRECTORY, "tmp", -1,
+ 1994, 7, 28, 2, 41 },
+
+ // Completely different date format (YYYY-MM-DD).
+ { "drwxrwxrwx 2 root root 4096 2012-02-07 00:31 notas_servico",
+ FtpDirectoryListingEntry::DIRECTORY, "notas_servico", -1,
+ 2012, 2, 7, 0, 31 },
+ { "-rwxrwxrwx 2 root root 4096 2012-02-07 00:31 notas_servico",
+ FtpDirectoryListingEntry::FILE, "notas_servico", 4096,
+ 2012, 2, 7, 0, 31 },
+
+ // Weird permission bits.
+ { "drwx--l--- 2 0 10 512 Dec 22 1994 swetzel",
+ FtpDirectoryListingEntry::DIRECTORY, "swetzel", -1,
+ 1994, 12, 22, 0, 0 },
+
+ { "drwxrwxr-x 1 500 244 660 Jan 1 00:0 bin",
+ FtpDirectoryListingEntry::DIRECTORY, "bin", -1,
+ 1994, 1, 1, 0, 0 },
+
+ // Garbage in date (but still parseable).
+ { "lrw-rw-rw- 1 user group 542 "
+ "/t11/member/incomingFeb 8 2007 "
+ "Shortcut to incoming.lnk -> /t11/member/incoming",
+ FtpDirectoryListingEntry::SYMLINK, "Shortcut to incoming.lnk", -1,
+ 2007, 2, 8, 0, 0 },
+
+ // Garbage in permissions (with no effect on other bits).
+ // Also test multiple "columns" resulting from the garbage.
+ { "garbage 1 ftp ftp 528 Nov 01 2007 README",
+ FtpDirectoryListingEntry::FILE, "README", 528,
+ 2007, 11, 1, 0, 0 },
+ { "gar bage 1 ftp ftp 528 Nov 01 2007 README",
+ FtpDirectoryListingEntry::FILE, "README", 528,
+ 2007, 11, 1, 0, 0 },
+ { "g a r b a g e 1 ftp ftp 528 Nov 01 2007 README",
+ FtpDirectoryListingEntry::FILE, "README", 528,
+ 2007, 11, 1, 0, 0 },
+ };
+ for (size_t i = 0; i < base::size(good_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i,
+ good_cases[i].input));
+
+ std::vector 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 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 entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingLs(GetSingleLineTestCase(bad_cases[i]),
+ GetMockCurrentTime(),
+ &entries));
+ }
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_unittest.cc b/net/ftp/ftp_directory_listing_parser_unittest.cc
new file mode 100644
index 0000000000000..de08f99478562
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_unittest.cc
@@ -0,0 +1,186 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser.h"
+
+#include "base/files/file_util.h"
+#include "base/format_macros.h"
+#include "base/path_service.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/net_errors.h"
+#include "net/ftp/ftp_directory_listing_parser.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+namespace {
+
+struct FtpTestParam {
+ const char* name;
+ Error expected_result;
+};
+
+std::string TestName(testing::TestParamInfo info) {
+ std::string result;
+ base::ReplaceChars(info.param.name, "-", "_", &result);
+ return result;
+}
+
+class FtpDirectoryListingParserTest
+ : public testing::TestWithParam {};
+
+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 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 lines = base::SplitStringUsingSubstr(
+ expected_listing, "\r\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ // Special case for empty listings.
+ if (lines.size() == 1 && lines[0].empty())
+ lines.clear();
+
+ ASSERT_EQ(9 * entries.size(), lines.size());
+
+ for (size_t i = 0; i < lines.size() / 9; i++) {
+ std::string type(lines[9 * i]);
+ std::string name(lines[9 * i + 1]);
+ int64_t size;
+ base::StringToInt64(lines[9 * i + 2], &size);
+
+ SCOPED_TRACE(base::StringPrintf("Filename: %s", name.c_str()));
+
+ int year, month, day_of_month, hour, minute;
+ base::StringToInt(lines[9 * i + 3], &year);
+ base::StringToInt(lines[9 * i + 4], &month);
+ base::StringToInt(lines[9 * i + 5], &day_of_month);
+ base::StringToInt(lines[9 * i + 6], &hour);
+ base::StringToInt(lines[9 * i + 7], &minute);
+
+ const FtpDirectoryListingEntry& entry = entries[i];
+
+ if (type == "d") {
+ EXPECT_EQ(FtpDirectoryListingEntry::DIRECTORY, entry.type);
+ } else if (type == "-") {
+ EXPECT_EQ(FtpDirectoryListingEntry::FILE, entry.type);
+ } else if (type == "l") {
+ EXPECT_EQ(FtpDirectoryListingEntry::SYMLINK, entry.type);
+ } else {
+ ADD_FAILURE() << "invalid gold test data: " << type;
+ }
+
+ EXPECT_EQ(base::UTF8ToUTF16(name), entry.name);
+ EXPECT_EQ(size, entry.size);
+
+ base::Time::Exploded time_exploded;
+ entry.last_modified.UTCExplode(&time_exploded);
+ EXPECT_EQ(year, time_exploded.year);
+ EXPECT_EQ(month, time_exploded.month);
+ EXPECT_EQ(day_of_month, time_exploded.day_of_month);
+ EXPECT_EQ(hour, time_exploded.hour);
+ EXPECT_EQ(minute, time_exploded.minute);
+ }
+}
+
+const FtpTestParam kTestParams[] = {
+ {"dir-listing-ls-1", OK},
+ {"dir-listing-ls-1-utf8", OK},
+ {"dir-listing-ls-2", OK},
+ {"dir-listing-ls-3", OK},
+ {"dir-listing-ls-4", OK},
+ {"dir-listing-ls-5", OK},
+ {"dir-listing-ls-6", OK},
+ {"dir-listing-ls-7", OK},
+ {"dir-listing-ls-8", OK},
+ {"dir-listing-ls-9", OK},
+ {"dir-listing-ls-10", OK},
+ {"dir-listing-ls-11", OK},
+ {"dir-listing-ls-12", OK},
+ {"dir-listing-ls-13", OK},
+ {"dir-listing-ls-14", OK},
+ {"dir-listing-ls-15", OK},
+ {"dir-listing-ls-16", OK},
+ {"dir-listing-ls-17", OK},
+ {"dir-listing-ls-18", OK},
+ {"dir-listing-ls-19", OK},
+ {"dir-listing-ls-20", OK},
+ {"dir-listing-ls-21", OK},
+ {"dir-listing-ls-22", OK},
+ {"dir-listing-ls-23", OK},
+ {"dir-listing-ls-24", OK},
+
+ // Tests for Russian listings. The only difference between those
+ // files is character encoding:
+ {"dir-listing-ls-25", OK}, // UTF-8
+ {"dir-listing-ls-26", OK}, // KOI8-R
+ {"dir-listing-ls-27", OK}, // windows-1251
+
+ {"dir-listing-ls-28", OK}, // Hylafax FTP server
+ {"dir-listing-ls-29", OK},
+ {"dir-listing-ls-30", OK},
+ {"dir-listing-ls-31", OK},
+ {"dir-listing-ls-32", OK}, // busybox
+ {"dir-listing-ls-33", OK},
+ {"dir-listing-ls-34", OK}, // Broken encoding. Should not fail.
+
+ {"dir-listing-netware-1", OK},
+ {"dir-listing-netware-2", OK},
+ {"dir-listing-netware-3", OK},
+ {"dir-listing-os2-1", ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT},
+ {"dir-listing-vms-1", OK},
+ {"dir-listing-vms-2", OK},
+ {"dir-listing-vms-3", OK},
+ {"dir-listing-vms-4", OK},
+ {"dir-listing-vms-5", OK},
+ {"dir-listing-vms-6", OK},
+ {"dir-listing-vms-7", OK},
+ {"dir-listing-vms-8", OK},
+ {"dir-listing-windows-1", OK},
+ {"dir-listing-windows-2", OK},
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+ FtpDirectoryListingParserTest,
+ testing::ValuesIn(kTestParams),
+ TestName);
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_unittest.h b/net/ftp/ftp_directory_listing_parser_unittest.h
new file mode 100644
index 0000000000000..9dfc44acfd999
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_unittest.h
@@ -0,0 +1,81 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
+
+#include
+
+#include
+
+#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 GetSingleLineTestCase(const std::string& text) {
+ std::vector lines;
+ lines.push_back(base::UTF8ToUTF16(text));
+ return lines;
+ }
+
+ void VerifySingleLineTestCase(
+ const SingleLineTestData& test_case,
+ const std::vector& entries) {
+ ASSERT_FALSE(entries.empty());
+
+ FtpDirectoryListingEntry entry = entries[0];
+ EXPECT_EQ(test_case.type, entry.type);
+ EXPECT_EQ(base::UTF8ToUTF16(test_case.filename), entry.name);
+ EXPECT_EQ(test_case.size, entry.size);
+
+ base::Time::Exploded time_exploded;
+ entry.last_modified.UTCExplode(&time_exploded);
+
+ // Only test members displayed on the directory listing.
+ EXPECT_EQ(test_case.year, time_exploded.year);
+ EXPECT_EQ(test_case.month, time_exploded.month);
+ EXPECT_EQ(test_case.day_of_month, time_exploded.day_of_month);
+ EXPECT_EQ(test_case.hour, time_exploded.hour);
+ EXPECT_EQ(test_case.minute, time_exploded.minute);
+
+ EXPECT_EQ(1U, entries.size());
+ }
+
+ base::Time GetMockCurrentTime() {
+ base::Time::Exploded mock_current_time_exploded = { 0 };
+ mock_current_time_exploded.year = 1994;
+ mock_current_time_exploded.month = 11;
+ mock_current_time_exploded.day_of_month = 15;
+ mock_current_time_exploded.hour = 12;
+ mock_current_time_exploded.minute = 45;
+
+ base::Time out_time;
+ EXPECT_TRUE(
+ base::Time::FromUTCExploded(mock_current_time_exploded, &out_time));
+ return out_time;
+ }
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_UNITTEST_H_
diff --git a/net/ftp/ftp_directory_listing_parser_vms.cc b/net/ftp/ftp_directory_listing_parser_vms.cc
new file mode 100644
index 0000000000000..7d0ca2a045f75
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms.cc
@@ -0,0 +1,313 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_directory_listing_parser_vms.h"
+
+#include
+
+#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 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 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 num_bytes = num_blocks;
+ num_bytes *= kBlockSize;
+
+ if (!num_bytes.IsValid())
+ return false; // Block count is too large.
+
+ *out_size = num_bytes.ValueOrDie();
+ return true;
+}
+
+bool ParseVmsFilesize(const std::u16string& input, int64_t* size) {
+ if (base::ContainsOnlyChars(input, u"*")) {
+ // Response consisting of asterisks means unknown size.
+ *size = -1;
+ return true;
+ }
+
+ int64_t num_blocks;
+ if (base::StringToInt64(input, &num_blocks))
+ return ApproximateFilesizeFromBlockCount(num_blocks, size);
+
+ std::vector parts = base::SplitStringPiece(
+ input, u"/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (parts.size() != 2)
+ return false;
+
+ int64_t blocks_used, blocks_allocated;
+ if (!base::StringToInt64(parts[0], &blocks_used))
+ return false;
+ if (!base::StringToInt64(parts[1], &blocks_allocated))
+ return false;
+ if (blocks_used > blocks_allocated)
+ return false;
+ if (blocks_used < 0 || blocks_allocated < 0)
+ return false;
+
+ return ApproximateFilesizeFromBlockCount(blocks_used, size);
+}
+
+bool LooksLikeVmsFileProtectionListingPart(const std::u16string& input) {
+ if (input.length() > 4)
+ return false;
+
+ // On VMS there are four different permission bits: Read, Write, Execute,
+ // and Delete. They appear in that order in the permission listing.
+ std::string pattern("RWED");
+ std::u16string match(input);
+ while (!match.empty() && !pattern.empty()) {
+ if (match[0] == pattern[0])
+ match = match.substr(1);
+ pattern = pattern.substr(1);
+ }
+ return match.empty();
+}
+
+bool LooksLikeVmsFileProtectionListing(const std::u16string& input) {
+ if (input.length() < 2)
+ return false;
+ if (input.front() != '(' || input.back() != ')')
+ return false;
+
+ // We expect four parts of the file protection listing: for System, Owner,
+ // Group, and World.
+ std::vector parts = base::SplitString(
+ std::u16string_view(input).substr(1, input.length() - 2), u",",
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (parts.size() != 4)
+ return false;
+
+ return LooksLikeVmsFileProtectionListingPart(parts[0]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[1]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[2]) &&
+ LooksLikeVmsFileProtectionListingPart(parts[3]);
+}
+
+bool LooksLikeVmsUserIdentificationCode(const std::u16string& input) {
+ if (input.length() < 2)
+ return false;
+ return input.front() == '[' && input.back() == ']';
+}
+
+bool LooksLikeVMSError(const std::u16string& text) {
+ static const char* const kPermissionDeniedMessages[] = {
+ "%RMS-E-FNF", // File not found.
+ "%RMS-E-PRV", // Access denied.
+ "%SYSTEM-F-NOPRIV",
+ "privilege",
+ };
+
+ for (size_t i = 0; i < std::ranges::size(kPermissionDeniedMessages); i++) {
+ if (text.find(base::ASCIIToUTF16(kPermissionDeniedMessages[i])) !=
+ std::u16string::npos)
+ return true;
+ }
+
+ return false;
+}
+
+bool VmsDateListingToTime(const std::vector& columns,
+ base::Time* time) {
+ DCHECK_EQ(4U, columns.size());
+
+ base::Time::Exploded time_exploded = { 0 };
+
+ // Date should be in format DD-MMM-YYYY.
+ std::vector date_parts = base::SplitStringPiece(
+ columns[2], u"-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (date_parts.size() != 3)
+ return false;
+ if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month))
+ return false;
+ if (!FtpUtil::AbbreviatedMonthToNumber(std::u16string(date_parts[1]),
+ &time_exploded.month))
+ return false;
+ if (!base::StringToInt(date_parts[2], &time_exploded.year))
+ return false;
+
+ // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the
+ // last type first. Do not parse the seconds, they will be ignored anyway.
+ std::u16string time_column(columns[3]);
+ if (time_column.length() == 11 && time_column[8] == '.')
+ time_column = time_column.substr(0, 8);
+ if (time_column.length() == 8 && time_column[5] == ':')
+ time_column = time_column.substr(0, 5);
+ if (time_column.length() != 5)
+ return false;
+ std::vector time_parts = base::SplitStringPiece(
+ time_column, u":", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (time_parts.size() != 2)
+ return false;
+ if (!base::StringToInt(time_parts[0], &time_exploded.hour))
+ return false;
+ if (!base::StringToInt(time_parts[1], &time_exploded.minute))
+ return false;
+
+ // We don't know the time zone of the server, so just use UTC.
+ return base::Time::FromUTCExploded(time_exploded, time);
+}
+
+} // namespace
+
+bool ParseFtpDirectoryListingVms(
+ const std::vector& lines,
+ std::vector* 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 columns =
+ base::SplitString(base::CollapseWhitespace(lines[i], false), space,
+ base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+
+ if (columns.size() == 1) {
+ // There can be no continuation if the current line is the last one.
+ if (i == lines.size() - 1)
+ return false;
+
+ // Skip the next line.
+ i++;
+
+ // This refers to the continuation line.
+ if (LooksLikeVMSError(lines[i])) {
+ seen_error = true;
+ continue;
+ }
+
+ // Join the current and next line and split them into columns.
+ columns = base::SplitString(
+ base::CollapseWhitespace(
+ lines[i - 1] + space + lines[i], false),
+ space, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ }
+
+ if (columns.empty())
+ return false;
+
+ FtpDirectoryListingEntry entry;
+ if (!ParseVmsFilename(columns[0], &entry.name, &entry.type))
+ return false;
+
+ // There are different variants of a VMS listing. Some display
+ // the protection listing and user identification code, some do not.
+ if (columns.size() == 6) {
+ if (!LooksLikeVmsFileProtectionListing(columns[5]))
+ return false;
+ if (!LooksLikeVmsUserIdentificationCode(columns[4]))
+ return false;
+
+ // Drop the unneeded data, so that the following code can always expect
+ // just four columns.
+ columns.resize(4);
+ }
+
+ if (columns.size() != 4)
+ return false;
+
+ if (!ParseVmsFilesize(columns[1], &entry.size))
+ return false;
+ if (entry.type != FtpDirectoryListingEntry::FILE)
+ entry.size = -1;
+ if (!VmsDateListingToTime(columns, &entry.last_modified))
+ return false;
+
+ entries->push_back(entry);
+ }
+
+ // The only place where we return true is after receiving the "Total" line,
+ // that should be present in every VMS listing. Alternatively, if the listing
+ // contains error messages, it's OK not to have the "Total" line.
+ return seen_error;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_vms.h b/net/ftp/ftp_directory_listing_parser_vms.h
new file mode 100644
index 0000000000000..e59a679303e23
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
+
+#include
+#include
+
+#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& lines,
+ std::vector* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_VMS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_vms_unittest.cc b/net/ftp/ftp_directory_listing_parser_vms_unittest.cc
new file mode 100644
index 0000000000000..88deedd5e2d28
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_vms_unittest.cc
@@ -0,0 +1,197 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/cxx17_backports.h"
+#include "base/format_macros.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/ftp/ftp_directory_listing_parser_vms.h"
+
+using base::ASCIIToUTF16;
+
+namespace net {
+
+namespace {
+
+typedef FtpDirectoryListingParserTest FtpDirectoryListingParserVmsTest;
+
+TEST_F(FtpDirectoryListingParserVmsTest, Good) {
+ const struct SingleLineTestData good_cases[] = {
+ {"README.TXT;4 2 18-APR-2000 10:40:39.90",
+ FtpDirectoryListingEntry::FILE, "readme.txt", 1024, 2000, 4, 18, 10, 40},
+ {".WELCOME;1 2 13-FEB-2002 23:32:40.47",
+ FtpDirectoryListingEntry::FILE, ".welcome", 1024, 2002, 2, 13, 23, 32},
+ {"FILE.;1 2 13-FEB-2002 23:32:40.47", FtpDirectoryListingEntry::FILE,
+ "file.", 1024, 2002, 2, 13, 23, 32},
+ {"EXAMPLE.TXT;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,)",
+ FtpDirectoryListingEntry::FILE, "example.txt", 512, 2009, 11, 4, 6, 2},
+ {"ANNOUNCE.TXT;2 1/16 12-MAR-2005 08:44:57 [SYSTEM] (RWED,RWED,RE,RE)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ {"TEST.DIR;1 1 4-MAR-1999 22:14:34 [UCX$NOBO,ANONYMOUS] "
+ "(RWE,RWE,RWE,RWE)",
+ FtpDirectoryListingEntry::DIRECTORY, "test", -1, 1999, 3, 4, 22, 14},
+ {"ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (,,,)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ {"ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (R,RW,RWD,RE)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ {"ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (ED,RED,WD,WED)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ {"VMS721.ISO;2 ****** 6-MAY-2008 09:29 [ANONY,ANONYMOUS] "
+ "(RE,RWED,RE,RE)",
+ FtpDirectoryListingEntry::FILE, "vms721.iso", -1, 2008, 5, 6, 9, 29},
+ // This has an unusually large allocated block size (INT64_MAX), but
+ // shouldn't matter as it is not used.
+ {"ANNOUNCE.TXT;2 1/9223372036854775807 12-MAR-2005 08:44:57 [SYSTEM] "
+ "(RWED,RWED,RE,RE)",
+ FtpDirectoryListingEntry::FILE, "announce.txt", 512, 2005, 3, 12, 8, 44},
+ };
+ for (size_t i = 0; i < base::size(good_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i,
+ good_cases[i].input));
+
+ std::vector 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 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 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 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 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 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 entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines,
+ &entries));
+ }
+ }
+}
+
+TEST_F(FtpDirectoryListingParserVmsTest, EmptyColumnZero) {
+ std::vector 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 entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, &entries));
+}
+
+TEST_F(FtpDirectoryListingParserVmsTest, EmptyColumnWhitespace) {
+ std::vector lines;
+
+ // The parser requires a directory header before accepting regular input.
+ lines.push_back(u"garbage");
+
+ lines.push_back(u" ");
+
+ std::vector entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingVms(lines, &entries));
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_windows.cc b/net/ftp/ftp_directory_listing_parser_windows.cc
new file mode 100644
index 0000000000000..0d7906f5d68db
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows.cc
@@ -0,0 +1,74 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_windows.h"
+
+#include
+
+#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& lines,
+ std::vector* entries) {
+ for (size_t i = 0; i < lines.size(); i++) {
+ if (lines[i].empty())
+ continue;
+
+ std::vector 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 "" 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], "")) {
+ entry.type = FtpDirectoryListingEntry::DIRECTORY;
+ entry.size = -1;
+ } else {
+ entry.type = FtpDirectoryListingEntry::FILE;
+ if (!base::StringToInt64(columns[2], &entry.size))
+ return false;
+ if (entry.size < 0)
+ return false;
+ }
+
+ if (!FtpUtil::WindowsDateListingToTime(columns[0],
+ columns[1],
+ &entry.last_modified)) {
+ return false;
+ }
+
+ entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], 3);
+ if (entry.name.empty()) {
+ // Some FTP servers send listing entries with empty names.
+ // It's not obvious how to display such an entry, so ignore them.
+ // We don't want to make the parsing fail at this point though.
+ // Other entries can still be useful.
+ continue;
+ }
+
+ entries->push_back(entry);
+ }
+
+ return true;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_directory_listing_parser_windows.h b/net/ftp/ftp_directory_listing_parser_windows.h
new file mode 100644
index 0000000000000..39aefe2929328
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
+#define NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
+
+#include
+#include
+
+#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& lines,
+ std::vector* entries);
+
+} // namespace net
+
+#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSER_WINDOWS_H_
diff --git a/net/ftp/ftp_directory_listing_parser_windows_unittest.cc b/net/ftp/ftp_directory_listing_parser_windows_unittest.cc
new file mode 100644
index 0000000000000..08b01b88f6017
--- /dev/null
+++ b/net/ftp/ftp_directory_listing_parser_windows_unittest.cc
@@ -0,0 +1,128 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_directory_listing_parser_unittest.h"
+
+#include "base/cxx17_backports.h"
+#include "base/format_macros.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "net/ftp/ftp_directory_listing_parser_windows.h"
+
+namespace net {
+
+namespace {
+
+typedef FtpDirectoryListingParserTest FtpDirectoryListingParserWindowsTest;
+
+TEST_F(FtpDirectoryListingParserWindowsTest, Good) {
+ const struct SingleLineTestData good_cases[] = {
+ { "11-02-09 05:32PM 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 My Directory",
+ FtpDirectoryListingEntry::DIRECTORY, "My Directory", -1,
+ 2009, 11, 2, 17, 32 },
+ { "12-25-10 12:00AM Christmas Midnight",
+ FtpDirectoryListingEntry::DIRECTORY, "Christmas Midnight", -1,
+ 2010, 12, 25, 0, 0 },
+ { "12-25-10 12:00PM 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 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 ", // http://crbug.com/66097
+ "12-07-10 12:05AM 1234 ",
+ "11-02-09 05:32 ",
+ "11-02-09 05:32PM ",
+ };
+ for (size_t i = 0; i < base::size(ignored_cases); i++) {
+ SCOPED_TRACE(base::StringPrintf("Test[%" PRIuS "]: %s", i,
+ ignored_cases[i]));
+
+ std::vector 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 ",
+ "11-02-09 05:32PM NT",
+ "11-FEB-09 05:32PM ",
+ "11-02 05:32PM ",
+ "11-02-09 05:32PM -1",
+ "11-FEB-09 05:32PM NT",
+ "11-02 05:32PM 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 entries;
+ EXPECT_FALSE(ParseFtpDirectoryListingWindows(
+ GetSingleLineTestCase(bad_cases[i]),
+ &entries));
+ }
+}
+
+} // namespace
+
+} // namespace net
diff --git a/net/ftp/ftp_network_layer.cc b/net/ftp/ftp_network_layer.cc
new file mode 100644
index 0000000000000..8a467f8896487
--- /dev/null
+++ b/net/ftp/ftp_network_layer.cc
@@ -0,0 +1,34 @@
+// Copyright (c) 2008 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_layer.h"
+
+#include "base/check.h"
+#include "net/ftp/ftp_network_session.h"
+#include "net/ftp/ftp_network_transaction.h"
+#include "net/socket/client_socket_factory.h"
+
+namespace net {
+
+FtpNetworkLayer::FtpNetworkLayer(HostResolver* host_resolver)
+ : session_(new FtpNetworkSession(host_resolver)),
+ suspended_(false) {
+ DCHECK(host_resolver);
+}
+
+FtpNetworkLayer::~FtpNetworkLayer() = default;
+
+std::unique_ptr FtpNetworkLayer::CreateTransaction() {
+ if (suspended_)
+ return nullptr;
+
+ return std::make_unique(
+ session_->host_resolver(), ClientSocketFactory::GetDefaultFactory());
+}
+
+void FtpNetworkLayer::Suspend(bool suspend) {
+ suspended_ = suspend;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_network_layer.h b/net/ftp/ftp_network_layer.h
new file mode 100644
index 0000000000000..59c4d2b04bdf3
--- /dev/null
+++ b/net/ftp/ftp_network_layer.h
@@ -0,0 +1,38 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_LAYER_H_
+#define NET_FTP_FTP_NETWORK_LAYER_H_
+
+#include
+
+#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 CreateTransaction() override;
+ void Suspend(bool suspend) override;
+
+ private:
+ std::unique_ptr session_;
+ bool suspended_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_NETWORK_LAYER_H_
diff --git a/net/ftp/ftp_network_session.cc b/net/ftp/ftp_network_session.cc
new file mode 100644
index 0000000000000..9de9796b08bf3
--- /dev/null
+++ b/net/ftp/ftp_network_session.cc
@@ -0,0 +1,14 @@
+// Copyright (c) 2010 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_session.h"
+
+namespace net {
+
+FtpNetworkSession::FtpNetworkSession(HostResolver* host_resolver)
+ : host_resolver_(host_resolver) {}
+
+FtpNetworkSession::~FtpNetworkSession() = default;
+
+} // namespace net
diff --git a/net/ftp/ftp_network_session.h b/net/ftp/ftp_network_session.h
new file mode 100644
index 0000000000000..0ef5b781eec4e
--- /dev/null
+++ b/net/ftp/ftp_network_session.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_SESSION_H_
+#define NET_FTP_FTP_NETWORK_SESSION_H_
+
+#include "base/memory/raw_ptr.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+class HostResolver;
+
+// This class holds session objects used by FtpNetworkTransaction objects.
+class NET_EXPORT_PRIVATE FtpNetworkSession {
+ public:
+ explicit FtpNetworkSession(HostResolver* host_resolver);
+ virtual ~FtpNetworkSession();
+
+ HostResolver* host_resolver() { return host_resolver_; }
+
+ private:
+ raw_ptr const host_resolver_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_NETWORK_SESSION_H_
diff --git a/net/ftp/ftp_network_transaction.cc b/net/ftp/ftp_network_transaction.cc
new file mode 100644
index 0000000000000..85053f403f224
--- /dev/null
+++ b/net/ftp/ftp_network_transaction.cc
@@ -0,0 +1,1298 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_network_transaction.h"
+
+#include
+
+#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:
+// ()
+// is a delimiter character, ideally to be |
+bool ExtractPortFromEPSVResponse(const FtpCtrlResponse& response, int* port) {
+ if (response.lines.size() != 1)
+ return false;
+
+ std::string_view epsv_line(response.lines[0]);
+ size_t start = epsv_line.find('(');
+ // If the line doesn't have a '(' or doesn't have enough characters after the
+ // first '(', fail.
+ if (start == std::string_view::npos || epsv_line.length() - start < 7)
+ return false;
+
+ char separator = epsv_line[start + 1];
+
+ // Make sure we have "(...", where 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 .
+ size_t end = epsv_line.find(separator, start);
+ if (end == std::string_view::npos)
+ return false;
+
+ return ParseInt32(epsv_line.substr(start, end - start),
+ ParseIntFormat::NON_NEGATIVE, port);
+}
+
+// There are two way we can receive IP address and port.
+// (127,0,0,1,23,21) IP address and port encapsulated in ().
+// 127,0,0,1,23,21 IP address and port without ().
+//
+// See RFC 959, Section 4.1.2
+bool ExtractPortFromPASVResponse(const FtpCtrlResponse& response, int* port) {
+ if (response.lines.size() != 1)
+ return false;
+
+ std::string line(response.lines[0]);
+ if (!base::IsStringASCII(line))
+ return false;
+ if (line.length() < 2)
+ return false;
+
+ size_t paren_pos = line.find('(');
+ if (paren_pos == std::string::npos) {
+ // Find the first comma and use it to locate the beginning
+ // of the response data.
+ size_t comma_pos = line.find(',');
+ if (comma_pos == std::string::npos)
+ return false;
+
+ size_t space_pos = line.rfind(' ', comma_pos);
+ if (space_pos != std::string::npos)
+ line = line.substr(space_pos + 1);
+ } else {
+ // Remove the parentheses and use the text inside them.
+ size_t closing_paren_pos = line.rfind(')');
+ if (closing_paren_pos == std::string::npos)
+ return false;
+ if (closing_paren_pos <= paren_pos)
+ return false;
+
+ line = line.substr(paren_pos + 1, closing_paren_pos - paren_pos - 1);
+ }
+
+ // Split the line into comma-separated pieces and extract
+ // the last two.
+ std::vector pieces = base::SplitStringPiece(
+ line, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+ if (pieces.size() != 6)
+ return false;
+
+ // Ignore the IP address supplied in the response. We are always going
+ // to connect back to the same server to prevent FTP PASV port scanning.
+ uint32_t p0, p1;
+ if (!ParseUint32(pieces[4], ParseIntFormat::NON_NEGATIVE, &p0))
+ return false;
+ if (!ParseUint32(pieces[5], ParseIntFormat::NON_NEGATIVE, &p1))
+ return false;
+ if (p0 > 0xFF || p1 > 0xFF)
+ return false;
+
+ *port = (p0 << 8) + p1;
+ return true;
+}
+
+} // namespace
+
+FtpNetworkTransaction::FtpNetworkTransaction(
+ HostResolver* resolver,
+ ClientSocketFactory* socket_factory)
+ : command_sent_(COMMAND_NONE),
+ io_callback_(base::BindRepeating(&FtpNetworkTransaction::OnIOComplete,
+ base::Unretained(this))),
+ request_(nullptr),
+ resolver_(resolver),
+ read_ctrl_buf_(base::MakeRefCounted(kCtrlBufLen)),
+ read_data_buf_len_(0),
+ last_error_(OK),
+ system_type_(SYSTEM_TYPE_UNKNOWN),
+ // Use image (binary) transfer by default. It should always work,
+ // whereas the ascii transfer may damage binary data.
+ data_type_(DATA_TYPE_IMAGE),
+ resource_type_(RESOURCE_TYPE_UNKNOWN),
+ use_epsv_(true),
+ data_connection_port_(0),
+ socket_factory_(socket_factory),
+ next_state_(STATE_NONE),
+ state_after_data_connect_complete_(STATE_NONE) {}
+
+FtpNetworkTransaction::~FtpNetworkTransaction() = default;
+
+int FtpNetworkTransaction::Stop(int error) {
+ if (command_sent_ == COMMAND_QUIT) {
+ if (error != ERR_EMPTY_RESPONSE)
+ return error;
+
+ // For empty responses, if this is propagating an error, then it will return
+ // the error. If the error occurred during a QUIT command, then this will
+ // return OK since there was no previous error. Some FTP servers are lazy
+ // and do not bother responding to QUIT commands.
+ // See https://crbug.com/633841
+ return last_error_;
+ }
+
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ last_error_ = error;
+ return OK;
+}
+
+int FtpNetworkTransaction::Start(
+ raw_ptr request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) {
+ net_log_ = net_log;
+ request_ = request_info;
+ traffic_annotation_ = MutableNetworkTrafficAnnotationTag(traffic_annotation);
+
+ ctrl_response_buffer_ = std::make_unique(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(kCtrlBufLen);
+ ctrl_response_buffer_ = std::make_unique(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(command.length() + 2);
+ write_buf_ = base::MakeRefCounted(
+ write_command_buf_, write_command_buf_->size());
+ memcpy(write_command_buf_->data(), command.data(), command.length());
+ memcpy(write_command_buf_->data() + command.length(), kCRLF, 2);
+
+ net_log_.AddEventWithStringParams(NetLogEventType::FTP_COMMAND_SENT,
+ "command", command_for_log);
+
+ next_state_ = STATE_CTRL_WRITE;
+ return OK;
+}
+
+std::string FtpNetworkTransaction::GetRequestPathForFtpCommand(
+ bool is_directory) const {
+ std::string path(current_remote_directory_ + unescaped_path_);
+
+ // Make sure that if the path is expected to be a file, it won't end
+ // with a trailing slash.
+ if (!is_directory && path.length() > 1 && path.back() == '/')
+ path.erase(path.length() - 1);
+
+ if (system_type_ == SYSTEM_TYPE_VMS) {
+ if (is_directory)
+ path = FtpUtil::UnixDirectoryPathToVMS(path);
+ else
+ path = FtpUtil::UnixFilePathToVMS(path);
+ }
+
+ DCHECK(IsValidFTPCommandSubstring(path));
+ return path;
+}
+
+void FtpNetworkTransaction::DetectTypecode() {
+ if (!request_->url.has_path())
+ return;
+ std::string gurl_path(request_->url.path());
+
+ // Extract the typecode, see RFC 1738 section 3.2.2. FTP url-path.
+ std::string::size_type pos = gurl_path.rfind(';');
+ if (pos == std::string::npos)
+ return;
+ std::string typecode_string(gurl_path.substr(pos));
+ if (typecode_string == ";type=a") {
+ data_type_ = DATA_TYPE_ASCII;
+ resource_type_ = RESOURCE_TYPE_FILE;
+ } else if (typecode_string == ";type=i") {
+ data_type_ = DATA_TYPE_IMAGE;
+ resource_type_ = RESOURCE_TYPE_FILE;
+ } else if (typecode_string == ";type=d") {
+ resource_type_ = RESOURCE_TYPE_DIRECTORY;
+ }
+}
+
+int FtpNetworkTransaction::DoLoop(int result) {
+ DCHECK(next_state_ != STATE_NONE);
+
+ int rv = result;
+ do {
+ State state = next_state_;
+ next_state_ = STATE_NONE;
+ switch (state) {
+ case STATE_CTRL_RESOLVE_HOST:
+ DCHECK(rv == OK);
+ rv = DoCtrlResolveHost();
+ break;
+ case STATE_CTRL_RESOLVE_HOST_COMPLETE:
+ rv = DoCtrlResolveHostComplete(rv);
+ break;
+ case STATE_CTRL_CONNECT:
+ DCHECK(rv == OK);
+ rv = DoCtrlConnect();
+ break;
+ case STATE_CTRL_CONNECT_COMPLETE:
+ rv = DoCtrlConnectComplete(rv);
+ break;
+ case STATE_CTRL_READ:
+ DCHECK(rv == OK);
+ rv = DoCtrlRead();
+ break;
+ case STATE_CTRL_READ_COMPLETE:
+ rv = DoCtrlReadComplete(rv);
+ break;
+ case STATE_CTRL_WRITE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWrite();
+ break;
+ case STATE_CTRL_WRITE_COMPLETE:
+ rv = DoCtrlWriteComplete(rv);
+ break;
+ case STATE_CTRL_WRITE_USER:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteUSER();
+ break;
+ case STATE_CTRL_WRITE_PASS:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePASS();
+ break;
+ case STATE_CTRL_WRITE_SYST:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteSYST();
+ break;
+ case STATE_CTRL_WRITE_PWD:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePWD();
+ break;
+ case STATE_CTRL_WRITE_TYPE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteTYPE();
+ break;
+ case STATE_CTRL_WRITE_EPSV:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteEPSV();
+ break;
+ case STATE_CTRL_WRITE_PASV:
+ DCHECK(rv == OK);
+ rv = DoCtrlWritePASV();
+ break;
+ case STATE_CTRL_WRITE_RETR:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteRETR();
+ break;
+ case STATE_CTRL_WRITE_SIZE:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteSIZE();
+ break;
+ case STATE_CTRL_WRITE_CWD:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteCWD();
+ break;
+ case STATE_CTRL_WRITE_LIST:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteLIST();
+ break;
+ case STATE_CTRL_WRITE_QUIT:
+ DCHECK(rv == OK);
+ rv = DoCtrlWriteQUIT();
+ break;
+ case STATE_DATA_CONNECT:
+ DCHECK(rv == OK);
+ rv = DoDataConnect();
+ break;
+ case STATE_DATA_CONNECT_COMPLETE:
+ rv = DoDataConnectComplete(rv);
+ break;
+ case STATE_DATA_READ:
+ DCHECK(rv == OK);
+ rv = DoDataRead();
+ break;
+ case STATE_DATA_READ_COMPLETE:
+ rv = DoDataReadComplete(rv);
+ break;
+ default:
+ NOTREACHED() << "bad state";
+ }
+ } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);
+ return rv;
+}
+
+int FtpNetworkTransaction::DoCtrlResolveHost() {
+ next_state_ = STATE_CTRL_RESOLVE_HOST_COMPLETE;
+
+ resolve_request_ =
+ resolver_->CreateRequest(url::SchemeHostPort(request_->url),
+ NetworkAnonymizationKey(), net_log_, std::nullopt);
+ return resolve_request_->Start(base::BindOnce(
+ &FtpNetworkTransaction::OnIOComplete, base::Unretained(this)));
+}
+
+int FtpNetworkTransaction::DoCtrlResolveHostComplete(int result) {
+ if (result == OK)
+ next_state_ = STATE_CTRL_CONNECT;
+ return result;
+}
+
+int FtpNetworkTransaction::DoCtrlConnect() {
+ next_state_ = STATE_CTRL_CONNECT_COMPLETE;
+ // TODO(https://crbug.com/1123197): Pass a non-null NetworkQualityEstimator.
+ NetworkQualityEstimator* network_quality_estimator = nullptr;
+
+ DCHECK(resolve_request_ && resolve_request_->GetAddressResults());
+ ctrl_socket_ = socket_factory_->CreateTransportClientSocket(
+ AddressList(*resolve_request_->GetAddressResults()), nullptr,
+ network_quality_estimator, net_log_.net_log(), net_log_.source());
+ net_log_.AddEventReferencingSource(NetLogEventType::FTP_CONTROL_CONNECTION,
+ ctrl_socket_->NetLog().source());
+ return ctrl_socket_->Connect(io_callback_);
+}
+
+int FtpNetworkTransaction::DoCtrlConnectComplete(int result) {
+ if (result == OK) {
+ // Put the peer's IP address and port into the response.
+ IPEndPoint ip_endpoint;
+ result = ctrl_socket_->GetPeerAddress(&ip_endpoint);
+ if (result == OK) {
+ response_.remote_endpoint = ip_endpoint;
+ next_state_ = STATE_CTRL_READ;
+
+ if (ip_endpoint.GetFamily() == ADDRESS_FAMILY_IPV4) {
+ // Do not use EPSV for IPv4 connections. Some servers become confused
+ // and we time out while waiting to connect. PASV is perfectly fine for
+ // IPv4. Note that this blocks IPv4 not to use EPSV instead of allowing
+ // IPv6 to use it, to make the code more future-proof:
+ // all future protocols should just use EPSV.
+ use_epsv_ = false;
+ }
+ }
+ }
+ return result;
+}
+
+int FtpNetworkTransaction::DoCtrlRead() {
+ next_state_ = STATE_CTRL_READ_COMPLETE;
+ return ctrl_socket_->Read(read_ctrl_buf_.get(), kCtrlBufLen, io_callback_);
+}
+
+int FtpNetworkTransaction::DoCtrlReadComplete(int result) {
+ if (result == 0) {
+ // Some servers (for example Pure-FTPd) apparently close the control
+ // connection when anonymous login is not permitted. For more details
+ // see http://crbug.com/25023.
+ if (command_sent_ == COMMAND_USER &&
+ credentials_.username() == u"anonymous") {
+ response_.needs_auth = true;
+ }
+ return Stop(ERR_EMPTY_RESPONSE);
+ }
+ if (result < 0)
+ return Stop(result);
+
+ ctrl_response_buffer_->ConsumeData(read_ctrl_buf_->data(), result);
+
+ if (!ctrl_response_buffer_->ResponseAvailable()) {
+ // Read more data from the control socket.
+ next_state_ = STATE_CTRL_READ;
+ return OK;
+ }
+
+ return ProcessCtrlResponse();
+}
+
+int FtpNetworkTransaction::DoCtrlWrite() {
+ next_state_ = STATE_CTRL_WRITE_COMPLETE;
+
+ return ctrl_socket_->Write(write_buf_.get(), write_buf_->BytesRemaining(),
+ io_callback_,
+ NetworkTrafficAnnotationTag(traffic_annotation_));
+}
+
+int FtpNetworkTransaction::DoCtrlWriteComplete(int result) {
+ if (result < 0)
+ return result;
+
+ write_buf_->DidConsume(result);
+ if (write_buf_->BytesRemaining() == 0) {
+ // Clear the write buffer.
+ write_buf_ = nullptr;
+ write_command_buf_ = nullptr;
+
+ next_state_ = STATE_CTRL_READ;
+ } else {
+ next_state_ = STATE_CTRL_WRITE;
+ }
+ return OK;
+}
+
+// FTP Commands and responses
+
+// USER Command.
+int FtpNetworkTransaction::DoCtrlWriteUSER() {
+ std::string command = "USER " + base::UTF16ToUTF8(credentials_.username());
+
+ if (!IsValidFTPCommandSubstring(command))
+ return Stop(ERR_MALFORMED_IDENTITY);
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, "USER ***", COMMAND_USER);
+}
+
+int FtpNetworkTransaction::ProcessResponseUSER(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SYST;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ next_state_ = STATE_CTRL_WRITE_PASS;
+ break;
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ case ERROR_CLASS_PERMANENT_ERROR:
+ response_.needs_auth = true;
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// PASS command.
+int FtpNetworkTransaction::DoCtrlWritePASS() {
+ std::string command = "PASS " + base::UTF16ToUTF8(credentials_.password());
+
+ if (!IsValidFTPCommandSubstring(command))
+ return Stop(ERR_MALFORMED_IDENTITY);
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, "PASS ***", COMMAND_PASS);
+}
+
+int FtpNetworkTransaction::ProcessResponsePASS(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SYST;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ case ERROR_CLASS_PERMANENT_ERROR:
+ response_.needs_auth = true;
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// SYST command.
+int FtpNetworkTransaction::DoCtrlWriteSYST() {
+ std::string command = "SYST";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_SYST);
+}
+
+int FtpNetworkTransaction::ProcessResponseSYST(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ // All important info should be on the first line.
+ std::string line = response.lines[0];
+ // The response should be ASCII, which allows us to do case-insensitive
+ // comparisons easily. If it is not ASCII, we leave the system type
+ // as unknown.
+ if (base::IsStringASCII(line)) {
+ line = base::ToLowerASCII(line);
+
+ // Remove all whitespace, to correctly handle cases like fancy "V M S"
+ // response instead of "VMS".
+ base::RemoveChars(line, base::kWhitespaceASCII, &line);
+
+ // The "magic" strings we test for below have been gathered by an
+ // empirical study. VMS needs to come first because some VMS systems
+ // also respond with "UNIX emulation", which is not perfect. It is much
+ // more reliable to talk to these servers in their native language.
+ if (line.find("vms") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_VMS;
+ } else if (line.find("l8") != std::string::npos ||
+ line.find("unix") != std::string::npos ||
+ line.find("bsd") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_UNIX;
+ } else if (line.find("win32") != std::string::npos ||
+ line.find("windows") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_WINDOWS;
+ } else if (line.find("os/2") != std::string::npos) {
+ system_type_ = SYSTEM_TYPE_OS2;
+ }
+ }
+ next_state_ = STATE_CTRL_WRITE_PWD;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ // Server does not recognize the SYST command so proceed.
+ next_state_ = STATE_CTRL_WRITE_PWD;
+ break;
+ }
+ return OK;
+}
+
+// PWD command.
+int FtpNetworkTransaction::DoCtrlWritePWD() {
+ std::string command = "PWD";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_PWD);
+}
+
+int FtpNetworkTransaction::ProcessResponsePWD(const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ // The info we look for should be on the first line.
+ std::string line = response.lines[0];
+ if (line.empty())
+ return Stop(ERR_INVALID_RESPONSE);
+ std::string::size_type quote_pos = line.find('"');
+ if (quote_pos != std::string::npos) {
+ line = line.substr(quote_pos + 1);
+ quote_pos = line.find('"');
+ if (quote_pos == std::string::npos)
+ return Stop(ERR_INVALID_RESPONSE);
+ line = line.substr(0, quote_pos);
+ }
+ if (system_type_ == SYSTEM_TYPE_VMS)
+ line = FtpUtil::VMSPathToUnix(line);
+ if (!line.empty() && line.back() == '/')
+ line.erase(line.length() - 1);
+ // Fail if the "path" contains characters not allowed in commands.
+ // This does mean that files with CRs or LFs in their names aren't
+ // handled, but that's probably for the best.
+ if (!IsValidFTPCommandSubstring(line))
+ return Stop(ERR_INVALID_RESPONSE);
+ current_remote_directory_ = line;
+ next_state_ = STATE_CTRL_WRITE_TYPE;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// TYPE command.
+int FtpNetworkTransaction::DoCtrlWriteTYPE() {
+ std::string command = "TYPE ";
+ if (data_type_ == DATA_TYPE_ASCII) {
+ command += "A";
+ } else if (data_type_ == DATA_TYPE_IMAGE) {
+ command += "I";
+ } else {
+ NOTREACHED();
+ }
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_TYPE);
+}
+
+int FtpNetworkTransaction::ProcessResponseTYPE(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_SIZE;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// EPSV command
+int FtpNetworkTransaction::DoCtrlWriteEPSV() {
+ const std::string command = "EPSV";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_EPSV);
+}
+
+int FtpNetworkTransaction::ProcessResponseEPSV(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK: {
+ int port;
+ if (!ExtractPortFromEPSVResponse(response, &port))
+ return Stop(ERR_INVALID_RESPONSE);
+ if (IsWellKnownPort(port) ||
+ !IsPortAllowedForScheme(port, url::kFtpScheme)) {
+ return Stop(ERR_UNSAFE_PORT);
+ }
+ data_connection_port_ = static_cast(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(port);
+ next_state_ = STATE_DATA_CONNECT;
+ break;
+ }
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// RETR command
+int FtpNetworkTransaction::DoCtrlWriteRETR() {
+ std::string command = "RETR " + GetRequestPathForFtpCommand(false);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_RETR);
+}
+
+int FtpNetworkTransaction::ProcessResponseRETR(
+ const FtpCtrlResponse& response) {
+ // Resource type should be either filled in by DetectTypecode() or
+ // detected with CWD. RETR is sent only when the resource is a file.
+ DCHECK_EQ(RESOURCE_TYPE_FILE, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ // We want the client to start reading the response at this point.
+ // It got here either through Start or RestartWithAuth. We want that
+ // method to complete. Not setting next state here will make DoLoop exit
+ // and in turn make Start/RestartWithAuth complete.
+ break;
+ case ERROR_CLASS_OK:
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+
+ return OK;
+}
+
+// SIZE command
+int FtpNetworkTransaction::DoCtrlWriteSIZE() {
+ std::string command = "SIZE " + GetRequestPathForFtpCommand(false);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_SIZE);
+}
+
+int FtpNetworkTransaction::ProcessResponseSIZE(
+ const FtpCtrlResponse& response) {
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ break;
+ case ERROR_CLASS_OK:
+ if (response.lines.size() != 1)
+ return Stop(ERR_INVALID_RESPONSE);
+ int64_t size;
+ if (!base::StringToInt64(response.lines[0], &size))
+ return Stop(ERR_INVALID_RESPONSE);
+ if (size < 0)
+ return Stop(ERR_INVALID_RESPONSE);
+
+ // A successful response to SIZE does not mean the resource is a file.
+ // Some FTP servers (for example, the qnx one) send a SIZE even for
+ // directories.
+ response_.expected_content_size = size;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ break;
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ break;
+ case ERROR_CLASS_PERMANENT_ERROR:
+ // It's possible that SIZE failed because the path is a directory.
+ // TODO(xunjieli): https://crbug.com/526724: Add a test for this case.
+ if (resource_type_ == RESOURCE_TYPE_UNKNOWN &&
+ response.status_code != 550) {
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ break;
+ }
+
+ // If the resource is known beforehand to be a file, RETR should be issued,
+ // otherwise do CWD which will detect the resource type.
+ if (resource_type_ == RESOURCE_TYPE_FILE)
+ EstablishDataConnection(STATE_CTRL_WRITE_RETR);
+ else
+ next_state_ = STATE_CTRL_WRITE_CWD;
+ return OK;
+}
+
+// CWD command
+int FtpNetworkTransaction::DoCtrlWriteCWD() {
+ std::string command = "CWD " + GetRequestPathForFtpCommand(true);
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_CWD);
+}
+
+int FtpNetworkTransaction::ProcessResponseCWD(const FtpCtrlResponse& response) {
+ // CWD should be invoked only when the resource is not a file.
+ DCHECK_NE(RESOURCE_TYPE_FILE, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_OK:
+ resource_type_ = RESOURCE_TYPE_DIRECTORY;
+ EstablishDataConnection(STATE_CTRL_WRITE_LIST);
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ // Some FTP servers send response 451 (not a valid CWD response according
+ // to RFC 959) instead of 550.
+ if (response.status_code == 451)
+ return ProcessResponseCWDNotADirectory();
+
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ if (response.status_code == 550)
+ return ProcessResponseCWDNotADirectory();
+
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+
+ return OK;
+}
+
+int FtpNetworkTransaction::ProcessResponseCWDNotADirectory() {
+ if (resource_type_ == RESOURCE_TYPE_DIRECTORY) {
+ // We're assuming that the resource is a directory, but the server
+ // says it's not true. The most probable interpretation is that it
+ // doesn't exist (with FTP we can't be sure).
+ return Stop(ERR_FILE_NOT_FOUND);
+ }
+
+ // If it is not a directory, it is probably a file.
+ resource_type_ = RESOURCE_TYPE_FILE;
+
+ EstablishDataConnection(STATE_CTRL_WRITE_RETR);
+ return OK;
+}
+
+// LIST command
+int FtpNetworkTransaction::DoCtrlWriteLIST() {
+ // Use the -l option for mod_ftp configured in LISTIsNLST mode: the option
+ // forces LIST output instead of NLST (which would be ambiguous for us
+ // to parse).
+ std::string command("LIST -l");
+ if (system_type_ == SYSTEM_TYPE_VMS)
+ command = "LIST *.*;0";
+
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_LIST);
+}
+
+int FtpNetworkTransaction::ProcessResponseLIST(
+ const FtpCtrlResponse& response) {
+ // Resource type should be either filled in by DetectTypecode() or
+ // detected with CWD. LIST is sent only when the resource is a directory.
+ DCHECK_EQ(RESOURCE_TYPE_DIRECTORY, resource_type_);
+
+ switch (GetErrorClass(response.status_code)) {
+ case ERROR_CLASS_INITIATED:
+ // We want the client to start reading the response at this point.
+ // It got here either through Start or RestartWithAuth. We want that
+ // method to complete. Not setting next state here will make DoLoop exit
+ // and in turn make Start/RestartWithAuth complete.
+ response_.is_directory_listing = true;
+ break;
+ case ERROR_CLASS_OK:
+ response_.is_directory_listing = true;
+ next_state_ = STATE_CTRL_WRITE_QUIT;
+ break;
+ case ERROR_CLASS_INFO_NEEDED:
+ return Stop(ERR_INVALID_RESPONSE);
+ case ERROR_CLASS_TRANSIENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ case ERROR_CLASS_PERMANENT_ERROR:
+ return Stop(GetNetErrorCodeForFtpResponseCode(response.status_code));
+ }
+ return OK;
+}
+
+// QUIT command
+int FtpNetworkTransaction::DoCtrlWriteQUIT() {
+ std::string command = "QUIT";
+ next_state_ = STATE_CTRL_READ;
+ return SendFtpCommand(command, command, COMMAND_QUIT);
+}
+
+int FtpNetworkTransaction::ProcessResponseQUIT(
+ const FtpCtrlResponse& response) {
+ ctrl_socket_->Disconnect();
+ return last_error_;
+}
+
+// Data Connection
+
+int FtpNetworkTransaction::DoDataConnect() {
+ next_state_ = STATE_DATA_CONNECT_COMPLETE;
+ IPEndPoint ip_endpoint;
+ AddressList data_address;
+ // Connect to the same host as the control socket to prevent PASV port
+ // scanning attacks.
+ int rv = ctrl_socket_->GetPeerAddress(&ip_endpoint);
+ if (rv != OK)
+ return Stop(rv);
+ data_address = AddressList::CreateFromIPAddress(
+ ip_endpoint.address(), data_connection_port_);
+ // TODO(https://crbug.com/1123197): Pass a non-null NetworkQualityEstimator.
+ NetworkQualityEstimator* network_quality_estimator = nullptr;
+
+ data_socket_ = socket_factory_->CreateTransportClientSocket(
+ data_address, nullptr, network_quality_estimator, net_log_.net_log(),
+ net_log_.source());
+ net_log_.AddEventReferencingSource(NetLogEventType::FTP_DATA_CONNECTION,
+ data_socket_->NetLog().source());
+ return data_socket_->Connect(io_callback_);
+}
+
+int FtpNetworkTransaction::DoDataConnectComplete(int result) {
+ if (result != OK && use_epsv_) {
+ // It's possible we hit a broken server, sadly. They can break in different
+ // ways. Some time out, some reset a connection. Fall back to PASV.
+ // TODO(phajdan.jr): https://crbug.com/526723: remember it for future
+ // transactions with this server.
+ use_epsv_ = false;
+ next_state_ = STATE_CTRL_WRITE_PASV;
+ return OK;
+ }
+
+ if (result != OK)
+ return Stop(result);
+
+ next_state_ = state_after_data_connect_complete_;
+ return OK;
+}
+
+int FtpNetworkTransaction::DoDataRead() {
+ DCHECK(read_data_buf_.get());
+ DCHECK_GT(read_data_buf_len_, 0);
+
+ if (!data_socket_ || !data_socket_->IsConnected()) {
+ // If we don't destroy the data socket completely, some servers will wait
+ // for us (http://crbug.com/21127). The half-closed TCP connection needs
+ // to be closed on our side too.
+ data_socket_.reset();
+
+ if (ctrl_socket_->IsConnected()) {
+ // Wait for the server's response, we should get it before sending QUIT.
+ next_state_ = STATE_CTRL_READ;
+ return OK;
+ }
+
+ // We are no longer connected to the server, so just finish the transaction.
+ return Stop(OK);
+ }
+
+ next_state_ = STATE_DATA_READ_COMPLETE;
+ read_data_buf_->data()[0] = 0;
+ return data_socket_->Read(
+ read_data_buf_.get(), read_data_buf_len_, io_callback_);
+}
+
+int FtpNetworkTransaction::DoDataReadComplete(int result) {
+ return result;
+}
+
+} // namespace net
diff --git a/net/ftp/ftp_network_transaction.h b/net/ftp/ftp_network_transaction.h
new file mode 100644
index 0000000000000..c7cb8753f8acf
--- /dev/null
+++ b/net/ftp/ftp_network_transaction.h
@@ -0,0 +1,268 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_NETWORK_TRANSACTION_H_
+#define NET_FTP_FTP_NETWORK_TRANSACTION_H_
+
+#include
+
+#include
+#include
+#include
+
+#include "base/compiler_specific.h"
+#include "base/gtest_prod_util.h"
+#include "base/memory/raw_ptr.h"
+#include "base/memory/ref_counted.h"
+#include "net/base/auth.h"
+#include "net/base/completion_once_callback.h"
+#include "net/base/completion_repeating_callback.h"
+#include "net/base/net_export.h"
+#include "net/dns/host_resolver.h"
+#include "net/ftp/ftp_ctrl_response_buffer.h"
+#include "net/ftp/ftp_response_info.h"
+#include "net/ftp/ftp_transaction.h"
+#include "net/log/net_log_with_source.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+
+namespace net {
+
+class ClientSocketFactory;
+class StreamSocket;
+
+class NET_EXPORT_PRIVATE FtpNetworkTransaction : public FtpTransaction {
+ public:
+ FtpNetworkTransaction(HostResolver* resolver,
+ ClientSocketFactory* socket_factory);
+ ~FtpNetworkTransaction() override;
+
+ int Stop(int error);
+
+ // FtpTransaction methods:
+ int Start(raw_ptr request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) override;
+ int RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback callback) override;
+ int Read(IOBuffer* buf,
+ int buf_len,
+ CompletionOnceCallback callback) override;
+ const FtpResponseInfo* GetResponseInfo() const override;
+ LoadState GetLoadState() const override;
+ uint64_t GetUploadProgress() const override;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(FtpNetworkTransactionTest,
+ DownloadTransactionEvilPasvUnsafeHost);
+
+ enum Command {
+ COMMAND_NONE,
+ COMMAND_USER,
+ COMMAND_PASS,
+ COMMAND_SYST,
+ COMMAND_TYPE,
+ COMMAND_EPSV,
+ COMMAND_PASV,
+ COMMAND_PWD,
+ COMMAND_SIZE,
+ COMMAND_RETR,
+ COMMAND_CWD,
+ COMMAND_LIST,
+ COMMAND_QUIT,
+ };
+
+ // Major categories of remote system types, as returned by SYST command.
+ enum SystemType {
+ SYSTEM_TYPE_UNKNOWN,
+ SYSTEM_TYPE_UNIX,
+ SYSTEM_TYPE_WINDOWS,
+ SYSTEM_TYPE_OS2,
+ SYSTEM_TYPE_VMS,
+ };
+
+ // Data representation type, see RFC 959 section 3.1.1. Data Types.
+ // We only support the two most popular data types.
+ enum DataType {
+ DATA_TYPE_ASCII,
+ DATA_TYPE_IMAGE,
+ };
+
+ // In FTP we need to issue different commands depending on whether a resource
+ // is a file or directory. If we don't know that, we're going to autodetect
+ // it.
+ enum ResourceType {
+ RESOURCE_TYPE_UNKNOWN,
+ RESOURCE_TYPE_FILE,
+ RESOURCE_TYPE_DIRECTORY,
+ };
+
+ enum State {
+ // Control connection states:
+ STATE_CTRL_RESOLVE_HOST,
+ STATE_CTRL_RESOLVE_HOST_COMPLETE,
+ STATE_CTRL_CONNECT,
+ STATE_CTRL_CONNECT_COMPLETE,
+ STATE_CTRL_READ,
+ STATE_CTRL_READ_COMPLETE,
+ STATE_CTRL_WRITE,
+ STATE_CTRL_WRITE_COMPLETE,
+ STATE_CTRL_WRITE_USER,
+ STATE_CTRL_WRITE_PASS,
+ STATE_CTRL_WRITE_SYST,
+ STATE_CTRL_WRITE_TYPE,
+ STATE_CTRL_WRITE_EPSV,
+ STATE_CTRL_WRITE_PASV,
+ STATE_CTRL_WRITE_PWD,
+ STATE_CTRL_WRITE_RETR,
+ STATE_CTRL_WRITE_SIZE,
+ STATE_CTRL_WRITE_CWD,
+ STATE_CTRL_WRITE_LIST,
+ STATE_CTRL_WRITE_QUIT,
+ // Data connection states:
+ STATE_DATA_CONNECT,
+ STATE_DATA_CONNECT_COMPLETE,
+ STATE_DATA_READ,
+ STATE_DATA_READ_COMPLETE,
+ STATE_NONE
+ };
+
+ // Resets the members of the transaction so it can be restarted.
+ void ResetStateForRestart();
+
+ // Establishes the data connection and switches to |state_after_connect|.
+ // |state_after_connect| should only be RETR or LIST.
+ void EstablishDataConnection(State state_after_connect);
+
+ void DoCallback(int result);
+ void OnIOComplete(int result);
+
+ // Executes correct ProcessResponse + command_name function based on last
+ // issued command. Returns error code.
+ int ProcessCtrlResponse();
+
+ int SendFtpCommand(const std::string& command,
+ const std::string& command_for_log,
+ Command cmd);
+
+ // Returns request path suitable to be included in an FTP command. If the path
+ // will be used as a directory, |is_directory| should be true.
+ std::string GetRequestPathForFtpCommand(bool is_directory) const;
+
+ // See if the request URL contains a typecode and make us respect it.
+ void DetectTypecode();
+
+ // Runs the state transition loop.
+ int DoLoop(int result);
+
+ // Each of these methods corresponds to a State value. Those with an input
+ // argument receive the result from the previous state. If a method returns
+ // ERR_IO_PENDING, then the result from OnIOComplete will be passed to the
+ // next state method as the result arg.
+ int DoCtrlResolveHost();
+ int DoCtrlResolveHostComplete(int result);
+ int DoCtrlConnect();
+ int DoCtrlConnectComplete(int result);
+ int DoCtrlRead();
+ int DoCtrlReadComplete(int result);
+ int DoCtrlWrite();
+ int DoCtrlWriteComplete(int result);
+ int DoCtrlWriteUSER();
+ int ProcessResponseUSER(const FtpCtrlResponse& response);
+ int DoCtrlWritePASS();
+ int ProcessResponsePASS(const FtpCtrlResponse& response);
+ int DoCtrlWriteSYST();
+ int ProcessResponseSYST(const FtpCtrlResponse& response);
+ int DoCtrlWritePWD();
+ int ProcessResponsePWD(const FtpCtrlResponse& response);
+ int DoCtrlWriteTYPE();
+ int ProcessResponseTYPE(const FtpCtrlResponse& response);
+ int DoCtrlWriteEPSV();
+ int ProcessResponseEPSV(const FtpCtrlResponse& response);
+ int DoCtrlWritePASV();
+ int ProcessResponsePASV(const FtpCtrlResponse& response);
+ int DoCtrlWriteRETR();
+ int ProcessResponseRETR(const FtpCtrlResponse& response);
+ int DoCtrlWriteSIZE();
+ int ProcessResponseSIZE(const FtpCtrlResponse& response);
+ int DoCtrlWriteCWD();
+ int ProcessResponseCWD(const FtpCtrlResponse& response);
+ int ProcessResponseCWDNotADirectory();
+ int DoCtrlWriteLIST();
+ int ProcessResponseLIST(const FtpCtrlResponse& response);
+ int DoCtrlWriteQUIT();
+ int ProcessResponseQUIT(const FtpCtrlResponse& response);
+
+ int DoDataConnect();
+ int DoDataConnectComplete(int result);
+ int DoDataRead();
+ int DoDataReadComplete(int result);
+
+ Command command_sent_;
+
+ CompletionRepeatingCallback io_callback_;
+ CompletionOnceCallback user_callback_;
+
+ NetLogWithSource net_log_;
+ raw_ptr request_;
+ MutableNetworkTrafficAnnotationTag traffic_annotation_;
+ FtpResponseInfo response_;
+
+ raw_ptr resolver_;
+ // Cancels the outstanding request on destruction.
+ std::unique_ptr resolve_request_;
+
+ // User buffer passed to the Read method for control socket.
+ scoped_refptr read_ctrl_buf_;
+
+ std::unique_ptr ctrl_response_buffer_;
+
+ scoped_refptr read_data_buf_;
+ int read_data_buf_len_;
+
+ // Buffer holding the command line to be written to the control socket.
+ scoped_refptr 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 write_buf_;
+
+ int last_error_;
+
+ SystemType system_type_;
+
+ // Data type to be used for the TYPE command.
+ DataType data_type_;
+
+ // Detected resource type (file or directory).
+ ResourceType resource_type_;
+
+ // Initially we favour EPSV over PASV for transfers but should any
+ // EPSV fail, we fall back to PASV for the duration of connection.
+ bool use_epsv_;
+
+ AuthCredentials credentials_;
+
+ // Current directory on the remote server, as returned by last PWD command,
+ // with any trailing slash removed.
+ std::string current_remote_directory_;
+
+ uint16_t data_connection_port_;
+
+ raw_ptr const socket_factory_;
+
+ std::string unescaped_path_;
+
+ std::unique_ptr ctrl_socket_;
+ std::unique_ptr data_socket_;
+
+ State next_state_;
+
+ // State to switch to after data connection is complete.
+ State state_after_data_connect_complete_;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_NETWORK_TRANSACTION_H_
diff --git a/net/ftp/ftp_network_transaction_unittest.cc b/net/ftp/ftp_network_transaction_unittest.cc
new file mode 100644
index 0000000000000..fb2facdd3c653
--- /dev/null
+++ b/net/ftp/ftp_network_transaction_unittest.cc
@@ -0,0 +1,1722 @@
+// Copyright (c) 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_network_transaction.h"
+
+#include "base/containers/circular_deque.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "net/base/io_buffer.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/sys_addrinfo.h"
+#include "net/base/test_completion_callback.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/ftp/ftp_request_info.h"
+#include "net/socket/socket_test_util.h"
+#include "net/test/gtest_util.h"
+#include "net/test/test_with_task_environment.h"
+#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+using net::test::IsError;
+using net::test::IsOk;
+
+namespace {
+
+// Size we use for IOBuffers used to receive data from the test data socket.
+const int kBufferSize = 128;
+
+} // namespace
+
+namespace net {
+
+class FtpSocketDataProvider : public SocketDataProvider {
+ public:
+ enum State {
+ NONE,
+ PRE_USER,
+ PRE_PASSWD,
+ PRE_SYST,
+ PRE_PWD,
+ PRE_TYPE,
+ PRE_SIZE,
+ PRE_LIST_EPSV,
+ PRE_LIST_PASV,
+ PRE_LIST,
+ PRE_RETR,
+ PRE_RETR_EPSV,
+ PRE_RETR_PASV,
+ PRE_CWD,
+ PRE_QUIT,
+ PRE_NOPASV,
+ QUIT
+ };
+
+ FtpSocketDataProvider()
+ : short_read_limit_(0),
+ allow_unconsumed_reads_(false),
+ failure_injection_state_(NONE),
+ multiline_welcome_(false),
+ use_epsv_(true),
+ data_type_('I') {
+ Init();
+ }
+ ~FtpSocketDataProvider() override = default;
+
+ // SocketDataProvider implementation.
+ MockRead OnRead() override {
+ if (reads_.empty())
+ return MockRead(SYNCHRONOUS, ERR_UNEXPECTED);
+ MockRead result = reads_.front();
+ if (short_read_limit_ == 0 || result.data_len <= short_read_limit_) {
+ reads_.pop_front();
+ } else {
+ result.data_len = short_read_limit_;
+ reads_.front().data += result.data_len;
+ reads_.front().data_len -= result.data_len;
+ }
+ return result;
+ }
+
+ MockWriteResult OnWrite(const std::string& data) override {
+ if (InjectFault())
+ return MockWriteResult(ASYNC, data.length());
+ switch (state()) {
+ case PRE_USER:
+ return Verify("USER anonymous\r\n", data, PRE_PASSWD,
+ "331 Password needed\r\n");
+ case PRE_PASSWD:
+ {
+ static const char response_one[] = "230 Welcome\r\n";
+ static const char response_multi[] =
+ "230- One\r\n230- Two\r\n230 Three\r\n";
+ return Verify("PASS chrome@example.com\r\n", data, PRE_SYST,
+ multiline_welcome_ ? response_multi : response_one);
+ }
+ case PRE_SYST:
+ return Verify("SYST\r\n", data, PRE_PWD, "215 UNIX\r\n");
+ case PRE_PWD:
+ return Verify("PWD\r\n", data, PRE_TYPE,
+ "257 \"/\" is your current location\r\n");
+ case PRE_TYPE:
+ return Verify(std::string("TYPE ") + data_type_ + "\r\n", data,
+ PRE_SIZE, "200 TYPE set successfully\r\n");
+ case PRE_LIST_EPSV:
+ return Verify("EPSV\r\n", data, PRE_LIST,
+ "227 Entering Extended Passive Mode (|||31744|)\r\n");
+ case PRE_LIST_PASV:
+ return Verify("PASV\r\n", data, PRE_LIST,
+ "227 Entering Passive Mode 127,0,0,1,123,123\r\n");
+ case PRE_RETR_EPSV:
+ return Verify("EPSV\r\n", data, PRE_RETR,
+ "227 Entering Extended Passive Mode (|||31744|)\r\n");
+ case PRE_RETR_PASV:
+ return Verify("PASV\r\n", data, PRE_RETR,
+ "227 Entering Passive Mode 127,0,0,1,123,123\r\n");
+ case PRE_NOPASV:
+ // Use unallocated 599 FTP error code to make sure it falls into the
+ // generic ERR_FTP_FAILED bucket.
+ return Verify("PASV\r\n", data, PRE_QUIT,
+ "599 fail\r\n");
+ case PRE_QUIT:
+ return Verify("QUIT\r\n", data, QUIT, "221 Goodbye.\r\n");
+ default:
+ NOTREACHED() << "State not handled " << state();
+ return MockWriteResult(ASYNC, ERR_UNEXPECTED);
+ }
+ }
+
+ void InjectFailure(State state, State next_state, const char* response) {
+ DCHECK_EQ(NONE, failure_injection_state_);
+ DCHECK_NE(NONE, state);
+ DCHECK_NE(NONE, next_state);
+ DCHECK_NE(state, next_state);
+ failure_injection_state_ = state;
+ failure_injection_next_state_ = next_state;
+ fault_response_ = response;
+ }
+
+ State state() const {
+ return state_;
+ }
+
+ void Reset() override {
+ reads_.clear();
+ Init();
+ }
+
+ bool AllReadDataConsumed() const override { return state_ == QUIT; }
+
+ bool AllWriteDataConsumed() const override { return state_ == QUIT; }
+
+ void set_multiline_welcome(bool multiline) { multiline_welcome_ = multiline; }
+
+ bool use_epsv() const { return use_epsv_; }
+ void set_use_epsv(bool use_epsv) { use_epsv_ = use_epsv; }
+
+ void set_data_type(char data_type) { data_type_ = data_type; }
+
+ int short_read_limit() const { return short_read_limit_; }
+ void set_short_read_limit(int limit) { short_read_limit_ = limit; }
+
+ void set_allow_unconsumed_reads(bool allow) {
+ allow_unconsumed_reads_ = allow;
+ }
+
+ protected:
+ void Init() {
+ state_ = PRE_USER;
+ SimulateRead("220 host TestFTPd\r\n");
+ }
+
+ // If protocol fault injection has been requested, adjusts state and mocked
+ // read and returns true.
+ bool InjectFault() {
+ if (state_ != failure_injection_state_)
+ return false;
+ SimulateRead(fault_response_);
+ state_ = failure_injection_next_state_;
+ return true;
+ }
+
+ MockWriteResult Verify(const std::string& expected,
+ const std::string& data,
+ State next_state,
+ const char* next_read,
+ const size_t next_read_length) {
+ EXPECT_EQ(expected, data);
+ if (expected == data) {
+ state_ = next_state;
+ SimulateRead(next_read, next_read_length);
+ return MockWriteResult(ASYNC, data.length());
+ }
+ return MockWriteResult(ASYNC, ERR_UNEXPECTED);
+ }
+
+ MockWriteResult Verify(const std::string& expected,
+ const std::string& data,
+ State next_state,
+ const char* next_read) {
+ return Verify(expected, data, next_state,
+ next_read, std::strlen(next_read));
+ }
+
+ // The next time there is a read from this socket, it will return |data|.
+ // Before calling SimulateRead next time, the previous data must be consumed.
+ void SimulateRead(const char* data, size_t length) {
+ if (!allow_unconsumed_reads_) {
+ EXPECT_TRUE(reads_.empty()) << "Unconsumed read: " << reads_.front().data;
+ }
+ reads_.push_back(MockRead(ASYNC, data, length));
+ }
+ void SimulateRead(const char* data) { SimulateRead(data, std::strlen(data)); }
+
+ private:
+ // List of reads to be consumed.
+ base::circular_deque 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,
+ public WithTaskEnvironment {
+ public:
+ FtpNetworkTransactionTest() : host_resolver_(new MockHostResolver) {
+ SetUpTransaction();
+
+ scoped_refptr 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();
+ transaction_ = std::make_unique(
+ 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 data_socket =
+ std::make_unique(data_reads,
+ base::span());
+ 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 io_buffer =
+ base::MakeRefCounted(kBufferSize);
+ memset(io_buffer->data(), 0, kBufferSize);
+ ASSERT_EQ(ERR_IO_PENDING, transaction_->Read(io_buffer.get(), kBufferSize,
+ callback_.callback()));
+ ASSERT_EQ(static_cast(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 host_resolver_;
+ std::unique_ptr mock_socket_factory_;
+ std::unique_ptr transaction_;
+ TestCompletionCallback callback_;
+};
+
+TEST_P(FtpNetworkTransactionTest, FailedLookup) {
+ FtpRequestInfo request_info = GetRequestInfo("ftp://badhost");
+ scoped_refptr 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());
+ 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(transaction_->data_socket_.get());
+ ASSERT_TRUE(data_socket);
+ ASSERT_TRUE(data_socket->IsConnected());
+
+ // Even if the PASV response specified some other address, we connect
+ // to the address we used for control connection (which could be 127.0.0.1
+ // or ::1 depending on whether we use IPv6).
+ for (auto it = data_socket->addresses().begin();
+ it != data_socket->addresses().end(); ++it) {
+ EXPECT_NE("10.1.2.3", it->ToStringWithoutPort());
+ }
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat1) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||22)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat2) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (||\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat3) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat4) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (||||)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvReallyBadFormat5) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ // Breaking the string in the next line prevents MSVC warning C4125.
+ const char response[] = "227 Portscan (\0\0\031" "773\0)\r\n";
+ FtpSocketDataProviderEvilEpsv ctrl_socket(response, sizeof(response)-1,
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort1) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||22|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort2) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||258|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort3) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||772|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvUnsafePort4) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||2049|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvInvalidPort) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|||4294973296|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvWeirdSep) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan ($$$31744$)\r\n",
+ FtpSocketDataProvider::PRE_RETR);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest,
+ DownloadTransactionEvilEpsvWeirdSepUnsafePort) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan ($$$317$)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_UNSAFE_PORT);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilEpsvIllegalHost) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderEvilEpsv ctrl_socket("227 Portscan (|2|::1|31744|)\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadUsername) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("hello%0Aworld", "test");
+ ExecuteTransaction(&ctrl_socket, "ftp://hello%0Aworld:test@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilLoginBadPassword) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello%0Dworld");
+ ExecuteTransaction(&ctrl_socket, "ftp://test:hello%0Dworld@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInLogin) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("hello world", "test");
+ ExecuteTransaction(&ctrl_socket, "ftp://hello%20world:test@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionSpaceInPassword) {
+ FtpSocketDataProviderEvilLogin ctrl_socket("test", "hello world");
+ ExecuteTransaction(&ctrl_socket, "ftp://test:hello%20world@host/file", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, FailOnInvalidUrls) {
+ const char* const kBadUrls[]{
+ // Make sure FtpNetworkTransaction doesn't request paths like
+ // "/foo/../bar". Doing so wouldn't be a security issue, client side, but
+ // just doesn't seem like a good idea.
+ "ftp://host/foo%2f..%2fbar%5c",
+
+ // LF
+ "ftp://host/foo%10.txt",
+ // CR
+ "ftp://host/foo%13.txt",
+
+ "ftp://host/foo%00.txt",
+ };
+
+ for (const char* bad_url : kBadUrls) {
+ SCOPED_TRACE(bad_url);
+
+ SetUpTransaction();
+ FtpRequestInfo request_info = GetRequestInfo(bad_url);
+ ASSERT_EQ(
+ ERR_INVALID_URL,
+ transaction_->Start(&request_info, callback_.callback(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ }
+}
+
+TEST_P(FtpNetworkTransactionTest, EvilRestartUser) {
+ FtpSocketDataProvider ctrl_socket1;
+ ctrl_socket1.InjectFailure(FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "530 Login authentication failed\r\n");
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket1);
+
+ FtpRequestInfo request_info = GetRequestInfo("ftp://host/file");
+
+ ASSERT_EQ(
+ ERR_IO_PENDING,
+ transaction_->Start(&request_info, callback_.callback(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsError(ERR_FTP_FAILED));
+
+ MockRead ctrl_reads[] = {
+ MockRead("220 host TestFTPd\r\n"),
+ MockRead("221 Goodbye!\r\n"),
+ MockRead(SYNCHRONOUS, OK),
+ };
+ MockWrite ctrl_writes[] = {
+ MockWrite("QUIT\r\n"),
+ };
+ StaticSocketDataProvider ctrl_socket2(ctrl_reads, ctrl_writes);
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket2);
+ ASSERT_EQ(ERR_IO_PENDING, transaction_->RestartWithAuth(
+ AuthCredentials(u"foo\nownz0red", u"innocent"),
+ callback_.callback()));
+ EXPECT_THAT(callback_.WaitForResult(), IsError(ERR_MALFORMED_IDENTITY));
+}
+
+TEST_P(FtpNetworkTransactionTest, EvilRestartPassword) {
+ FtpSocketDataProvider ctrl_socket1;
+ ctrl_socket1.InjectFailure(FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "530 Login authentication failed\r\n");
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket1);
+
+ FtpRequestInfo request_info = GetRequestInfo("ftp://host/file");
+
+ ASSERT_EQ(
+ ERR_IO_PENDING,
+ transaction_->Start(&request_info, callback_.callback(),
+ NetLogWithSource(), TRAFFIC_ANNOTATION_FOR_TESTS));
+ ASSERT_THAT(callback_.WaitForResult(), IsError(ERR_FTP_FAILED));
+
+ MockRead ctrl_reads[] = {
+ MockRead("220 host TestFTPd\r\n"),
+ MockRead("331 User okay, send password\r\n"),
+ MockRead("221 Goodbye!\r\n"),
+ MockRead(SYNCHRONOUS, OK),
+ };
+ MockWrite ctrl_writes[] = {
+ MockWrite("USER innocent\r\n"),
+ MockWrite("QUIT\r\n"),
+ };
+ StaticSocketDataProvider ctrl_socket2(ctrl_reads, ctrl_writes);
+ mock_socket_factory_->AddSocketDataProvider(&ctrl_socket2);
+ ASSERT_EQ(ERR_IO_PENDING, transaction_->RestartWithAuth(
+ AuthCredentials(u"innocent", u"foo\nownz0red"),
+ callback_.callback()));
+ EXPECT_THAT(callback_.WaitForResult(), IsError(ERR_MALFORMED_IDENTITY));
+}
+
+TEST_P(FtpNetworkTransactionTest, Escaping) {
+ const struct TestCase {
+ const char* url;
+ const char* expected_path;
+ } kTestCases[] = {
+ {"ftp://host/%20%21%22%23%24%25%79%80%81", "/ !\"#$%y\200\201"},
+ // This is no allowed to be unescaped by UnescapeURLComponent, since it's
+ // a lock icon. But it has no special meaning or security concern in the
+ // context of making FTP requests.
+ {"ftp://host/%F0%9F%94%92", "/\xF0\x9F\x94\x92"},
+ // Invalid UTF-8 character, which again has no special meaning over FTP.
+ {"ftp://host/%81", "/\x81"},
+ };
+
+ for (const auto& test_case : kTestCases) {
+ SCOPED_TRACE(test_case.url);
+
+ SetUpTransaction();
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.set_file_path(test_case.expected_path);
+ ExecuteTransaction(&ctrl_socket, test_case.url, OK);
+ }
+}
+
+// Test for http://crbug.com/23794.
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionEvilSize) {
+ // Try to overflow int64_t in the response.
+ FtpSocketDataProviderEvilSize ctrl_socket(
+ "213 99999999999999999999999999999999\r\n",
+ FtpSocketDataProvider::PRE_QUIT);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+}
+
+// Test for http://crbug.com/36360.
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionBigSize) {
+ // Pass a valid, but large file size. The transaction should not fail.
+ FtpSocketDataProviderEvilSize ctrl_socket(
+ "213 3204427776\r\n",
+ FtpSocketDataProvider::PRE_CWD);
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", OK);
+ EXPECT_EQ(3204427776LL,
+ transaction_->GetResponseInfo()->expected_content_size);
+}
+
+// Regression test for http://crbug.com/25023.
+TEST_P(FtpNetworkTransactionTest, CloseConnection) {
+ FtpSocketDataProvider ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_USER,
+ FtpSocketDataProvider::PRE_QUIT, "");
+ ExecuteTransaction(&ctrl_socket, "ftp://host", ERR_EMPTY_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailUser) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_USER,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPass) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "530 Login authentication failed\r\n",
+ ERR_FTP_FAILED);
+}
+
+// Regression test for http://crbug.com/38707.
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPass503) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "503 Bad sequence of commands\r\n",
+ ERR_FTP_BAD_COMMAND_SEQUENCE);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailSyst) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_SYST,
+ FtpSocketDataProvider::PRE_PWD,
+ "599 fail\r\n",
+ OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailPwd) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailType) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_TYPE,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailEpsv) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host", FtpSocketDataProvider::PRE_LIST_EPSV,
+ FtpSocketDataProvider::PRE_NOPASV, "599 fail\r\n", ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailCwd) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host",
+ FtpSocketDataProvider::PRE_CWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DirectoryTransactionFailList) {
+ FtpSocketDataProviderVMSDirectoryListing ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/dir",
+ FtpSocketDataProvider::PRE_LIST,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailUser) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_USER,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailPass) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "530 Login authentication failed\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailSyst) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_SYST,
+ FtpSocketDataProvider::PRE_PWD,
+ "599 fail\r\n",
+ OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailPwd) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailType) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_TYPE,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailEpsv) {
+ // This test makes no sense for IPv4 connections (we don't use EPSV there).
+ if (GetFamily() == AF_INET)
+ return;
+
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host/file", FtpSocketDataProvider::PRE_RETR_EPSV,
+ FtpSocketDataProvider::PRE_NOPASV, "599 fail\r\n", ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, DownloadTransactionFailRetr) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ // Use unallocated 599 FTP error code to make sure it falls into the generic
+ // ERR_FTP_FAILED bucket.
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_RETR,
+ FtpSocketDataProvider::PRE_QUIT,
+ "599 fail\r\n",
+ ERR_FTP_FAILED);
+}
+
+TEST_P(FtpNetworkTransactionTest, FileNotFound) {
+ FtpSocketDataProviderFileNotFound ctrl_socket;
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_FTP_FAILED);
+}
+
+// Test for http://crbug.com/38845.
+TEST_P(FtpNetworkTransactionTest, ZeroLengthDirInPWD) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(&ctrl_socket,
+ "ftp://host/file",
+ FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_TYPE,
+ "257 \"\"\r\n",
+ OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, UnexpectedInitiatedResponseForDirectory) {
+ // The states for a directory listing where an initiated response will cause
+ // an error. Includes all commands sent on the directory listing path, except
+ // CWD, SIZE, LIST, and QUIT commands.
+ FtpSocketDataProvider::State kFailingStates[] = {
+ FtpSocketDataProvider::PRE_USER, FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_SYST, FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_TYPE,
+ GetFamily() != AF_INET ? FtpSocketDataProvider::PRE_LIST_EPSV
+ : FtpSocketDataProvider::PRE_LIST_PASV,
+ FtpSocketDataProvider::PRE_CWD,
+ };
+
+ for (FtpSocketDataProvider::State state : kFailingStates) {
+ SetUpTransaction();
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(state, FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+ }
+}
+
+TEST_P(FtpNetworkTransactionTest, UnexpectedInitiatedResponseForFile) {
+ // The states for a download where an initiated response will cause an error.
+ // Includes all commands sent on the file download path, except CWD, SIZE, and
+ // QUIT commands.
+ const FtpSocketDataProvider::State kFailingStates[] = {
+ FtpSocketDataProvider::PRE_USER, FtpSocketDataProvider::PRE_PASSWD,
+ FtpSocketDataProvider::PRE_SYST, FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_TYPE,
+ GetFamily() != AF_INET ? FtpSocketDataProvider::PRE_RETR_EPSV
+ : FtpSocketDataProvider::PRE_RETR_PASV,
+ FtpSocketDataProvider::PRE_CWD};
+
+ for (FtpSocketDataProvider::State state : kFailingStates) {
+ LOG(ERROR) << "??: " << state;
+ SetUpTransaction();
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ ctrl_socket.InjectFailure(state, FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/file", ERR_INVALID_RESPONSE);
+ }
+}
+
+// Make sure that receiving extra unexpected responses correctly results in
+// sending a QUIT message, without triggering a DCHECK.
+TEST_P(FtpNetworkTransactionTest, ExtraResponses) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_TYPE,
+ FtpSocketDataProvider::PRE_QUIT,
+ "157 Foo\r\n"
+ "157 Bar\r\n"
+ "157 Trombones\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+}
+
+// Make sure that receiving extra unexpected responses to a QUIT message
+// correctly results in ending the transaction with an error, without triggering
+// a DCHECK.
+TEST_P(FtpNetworkTransactionTest, ExtraQuitResponses) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_QUIT,
+ FtpSocketDataProvider::QUIT,
+ "221 Foo\r\n"
+ "221 Bar\r\n"
+ "221 Trombones\r\n");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", ERR_INVALID_RESPONSE);
+}
+
+// Test case for https://crbug.com/633841 - similar to the ExtraQuitResponses
+// test case, but with an empty response.
+TEST_P(FtpNetworkTransactionTest, EmptyQuitResponse) {
+ FtpSocketDataProviderDirectoryListing ctrl_socket;
+ ctrl_socket.InjectFailure(FtpSocketDataProvider::PRE_QUIT,
+ FtpSocketDataProvider::QUIT, "");
+ ExecuteTransaction(&ctrl_socket, "ftp://host/", OK);
+}
+
+TEST_P(FtpNetworkTransactionTest, InvalidRemoteDirectory) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host/file", FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "257 \"foo\rbar\" is your current location\r\n", ERR_INVALID_RESPONSE);
+}
+
+TEST_P(FtpNetworkTransactionTest, InvalidRemoteDirectory2) {
+ FtpSocketDataProviderFileDownload ctrl_socket;
+ TransactionFailHelper(
+ &ctrl_socket, "ftp://host/file", FtpSocketDataProvider::PRE_PWD,
+ FtpSocketDataProvider::PRE_QUIT,
+ "257 \"foo\nbar\" is your current location\r\n", ERR_INVALID_RESPONSE);
+}
+
+INSTANTIATE_TEST_SUITE_P(Ftp,
+ FtpNetworkTransactionTest,
+ ::testing::Values(AF_INET, AF_INET6));
+
+} // namespace net
diff --git a/net/ftp/ftp_request_info.h b/net/ftp/ftp_request_info.h
new file mode 100644
index 0000000000000..be7702fd3d9c0
--- /dev/null
+++ b/net/ftp/ftp_request_info.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2010 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_REQUEST_INFO_H_
+#define NET_FTP_FTP_REQUEST_INFO_H_
+
+#include "url/gurl.h"
+
+namespace net {
+
+class FtpRequestInfo {
+ public:
+ // The requested URL.
+ GURL url;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_REQUEST_INFO_H_
diff --git a/net/ftp/ftp_response_info.cc b/net/ftp/ftp_response_info.cc
new file mode 100644
index 0000000000000..51b3c0e3aa367
--- /dev/null
+++ b/net/ftp/ftp_response_info.cc
@@ -0,0 +1,17 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/ftp/ftp_response_info.h"
+
+namespace net {
+
+FtpResponseInfo::FtpResponseInfo()
+ : needs_auth(false),
+ expected_content_size(-1),
+ is_directory_listing(false) {
+}
+
+FtpResponseInfo::~FtpResponseInfo() = default;
+
+} // namespace net
diff --git a/net/ftp/ftp_response_info.h b/net/ftp/ftp_response_info.h
new file mode 100644
index 0000000000000..576ef2501b465
--- /dev/null
+++ b/net/ftp/ftp_response_info.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2010 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_RESPONSE_INFO_H_
+#define NET_FTP_FTP_RESPONSE_INFO_H_
+
+#include
+
+#include "base/time/time.h"
+#include "net/base/ip_endpoint.h"
+#include "net/base/net_export.h"
+
+namespace net {
+
+class NET_EXPORT_PRIVATE FtpResponseInfo {
+ public:
+ FtpResponseInfo();
+ ~FtpResponseInfo();
+
+ // True if authentication failed and valid authentication credentials are
+ // needed.
+ bool needs_auth;
+
+ // The time at which the request was made that resulted in this response.
+ // For cached responses, this time could be "far" in the past.
+ base::Time request_time;
+
+ // The time at which the response headers were received. For cached
+ // responses, this time could be "far" in the past.
+ base::Time response_time;
+
+ // Expected content size, in bytes, as reported by SIZE command. Only valid
+ // for file downloads. -1 means unknown size.
+ int64_t expected_content_size;
+
+ // True if the response data is of a directory listing.
+ bool is_directory_listing;
+
+ // Remote address of the socket which fetched this resource.
+ IPEndPoint remote_endpoint;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_RESPONSE_INFO_H_
diff --git a/net/ftp/ftp_server_type.h b/net/ftp/ftp_server_type.h
new file mode 100644
index 0000000000000..18b0595bd9500
--- /dev/null
+++ b/net/ftp/ftp_server_type.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2009 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_SERVER_TYPE_H_
+#define NET_FTP_FTP_SERVER_TYPE_H_
+
+namespace net {
+
+enum FtpServerType {
+ // Cases in which we couldn't parse the server's response. That means
+ // a server type we don't recognize, a security attack (when what we're
+ // connecting to isn't an FTP server), or a broken server.
+ SERVER_UNKNOWN = 0,
+
+ SERVER_LS = 1, // Server using /bin/ls -l listing style.
+ SERVER_WINDOWS = 2, // Server using Windows listing style.
+ SERVER_VMS = 3, // Server using VMS listing style.
+ SERVER_NETWARE = 4, // OBSOLETE. Server using Netware listing style.
+ SERVER_OS2 = 5, // OBSOLETE. Server using OS/2 listing style.
+
+ NUM_OF_SERVER_TYPES
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_SERVER_TYPE_H_
diff --git a/net/ftp/ftp_transaction.h b/net/ftp/ftp_transaction.h
new file mode 100644
index 0000000000000..dcb67243f9dae
--- /dev/null
+++ b/net/ftp/ftp_transaction.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_TRANSACTION_H_
+#define NET_FTP_FTP_TRANSACTION_H_
+
+#include
+
+#include "net/base/completion_once_callback.h"
+#include "net/base/io_buffer.h"
+#include "net/base/load_states.h"
+#include "net/base/net_export.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+
+namespace net {
+
+class AuthCredentials;
+class FtpResponseInfo;
+class FtpRequestInfo;
+class NetLogWithSource;
+
+// Represents a single FTP transaction.
+class NET_EXPORT_PRIVATE FtpTransaction {
+ public:
+ // Stops any pending IO and destroys the transaction object.
+ virtual ~FtpTransaction() {}
+
+ // Starts the FTP transaction (i.e., sends the FTP request).
+ //
+ // Returns OK if the transaction could be started synchronously, which means
+ // that the request was served from the cache (only supported for directory
+ // listings). ERR_IO_PENDING is returned to indicate that |callback| will be
+ // notified once response info is available or if an IO error occurs. Any
+ // other return value indicates that the transaction could not be started.
+ //
+ // Regardless of the return value, the caller is expected to keep the
+ // request_info object alive until Destroy is called on the transaction.
+ //
+ // NOTE: The transaction is not responsible for deleting the callback object.
+ //
+ // Profiling information for the request is saved to |net_log| if non-NULL.
+ virtual int Start(raw_ptr request_info,
+ CompletionOnceCallback callback,
+ const NetLogWithSource& net_log,
+ const NetworkTrafficAnnotationTag& traffic_annotation) = 0;
+
+ // Restarts the FTP transaction with authentication credentials.
+ virtual int RestartWithAuth(const AuthCredentials& credentials,
+ CompletionOnceCallback callback) = 0;
+
+ // Once response info is available for the transaction, response data may be
+ // read by calling this method.
+ //
+ // Response data is copied into the given buffer and the number of bytes
+ // copied is returned. ERR_IO_PENDING is returned if response data is not yet
+ // available. |callback| is notified when the data copy completes, and it is
+ // passed the number of bytes that were successfully copied. Or, if a read
+ // error occurs, |callback| is notified of the error. Any other negative
+ // return value indicates that the transaction could not be read.
+ //
+ // NOTE: The transaction is not responsible for deleting the callback object.
+ //
+ virtual int Read(IOBuffer* buf,
+ int buf_len,
+ CompletionOnceCallback callback) = 0;
+
+ // Returns the response info for this transaction or NULL if the response
+ // info is not available.
+ virtual const FtpResponseInfo* GetResponseInfo() const = 0;
+
+ // Returns the load state for this transaction.
+ virtual LoadState GetLoadState() const = 0;
+
+ // Returns the upload progress in bytes. If there is no upload data,
+ // zero will be returned.
+ virtual uint64_t GetUploadProgress() const = 0;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_TRANSACTION_H_
diff --git a/net/ftp/ftp_transaction_factory.h b/net/ftp/ftp_transaction_factory.h
new file mode 100644
index 0000000000000..5f4270f25ca7d
--- /dev/null
+++ b/net/ftp/ftp_transaction_factory.h
@@ -0,0 +1,31 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_FTP_FTP_TRANSACTION_FACTORY_H_
+#define NET_FTP_FTP_TRANSACTION_FACTORY_H_
+
+#include
+
+#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 CreateTransaction() = 0;
+
+ // Suspends the creation of new transactions. If |suspend| is false, creation
+ // of new transactions is resumed.
+ virtual void Suspend(bool suspend) = 0;
+};
+
+} // namespace net
+
+#endif // NET_FTP_FTP_TRANSACTION_FACTORY_H_
diff --git a/net/ftp/ftp_util.cc b/net/ftp/ftp_util.cc
new file mode 100644
index 0000000000000..5d06d8d81d459
--- /dev/null
+++ b/net/ftp/ftp_util.cc
@@ -0,0 +1,378 @@
+// Copyright (c) 2011 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef UNSAFE_BUFFERS_BUILD
+// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
+#pragma allow_unsafe_buffers
+#endif
+
+#include "net/ftp/ftp_util.h"
+
+#include