Description: Add CRLF to MLSD response and associated tests
 CRLF omitted from MLSD response in violation of protocol. Certain clients
 fail to receive MLSD as a result.
Author: Brian Morton <bmorton@dvidshub.net>
Origin: upstream, https://github.com/proftpd/proftpd/pull/152
Bug: http://bugs.proftpd.org/show_bug.cgi?id=4202
Bug-Ubuntu: https://launchpad.net/bugs/1613737
Last-Update: 2018-03-06

--- a/modules/mod_facts.c
+++ b/modules/mod_facts.c
@@ -49,6 +49,7 @@ static unsigned long facts_opts = 0;
 static unsigned long facts_mlinfo_opts = 0;
 #define FACTS_MLINFO_FL_SHOW_SYMLINKS			0x00001
 #define FACTS_MLINFO_FL_SHOW_SYMLINKS_USE_SLINK		0x00002
+#define FACTS_MLINFO_FL_APPEND_CRLF			0x00008
 
 struct mlinfo {
   pool *pool;
@@ -203,7 +204,8 @@ static time_t facts_mktime(unsigned int
   return res;
 }
 
-static size_t facts_mlinfo_fmt(struct mlinfo *info, char *buf, size_t bufsz) {
+static size_t facts_mlinfo_fmt(struct mlinfo *info, char *buf, size_t bufsz,
+ int flags) {
   char *ptr;
   size_t buflen = 0;
 
@@ -272,8 +274,8 @@ static size_t facts_mlinfo_fmt(struct ml
    * But MLSD entries DO need the trailing LF, so that it can be converted
    * into a CRLF sequence by pr_data_xfer().
    */
-  if (strcmp(session.curr_cmd, C_MLSD) == 0) {
-    snprintf(ptr, bufsz - buflen, " %s\n", info->path);
+  if (flags & FACTS_MLINFO_FL_APPEND_CRLF) {
+    snprintf(ptr, bufsz - buflen, " %s\r\n", info->path);
 
   } else {
     snprintf(ptr, bufsz - buflen, " %s", info->path);
@@ -311,11 +313,11 @@ static void facts_mlinfobuf_init(void) {
   mlinfo_buflen = 0;
 }
 
-static void facts_mlinfobuf_add(struct mlinfo *info) {
+static void facts_mlinfobuf_add(struct mlinfo *info, int flags) {
   char buf[PR_TUNABLE_BUFFER_SIZE];
   size_t buflen;
  
-  buflen = facts_mlinfo_fmt(info, buf, sizeof(buf));
+  buflen = facts_mlinfo_fmt(info, buf, sizeof(buf), flags);
 
   /* If this buffer will exceed the capacity of mlinfo_buf, then flush
    * mlinfo_buf.
@@ -538,10 +540,10 @@ static int facts_mlinfo_get(struct mlinf
   return 0;
 }
 
-static void facts_mlinfo_add(struct mlinfo *info) {
+static void facts_mlinfo_add(struct mlinfo *info, int flags) {
   char buf[PR_TUNABLE_BUFFER_SIZE];
 
-  (void) facts_mlinfo_fmt(info, buf, sizeof(buf));
+  (void) facts_mlinfo_fmt(info, buf, sizeof(buf), flags);
 
   /* The trailing CRLF will be added by pr_response_add(). */
   pr_response_add(R_DUP, "%s", buf);
@@ -1292,7 +1294,7 @@ MODRET facts_mlsd(cmd_rec *cmd) {
      */
     info.path = pr_fs_encode_path(cmd->tmp_pool, dent->d_name);
 
-    facts_mlinfobuf_add(&info);
+    facts_mlinfobuf_add(&info, FACTS_MLINFO_FL_APPEND_CRLF);
 
     if (XFER_ABORTED) {
       pr_data_abort(0, 0);
@@ -1458,7 +1460,7 @@ MODRET facts_mlst(cmd_rec *cmd) {
   }
 
   pr_response_add(R_250, _("Start of list for %s"), path);
-  facts_mlinfo_add(&info);
+  facts_mlinfo_add(&info, 0);
   pr_response_add(R_250, _("End of list"));
 
   return PR_HANDLED(cmd);
--- a/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm
+++ b/tests/t/lib/ProFTPD/Tests/Commands/MLSD.pm
@@ -188,6 +188,7 @@ sub mlsd_ok_raw_active {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw();
       unless ($conn) {
@@ -202,7 +203,10 @@ sub mlsd_ok_raw_active {
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
+      $self->assert(scalar(@$lines) > 1,
+        test_msg("Expected several MLSD lines, got " . scalar(@$lines)));
+
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -337,6 +341,7 @@ sub mlsd_ok_raw_passive {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port, 1);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw();
       unless ($conn) {
@@ -351,7 +356,7 @@ sub mlsd_ok_raw_passive {
       # We have to be careful of the fact that readdir returns directory
       # entries in an unordered fashion.
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -488,6 +493,7 @@ sub mlsd_fails_file {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($test_file);
       if ($conn) {
@@ -610,6 +616,7 @@ sub mlsd_ok_dir {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($home_dir);
       unless ($conn) {
@@ -622,7 +629,7 @@ sub mlsd_ok_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -740,6 +747,7 @@ sub mlsd_ok_chrooted_dir {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('/');
       unless ($conn) {
@@ -752,7 +760,7 @@ sub mlsd_ok_chrooted_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -870,6 +878,7 @@ sub mlsd_ok_empty_dir {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($test_dir);
       unless ($conn) {
@@ -882,7 +891,7 @@ sub mlsd_ok_empty_dir {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$2} = $1;
@@ -1010,6 +1019,7 @@ sub mlsd_ok_no_path {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw();
       unless ($conn) {
@@ -1022,7 +1032,7 @@ sub mlsd_ok_no_path {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -1140,6 +1150,7 @@ sub mlsd_ok_glob {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd('?foo*');
       unless ($conn) {
@@ -1357,6 +1368,7 @@ sub mlsd_fails_enoent {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my ($resp_code, $resp_msg);
       $client->port();
@@ -1637,6 +1649,7 @@ sub mlsd_ok_hidden_file {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw($home_dir);
       unless ($conn) {
@@ -1649,7 +1662,7 @@ sub mlsd_ok_hidden_file {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -1771,6 +1784,7 @@ sub mlsd_ok_path_with_spaces {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('test foo');
       unless ($conn) {
@@ -1783,7 +1797,7 @@ sub mlsd_ok_path_with_spaces {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -1901,6 +1915,7 @@ sub mlsd_nonascii_chars_bug3032 {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw("test\b");
       unless ($conn) {
@@ -1913,7 +1928,7 @@ sub mlsd_nonascii_chars_bug3032 {
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=\S+;UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$1} = 1;
@@ -2059,6 +2074,7 @@ sub mlsd_symlink_showsymlinks_off_bug331
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2070,7 +2086,7 @@ sub mlsd_symlink_showsymlinks_off_bug331
       $conn->read($buf, 8192, 30);
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=\S+;unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$2} = $1;
@@ -2225,6 +2241,7 @@ sub mlsd_symlink_showsymlinks_on_bug3318
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
 
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2237,7 +2254,7 @@ sub mlsd_symlink_showsymlinks_on_bug3318
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$3} = { type => $1, unique => $2 };
@@ -2394,6 +2411,7 @@ sub mlsd_symlinked_dir_bug3859 {
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2561,6 +2579,7 @@ sub mlsd_symlinked_dir_showsymlinks_off_
     eval {
       my $client = ProFTPD::TestSuite::FTP->new('127.0.0.1', $port);
       $client->login($user, $passwd);
+      $client->type('binary');
 
       my $conn = $client->mlsd_raw('foo');
       unless ($conn) {
@@ -2573,7 +2592,7 @@ sub mlsd_symlinked_dir_showsymlinks_off_
       eval { $conn->close() };
 
       my $res = {};
-      my $lines = [split(/\n/, $buf)];
+      my $lines = [split(/\r\n/, $buf)];
       foreach my $line (@$lines) {
         if ($line =~ /^modify=\S+;perm=\S+;type=(\S+);unique=(\S+);UNIX\.group=\d+;UNIX\.mode=\d+;UNIX.owner=\d+; (.*?)$/) {
           $res->{$3} = { type => $1, unique => $2 };
