diff --git a/composer.json b/composer.json index 6b1032f..0848579 100755 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "ext-pdo": "*", "ext-fileinfo": "*", "ext-zip": "*", - "php-http/curl-client": "^2.4" + "php-http/curl-client": "^2.4", + "ext-sodium": "*" }, "require-dev": { "phpstan/phpstan": "^2.1", diff --git a/db/migration/005_create_icloud_accounts.php b/db/migration/007_create_icloud_accounts.php similarity index 68% rename from db/migration/005_create_icloud_accounts.php rename to db/migration/007_create_icloud_accounts.php index 9097cd7..af53c10 100644 --- a/db/migration/005_create_icloud_accounts.php +++ b/db/migration/007_create_icloud_accounts.php @@ -9,12 +9,17 @@ return function (PDO $db): void { id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, remote_name TEXT NOT NULL UNIQUE, + apple_id TEXT NOT NULL, - trust_token TEXT, - cookies TEXT, - status TEXT NOT NULL DEFAULT 'pending', - connected_at INTEGER, + password TEXT NOT NULL, + + trust_token TEXT NOT NULL, + cookies TEXT NOT NULL, + + status TEXT NOT NULL DEFAULT 'connected', + connected_at INTEGER NOT NULL, created_at INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) ); "); diff --git a/public/index.php b/public/index.php index 12fc346..74e519b 100755 --- a/public/index.php +++ b/public/index.php @@ -141,8 +141,7 @@ $routes = static function (RouteCollector $r): void { $r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']); $r->get('/icloud/connect', [ICloudAuthController::class, 'connectForm']); - $r->post('/icloud/connect', [ICloudAuthController::class, 'submitCredentials']); - $r->post('/icloud/2fa', [ICloudAuthController::class, 'submit2fa']); + $r->post('/icloud/connect', [ICloudAuthController::class, 'submit']); }; diff --git a/resources/views/login_icloud.php b/resources/views/login_icloud.php index a987a73..639ec8f 100644 --- a/resources/views/login_icloud.php +++ b/resources/views/login_icloud.php @@ -1,114 +1,62 @@ -
-

title ?? 'iCloud Login') ?>

+

Connect iCloud

error)) : ?>

error) ?>

- show2fa) && $viewModel->show2fa): ?> -
-

- Enter the 6-digit verification code sent to your trusted devices. -

-
+
+
    +
  1. Open icloud.com and log in
  2. +
  3. Complete 2FA
  4. +
  5. Open DevTools → Network
  6. +
  7. Copy Request Header → Cookie
  8. +
  9. Copy X-APPLE-WEBAUTH-HSA-TRUST value
  10. +
+
-
- - + + + - - + + - + + -
- - ← Back to login - -
+ + - -
+ - -
- - - - - - - - - -
- + +
- - - - diff --git a/src/Controllers/ICloudAuthController.php b/src/Controllers/ICloudAuthController.php index fd6f060..c4ecc89 100644 --- a/src/Controllers/ICloudAuthController.php +++ b/src/Controllers/ICloudAuthController.php @@ -5,7 +5,6 @@ namespace Din9xtrCloud\Controllers; use Din9xtrCloud\Middlewares\CsrfMiddleware; use Din9xtrCloud\Rclone\RcloneClient; -use Din9xtrCloud\Rclone\RcloneICloudConfigurator; use Din9xtrCloud\Repositories\IcloudAccountRepository; use Din9xtrCloud\View; use Din9xtrCloud\ViewModels\Icloud\ICloudLoginViewModel; @@ -18,7 +17,6 @@ use Throwable; final readonly class ICloudAuthController { public function __construct( - private RcloneICloudConfigurator $configurator, private ResponseFactoryInterface $responseFactory, private LoggerInterface $logger, private IcloudAccountRepository $repository, @@ -33,122 +31,107 @@ final readonly class ICloudAuthController session_start(); } - $query = $request->getQueryParams(); - $show2fa = (bool)($query['show2fa'] ?? false); - $appleId = (string)($query['apple_id'] ?? ''); - - $error = ''; - if (isset($_SESSION['icloud_error'])) { - $error = (string)$_SESSION['icloud_error']; - unset($_SESSION['icloud_error']); - } - - $title = $show2fa ? 'iCloud 2FA Verification' : 'iCloud Login'; + $error = $_SESSION['icloud_error'] ?? ''; + unset($_SESSION['icloud_error']); return View::display( new ICloudLoginViewModel( - title: $title, + title: 'Connect iCloud', csrf: CsrfMiddleware::generateToken($request), - show2fa: $show2fa, - error: $error, - appleId: $appleId + error: $error ) ); } - public function submitCredentials(ServerRequestInterface $request): ResponseInterface + public function submit(ServerRequestInterface $request): ResponseInterface { $data = (array)$request->getParsedBody(); $user = $request->getAttribute('user'); - $appleId = (string)($data['apple_id'] ?? ''); - $password = (string)($data['password'] ?? ''); - $remote = 'icloud_' . $user->id; + $appleId = trim((string)($data['apple_id'] ?? '')); + $password = trim((string)($data['password'] ?? '')); + $cookies = trim((string)($data['cookies'] ?? '')); + $trustToken = trim((string)($data['trust_token'] ?? '')); - try { - $this->configurator->createRemote($remote, $appleId); - $this->configurator->setPassword($remote, $password); - - $config = $this->configurator->getConfig($remote); - $trustToken = $config['trust_token'] ?? null; - $cookies = $config['cookies'] ?? null; - - $this->repository->create( - $user->id, - $remote, - $appleId, - $trustToken, - $cookies - ); - - return $this->responseFactory - ->createResponse(302) - ->withHeader('Location', '/icloud/connect?show2fa=1&apple_id=' . urlencode($appleId)); - - } catch (Throwable $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - - if (session_status() === PHP_SESSION_NONE) session_start(); - $_SESSION['icloud_error'] = $e->getMessage(); - - return $this->responseFactory - ->createResponse(302) - ->withHeader('Location', '/icloud/connect'); + if ($appleId === '' || $password === '' || $cookies === '' || $trustToken === '') { + return $this->fail('All fields are required'); } - } - - public function submit2fa(ServerRequestInterface $request): ResponseInterface - { - $data = (array)$request->getParsedBody(); - $user = $request->getAttribute('user'); - - $code = (string)($data['code'] ?? ''); - $appleId = (string)($data['apple_id'] ?? ''); $remote = 'icloud_' . $user->id; try { - $res = $this->configurator->submit2fa($remote, $code); + $encryptedAppleId = encryptString($appleId); + $encryptedPassword = encryptString($password); - $account = $this->repository->findByUserId($user->id); - if ($account) { - $this->repository->update($account, [ - 'status' => 'connected', - 'connected_at' => time(), - 'trust_token' => $res['trust_token'], - 'cookies' => $res['cookies'], + try { + $this->rclone->call('config/create', [ + 'name' => $remote, + 'type' => 'iclouddrive', + 'parameters' => [ + 'apple_id' => $appleId, + 'password' => $password, + 'cookies' => $cookies, + 'trust_token' => $trustToken, + ], + 'nonInteractive' => true, + ]); + } catch (Throwable $e) { + $this->logger->warning('iCloud remote exists, updating', [ + 'remote' => $remote, + 'exception' => $e, + ]); + + $this->rclone->call('config/update', [ + 'name' => $remote, + 'parameters' => [ + 'apple_id' => $appleId, + 'password' => $password, + 'cookies' => $cookies, + 'trust_token' => $trustToken, + ], ]); } - $contents = $this->rclone->call('operations/list', [ + // health-check + $list = $this->rclone->call('operations/list', [ 'fs' => $remote . ':', 'remote' => '', - 'opt' => [], - 'recurse' => true, - 'dirsOnly' => false, - 'filesOnly' => false, - 'metadata' => true, + 'maxDepth' => 1, ]); - $this->logger->info('iCloud contents', ['user' => $user->id, '$contents' => $contents]); + $this->logger->info('icloud list', $list); + + $this->repository->createOrUpdate( + userId: $user->id, + remoteName: $remote, + appleId: $encryptedAppleId, + password: $encryptedPassword, + trustToken: $trustToken, + cookies: $cookies, + ); return $this->responseFactory ->createResponse(302) ->withHeader('Location', '/storage'); } catch (Throwable $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); + $this->logger->error('iCloud connect failed', [ + 'remote' => $remote, + 'exception' => $e, + ]); - if (session_status() === PHP_SESSION_NONE) session_start(); - $_SESSION['icloud_error'] = $e->getMessage(); - - return $this->responseFactory - ->createResponse(302) - ->withHeader('Location', '/icloud/connect?' . http_build_query([ - 'show2fa' => 1, - 'apple_id' => $appleId - ])); + return $this->fail($e->getMessage()); } } -} \ No newline at end of file + + private function fail(string $message): ResponseInterface + { + if (session_status() === PHP_SESSION_NONE) session_start(); + $_SESSION['icloud_error'] = $message; + + return $this->responseFactory + ->createResponse(302) + ->withHeader('Location', '/icloud/connect'); + } +} diff --git a/src/Helpers/helpers.php b/src/Helpers/helpers.php index db78f4d..d7cc517 100644 --- a/src/Helpers/helpers.php +++ b/src/Helpers/helpers.php @@ -1,5 +1,7 @@ rclone->call('config/create', [ - 'name' => $remote, - 'type' => 'iclouddrive', - 'parameters' => [ - 'apple_id' => $appleId, - ], - ]); - - } - - /** - * @throws ClientExceptionInterface - * @throws JsonException - */ - public function setPassword(string $remote, string $password): void - { - $this->rclone->call('config/password', [ - 'name' => $remote, - 'parameters' => [ - 'password' => $password, - ], - ]); - } - - /** - * @throws ClientExceptionInterface - * @throws JsonException - */ - public function submit2fa(string $remote, string $code): array - { - $this->rclone->call('config/update', [ - 'name' => $remote, - 'parameters' => [ - 'config_2fa' => $code, - ], - ]); - - $config = $this->getConfig($remote); - return [ - 'trust_token' => $config['trust_token'] ?? null, - 'cookies' => $config['cookies'] ?? null, - ]; - } - - /** - * @throws ClientExceptionInterface - * @throws JsonException - */ - public function getConfig(string $remote): array - { - return $this->rclone->call('config/show', [ - 'name' => $remote - ]); - } -} diff --git a/src/Repositories/IcloudAccountRepository.php b/src/Repositories/IcloudAccountRepository.php index 07ad72b..001b54e 100644 --- a/src/Repositories/IcloudAccountRepository.php +++ b/src/Repositories/IcloudAccountRepository.php @@ -12,60 +12,92 @@ final readonly class IcloudAccountRepository { } - public function create( - int $userId, - string $remoteName, - string $appleId, - ?string $trustToken = null, - ?string $cookies = null + public function createOrUpdate( + int $userId, + string $remoteName, + string $appleId, + string $password, + string $trustToken, + string $cookies ): IcloudAccount { + $existing = $this->findByUserId($userId); + $now = time(); + + if ($existing) { + $stmt = $this->db->prepare(" + UPDATE icloud_accounts + SET apple_id = :apple_id, + password = :password, + trust_token = :trust_token, + cookies = :cookies, + status = 'connected', + connected_at = :connected_at + WHERE id = :id + "); + + $stmt->execute([ + ':apple_id' => $appleId, + ':password' => $password, + ':trust_token' => $trustToken, + ':cookies' => $cookies, + ':connected_at' => $now, + ':id' => $existing->id, + ]); + + return new IcloudAccount( + id: $existing->id, + userId: $userId, + remoteName: $remoteName, + appleId: $appleId, + password: $password, + trustToken: $trustToken, + cookies: $cookies, + status: 'connected', + connectedAt: $now, + createdAt: $existing->createdAt, + ); + } + $stmt = $this->db->prepare(" - INSERT INTO icloud_accounts (user_id, remote_name, apple_id, trust_token, cookies) - VALUES (:user_id, :remote_name, :apple_id, :trust_token, :cookies) + INSERT INTO icloud_accounts + (user_id, remote_name, apple_id, password, trust_token, cookies, status, connected_at, created_at) + VALUES + (:user_id, :remote_name, :apple_id, :password, :trust_token, :cookies, 'connected', :connected_at, :created_at) "); + $stmt->execute([ ':user_id' => $userId, ':remote_name' => $remoteName, ':apple_id' => $appleId, + ':password' => $password, ':trust_token' => $trustToken, ':cookies' => $cookies, + ':connected_at' => $now, + ':created_at' => $now, ]); - $id = (int)$this->db->lastInsertId(); - return new IcloudAccount( - id: $id, + id: (int)$this->db->lastInsertId(), userId: $userId, remoteName: $remoteName, appleId: $appleId, + password: $password, trustToken: $trustToken, cookies: $cookies, - status: 'pending', - connectedAt: null, - createdAt: time(), + status: 'connected', + connectedAt: $now, + createdAt: $now, ); } - public function update( - IcloudAccount $account, - array $fields - ): void - { - $set = []; - $params = [':id' => $account->id]; - foreach ($fields as $key => $value) { - $set[] = "$key = :$key"; - $params[":$key"] = $value; - } - $stmt = $this->db->prepare("UPDATE icloud_accounts SET " . implode(', ', $set) . " WHERE id = :id"); - $stmt->execute($params); - } - public function findByUserId(int $userId): ?IcloudAccount { - $stmt = $this->db->prepare("SELECT * FROM icloud_accounts WHERE user_id = :user_id"); + $stmt = $this->db->prepare(" + SELECT * FROM icloud_accounts WHERE user_id = :user_id + "); $stmt->execute([':user_id' => $userId]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ? new IcloudAccount( @@ -73,10 +105,11 @@ final readonly class IcloudAccountRepository userId: (int)$row['user_id'], remoteName: $row['remote_name'], appleId: $row['apple_id'], + password: $row['password'], trustToken: $row['trust_token'], cookies: $row['cookies'], status: $row['status'], - connectedAt: $row['connected_at'] ? (int)$row['connected_at'] : null, + connectedAt: (int)$row['connected_at'], createdAt: (int)$row['created_at'], ) : null; }