279 lines
9.6 KiB
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);
|
|
}
|
|
}
|
|
}
|