487 lines
15 KiB
Bash
487 lines
15 KiB
Bash
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
PHPMYADMIN_PRIVATE_DIR="${PHPMYADMIN_PRIVATE_DIR:-${PHPMYADMIN_DIR:-/var/www/html/alt-mysql-phpmyadmin}}"
|
|
PHPMYADMIN_VERSION="${PHPMYADMIN_VERSION:-5.2.3}"
|
|
PHPMYADMIN_TARBALL_URL="${PHPMYADMIN_TARBALL_URL:-https://files.phpmyadmin.net/phpMyAdmin/5.2.3/phpMyAdmin-5.2.3-all-languages.tar.gz}"
|
|
PHPMYADMIN_DOWNLOAD_DIR="${PHPMYADMIN_DOWNLOAD_DIR:-/usr/local/src}"
|
|
PHPMYADMIN_SOURCE_DIR="${PHPMYADMIN_SOURCE_DIR:-}"
|
|
RUNTIME_DIR="${PHPMYADMIN_PRIVATE_DIR}/runtime"
|
|
TICKETS_DIR="${RUNTIME_DIR}/tickets"
|
|
CONFIG_INC="${PHPMYADMIN_PRIVATE_DIR}/config.inc.php"
|
|
SIGNON_SCRIPT="${PHPMYADMIN_PRIVATE_DIR}/da_signon.php"
|
|
SIGNON_URL_PAGE="${PHPMYADMIN_PRIVATE_DIR}/da_login.php"
|
|
BLOWFISH_SECRET_FILE="${RUNTIME_DIR}/blowfish_secret"
|
|
|
|
if [ "${PHPMYADMIN_INSTALL_ASSUME_ROOT:-0}" != "1" ] && [ "${EUID:-$(id -u)}" -ne 0 ]; then
|
|
echo "Skrypt musi być uruchomiony jako root." >&2
|
|
exit 1
|
|
fi
|
|
|
|
log() {
|
|
printf 'phpMyAdmin alt-mysql: %s\n' "$*"
|
|
}
|
|
|
|
die() {
|
|
printf 'phpMyAdmin alt-mysql: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
install_private_copy_from_source() {
|
|
local source_dir="$1"
|
|
[ -d "$source_dir" ] || die "Katalog źródłowy phpMyAdmin nie istnieje: $source_dir"
|
|
[ -f "$source_dir/index.php" ] || die "Katalog źródłowy nie wygląda jak phpMyAdmin: $source_dir"
|
|
|
|
mkdir -p "$PHPMYADMIN_PRIVATE_DIR"
|
|
cp -a "$source_dir"/. "$PHPMYADMIN_PRIVATE_DIR"/
|
|
}
|
|
|
|
download_private_copy() {
|
|
local tarball extracted tmp_dir
|
|
|
|
command -v curl >/dev/null 2>&1 || die "Brak curl; ustaw PHPMYADMIN_SOURCE_DIR albo doinstaluj curl."
|
|
command -v tar >/dev/null 2>&1 || die "Brak tar."
|
|
|
|
mkdir -p "$PHPMYADMIN_DOWNLOAD_DIR"
|
|
tarball="$PHPMYADMIN_DOWNLOAD_DIR/phpMyAdmin-${PHPMYADMIN_VERSION}-all-languages.tar.gz"
|
|
if [ ! -s "$tarball" ]; then
|
|
log "Pobieram prywatny phpMyAdmin ${PHPMYADMIN_VERSION}."
|
|
curl -fL --retry 3 --connect-timeout 20 -o "$tarball" "$PHPMYADMIN_TARBALL_URL"
|
|
fi
|
|
|
|
tmp_dir="$(mktemp -d)"
|
|
tar -xzf "$tarball" -C "$tmp_dir"
|
|
extracted="$(find "$tmp_dir" -mindepth 1 -maxdepth 1 -type d -name 'phpMyAdmin-*' -print -quit)"
|
|
[ -n "$extracted" ] || {
|
|
rm -rf "$tmp_dir"
|
|
die "Tarball phpMyAdmin nie zawiera oczekiwanego katalogu."
|
|
}
|
|
|
|
install_private_copy_from_source "$extracted"
|
|
rm -rf "$tmp_dir"
|
|
}
|
|
|
|
ensure_private_copy() {
|
|
if [ -f "$PHPMYADMIN_PRIVATE_DIR/index.php" ]; then
|
|
return 0
|
|
fi
|
|
|
|
rm -rf "$PHPMYADMIN_PRIVATE_DIR"
|
|
if [ -n "$PHPMYADMIN_SOURCE_DIR" ]; then
|
|
install_private_copy_from_source "$PHPMYADMIN_SOURCE_DIR"
|
|
else
|
|
download_private_copy
|
|
fi
|
|
}
|
|
|
|
generate_secret() {
|
|
if [ -r "$BLOWFISH_SECRET_FILE" ]; then
|
|
local existing
|
|
existing="$(tr -d '\r\n' < "$BLOWFISH_SECRET_FILE")"
|
|
if [ -n "$existing" ]; then
|
|
printf '%s\n' "$existing"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -hex 32
|
|
return 0
|
|
fi
|
|
|
|
tr -dc 'a-f0-9' </dev/urandom | head -c 64
|
|
printf '\n'
|
|
}
|
|
|
|
write_private_config() {
|
|
local secret
|
|
|
|
mkdir -p "$RUNTIME_DIR" "$TICKETS_DIR"
|
|
secret="$(generate_secret)"
|
|
printf '%s\n' "$secret" > "$BLOWFISH_SECRET_FILE"
|
|
chmod 600 "$BLOWFISH_SECRET_FILE" 2>/dev/null || true
|
|
cat > "$RUNTIME_DIR/.htaccess" <<'EOF'
|
|
Require all denied
|
|
Deny from all
|
|
EOF
|
|
cat > "$TICKETS_DIR/.htaccess" <<'EOF'
|
|
Require all denied
|
|
Deny from all
|
|
EOF
|
|
printf '%s\n' '<!doctype html><title>Forbidden</title>' > "$RUNTIME_DIR/index.html"
|
|
printf '%s\n' '<!doctype html><title>Forbidden</title>' > "$TICKETS_DIR/index.html"
|
|
|
|
cat > "$CONFIG_INC" <<'PHP'
|
|
<?php
|
|
declare(strict_types=1);
|
|
|
|
const DA_MYSQL_PMA_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf';
|
|
const DA_MYSQL_PMA_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
|
|
const DA_MYSQL_PMA_BLOWFISH_SECRET_FILE = __DIR__ . '/runtime/blowfish_secret';
|
|
|
|
function da_mysql_phpmyadmin_runtime_config(): array
|
|
{
|
|
if (!is_readable(DA_MYSQL_PMA_RUNTIME_CONFIG)) {
|
|
return [];
|
|
}
|
|
|
|
$raw = file_get_contents(DA_MYSQL_PMA_RUNTIME_CONFIG);
|
|
if (!is_string($raw) || $raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
function da_mysql_phpmyadmin_parse_conf(string $path): array
|
|
{
|
|
$data = [
|
|
'host' => '127.0.0.1',
|
|
'port' => '33033',
|
|
'socket' => '',
|
|
];
|
|
|
|
if (!is_readable($path)) {
|
|
return $data;
|
|
}
|
|
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
|
if ($lines === false) {
|
|
return $data;
|
|
}
|
|
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || $line[0] === '#' || strpos($line, '=') === false) {
|
|
continue;
|
|
}
|
|
[$key, $value] = explode('=', $line, 2);
|
|
$key = strtolower(trim($key));
|
|
$value = trim(trim($value), "\"'");
|
|
if ($key === 'host' && $value !== '') {
|
|
$data['host'] = $value;
|
|
} elseif (($key === 'port' || $key === 'mysql_port') && ctype_digit($value)) {
|
|
$data['port'] = $value;
|
|
} elseif (($key === 'socket' || $key === 'mysql_socket') && $value !== '') {
|
|
$data['socket'] = $value;
|
|
}
|
|
}
|
|
|
|
if ($data['host'] === '' || $data['host'] === 'localhost') {
|
|
$data['host'] = '127.0.0.1';
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
function da_mysql_phpmyadmin_blowfish_secret(): string
|
|
{
|
|
if (is_readable(DA_MYSQL_PMA_BLOWFISH_SECRET_FILE)) {
|
|
$secret = trim((string)file_get_contents(DA_MYSQL_PMA_BLOWFISH_SECRET_FILE));
|
|
if ($secret !== '') {
|
|
return $secret;
|
|
}
|
|
}
|
|
|
|
return 'alt-mysql-phpmyadmin-secret-change-me';
|
|
}
|
|
|
|
function da_mysql_phpmyadmin_url_path(array $runtimeConfig): string
|
|
{
|
|
$path = trim((string)($runtimeConfig['phpmyadmin_url_path'] ?? '/alt-mysql-phpmyadmin'));
|
|
if ($path === '') {
|
|
return '/alt-mysql-phpmyadmin';
|
|
}
|
|
if ($path[0] !== '/') {
|
|
$path = '/' . $path;
|
|
}
|
|
|
|
$path = rtrim($path, '/');
|
|
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
|
|
}
|
|
|
|
function da_mysql_phpmyadmin_public_base_url(array $runtimeConfig): string
|
|
{
|
|
$configured = trim((string)($runtimeConfig['phpmyadmin_public_base_url'] ?? ''));
|
|
if (preg_match('#^https?://#i', $configured) === 1) {
|
|
return rtrim($configured, '/');
|
|
}
|
|
|
|
$scheme = 'http';
|
|
$https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
|
|
if ($https !== '' && $https !== 'off' && $https !== '0') {
|
|
$scheme = 'https';
|
|
} elseif (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') {
|
|
$scheme = 'https';
|
|
} elseif (strtolower((string)($_SERVER['REQUEST_SCHEME'] ?? '')) === 'https') {
|
|
$scheme = 'https';
|
|
}
|
|
|
|
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
|
|
if ($host === '') {
|
|
$host = trim((string)($_SERVER['SERVER_NAME'] ?? 'localhost'));
|
|
}
|
|
if ($host === '') {
|
|
$host = 'localhost';
|
|
}
|
|
|
|
return $scheme . '://' . $host;
|
|
}
|
|
|
|
function da_mysql_phpmyadmin_signon_url(array $runtimeConfig): string
|
|
{
|
|
return da_mysql_phpmyadmin_public_base_url($runtimeConfig)
|
|
. da_mysql_phpmyadmin_url_path($runtimeConfig)
|
|
. '/da_login.php';
|
|
}
|
|
|
|
$runtimeConfig = da_mysql_phpmyadmin_runtime_config();
|
|
$credentialsFile = (string)($runtimeConfig['credentials_file'] ?? DA_MYSQL_PMA_DEFAULT_CONF);
|
|
if ($credentialsFile === '') {
|
|
$credentialsFile = DA_MYSQL_PMA_DEFAULT_CONF;
|
|
}
|
|
$daMysqlServer = da_mysql_phpmyadmin_parse_conf($credentialsFile);
|
|
|
|
$cfg = [];
|
|
$cfg['blowfish_secret'] = da_mysql_phpmyadmin_blowfish_secret();
|
|
$cfg['TempDir'] = __DIR__ . '/runtime/tmp';
|
|
$cfg['LoginCookieValidity'] = 900;
|
|
|
|
$i = 0;
|
|
$i++;
|
|
$cfg['Servers'][$i]['host'] = $daMysqlServer['host'];
|
|
$cfg['Servers'][$i]['port'] = $daMysqlServer['port'];
|
|
$cfg['Servers'][$i]['socket'] = $daMysqlServer['host'] === 'localhost' ? $daMysqlServer['socket'] : '';
|
|
$cfg['Servers'][$i]['auth_type'] = 'signon';
|
|
$cfg['Servers'][$i]['SignonScript'] = __DIR__ . '/da_signon.php';
|
|
$cfg['Servers'][$i]['SignonSession'] = 'DA_MYSQL_PHPMYADMIN';
|
|
$cfg['Servers'][$i]['SignonURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig);
|
|
$cfg['Servers'][$i]['LogoutURL'] = da_mysql_phpmyadmin_signon_url($runtimeConfig);
|
|
$cfg['Servers'][$i]['AllowNoPassword'] = false;
|
|
$cfg['Servers'][$i]['verbose'] = 'DirectAdmin alt-mysql';
|
|
$cfg['Servers'][$i]['only_db'] = '';
|
|
$cfg['ServerDefault'] = 1;
|
|
$cfg['AllowArbitraryServer'] = false;
|
|
PHP
|
|
|
|
cat > "$SIGNON_URL_PAGE" <<'PHP'
|
|
<?php
|
|
declare(strict_types=1);
|
|
|
|
const DA_MYSQL_SESSION = 'DA_MYSQL_PHPMYADMIN';
|
|
const DA_MYSQL_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
|
|
|
|
function da_mysql_login_runtime_config(): array
|
|
{
|
|
if (!is_readable(DA_MYSQL_RUNTIME_CONFIG)) {
|
|
return [];
|
|
}
|
|
|
|
$raw = file_get_contents(DA_MYSQL_RUNTIME_CONFIG);
|
|
if (!is_string($raw) || $raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
function da_mysql_login_url_path(array $runtimeConfig): string
|
|
{
|
|
$path = trim((string)($runtimeConfig['phpmyadmin_url_path'] ?? '/alt-mysql-phpmyadmin'));
|
|
if ($path === '') {
|
|
return '/alt-mysql-phpmyadmin';
|
|
}
|
|
if ($path[0] !== '/') {
|
|
$path = '/' . $path;
|
|
}
|
|
|
|
$path = rtrim($path, '/');
|
|
return $path !== '' ? $path : '/alt-mysql-phpmyadmin';
|
|
}
|
|
|
|
function da_mysql_login_ticket_dir(array $runtimeConfig): string
|
|
{
|
|
$dir = trim((string)($runtimeConfig['ticket_dir'] ?? ''));
|
|
if ($dir !== '') {
|
|
return rtrim($dir, '/');
|
|
}
|
|
|
|
return __DIR__ . '/runtime/tickets';
|
|
}
|
|
|
|
function da_mysql_login_error(string $message): void
|
|
{
|
|
http_response_code(400);
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
$safe = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>alt-mysql phpMyAdmin</title></head><body>';
|
|
echo '<h1>alt-mysql phpMyAdmin</h1><p>' . $safe . '</p>';
|
|
echo '</body></html>';
|
|
exit;
|
|
}
|
|
|
|
function da_mysql_login_consume_ticket(array $runtimeConfig): array
|
|
{
|
|
$token = strtolower((string)($_GET['ticket'] ?? ''));
|
|
if (preg_match('/\A[a-f0-9]{64}\z/', $token) !== 1) {
|
|
da_mysql_login_error('Missing or invalid phpMyAdmin login ticket.');
|
|
}
|
|
|
|
$path = da_mysql_login_ticket_dir($runtimeConfig) . '/' . $token . '.json';
|
|
if (!is_readable($path)) {
|
|
da_mysql_login_error('The phpMyAdmin login ticket is not available or has already been used.');
|
|
}
|
|
|
|
$raw = file_get_contents($path);
|
|
@unlink($path);
|
|
$ticket = is_string($raw) ? json_decode($raw, true) : null;
|
|
if (!is_array($ticket)) {
|
|
da_mysql_login_error('The phpMyAdmin login ticket is invalid.');
|
|
}
|
|
|
|
if ((int)($ticket['expires_at'] ?? 0) < time()) {
|
|
da_mysql_login_error('The phpMyAdmin login ticket has expired.');
|
|
}
|
|
|
|
$auth = $ticket['auth'] ?? [];
|
|
if (!is_array($auth) || (string)($auth['user'] ?? '') === '' || (string)($auth['password'] ?? '') === '') {
|
|
da_mysql_login_error('The phpMyAdmin login ticket does not contain credentials.');
|
|
}
|
|
|
|
return $ticket;
|
|
}
|
|
|
|
function da_mysql_login_redirect_target(array $ticket): string
|
|
{
|
|
$route = (string)($ticket['route'] ?? '/database/structure');
|
|
if (preg_match('#^/[A-Za-z0-9_./-]+$#', $route) !== 1 || strpos($route, '//') !== false) {
|
|
$route = '/';
|
|
}
|
|
|
|
$query = ['route' => $route];
|
|
$db = (string)($ticket['db'] ?? ($ticket['auth']['db'] ?? ''));
|
|
if ($db !== '') {
|
|
$query['db'] = $db;
|
|
}
|
|
|
|
return 'index.php?' . http_build_query($query);
|
|
}
|
|
|
|
$runtimeConfig = da_mysql_login_runtime_config();
|
|
$ticket = da_mysql_login_consume_ticket($runtimeConfig);
|
|
$secure = false;
|
|
$https = strtolower((string)($_SERVER['HTTPS'] ?? ''));
|
|
if ($https !== '' && $https !== 'off' && $https !== '0') {
|
|
$secure = true;
|
|
} elseif (strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https') {
|
|
$secure = true;
|
|
}
|
|
|
|
session_name(DA_MYSQL_SESSION);
|
|
session_set_cookie_params([
|
|
'lifetime' => 0,
|
|
'path' => da_mysql_login_url_path($runtimeConfig),
|
|
'secure' => $secure,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
session_start();
|
|
$_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] = $ticket['auth'];
|
|
session_write_close();
|
|
|
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
header('Pragma: no-cache');
|
|
header('Location: ' . da_mysql_login_redirect_target($ticket), true, 302);
|
|
exit;
|
|
PHP
|
|
|
|
cat > "$SIGNON_SCRIPT" <<'PHP'
|
|
<?php
|
|
declare(strict_types=1);
|
|
|
|
const DA_MYSQL_SESSION = 'DA_MYSQL_PHPMYADMIN';
|
|
const DA_MYSQL_DEFAULT_CONF = '/usr/local/directadmin/conf/alt-mysql.conf';
|
|
const DA_MYSQL_RUNTIME_CONFIG = __DIR__ . '/runtime/config.json';
|
|
|
|
function da_mysql_runtime_config(): array
|
|
{
|
|
if (!is_readable(DA_MYSQL_RUNTIME_CONFIG)) {
|
|
return [];
|
|
}
|
|
|
|
$raw = file_get_contents(DA_MYSQL_RUNTIME_CONFIG);
|
|
if (!is_string($raw) || $raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
function da_mysql_signon_session_auth(): array
|
|
{
|
|
$payload = $_SESSION['DA_MYSQL_PHPMYADMIN_AUTH'] ?? [];
|
|
return is_array($payload) ? $payload : [];
|
|
}
|
|
|
|
function da_mysql_signon_read_auth(): array
|
|
{
|
|
$previousActive = session_status() === PHP_SESSION_ACTIVE;
|
|
$previousName = session_name();
|
|
$previousId = $previousActive ? session_id() : '';
|
|
|
|
if ($previousActive && $previousName === DA_MYSQL_SESSION) {
|
|
return da_mysql_signon_session_auth();
|
|
}
|
|
|
|
if ($previousActive) {
|
|
session_write_close();
|
|
}
|
|
|
|
session_name(DA_MYSQL_SESSION);
|
|
session_id('');
|
|
$ssoSessionId = (string)($_COOKIE[DA_MYSQL_SESSION] ?? '');
|
|
if ($ssoSessionId !== '' && preg_match('/\A[A-Za-z0-9,-]+\z/', $ssoSessionId) === 1) {
|
|
session_id($ssoSessionId);
|
|
}
|
|
session_start();
|
|
$payload = da_mysql_signon_session_auth();
|
|
session_write_close();
|
|
|
|
if ($previousActive) {
|
|
session_name($previousName);
|
|
if ($previousId !== '') {
|
|
session_id($previousId);
|
|
}
|
|
session_start();
|
|
} elseif ($previousName !== '') {
|
|
session_name($previousName);
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
function get_login_credentials($user = ''): array
|
|
{
|
|
$payload = da_mysql_signon_read_auth();
|
|
$username = (string)($payload['user'] ?? '');
|
|
$password = (string)($payload['password'] ?? '');
|
|
return [$username, $password];
|
|
}
|
|
PHP
|
|
|
|
mkdir -p "$RUNTIME_DIR/tmp"
|
|
chown -R diradmin:diradmin "$RUNTIME_DIR" "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" 2>/dev/null || true
|
|
chmod 755 "$RUNTIME_DIR" "$TICKETS_DIR" "$RUNTIME_DIR/tmp" 2>/dev/null || true
|
|
chmod 644 "$CONFIG_INC" "$SIGNON_SCRIPT" "$SIGNON_URL_PAGE" "$RUNTIME_DIR/.htaccess" "$TICKETS_DIR/.htaccess" "$RUNTIME_DIR/index.html" "$TICKETS_DIR/index.html" 2>/dev/null || true
|
|
}
|
|
|
|
ensure_private_copy
|
|
write_private_config
|
|
|
|
log "Prywatna kopia phpMyAdmin dla alt-mysql jest gotowa w ${PHPMYADMIN_PRIVATE_DIR}."
|