feat(service): custom product, lịch sử giám sát VM và mã hoá response theo phiên
- Dịch vụ custom: bảng custom_products + services.custom_product_id; createService nhận
custom{} thay product_id, giá = đơn giá tháng x số tháng chu kỳ (1/3/6/12/24/36),
gia hạn tính theo cùng đơn giá, ServiceClientDetailResource trả custom_product và specs.
- Giám sát VM: GET v1/system/vm-metrics/history gom lịch sử về tối đa 240 điểm;
lỗi lấy dung lượng đĩa không còn làm hỏng cả thẻ metrics.
- SealResponse: AES-256-GCM theo khoá phiên do proxy Next cấp qua header x-seal-key
(HMAC của session id). Client opt-in bằng x-seal; chi tiết dịch vụ, mật khẩu VM và
console ticket luôn được mã hoá. Body gửi lên dạng {enc} được giải mã trước validate;
khoá lệch trả 428 để client lấy khoá mới. Đây là che mắt DevTools, không phải bảo mật
chống chính chủ tài khoản.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8108e8fe53
commit
6315a392c0
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Http\Middleware\SealResponse;
|
||||
use App\Support\ResponseCipher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ResponseCipherTest extends TestCase
|
||||
{
|
||||
private function key(): string
|
||||
{
|
||||
return str_repeat('k', 32);
|
||||
}
|
||||
|
||||
private function request(array $body = [], bool $optIn = false, ?string $key = null): Request
|
||||
{
|
||||
$request = Request::create('/x', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($body));
|
||||
$request->headers->set('x-seal-key', base64_encode($key ?? $this->key()));
|
||||
if ($optIn) {
|
||||
$request->headers->set('x-seal', '1');
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function test_always_mode_seals_with_session_key_and_is_reversible(): void
|
||||
{
|
||||
$response = (new SealResponse())->handle(
|
||||
$this->request(),
|
||||
fn () => new JsonResponse(['data' => ['password' => 'Cloud226*()']]),
|
||||
'always',
|
||||
);
|
||||
|
||||
$body = $response->getData(true);
|
||||
$this->assertArrayNotHasKey('data', $body);
|
||||
$this->assertStringNotContainsString('Cloud226', $response->getContent());
|
||||
$this->assertSame(
|
||||
['data' => ['password' => 'Cloud226*()']],
|
||||
json_decode(ResponseCipher::open($body['enc'], $this->key()), true),
|
||||
);
|
||||
$this->assertNull(ResponseCipher::open($body['enc'], str_repeat('x', 32)));
|
||||
}
|
||||
|
||||
public function test_passthrough_without_key_or_opt_in(): void
|
||||
{
|
||||
$noKey = Request::create('/x');
|
||||
$noKey->headers->set('x-seal', '1');
|
||||
$plain = (new SealResponse())->handle($noKey, fn () => new JsonResponse(['a' => 1]));
|
||||
$this->assertSame(['a' => 1], $plain->getData(true));
|
||||
|
||||
$noOptIn = (new SealResponse())->handle($this->request(), fn () => new JsonResponse(['a' => 1]));
|
||||
$this->assertSame(['a' => 1], $noOptIn->getData(true));
|
||||
}
|
||||
|
||||
public function test_opt_in_seals_errors_and_opens_request_body(): void
|
||||
{
|
||||
$sealed = ['enc' => ResponseCipher::seal(json_encode(['name' => 'An']), $this->key())];
|
||||
|
||||
$response = (new SealResponse())->handle(
|
||||
$this->request($sealed, optIn: true),
|
||||
fn (Request $r) => new JsonResponse(['got' => $r->input('name')], 422),
|
||||
);
|
||||
|
||||
$this->assertSame(422, $response->getStatusCode());
|
||||
$this->assertSame(
|
||||
['got' => 'An'],
|
||||
json_decode(ResponseCipher::open($response->getData(true)['enc'], $this->key()), true),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_request_sealed_with_stale_key_gets_428(): void
|
||||
{
|
||||
$sealed = ['enc' => ResponseCipher::seal('{"a":1}', str_repeat('o', 32))];
|
||||
|
||||
$response = (new SealResponse())->handle(
|
||||
$this->request($sealed, optIn: true),
|
||||
fn () => new JsonResponse(['ok' => true]),
|
||||
);
|
||||
|
||||
$this->assertSame(428, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user