170 lines
7.4 KiB
PHP
170 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\ClientInterface;
|
|
use Throwable;
|
|
|
|
class StoreAccountPasswordClient
|
|
{
|
|
private ClientInterface $http;
|
|
|
|
public function __construct(?ClientInterface $http = null)
|
|
{
|
|
// Dedicated transport: do not dispatch Laravel HTTP logging events or
|
|
// attach the application's general request/response collectors.
|
|
$this->http = $http ?? new Client();
|
|
}
|
|
|
|
public function reveal(string $accountId, int $actorId, string $purpose): array
|
|
{
|
|
$template = (string) config('store_account_password.url');
|
|
$token = (string) config('store_account_password.token');
|
|
if (substr_count($template, '{id}') !== 1 || parse_url($template, PHP_URL_SCHEME) !== 'https'
|
|
|| ! preg_match('/^[A-Za-z0-9_-]{43,128}$/D', $token)) {
|
|
return ['status' => 503];
|
|
}
|
|
try {
|
|
$response = $this->http->request('POST', str_replace('{id}', rawurlencode($accountId), $template), [
|
|
'headers' => ['Authorization' => 'Bearer '.$token, 'Accept' => 'application/json'],
|
|
'json' => ['requesterId' => 'finance-user-'.$actorId, 'purpose' => $purpose],
|
|
'verify' => true, 'allow_redirects' => false, 'http_errors' => false,
|
|
'connect_timeout' => 3, 'timeout' => 10,
|
|
'sink' => fopen('php://memory', 'w+'),
|
|
]);
|
|
$status = $response->getStatusCode();
|
|
if ($response->getBody()->isSeekable()) {
|
|
$response->getBody()->rewind();
|
|
}
|
|
$body = $response->getBody()->read(65537);
|
|
$response->getBody()->close();
|
|
if (strlen($body) > 65536) {
|
|
return ['status' => 503];
|
|
}
|
|
$data = json_decode($body, true);
|
|
unset($body);
|
|
$requestId = $data['requestId'] ?? null;
|
|
if (! is_string($requestId) || ! preg_match('/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iD', $requestId)) {
|
|
$requestId = null;
|
|
}
|
|
if ($status !== 200) {
|
|
return ['status' => in_array($status, [404, 429], true) ? $status : 503, 'upstream_request_id' => $requestId];
|
|
}
|
|
if (! $requestId || ! preg_match('/(?:^|,)\s*no-store\s*(?:,|$)/i', $response->getHeaderLine('Cache-Control'))
|
|
|| ! is_string($data['password'] ?? null) || $data['password'] === '') {
|
|
return ['status' => 503, 'upstream_request_id' => $requestId];
|
|
}
|
|
return ['status' => 200, 'password' => $data['password'], 'upstream_request_id' => $requestId];
|
|
} catch (Throwable) {
|
|
// Never log exception objects: transport errors may contain response bodies.
|
|
return ['status' => 503];
|
|
}
|
|
}
|
|
|
|
public function revealVisiblePage(array $accountIds, int $actorId): array
|
|
{
|
|
$url = (string) config('store_account_password.visible_page_url');
|
|
$token = (string) config('store_account_password.token');
|
|
$accountIds = array_values($accountIds);
|
|
if (parse_url($url, PHP_URL_SCHEME) !== 'https'
|
|
|| ! preg_match('/^[A-Za-z0-9_-]{43,128}$/D', $token)
|
|
|| count($accountIds) < 1 || count($accountIds) > 50
|
|
|| count(array_unique($accountIds)) !== count($accountIds)
|
|
|| collect($accountIds)->contains(fn ($id) => ! is_string($id) || ! preg_match('/^[A-Za-z0-9_-]{1,128}$/D', $id))) {
|
|
return ['status' => 503];
|
|
}
|
|
|
|
$requesterId = 'finance-user-'.$actorId;
|
|
$chunks = [];
|
|
$chunk = [];
|
|
foreach ($accountIds as $accountId) {
|
|
$candidate = [...$chunk, $accountId];
|
|
$payload = ['requesterId' => $requesterId, 'purpose' => 'view', 'accountIds' => $candidate];
|
|
if ($chunk !== [] && (count($candidate) > 25 || strlen((string) json_encode($payload)) > 1000)) {
|
|
$chunks[] = $chunk;
|
|
$chunk = [$accountId];
|
|
} else {
|
|
$chunk = $candidate;
|
|
}
|
|
}
|
|
$chunks[] = $chunk;
|
|
|
|
$items = [];
|
|
foreach ($chunks as $accountIdChunk) {
|
|
$result = $this->revealVisiblePageChunk($url, $token, $requesterId, $accountIdChunk);
|
|
if (($result['status'] ?? 503) !== 200) {
|
|
foreach ($items as &$item) {
|
|
unset($item['password']);
|
|
}
|
|
unset($item, $items);
|
|
|
|
return ['status' => $result['status'] ?? 503];
|
|
}
|
|
$items += $result['items'];
|
|
}
|
|
|
|
return ['status' => 200, 'items' => $items];
|
|
}
|
|
|
|
private function revealVisiblePageChunk(string $url, string $token, string $requesterId, array $accountIds): array
|
|
{
|
|
try {
|
|
$response = $this->http->request('POST', $url, [
|
|
'headers' => ['Authorization' => 'Bearer '.$token, 'Accept' => 'application/json'],
|
|
'json' => ['requesterId' => $requesterId, 'purpose' => 'view', 'accountIds' => $accountIds],
|
|
'verify' => true, 'allow_redirects' => false, 'http_errors' => false,
|
|
'connect_timeout' => 3, 'timeout' => 10,
|
|
'sink' => fopen('php://memory', 'w+'),
|
|
]);
|
|
$status = $response->getStatusCode();
|
|
if ($response->getBody()->isSeekable()) {
|
|
$response->getBody()->rewind();
|
|
}
|
|
$body = $response->getBody()->read(131073);
|
|
$response->getBody()->close();
|
|
if (strlen($body) > 131072) {
|
|
return ['status' => 503];
|
|
}
|
|
$data = json_decode($body, true);
|
|
unset($body);
|
|
if ($status !== 200) {
|
|
return ['status' => $status === 429 ? 429 : 503];
|
|
}
|
|
if (! preg_match('/(?:^|,)\s*no-store\s*(?:,|$)/i', $response->getHeaderLine('Cache-Control'))
|
|
|| ! $this->uuid($data['requestId'] ?? null) || ! is_array($data['items'] ?? null)
|
|
|| count($data['items']) !== count($accountIds)) {
|
|
return ['status' => 503];
|
|
}
|
|
$items = [];
|
|
foreach ($data['items'] as $item) {
|
|
$accountId = $item['accountId'] ?? null;
|
|
$itemStatus = $item['status'] ?? null;
|
|
if (! is_string($accountId) || ! in_array($accountId, $accountIds, true) || isset($items[$accountId])
|
|
|| ! $this->uuid($item['requestId'] ?? null)
|
|
|| ! in_array($itemStatus, [200, 404, 429, 503], true)
|
|
|| ($itemStatus === 200 && (! is_string($item['password'] ?? null) || $item['password'] === ''))
|
|
|| ($itemStatus !== 200 && array_key_exists('password', $item))) {
|
|
return ['status' => 503];
|
|
}
|
|
$items[$accountId] = [
|
|
'status' => $itemStatus,
|
|
'password' => $itemStatus === 200 ? $item['password'] : null,
|
|
'upstream_request_id' => $item['requestId'],
|
|
];
|
|
}
|
|
if (array_keys($items) !== $accountIds) {
|
|
return ['status' => 503];
|
|
}
|
|
return ['status' => 200, 'items' => $items];
|
|
} catch (Throwable) {
|
|
return ['status' => 503];
|
|
}
|
|
}
|
|
|
|
private function uuid(mixed $value): bool
|
|
{
|
|
return is_string($value) && (bool) preg_match('/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iD', $value);
|
|
}
|
|
}
|