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 @@
-
-
= htmlspecialchars($viewModel->title ?? 'iCloud Login') ?>
+
Connect iCloud
error)) : ?>
= htmlspecialchars($viewModel->error) ?>
- show2fa) && $viewModel->show2fa): ?>
-
-
- Enter the 6-digit verification code sent to your trusted devices.
-
-
+
+
+ - Open icloud.com and log in
+ - Complete 2FA
+ - Open DevTools → Network
+ - Copy Request Header → Cookie
+ - Copy X-APPLE-WEBAUTH-HSA-TRUST value
+
+
-
+
-
-
-
+
+
-
-
-
-
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;
}