From 817ea85435027b37f7a3ecd9ad1f04749115a32b Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 25 Aug 2026 15:08:30 +0000 Subject: [PATCH 1/4] ext/ftp/tests: add with_data_connection to mock server --- ext/ftp/tests/server.inc | 280 ++++++++++++++++----------------------- 1 file changed, 113 insertions(+), 167 deletions(-) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 2c7a3a8a5643..f6a084974ace 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -48,6 +48,52 @@ if ($pid) { if (!$cwd) $cwd = '/'; } + function with_data_connection(callable $callback) + { + global $s, $pasv, $pasvs, $host, $port, $ssl; + + if (empty($pasv)) { + fputs($s, "150 File status okay; about to open data connection\r\n"); + + $fs = @stream_socket_client("tcp://$host:$port"); + if (!$fs) { + fputs($s, "425 Can't open data connection\r\n"); + return false; + } + } else { + fputs($s, "125 Data connection already open; transfer starting.\r\n"); + + $fs = $pasvs; + } + + if (!empty($ssl)) { + if (!stream_socket_enable_crypto( + $fs, + true, + STREAM_CRYPTO_METHOD_TLS_SERVER + )) { + fclose($fs); + fputs($s, "425 TLS negotiation failed\r\n"); + return false; + } + } + + $e = null; + try { + $callback($fs); + if (is_resource($fs)) fclose($fs); + fputs($s, "226 Closing data Connection.\r\n"); + } catch (Throwable $e) { + if (is_resource($fs)) fclose($fs); + fputs($s, "451 {$e->getMessage()}\r\n"); + } + + $pasv = false; + unset($pasvs); + + return true; + } + $s = stream_socket_accept($socket); if (!$s) die("Error accepting a new connection\n"); @@ -192,65 +238,26 @@ if ($pid) { fputs($s, "200 OK.\r\n"); } elseif (preg_match("~^STOR ([\w/.-]+)\r\n$~", $buf, $m)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); - - if(empty($pasv)) - { - if (!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; - } - + with_data_connection(function ($fs) use ($m, $ascii, &$bug39583) { $data = stream_get_contents($fs); $orig = file_get_contents(dirname(__FILE__).'/'.$m[1]); - if (isset($ascii) && !$ascii && $orig === $data) { - fputs($s, "226 Closing data Connection.\r\n"); - + // binary mode, and data is equal } elseif ((!empty($ascii) || isset($bug39583)) && $data === strtr($orig, array("\r\n" => "\n", "\r" => "\n", "\n" => "\r\n"))) { - fputs($s, "226 Closing data Connection.\r\n"); - - } else { - var_dump($data); - var_dump($orig); - fputs($s, "552 Requested file action aborted.\r\n"); - } - fclose($fs); - }else{ - $data = file_get_contents('nm2.php'); - $orig = file_get_contents(dirname(__FILE__).'/'.$m[1]); - if ( $orig === $data) { - fputs($s, "226 Closing data Connection.\r\n"); - + // ascii mode, data is equal modulo newlines } else { + // data is different var_dump($data); var_dump($orig); - fputs($s, "552 Requested file action aborted.\r\n"); + throw new Exception("uploaded content and local file contain different data"); } - } - + }); } elseif (preg_match("~^APPE ([\w/.-]+)\r\n$~", $buf, $m)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); - - if(empty($pasv)) - { - if (!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; - } - + with_data_connection(function ($fs) use ($m) { $data = stream_get_contents($fs); file_put_contents(__DIR__.'/'.$m[1], $data, FILE_APPEND); - fputs($s, "226 Closing data Connection.\r\n"); - fclose($fs); - }else{ - $data = stream_get_contents($fs); - file_put_contents(__DIR__.'/'.$m[1], $data, FILE_APPEND); - fputs($s, "226 Closing data Connection.\r\n"); - fclose($fs); - } - + }); }elseif (preg_match("~^CWD ([A-Za-z./]+)\r\n$~", $buf, $m)) { if (isset($bug77680)) { fputs($s, "550 Directory change to $m[1] failed: file does not exist\r\n"); @@ -273,29 +280,11 @@ if ($pid) { continue; } - if (empty($pasv)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); - if (!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; + with_data_connection(function ($fs) use ($m, &$nlst_data) { + if (empty($m[1]) || $m[1] !== 'emptydir') { + fputs($fs, $nlst_data ?? "file1\r\nfile1\r\nfile\nb0rk\r\n"); } - } else { - fputs($s, "125 Data connection already open; transfer starting.\r\n"); - $fs=$pasvs; - } - - - if ((!empty($ssl)) && (!stream_socket_enable_crypto($pasvs, true, STREAM_CRYPTO_METHOD_SSLv23_SERVER))) { - die("SSLv23 handshake failed.\n"); - } - - if (empty($m[1]) || $m[1] !== 'emptydir') { - fputs($fs, $nlst_data ?? "file1\r\nfile1\r\nfile\nb0rk\r\n"); - } - - fputs($s, "226 Closing data Connection.\r\n"); - fclose($fs); - + }); } elseif (preg_match("~^MKD ([A-Za-z./]+)\r\n$~", $buf, $m)) { if (isset($bug7216)) { fputs($s, "257 OK.\r\n"); @@ -328,61 +317,53 @@ if ($pid) { break; } }elseif (preg_match('/^RETR ([\/]*[\w\h]+)/', $buf, $matches)) { - if(!empty($pasv)){ - ; - } - else if (!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; - } - switch($matches[1]){ case "pasv": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - //the data connection is handled in another forked process - // called from outside this while loop - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + //the data connection is handled in another forked process + // called from outside this while loop + }); break; case "a story": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - fputs($fs, "For sale: baby shoes, never worn.\r\n"); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + fputs($fs, "For sale: baby shoes, never worn.\r\n"); + }); break; case "binary data": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; - fputs($fs, $transfer_type."Foo\0Bar\r\n"); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) use (&$ascii) { + $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + fputs($fs, $transfer_type."Foo\0Bar\r\n"); + }); break; case "fget": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; - fputs($fs, $transfer_type."FooBar\r\n"); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) use (&$ascii) { + $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + fputs($fs, $transfer_type."FooBar\r\n"); + }); break; case "fgetresume": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; - fputs($fs, "Bar\r\n"); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) use (&$ascii) { + $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + fputs($fs, "Bar\r\n"); + }); break; case "fget_large": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; - if ($GLOBALS['rest_pos'] == '5368709119') { - fputs($fs, "X"); - } else { - fputs($fs, "Y"); - } - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) use (&$ascii) { + $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + if ($GLOBALS['rest_pos'] == '5368709119') { + fputs($fs, "X"); + } else { + fputs($fs, "Y"); + } + }); break; case "mediumfile": - fputs($s, "150 File status okay; about to open data connection.\r\n"); - for($i = 0; $i < 150; $i++){ - fputs($fs, "This is line $i of the test data.\n"); - } - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + for($i = 0; $i < 150; $i++){ + fputs($fs, "This is line $i of the test data.\n"); + } + }); break; case "/bug73457": fputs($s, "150 File status okay; about to open data connection.\r\n"); @@ -394,31 +375,29 @@ if ($pid) { case "crlf_boundary": // A CRLF whose CR lands on the final byte of the first // FTP_BUFSIZE (4096) read, so the LF arrives in the next read. - fputs($s, "150 File status okay; about to open data connection.\r\n"); - fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10)); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + fputs($fs, str_repeat("A", 4095) . "\r\n" . str_repeat("B", 10)); + }); break; case "bare_cr": // A bare CR (not part of CRLF) mid-stream, plus a bare CR on // the final byte of the first FTP_BUFSIZE (4096) read followed // by a non-LF byte in the next read. - fputs($s, "150 File status okay; about to open data connection.\r\n"); - fputs($fs, "line1\r\nba\rre\r\nend" . str_repeat("X", 4078) . "\r" . str_repeat("Y", 10)); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + fputs($fs, "line1\r\nba\rre\r\nend" . str_repeat("X", 4078) . "\r" . str_repeat("Y", 10)); + }); break; case "trailing_cr": // The whole transfer ends on a bare CR. - fputs($s, "150 File status okay; about to open data connection.\r\n"); - fputs($fs, "trail\r"); - fputs($s, "226 Closing data Connection.\r\n"); + with_data_connection(function ($fs) { + fputs($fs, "trail\r"); + }); break; default: fputs($s, "550 {$matches[1]}: No such file or directory \r\n"); break; } - if(isset($fs)) - fclose($fs); }elseif (preg_match('/^PASV/', $buf, $matches)) { @@ -449,6 +428,7 @@ if ($pid) { if (empty($bug73457)) { $pasvs = stream_socket_accept($soc,10); + fclose($soc); } } elseif (preg_match('/^EPSV/', $buf, $matches)) { @@ -472,27 +452,9 @@ if ($pid) { fputs($s, "200 " . $matches[1] . " bytes allocated\r\n"); }elseif (preg_match('/^LIST www\//', $buf, $matches)) { - if (empty($pasv)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); - if (!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; - } - } else { - fputs($s, "125 Data connection already open; transfer starting.\r\n"); - $fs = $pasvs; - } - - - if ((!empty($ssl)) && (!stream_socket_enable_crypto($pasvs, true, STREAM_CRYPTO_METHOD_SSLv23_SERVER))) { - die("SSLv23 handshake failed.\n"); - } - - fputs($fs, "file1\r\nfile1\r\nfile\nb0rk\r\n"); - fputs($s, "226 Closing data Connection.\r\n"); - fclose($fs); - - fputs($s, "226 Transfer complete\r\n"); + with_data_connection(function ($fs) { + fputs($fs, "file1\r\nfile1\r\nfile\nb0rk\r\n"); + }); }elseif (preg_match('/^LIST no_exists\//', $buf, $matches)) { fputs($s, "425 Error establishing connection\r\n"); @@ -522,33 +484,17 @@ if ($pid) { continue; } - if(empty($pasv)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); - if(!$fs = stream_socket_client("tcp://$host:$port")) { - fputs($s, "425 Can't open data connection\r\n"); - continue; + with_data_connection(function ($fs) use ($m) { + if(empty($m[1]) || $m[1] !== 'emptydir') { + fputs($fs, "modify=20170127230002;perm=flcdmpe;type=cdir;unique=811U4340002;UNIX.group=33;UNIX.mode=0755;UNIX.owner=33; .\r\n"); + fputs($fs, "modify=20170127230002;perm=flcdmpe;type=pdir;unique=811U4340002;UNIX.group=33;UNIX.mode=0755;UNIX.owner=33; ..\r\n"); + fputs($fs, "modify=20170126121225;perm=adfrw;size=4729;type=file;unique=811U4340CB9;UNIX.group=33;UNIX.mode=0644;UNIX.owner=33; foobar\r\n"); + fputs($fs, "fact=val=ue;empty=; path;name\r\n"); + fputs($fs, "no_space\r\n"); + fputs($fs, "no_semi pathname\r\n"); + fputs($fs, "no_eq; pathname\r\n"); } - } else { - fputs($s, "125 Data connection already open; transfer starting.\r\n"); - $fs = $pasvs; - } - - if((!empty($ssl)) && (!stream_socket_enable_crypto($pasvs, TRUE, STREAM_CRYPTO_METHOD_SSLv23_SERVER))) { - die("SSLv23 handshake failed.\n"); - } - - if(empty($m[1]) || $m[1] !== 'emptydir') { - fputs($fs, "modify=20170127230002;perm=flcdmpe;type=cdir;unique=811U4340002;UNIX.group=33;UNIX.mode=0755;UNIX.owner=33; .\r\n"); - fputs($fs, "modify=20170127230002;perm=flcdmpe;type=pdir;unique=811U4340002;UNIX.group=33;UNIX.mode=0755;UNIX.owner=33; ..\r\n"); - fputs($fs, "modify=20170126121225;perm=adfrw;size=4729;type=file;unique=811U4340CB9;UNIX.group=33;UNIX.mode=0644;UNIX.owner=33; foobar\r\n"); - fputs($fs, "fact=val=ue;empty=; path;name\r\n"); - fputs($fs, "no_space\r\n"); - fputs($fs, "no_semi pathname\r\n"); - fputs($fs, "no_eq; pathname\r\n"); - } - - fputs($s, "226 Closing data Connection.\r\n"); - fclose($fs); + }); }elseif (preg_match('/^SIZE \/bug73457/', $buf)) { fputs($s, "213 10\r\n"); }elseif (preg_match("/^SITE/", $buf)) { From 62336ef9606abc2e30c464e0e4637c8d117252f0 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 25 Aug 2026 18:10:39 +0000 Subject: [PATCH 2/4] Move accepting passive connection to with_data_connection --- ext/ftp/tests/server.inc | 42 +++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index f6a084974ace..7ee84a136e45 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -50,20 +50,23 @@ if ($pid) { function with_data_connection(callable $callback) { - global $s, $pasv, $pasvs, $host, $port, $ssl; + global $s, $pasv, $pasv_listener, $host, $port, $ssl, $bug73457; - if (empty($pasv)) { - fputs($s, "150 File status okay; about to open data connection\r\n"); + fputs($s, "150 File status okay; about to open data connection\r\n"); + $fs = null; + if (empty($pasv)) { $fs = @stream_socket_client("tcp://$host:$port"); - if (!$fs) { - fputs($s, "425 Can't open data connection\r\n"); - return false; - } } else { - fputs($s, "125 Data connection already open; transfer starting.\r\n"); + if (empty($bug73457)) { + $fs = stream_socket_accept($pasv_listener, 10); + fclose($pasv_listener); + } + } - $fs = $pasvs; + if (!$fs) { + fputs($s, "425 Can't open data connection\r\n"); + return false; } if (!empty($ssl)) { @@ -78,7 +81,6 @@ if ($pid) { } } - $e = null; try { $callback($fs); if (is_resource($fs)) fclose($fs); @@ -88,8 +90,9 @@ if ($pid) { fputs($s, "451 {$e->getMessage()}\r\n"); } + $fs = null; $pasv = false; - unset($pasvs); + $pasv_listener = null; return true; } @@ -124,8 +127,8 @@ if ($pid) { exit; } - if (!stream_socket_enable_crypto($s, true, STREAM_CRYPTO_METHOD_SSLv23_SERVER)) { - die("SSLv23 handshake failed.\n"); + if (!stream_socket_enable_crypto($s, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) { + die("TLS handshake failed.\n"); } if (!preg_match('/^PBSZ \d+\r\n$/', $buf = fread($s, 2048))) { @@ -407,16 +410,16 @@ if ($pid) { if (empty($bug73457)) { if (!empty($ssl)) { - $soc = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context); + $pasv_listener = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context); } else { - $soc = stream_socket_server("tcp://127.0.0.1:0"); + $pasv_listener = stream_socket_server("tcp://127.0.0.1:0"); } - if (!$soc) { + if (!$pasv_listener) { echo "$errstr ($errno)\n"; die("could not bind passive port\n"); } - $soc_name = stream_socket_get_name($soc, false); + $soc_name = stream_socket_get_name($pasv_listener, false); $pasv_port = (int) substr($soc_name, strrpos($soc_name, ':') + 1); } else { $pasv_port = 1234; @@ -426,11 +429,6 @@ if ($pid) { $p1 = ($pasv_port-$p2)/((int) 1 << 8); fputs($s, "227 Entering Passive Mode. (127,0,0,1,{$p1},{$p2})\r\n"); - if (empty($bug73457)) { - $pasvs = stream_socket_accept($soc,10); - fclose($soc); - } - } elseif (preg_match('/^EPSV/', $buf, $matches)) { fputs($s, "550 Extended passive mode not supported.\r\n"); } elseif (preg_match('/^SITE EXEC/', $buf, $matches)) { From 3c187dbc44ca995bd3d725896c5e1978b53052cd Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 25 Aug 2026 18:13:06 +0000 Subject: [PATCH 3/4] Use fgets instead of fread FTP is a line-oriented protocol, so fgets is better. --- ext/ftp/tests/server.inc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 7ee84a136e45..14e383b619f7 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -107,7 +107,7 @@ if ($pid) { }); fputs($s, "220----- PHP FTP server 0.3 -----\r\n220 Service ready\r\n"); - $buf = fread($s, 2048); + $buf = fgets($s, 2048); function user_auth($buf) { global $user, $s, $ssl, $bug37799; @@ -131,13 +131,13 @@ if ($pid) { die("TLS handshake failed.\n"); } - if (!preg_match('/^PBSZ \d+\r\n$/', $buf = fread($s, 2048))) { + if (!preg_match('/^PBSZ \d+\r\n$/', $buf = fgets($s, 2048))) { fputs($s, "501 bogus data\r\n"); dump_and_exit($buf); } fputs($s, "200 OK\r\n"); - $buf = fread($s, 2048); + $buf = fgets($s, 2048); if ($buf !== "PROT P\r\n") { fputs($s, "504 Wrong protection.\r\n"); @@ -146,7 +146,7 @@ if ($pid) { fputs($s, "200 OK\r\n"); - $buf = fread($s, 2048); + $buf = fgets($s, 2048); } if ($buf == "AUTH TLS\r\n") { @@ -168,7 +168,7 @@ if ($pid) { } else { fputs($s, "331 User name ok, need password\r\n"); - if (!preg_match('/^PASS (\w+)\r\n$/', $buf = fread($s, 100), $m)) { + if (!preg_match('/^PASS (\w+)\r\n$/', $buf = fgets($s, 100), $m)) { fputs($s, "500 Syntax error, command unrecognized.\r\n"); dump_and_exit($buf); } @@ -188,7 +188,7 @@ if ($pid) { $cwd = '/'; $num_bogus_cmds = 0; - while($buf = fread($s, 4098)) { + while (false !== ($buf = fgets($s, 4098))) { if (!empty($bogus)) { fputs($s, "502 Command not implemented (".$num_bogus_cmds++.").\r\n"); From 66046ab39a0cb02f8bc815e44e2edbb2148365a4 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Tue, 25 Aug 2026 18:18:02 +0000 Subject: [PATCH 4/4] Remove unused $transfer_type --- ext/ftp/tests/server.inc | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ext/ftp/tests/server.inc b/ext/ftp/tests/server.inc index 14e383b619f7..6a1222953ea9 100644 --- a/ext/ftp/tests/server.inc +++ b/ext/ftp/tests/server.inc @@ -346,14 +346,12 @@ if ($pid) { }); break; case "fgetresume": - with_data_connection(function ($fs) use (&$ascii) { - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + with_data_connection(function ($fs) { fputs($fs, "Bar\r\n"); }); break; case "fget_large": - with_data_connection(function ($fs) use (&$ascii) { - $transfer_type = $ascii? 'ASCII' : 'BINARY' ; + with_data_connection(function ($fs) { if ($GLOBALS['rest_pos'] == '5368709119') { fputs($fs, "X"); } else {