matab-panel/scripts/ftp-server.php

279 lines
9.6 KiB
PHP

<?php
set_time_limit(0);
error_reporting(E_ERROR);
$FTP_HOST = '0.0.0.0';
$FTP_PORT = 2121;
$FTP_USER = 'root';
$FTP_PASS = 'Mjoshanitcm7717';
$FTP_ROOT = realpath(__DIR__ . '/../storage/app/public');
$envFile = __DIR__ . '/../.env';
if (file_exists($envFile)) {
foreach (file($envFile) as $line) {
$line = trim($line);
if (strpos($line, '=') === false || strpos($line, '#') === 0) continue;
[$key, $val] = explode('=', $line, 2);
$key = trim($key); $val = trim($val, "\" \t\n\r");
if ($key === 'FTP_PORT') $FTP_PORT = (int)$val;
if ($key === 'FTP_USER') $FTP_USER = $val;
if ($key === 'FTP_PASS') $FTP_PASS = $val;
if ($key === 'FTP_ROOT') $FTP_ROOT = $val;
}
}
if (!$FTP_ROOT || !is_dir($FTP_ROOT)) {
die("ERROR: Root dir not found: {$FTP_ROOT}\n");
}
$server = @stream_socket_server("tcp://{$FTP_HOST}:{$FTP_PORT}", $errno, $errstr);
if (!$server) {
die("ERROR: Cannot start FTP server on port {$FTP_PORT}: {$errstr}\n"
. "Hint: Run as Administrator or change FTP_PORT in .env\n");
}
echo "=========================================\n";
echo " matab-panel FTP Server\n";
echo "=========================================\n";
echo "Port : {$FTP_PORT}\n";
echo "User : {$FTP_USER}\n";
echo "Root : {$FTP_ROOT}\n";
echo "Press Ctrl+C to stop\n\n";
while ($ctrl = stream_socket_accept($server, -1)) {
$peer = stream_socket_get_name($ctrl, true);
echo "[" . date('H:i:s') . "] Connected: {$peer}\n";
try {
ftpSession($ctrl, $FTP_USER, $FTP_PASS, $FTP_ROOT);
} catch (\Throwable $e) {
echo "Error: " . $e->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;
ftpSend($ctrl, '220 matab-panel FTP Server');
while (!feof($ctrl)) {
$line = ftpRead($ctrl);
if ($line === '') continue;
$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(49152, 65534);
$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);
$data = ftpOpenData($pasvSock);
if (!$data) { ftpSend($ctrl, '425 No data connection'); break; }
ftpSend($ctrl, '150 Ready to receive');
$fp = fopen($full, 'wb');
if (!$fp) { fclose($data); ftpSend($ctrl, '550 Cannot write file'); break; }
while (!feof($data)) {
$buf = fread($data, 65536);
if ($buf !== false && $buf !== '') fwrite($fp, $buf);
}
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 'NOOP':
ftpSend($ctrl, '200 OK');
break;
case 'QUIT':
ftpSend($ctrl, '221 Goodbye');
return;
default:
ftpSend($ctrl, '502 Not implemented: ' . $cmd);
}
}
}