v1.3.0
AtheraStudio SyncDrop
- <?php
- declare(strict_types=1);
-
- defined('SD_BOOT') || exit;
-
- const SD_SETUP_TTL = 3600;
- const SD_STAGE_TTL = 300;
-
- function sd_setup_code(): string
- {
- $file = sd_path('setup');
- $data = is_file($file) ? json_decode((string)@file_get_contents($file), true) : null;
- if (is_array($data) && isset($data['code'], $data['exp']) && (int)$data['exp'] > time()) return (string)$data['code'];
- $raw = sd_b32_encode(random_bytes(10));
- $code = implode('-', str_split(substr($raw, 0, 16), 4));
- $content = json_encode(['code' => $code, 'exp' => time() + SD_SETUP_TTL, 'instance' => sd_instance(), 'info' => 'AtheraStudio SyncDrop setup code']);
- @file_put_contents($file, $content . "\n", LOCK_EX);
- @chmod($file, 0600);
- return $code;
- }
-
- function sd_norm_code(string $code): string
- {
- return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $code));
- }
-
- function sd_default_target(): ?string
- {
- $doc = sd_docroot();
- $candidates = sd_app() !== $doc ? [$doc] : [];
- for ($dir = dirname($doc); dirname($dir) !== $dir; $dir = dirname($dir)) {
- if (sd_webroot_named($dir, SD_WEBROOTS)) {
- $candidates[] = $dir;
- break;
- }
- }
- foreach ($candidates as $dir) {
- $path = sd_target_path_check($dir, $err);
- if ($path !== null) return $path;
- }
- return null;
- }
-
- function sd_route_setup(): never
- {
- sd_session_start();
- $code = sd_setup_code();
- $errors = [];
- $old = [];
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- $old = ['user' => (string)($_POST['user'] ?? ''), 'email' => (string)($_POST['email'] ?? ''), 'lang' => (string)($_POST['lang'] ?? '')];
- if (sd_locked() > 0) {
- $errors[] = 'err_locked_short';
- } elseif (!sd_csrf_ok() || !sd_honeypot_ok() || !sd_pow_verify('setup')) {
- $errors[] = 'err_antibot';
- } elseif (!hash_equals(sd_norm_code($code), sd_norm_code((string)($_POST['code'] ?? '')))) {
- sd_fail('setup_code');
- $errors[] = 'err_setup_code';
- } else {
- $user = trim($old['user']);
- $email = trim($old['email']);
- $password = (string)($_POST['password'] ?? '');
- $lang = isset(SD_LANGS[$old['lang']]) ? $old['lang'] : sd_lang();
- if (!preg_match('/^[A-Za-z0-9._-]{3,32}$/', $user)) $errors[] = 'err_username';
- if (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) $errors[] = 'err_email';
- if (!sd_password_strong($password, $user)) $errors[] = 'err_password_weak';
- if (!hash_equals($password, (string)($_POST['password2'] ?? ''))) $errors[] = 'err_password_match';
- if (!$errors) {
- $targets = [];
- $main = sd_default_target();
- if ($main !== null) {
- $targets['main'] = ['name' => basename(dirname($main)) ?: basename($main), 'path' => $main, 'exclude' => SD_DEFAULT_EXCLUDES];
- }
- sd_env_save([
- 'SD_USER' => $user,
- 'SD_PASS' => sd_password_hash($password),
- 'SD_EMAIL' => $email,
- 'SD_ALERTS' => 1,
- 'SD_LANG' => $lang,
- 'SD_SECRET' => bin2hex(random_bytes(32)),
- 'SD_EPOCH' => 1,
- 'SD_TOTP' => '',
- 'SD_TOTP_STEP' => 0,
- 'SD_RECOVERY' => [],
- 'SD_TARGETS' => $targets,
- 'SD_CREATED' => gmdate('c'),
- 'SD_INSTANCE' => sd_instance(),
- ]);
- @unlink(sd_path('setup'));
- sd_set_lang($lang);
- sd_session_elevate('enroll');
- $_SESSION['stage_at'] = time();
- sd_audit('setup_done', ['user' => $user]);
- sd_redirect('enroll');
- }
- }
- }
- sd_view_setup($errors, $old);
- exit;
- }
-
- function sd_route_login(): never
- {
- $error = '';
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- $locked = sd_locked();
- if ($locked > 0) {
- sd_view_login('', $locked);
- exit;
- }
- if (!sd_csrf_ok() || !sd_honeypot_ok() || !sd_pow_verify('login')) {
- sd_fail('antibot');
- $error = 'err_antibot';
- } else {
- $env = sd_env();
- $user = (string)($_POST['user'] ?? '');
- $password = (string)($_POST['password'] ?? '');
- $userOk = hash_equals((string)$env['SD_USER'], $user);
- $passOk = password_verify($password, (string)$env['SD_PASS']);
- if ($userOk && $passOk) {
- if (password_needs_rehash((string)$env['SD_PASS'], defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT)) {
- sd_env_set(['SD_PASS' => sd_password_hash($password)]);
- }
- $next = !empty($env['SD_TOTP']) ? '2fa' : 'enroll';
- sd_session_elevate($next);
- $_SESSION['stage_at'] = time();
- $_SESSION['tries'] = 0;
- sd_audit('password_ok');
- sd_redirect($next);
- }
- sd_fail('password');
- $error = 'err_login';
- }
- }
- sd_view_login($error, sd_locked());
- exit;
- }
-
- function sd_remember_host(): void
- {
- $host = sd_host();
- if ($host !== '' && (string)(sd_env()['SD_HOST'] ?? '') !== $host) sd_env_set(['SD_HOST' => $host]);
- }
-
- function sd_stage_fresh(): bool
- {
- return time() - (int)($_SESSION['stage_at'] ?? 0) <= SD_STAGE_TTL;
- }
-
- function sd_complete_login(string $method): never
- {
- sd_session_elevate('full');
- sd_fail_reset();
- sd_shield_trust();
- sd_remember_host();
- sd_audit('login_ok', ['method' => $method]);
- sd_alert('alert_login_subject', 'alert_login_body', ['method' => t('method_' . $method)]);
- sd_redirect('sync');
- }
-
- function sd_route_twofa(): never
- {
- if (!sd_stage_fresh()) {
- sd_session_reset();
- sd_flash('error', t('err_stage_expired'));
- sd_redirect('login');
- }
- $error = '';
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (!sd_csrf_ok()) sd_redirect('login');
- $env = sd_env();
- $input = trim((string)($_POST['code'] ?? ''));
- if (preg_match('/^\d[\d ]{4,6}\d$/', $input)) {
- $step = sd_totp_match((string)$env['SD_TOTP'], $input, (int)($env['SD_TOTP_STEP'] ?? 0));
- if ($step !== null) {
- sd_env_set(['SD_TOTP_STEP' => $step]);
- sd_complete_login('totp');
- }
- } else {
- $norm = sd_recovery_normalize($input);
- $codes = (array)($env['SD_RECOVERY'] ?? []);
- foreach ($codes as $i => $hash) {
- if (strlen($norm) === 12 && password_verify($norm, (string)$hash)) {
- unset($codes[$i]);
- sd_env_set(['SD_RECOVERY' => array_values($codes)]);
- sd_audit('recovery_used', ['left' => count($codes)]);
- sd_alert('alert_recovery_subject', 'alert_recovery_body', ['left' => (string)count($codes)]);
- sd_complete_login('recovery');
- }
- }
- }
- $_SESSION['tries'] = (int)($_SESSION['tries'] ?? 0) + 1;
- sd_fail('totp');
- if ($_SESSION['tries'] >= 5) {
- sd_session_reset();
- sd_flash('error', t('err_too_many'));
- sd_redirect('login');
- }
- $error = 'err_code';
- }
- sd_view_twofa($error);
- exit;
- }
-
- function sd_store_recovery(): array
- {
- $codes = sd_recovery_codes();
- $hashes = array_map(static fn($c) => password_hash(sd_recovery_normalize($c), PASSWORD_BCRYPT, ['cost' => 10]), $codes);
- sd_env_set(['SD_RECOVERY' => $hashes]);
- return $codes;
- }
-
- function sd_route_enroll(): never
- {
- if (!sd_stage_fresh() && empty($_SESSION['enroll_secret'])) {
- sd_session_reset();
- sd_redirect('login');
- }
- $env = sd_env();
- if (!empty($env['SD_TOTP'])) {
- sd_session_reset();
- sd_redirect('login');
- }
- if (empty($_SESSION['enroll_secret'])) $_SESSION['enroll_secret'] = sd_totp_secret();
- $secret = (string)$_SESSION['enroll_secret'];
- $error = '';
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (!sd_csrf_ok()) sd_redirect('login');
- $step = sd_totp_match($secret, (string)($_POST['code'] ?? ''));
- if ($step !== null) {
- sd_env_set(['SD_TOTP' => $secret, 'SD_TOTP_STEP' => $step]);
- $codes = sd_store_recovery();
- sd_session_elevate('full');
- $_SESSION['recovery_show'] = $codes;
- sd_fail_reset();
- sd_shield_trust();
- sd_remember_host();
- sd_audit('totp_enrolled');
- sd_alert('alert_2fa_subject', 'alert_2fa_body');
- sd_redirect('recovery');
- }
- $error = 'err_code';
- }
- sd_view_enroll($secret, sd_totp_uri($secret, (string)$env['SD_USER']), $error);
- exit;
- }
-
- function sd_route_recovery(): never
- {
- $codes = $_SESSION['recovery_show'] ?? null;
- unset($_SESSION['recovery_show']);
- if (!is_array($codes) || !$codes) sd_redirect('sync');
- sd_view_recovery($codes, !empty($_SESSION['recovery_app']));
- unset($_SESSION['recovery_app']);
- exit;
- }
-
- function sd_route_history(): never
- {
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (!sd_csrf_ok()) sd_redirect('history');
- if (($_POST['action'] ?? '') === 'rollback') {
- $result = sd_with_lock(static fn(): array => sd_rollback((string)($_POST['id'] ?? '')));
- if (isset($result['ok'])) {
- sd_flash($result['skipped'] ? 'warn' : 'ok', t($result['skipped'] ? 'msg_rollback_partial' : 'msg_rollback_ok', ['n' => (string)$result['restored'], 's' => (string)count($result['skipped'])]));
- } else {
- sd_flash('error', t('err_rollback'));
- }
- }
- sd_redirect('history');
- }
- sd_view_history();
- exit;
- }
-
- function sd_verify_current_password(): bool
- {
- $ok = password_verify((string)($_POST['current'] ?? ''), (string)(sd_env()['SD_PASS'] ?? ''));
- if (!$ok) sd_fail('settings_password');
- return $ok;
- }
-
- function sd_route_settings(): never
- {
- $errors = [];
- if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (!sd_csrf_ok()) sd_redirect('settings');
- $env = sd_env();
- $action = (string)($_POST['action'] ?? '');
- switch ($action) {
- case 'alerts':
- $email = trim((string)($_POST['email'] ?? ''));
- if (!sd_verify_current_password()) $errors[] = 'err_password_current';
- elseif (!filter_var($email, FILTER_VALIDATE_EMAIL) || preg_match('/[\r\n]/', $email)) $errors[] = 'err_email';
- else {
- $old = (string)$env['SD_EMAIL'];
- sd_env_set(['SD_EMAIL' => $email, 'SD_ALERTS' => !empty($_POST['alerts']) ? 1 : 0]);
- sd_audit('alerts_changed');
- if ($old !== $email) sd_alert('alert_email_subject', 'alert_email_body', ['email' => $email], $old);
- if (!empty($_POST['test'])) {
- $sent = sd_alert('alert_test_subject', 'alert_test_body');
- sd_flash($sent ? 'ok' : 'warn', t($sent ? 'msg_test_sent' : 'msg_test_failed'));
- } else {
- sd_flash('ok', t('msg_saved'));
- }
- sd_redirect('settings');
- }
- break;
- case 'password':
- $new = (string)($_POST['password'] ?? '');
- if (!sd_verify_current_password()) $errors[] = 'err_password_current';
- elseif (!sd_password_strong($new, (string)$env['SD_USER'])) $errors[] = 'err_password_weak';
- elseif (!hash_equals($new, (string)($_POST['password2'] ?? ''))) $errors[] = 'err_password_match';
- else {
- sd_env_set(['SD_PASS' => sd_password_hash($new), 'SD_EPOCH' => (int)$env['SD_EPOCH'] + 1]);
- sd_session_elevate('full');
- sd_audit('password_changed');
- sd_alert('alert_password_subject', 'alert_password_body');
- sd_flash('ok', t('msg_password_changed'));
- sd_redirect('settings');
- }
- break;
- case 'recovery':
- $step = sd_totp_match((string)$env['SD_TOTP'], (string)($_POST['code'] ?? ''), (int)($env['SD_TOTP_STEP'] ?? 0));
- if ($step === null) {
- sd_fail('settings_totp');
- $errors[] = 'err_code';
- } else {
- sd_env_set(['SD_TOTP_STEP' => $step]);
- $_SESSION['recovery_show'] = sd_store_recovery();
- $_SESSION['recovery_app'] = true;
- sd_audit('recovery_regenerated');
- sd_alert('alert_recovery_new_subject', 'alert_recovery_new_body');
- sd_redirect('recovery');
- }
- break;
- case 'sessions':
- sd_env_set(['SD_EPOCH' => (int)$env['SD_EPOCH'] + 1]);
- sd_session_elevate('full');
- sd_audit('sessions_revoked');
- sd_flash('ok', t('msg_sessions'));
- sd_redirect('settings');
- case 'lang':
- $lang = (string)($_POST['lang'] ?? '');
- if (isset(SD_LANGS[$lang])) {
- sd_env_set(['SD_LANG' => $lang]);
- sd_set_lang($lang);
- }
- sd_redirect('settings');
- case 'target_add':
- $name = trim((string)($_POST['name'] ?? ''));
- $path = sd_target_path_check((string)($_POST['path'] ?? ''), $err);
- if ($name === '' || mb_strlen($name) > 60) $errors[] = 'err_target_name';
- elseif ($path === null) $errors[] = $err;
- else {
- $targets = sd_targets();
- foreach ($targets as $t) {
- if (realpath((string)$t['path']) === $path) {
- $errors[] = 'err_target_exists';
- break 2;
- }
- }
- $id = 't' . bin2hex(random_bytes(4));
- $targets[$id] = ['name' => $name, 'path' => $path, 'exclude' => SD_DEFAULT_EXCLUDES];
- sd_env_set(['SD_TARGETS' => $targets]);
- sd_audit('target_added', ['id' => $id, 'path' => $path]);
- sd_flash('ok', t('msg_saved'));
- sd_redirect('settings', ['t' => $id]);
- }
- break;
- case 'target_save':
- $id = (string)($_POST['id'] ?? '');
- $targets = sd_targets();
- if (!isset($targets[$id])) sd_redirect('settings');
- if (!empty($_POST['remove'])) {
- unset($targets[$id]);
- sd_env_set(['SD_TARGETS' => $targets]);
- sd_audit('target_removed', ['id' => $id]);
- sd_flash('ok', t('msg_saved'));
- sd_redirect('settings');
- }
- $name = trim((string)($_POST['name'] ?? ''));
- if ($name === '' || mb_strlen($name) > 60) {
- $errors[] = 'err_target_name';
- break;
- }
- $patterns = [];
- foreach (preg_split('/\R/', (string)($_POST['exclude'] ?? '')) as $line) {
- $line = trim($line);
- if ($line !== '' && mb_strlen($line) <= 200 && !preg_match('/[\x00-\x1F]/', $line)) $patterns[] = $line;
- }
- $targets[$id]['name'] = $name;
- $targets[$id]['exclude'] = array_values(array_unique(array_slice($patterns, 0, 200)));
- sd_env_set(['SD_TARGETS' => $targets]);
- sd_audit('target_saved', ['id' => $id]);
- sd_flash('ok', t('msg_saved'));
- sd_redirect('settings');
- }
- }
- sd_view_settings($errors);
- exit;
- }
-
- function sd_boot(): void
- {
- ini_set('display_errors', '0');
- ini_set('log_errors', '1');
- ini_set('expose_php', '0');
- sd_headers();
- if (sd_base() === null) sd_view_fatal(t('fatal_title'), t('fatal_base'));
- if (!is_dir(sd_data()) || !is_writable(sd_data())) sd_view_fatal(t('fatal_title'), t('fatal_data'));
- ini_set('error_log', sd_data('logs') . '/php-error.log');
- sd_shield_gate();
- if (!sd_https() && !sd_local()) {
- if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
- header('Location: https://' . sd_host() . (string)($_SERVER['REQUEST_URI'] ?? '/'), true, 301);
- exit;
- }
- http_response_code(403);
- exit;
- }
- if (!function_exists('hash_hmac') || !function_exists('random_bytes') || !function_exists('mb_strlen')) sd_view_fatal(t('fatal_title'), t('fatal_ext'));
- }
-
- function sd_run(): void
- {
- sd_boot();
- $api = $_GET['api'] ?? null;
- if (is_string($api)) sd_api($api);
- $page = is_string($_GET['p'] ?? null) ? $_GET['p'] : '';
- if (sd_env() === null) sd_route_setup();
- sd_session_start();
- if ($page === 'lang' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (sd_csrf_ok()) sd_set_lang((string)($_POST['lang'] ?? ''));
- sd_redirect(sd_stage() === 'full' ? 'settings' : 'login');
- }
- if ($page === 'logout' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
- if (sd_csrf_ok()) {
- if (sd_stage() === 'full') sd_audit('logout');
- session_destroy();
- setcookie(sd_cookie_name(), '', ['expires' => time() - 3600, 'path' => '/', 'secure' => sd_https(), 'httponly' => true, 'samesite' => 'Strict']);
- }
- sd_redirect('login');
- }
- $stage = sd_stage();
- if ($stage === '2fa') sd_route_twofa();
- if ($stage === 'enroll') sd_route_enroll();
- if ($stage !== 'full') sd_route_login();
- match ($page) {
- 'recovery' => sd_route_recovery(),
- 'history' => sd_route_history(),
- 'shield' => sd_route_shield(),
- 'update' => sd_route_update(),
- 'settings' => sd_route_settings(),
- default => (static function (): never {
- sd_view_sync();
- exit;
- })(),
- };
- }
-