getMessage() . "\n"; } fclose($ctrl); echo "[" . date('H:i:s') . "] Disconnected: {$peer}\n"; } fclose($server); function ftpSend($ctrl, string $msg): void { fwrite($ctrl, $msg . "\r\n"); } function ftpRead($ctrl): string { $line = fgets($ctrl, 8192); return $line ? rtrim($line, "\r\n") : ''; } function ftpOpenData(&$pasvSock): mixed { if (!$pasvSock) return null; stream_set_timeout($pasvSock, 15); $data = stream_socket_accept($pasvSock, 15); fclose($pasvSock); $pasvSock = null; return $data ?: null; } function ftpResolvePath(string $cwd, string $arg, string $root): ?string { if ($arg === '') { $path = $cwd; } elseif ($arg[0] === '/') { $path = $arg; } else { $path = rtrim($cwd, '/') . '/' . $arg; } // Normalize $parts = explode('/', $path); $out = []; foreach ($parts as $p) { if ($p === '' || $p === '.') continue; if ($p === '..') { array_pop($out); } else { $out[] = $p; } } $path = '/' . implode('/', $out); $real = realpath($root . $path); if ($real !== false && strpos($real, realpath($root)) !== 0) return null; return $path; } function ftpSession($ctrl, string $user, string $pass, string $root): void { $cwd = '/'; $authed = false; $pendUser = ''; $pasvSock = null; stream_set_timeout($ctrl, 300); ftpSend($ctrl, '220 matab-panel FTP Server'); while (!feof($ctrl)) { $line = ftpRead($ctrl); if ($line === '') { $info = stream_get_meta_data($ctrl); if ($info['timed_out']) continue; break; } $parts = explode(' ', $line, 2); $cmd = strtoupper($parts[0]); $arg = isset($parts[1]) ? trim($parts[1]) : ''; switch ($cmd) { case 'USER': $pendUser = $arg; ftpSend($ctrl, '331 Password required'); break; case 'PASS': if ($pendUser === $user && $arg === $pass) { $authed = true; ftpSend($ctrl, '230 Login successful'); echo " Auth OK\n"; } else { ftpSend($ctrl, '530 Login incorrect'); } break; case 'SYST': ftpSend($ctrl, '215 UNIX Type: L8'); break; case 'FEAT': ftpSend($ctrl, "211-Features:\r\n PASV\r\n211 End"); break; case 'TYPE': ftpSend($ctrl, '200 Type set to ' . ($arg ?: 'I')); break; case 'PWD': case 'XPWD': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } ftpSend($ctrl, '257 "' . $cwd . '"'); break; case 'CWD': case 'XCWD': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $p = ftpResolvePath($cwd, $arg, $root); if ($p !== null && is_dir($root . $p)) { $cwd = $p; ftpSend($ctrl, '250 OK'); } else { ftpSend($ctrl, '550 No such directory'); } break; case 'CDUP': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $cwd = dirname($cwd) ?: '/'; ftpSend($ctrl, '200 OK'); break; case 'MKD': case 'XMKD': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $p = ftpResolvePath($cwd, $arg, $root); if ($p !== null) { $full = $root . $p; if (!is_dir($full)) mkdir($full, 0755, true); ftpSend($ctrl, '257 "' . $p . '" created'); } else { ftpSend($ctrl, '550 Failed'); } break; case 'PASV': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } if ($pasvSock) fclose($pasvSock); $pasvPort = rand(60000, 60020); // fixed range — must match firewall rule $pasvSock = @stream_socket_server("tcp://0.0.0.0:{$pasvPort}", $e1, $e2); if (!$pasvSock) { ftpSend($ctrl, '425 Cannot open passive connection'); break; } $localAddr = stream_socket_get_name($ctrl, false); $localIp = explode(':', $localAddr)[0]; $p1 = intdiv($pasvPort, 256); $p2 = $pasvPort % 256; ftpSend($ctrl, '227 Entering Passive Mode (' . str_replace('.', ',', $localIp) . ",{$p1},{$p2})"); break; case 'LIST': case 'NLST': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $data = ftpOpenData($pasvSock); if (!$data) { ftpSend($ctrl, '425 No data connection'); break; } ftpSend($ctrl, '150 Opening data connection'); $dirPath = $root . $cwd; $list = ''; if (is_dir($dirPath)) { foreach (scandir($dirPath) as $f) { if ($f === '.' || $f === '..') continue; $fp = $dirPath . '/' . $f; $size = is_file($fp) ? filesize($fp) : 0; $type = is_dir($fp) ? 'd' : '-'; $list .= "{$type}rw-r--r-- 1 root root {$size} Jan 1 00:00 {$f}\r\n"; } } fwrite($data, $list); fclose($data); ftpSend($ctrl, '226 Transfer complete'); break; case 'STOR': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $p = ftpResolvePath($cwd, $arg, $root); if ($p === null) { ftpSend($ctrl, '550 Invalid path'); break; } $full = $root . $p; $dir = dirname($full); if (!is_dir($dir)) mkdir($dir, 0755, true); $fp = @fopen($full, 'wb'); if (!$fp) { ftpSend($ctrl, '550 Cannot write file'); break; } $data = ftpOpenData($pasvSock); if (!$data) { fclose($fp); ftpSend($ctrl, '425 No data connection'); break; } ftpSend($ctrl, '150 Ready to receive'); stream_set_timeout($data, 30); stream_copy_to_stream($data, $fp); fclose($fp); fclose($data); echo " STOR: {$p} (" . number_format(filesize($full)) . " bytes)\n"; ftpSend($ctrl, '226 Transfer complete'); break; case 'RETR': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $p = ftpResolvePath($cwd, $arg, $root); if ($p === null || !file_exists($root . $p)) { ftpSend($ctrl, '550 File not found'); break; } $data = ftpOpenData($pasvSock); if (!$data) { ftpSend($ctrl, '425 No data connection'); break; } ftpSend($ctrl, '150 Opening data connection'); $fp = fopen($root . $p, 'rb'); while (!feof($fp)) { $buf = fread($fp, 65536); if ($buf !== false && $buf !== '') fwrite($data, $buf); } fclose($fp); fclose($data); echo " RETR: {$p}\n"; ftpSend($ctrl, '226 Transfer complete'); break; case 'SIZE': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } $p = ftpResolvePath($cwd, $arg, $root); if ($p !== null && file_exists($root . $p)) { ftpSend($ctrl, '213 ' . filesize($root . $p)); } else { ftpSend($ctrl, '550 File not found'); } break; case 'OPTS': ftpSend($ctrl, '200 OK'); break; case 'PBSZ': ftpSend($ctrl, '200 PBSZ=0'); break; case 'PROT': ftpSend($ctrl, '200 Protection level set to ' . ($arg ?: 'C')); break; case 'EPSV': if (!$authed) { ftpSend($ctrl, '530 Not logged in'); break; } if ($pasvSock) fclose($pasvSock); $pasvPort = rand(60000, 60020); $pasvSock = @stream_socket_server("tcp://0.0.0.0:{$pasvPort}", $e1, $e2); if (!$pasvSock) { ftpSend($ctrl, '425 Cannot open passive connection'); break; } ftpSend($ctrl, "229 Entering Extended Passive Mode (|||{$pasvPort}|)"); break; case 'EPRT': ftpSend($ctrl, '200 OK'); break; case 'NOOP': ftpSend($ctrl, '200 OK'); break; case 'QUIT': ftpSend($ctrl, '221 Goodbye'); return; default: echo " [UNKNOWN CMD] {$cmd} {$arg}\n"; ftpSend($ctrl, '502 Not implemented: ' . $cmd); } } }