feat(frontend): show intercepted request & response
This commit is contained in:
@@ -36,6 +36,9 @@
|
|||||||
sqlite
|
sqlite
|
||||||
gemini-cli
|
gemini-cli
|
||||||
biome
|
biome
|
||||||
|
sqlx-cli
|
||||||
|
typos
|
||||||
|
lazysql
|
||||||
];
|
];
|
||||||
buildInputs = [ ];
|
buildInputs = [ ];
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs);
|
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import RulesList from "./components/RulesList";
|
|||||||
import CommandQueue from "./components/CommandQueue";
|
import CommandQueue from "./components/CommandQueue";
|
||||||
import Login from "./components/Login";
|
import Login from "./components/Login";
|
||||||
import ChangePassword from "./components/ChangePassword";
|
import ChangePassword from "./components/ChangePassword";
|
||||||
import RequestLog from "./components/RequestLog";
|
import RequestLogs from "./components/RequestLogs";
|
||||||
import { authStore } from "./api/auth";
|
import { authStore } from "./api/auth";
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
@@ -80,7 +80,7 @@ const App: Component = () => {
|
|||||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Request Log
|
Request Logs
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +89,7 @@ const App: Component = () => {
|
|||||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||||
{activeTab() === "rules" && <RulesList />}
|
{activeTab() === "rules" && <RulesList />}
|
||||||
{activeTab() === "commands" && <CommandQueue />}
|
{activeTab() === "commands" && <CommandQueue />}
|
||||||
{activeTab() === "logs" && <RequestLog />}
|
{activeTab() === "logs" && <RequestLogs />}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Show when={showChangePassword()}>
|
<Show when={showChangePassword()}>
|
||||||
|
|||||||
@@ -83,6 +83,38 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
|
|||||||
<TreeView name="body" data={props.log.response_body} isRoot={true} />
|
<TreeView name="body" data={props.log.response_body} isRoot={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<Show when={props.log.intercepted_request}>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2 flex items-center">
|
||||||
|
Intercepted Request
|
||||||
|
<Show when={props.log.request_interception_action}>
|
||||||
|
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
{props.log.request_interception_action}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</h4>
|
||||||
|
<div class="bg-gray-50 p-3 rounded overflow-x-auto">
|
||||||
|
<TreeView name="body" data={props.log.intercepted_request} isRoot={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.log.intercepted_response}>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2 flex items-center">
|
||||||
|
Intercepted Response
|
||||||
|
<Show when={props.log.response_interception_action}>
|
||||||
|
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
{props.log.response_interception_action}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</h4>
|
||||||
|
<div class="bg-gray-50 p-3 rounded overflow-x-auto">
|
||||||
|
<TreeView name="body" data={props.log.intercepted_response} isRoot={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Input } from "./ui/Input";
|
|||||||
import { Select } from "./ui/Select";
|
import { Select } from "./ui/Select";
|
||||||
import RequestDetails from "./RequestDetails";
|
import RequestDetails from "./RequestDetails";
|
||||||
|
|
||||||
const RequestLog: Component = () => {
|
const RequestLogs: Component = () => {
|
||||||
const [search, setSearch] = createSignal("");
|
const [search, setSearch] = createSignal("");
|
||||||
const [inputValue, setInputValue] = createSignal("");
|
const [inputValue, setInputValue] = createSignal("");
|
||||||
const [method, setMethod] = createSignal("");
|
const [method, setMethod] = createSignal("");
|
||||||
@@ -51,7 +51,7 @@ const RequestLog: Component = () => {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-2xl font-semibold">
|
<h2 class="text-2xl font-semibold">
|
||||||
Request Log
|
Request Logs
|
||||||
<Show when={logs() && !logs.loading && !allLogs.loading}>
|
<Show when={logs() && !logs.loading && !allLogs.loading}>
|
||||||
<span class="text-gray-500 text-base ml-2">
|
<span class="text-gray-500 text-base ml-2">
|
||||||
({logs()?.length} of {allLogs()?.length} logs)
|
({logs()?.length} of {allLogs()?.length} logs)
|
||||||
@@ -138,4 +138,4 @@ const RequestLog: Component = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequestLog;
|
export default RequestLogs;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
export interface InterceptionRule {
|
export interface InterceptionRule {
|
||||||
id: number;
|
id: number;
|
||||||
method_name: string;
|
method_name: string;
|
||||||
action: "passthrough" | "modify" | "replace";
|
action: InterceptionAction;
|
||||||
custom_response: any | null;
|
custom_response: any | null;
|
||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InterceptionAction = "passthrough" | "modify" | "replace";
|
||||||
|
|
||||||
export interface Command {
|
export interface Command {
|
||||||
id: number;
|
id: number;
|
||||||
command: any;
|
command: any;
|
||||||
@@ -50,7 +52,9 @@ export interface RequestLog {
|
|||||||
method: string;
|
method: string;
|
||||||
request_body: object;
|
request_body: object;
|
||||||
response_body: object;
|
response_body: object;
|
||||||
request_interception_action: string;
|
intercepted_request: object;
|
||||||
response_interception_action: string;
|
intercepted_response: object;
|
||||||
|
request_interception_action?: InterceptionAction;
|
||||||
|
response_interception_action?: InterceptionAction;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE request_logs
|
||||||
|
ADD intercepted_request TEXT;
|
||||||
|
ALTER TABLE request_logs
|
||||||
|
ADD intercepted_response TEXT;
|
||||||
@@ -4,6 +4,8 @@ use chrono::{DateTime, TimeZone, Utc};
|
|||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::db::models::InterceptionAction;
|
||||||
|
|
||||||
// Authentication models
|
// Authentication models
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
@@ -48,7 +50,7 @@ pub struct UpdateCommandRequest {
|
|||||||
pub struct RuleResponse {
|
pub struct RuleResponse {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub method_name: String,
|
pub method_name: String,
|
||||||
pub action: String,
|
pub action: InterceptionAction,
|
||||||
pub custom_response: Option<String>,
|
pub custom_response: Option<String>,
|
||||||
pub is_enabled: bool,
|
pub is_enabled: bool,
|
||||||
#[serde(serialize_with = "serialize_dt")]
|
#[serde(serialize_with = "serialize_dt")]
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ impl Cryptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt(&self, plaintext: String) -> String {
|
pub fn encrypt(&self, plaintext: String) -> String {
|
||||||
// Allocate buffer with extra space for padding (AES block size is 16 bytes)
|
// Prepare buffer with extra space for padding (AES block size is 16 bytes)
|
||||||
let plaintext_bytes = plaintext.as_bytes();
|
let len = plaintext.len();
|
||||||
let mut buffer = vec![0u8; 16 * (plaintext_bytes.len() / 16 + 1)];
|
let mut buffer = plaintext.into_bytes();
|
||||||
buffer[..plaintext_bytes.len()].copy_from_slice(plaintext_bytes);
|
buffer.extend(std::iter::repeat_n(0, 16 * (len / 16 + 1)));
|
||||||
let ciphertext = self
|
let ciphertext = self
|
||||||
.encryptor
|
.encryptor
|
||||||
.clone()
|
.clone()
|
||||||
.encrypt_padded_mut::<Pkcs7>(&mut buffer, plaintext_bytes.len())
|
.encrypt_padded_mut::<Pkcs7>(&mut buffer, len)
|
||||||
.expect("enough space for encrypting is allocated");
|
.expect("enough space for encrypting is allocated");
|
||||||
|
|
||||||
STANDARD.encode(ciphertext)
|
STANDARD.encode(ciphertext)
|
||||||
|
|||||||
@@ -4,11 +4,22 @@ use chrono::{DateTime, TimeZone, Utc};
|
|||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[sqlx(rename_all = "snake_case")]
|
||||||
|
pub enum InterceptionAction {
|
||||||
|
Passthrough,
|
||||||
|
Modify,
|
||||||
|
Replace,
|
||||||
|
#[serde(other)]
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
pub struct InterceptionRule {
|
pub struct InterceptionRule {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub method_name: String,
|
pub method_name: String,
|
||||||
pub action: String,
|
pub action: InterceptionAction,
|
||||||
pub custom_response: Option<String>,
|
pub custom_response: Option<String>,
|
||||||
pub is_enabled: bool,
|
pub is_enabled: bool,
|
||||||
#[serde(serialize_with = "serialize_dt")]
|
#[serde(serialize_with = "serialize_dt")]
|
||||||
@@ -45,8 +56,10 @@ pub struct RequestLog {
|
|||||||
pub method: String,
|
pub method: String,
|
||||||
pub request_body: Value,
|
pub request_body: Value,
|
||||||
pub response_body: Value,
|
pub response_body: Value,
|
||||||
pub request_interception_action: String,
|
pub intercepted_request: Option<Value>,
|
||||||
pub response_interception_action: String,
|
pub intercepted_response: Option<Value>,
|
||||||
|
pub request_interception_action: Option<InterceptionAction>,
|
||||||
|
pub response_interception_action: Option<InterceptionAction>,
|
||||||
#[serde(serialize_with = "serialize_dt")]
|
#[serde(serialize_with = "serialize_dt")]
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::db::models::RequestLog;
|
use crate::db::models::{InterceptionAction, RequestLog};
|
||||||
use serde_json::Value;
|
|
||||||
use sqlx::{QueryBuilder, SqlitePool};
|
use sqlx::{QueryBuilder, SqlitePool};
|
||||||
|
|
||||||
pub async fn list(
|
pub async fn list(
|
||||||
@@ -37,23 +36,29 @@ pub async fn list(
|
|||||||
pub async fn create(
|
pub async fn create(
|
||||||
pool: &SqlitePool,
|
pool: &SqlitePool,
|
||||||
method: String,
|
method: String,
|
||||||
request_body: Value,
|
request_body: String,
|
||||||
response_body: Value,
|
response_body: String,
|
||||||
request_interception_action: String,
|
intercepted_request: Option<String>,
|
||||||
response_interception_action: String,
|
intercepted_response: Option<String>,
|
||||||
|
request_interception_action: Option<InterceptionAction>,
|
||||||
|
response_interception_action: Option<InterceptionAction>,
|
||||||
) -> Result<i64, sqlx::Error> {
|
) -> Result<i64, sqlx::Error> {
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
"INSERT INTO request_logs (
|
"INSERT INTO request_logs (
|
||||||
method,
|
method,
|
||||||
request_body,
|
request_body,
|
||||||
response_body,
|
response_body,
|
||||||
|
intercepted_request,
|
||||||
|
intercepted_response,
|
||||||
request_interception_action,
|
request_interception_action,
|
||||||
response_interception_action
|
response_interception_action
|
||||||
) VALUES (?, ?, ?, ?, ?)",
|
) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
)
|
)
|
||||||
.bind(method)
|
.bind(method)
|
||||||
.bind(request_body)
|
.bind(request_body)
|
||||||
.bind(response_body)
|
.bind(response_body)
|
||||||
|
.bind(intercepted_request)
|
||||||
|
.bind(intercepted_response)
|
||||||
.bind(request_interception_action)
|
.bind(request_interception_action)
|
||||||
.bind(response_interception_action)
|
.bind(response_interception_action)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|||||||
@@ -6,12 +6,23 @@ use axum::{
|
|||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{AppContext, db};
|
use crate::{
|
||||||
|
AppContext,
|
||||||
|
db::{
|
||||||
|
self,
|
||||||
|
models::{InterceptionAction, InterceptionRule},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Processed {
|
||||||
|
original: String,
|
||||||
|
final_: Option<(String, InterceptionAction)>,
|
||||||
|
encrypted: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Main middleware to intercept, decrypt, modify, and log requests and responses.
|
/// Main middleware to intercept, decrypt, modify, and log requests and responses.
|
||||||
pub async fn middleware(
|
pub async fn middleware(
|
||||||
@@ -20,8 +31,15 @@ pub async fn middleware(
|
|||||||
next: Next,
|
next: Next,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let (parts, body) = req.into_parts();
|
let (parts, body) = req.into_parts();
|
||||||
let body_bytes = match body.collect().await {
|
let body_bytes;
|
||||||
Ok(body) => body.to_bytes(),
|
let body = match body.collect().await {
|
||||||
|
Ok(body) => {
|
||||||
|
body_bytes = body.to_bytes();
|
||||||
|
str::from_utf8(&body_bytes).unwrap_or_else(|e| {
|
||||||
|
warn!("Received request with invalid UTF-8: {e}. Replacing with an empty string");
|
||||||
|
""
|
||||||
|
})
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to read request body: {}", e);
|
warn!("Failed to read request body: {}", e);
|
||||||
return (
|
return (
|
||||||
@@ -33,16 +51,27 @@ pub async fn middleware(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Process request: decrypt, deserialize, modify, re-encrypt
|
// Process request: decrypt, deserialize, modify, re-encrypt
|
||||||
let (processed_req_body, decrypted_request, method) = process_request(body_bytes, &ctx).await;
|
let (processed_req, method) = process_request(body, &ctx).await;
|
||||||
|
|
||||||
// Pass modified request to the next handler
|
// Pass modified request to the next handler
|
||||||
let req = Request::from_parts(parts, axum::body::Body::from(processed_req_body));
|
let req = Request::from_parts(parts, axum::body::Body::from(processed_req.encrypted));
|
||||||
let res = next.run(req).await;
|
let res = next.run(req).await;
|
||||||
|
|
||||||
// Process response: decrypt, deserialize, modify, re-encrypt
|
// Process response: decrypt, deserialize, modify, re-encrypt
|
||||||
let (resp_parts, body) = res.into_parts();
|
let (resp_parts, body) = res.into_parts();
|
||||||
let body_bytes = match body.collect().await {
|
let body_bytes;
|
||||||
Ok(b) => b.to_bytes(),
|
let body = match body.collect().await {
|
||||||
|
Ok(b) => {
|
||||||
|
body_bytes = b.to_bytes();
|
||||||
|
str::from_utf8(&body_bytes)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
warn!(
|
||||||
|
"Received response with invalid UTF-8: {e}. Replacing with an empty string"
|
||||||
|
);
|
||||||
|
""
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to read response body: {}", e);
|
warn!("Failed to read response body: {}", e);
|
||||||
return (
|
return (
|
||||||
@@ -53,25 +82,24 @@ pub async fn middleware(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (final_response_body, decrypted_response) =
|
let processed_resp = process_response(body, &method, &ctx).await;
|
||||||
process_response(body_bytes, &method, &ctx).await;
|
|
||||||
|
|
||||||
// Log the decrypted request and response
|
|
||||||
debug!(
|
|
||||||
"\nRequest:\n{}\nResponse:\n{}\n{}",
|
|
||||||
serde_json::to_string_pretty(&decrypted_request).unwrap_or_default(),
|
|
||||||
serde_json::to_string_pretty(&decrypted_response).unwrap_or_default(),
|
|
||||||
"-".repeat(80),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
let (req_body, req_action) = processed_req
|
||||||
|
.final_
|
||||||
|
.map_or((None, None), |(a, b)| (Some(a), Some(b)));
|
||||||
|
let (resp_body, resp_action) = processed_resp
|
||||||
|
.final_
|
||||||
|
.map_or((None, None), |(a, b)| (Some(a), Some(b)));
|
||||||
// Write log to database
|
// Write log to database
|
||||||
if let Err(e) = db::repositories::logs::create(
|
if let Err(e) = db::repositories::logs::create(
|
||||||
&ctx.db,
|
&ctx.db,
|
||||||
method,
|
method,
|
||||||
decrypted_request,
|
processed_req.original,
|
||||||
decrypted_response,
|
processed_resp.original,
|
||||||
"".into(),
|
req_body,
|
||||||
"".into(),
|
resp_body,
|
||||||
|
req_action,
|
||||||
|
resp_action,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -84,16 +112,14 @@ pub async fn middleware(
|
|||||||
*response_builder.headers_mut().unwrap() = resp_parts.headers;
|
*response_builder.headers_mut().unwrap() = resp_parts.headers;
|
||||||
}
|
}
|
||||||
response_builder
|
response_builder
|
||||||
.body(axum::body::Body::from(final_response_body))
|
.body(axum::body::Body::from(processed_resp.encrypted))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the incoming request body.
|
/// Processes the incoming request body.
|
||||||
/// Returns the re-encrypted body for the next handler, the decrypted JSON for logging, and the method name.
|
/// Returns the re-encrypted body for the next handler, the decrypted JSON for logging, and the method name.
|
||||||
async fn process_request(body_bytes: Bytes, ctx: &Arc<AppContext>) -> (String, Value, String) {
|
async fn process_request(body: &str, ctx: &Arc<AppContext>) -> (Processed, String) {
|
||||||
let body_str = String::from_utf8(body_bytes.to_vec()).unwrap_or_default();
|
let mut plain_request: Value = serde_json::from_str(body).unwrap_or_else(|err| {
|
||||||
|
|
||||||
let mut plain_request: Value = serde_json::from_str(&body_str).unwrap_or_else(|err| {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to deserialize request body: {}. Using fallback string value.",
|
"Failed to deserialize request body: {}. Using fallback string value.",
|
||||||
err
|
err
|
||||||
@@ -101,42 +127,54 @@ async fn process_request(body_bytes: Bytes, ctx: &Arc<AppContext>) -> (String, V
|
|||||||
Value::String("Could not deserialize request".into())
|
Value::String("Could not deserialize request".into())
|
||||||
});
|
});
|
||||||
decrypt_params(&mut plain_request, ctx);
|
decrypt_params(&mut plain_request, ctx);
|
||||||
|
let original = serde_json::to_string(&plain_request).expect("deserialization succeeded");
|
||||||
|
|
||||||
let method = plain_request
|
let method = plain_request
|
||||||
.get("method")
|
.get("method")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.unwrap_or_default()
|
.unwrap_or_else(|| {
|
||||||
|
warn!("No JSON-RPC method found in request body, fallback to an empty string");
|
||||||
|
""
|
||||||
|
})
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if let Err(e) = modify_request(&mut plain_request, &method, ctx).await {
|
let action = match modify_request(&mut plain_request, &method, ctx).await {
|
||||||
|
Ok(action) => action,
|
||||||
|
Err(e) => {
|
||||||
warn!("Failed to modify request: {}", e);
|
warn!("Failed to modify request: {}", e);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
let final_ = action.map(|action| {
|
||||||
|
(
|
||||||
|
serde_json::to_string(&plain_request).expect("deserialization succeeded"),
|
||||||
|
action,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut crypted_request = plain_request.clone();
|
let mut encrypted_request = plain_request.clone();
|
||||||
encrypt_params(&mut crypted_request, ctx);
|
encrypt_params(&mut encrypted_request, ctx);
|
||||||
|
|
||||||
(
|
(
|
||||||
serde_json::to_string(&crypted_request).expect("deserialization succeeded"),
|
Processed {
|
||||||
plain_request,
|
original,
|
||||||
|
final_,
|
||||||
|
encrypted: serde_json::to_string(&encrypted_request)
|
||||||
|
.expect("deserialization succeeded"),
|
||||||
|
},
|
||||||
method,
|
method,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the outgoing response body.
|
/// Processes the outgoing response body.
|
||||||
/// Returns the final encrypted body for the client and the decrypted JSON for logging.
|
/// Returns the final encrypted body for the client and the decrypted JSON for logging.
|
||||||
async fn process_response(
|
async fn process_response(body: String, method: &str, ctx: &Arc<AppContext>) -> Processed {
|
||||||
body_bytes: Bytes,
|
let decrypted_body = ctx.cryptor.decrypt(body.clone()).unwrap_or_else(|err| {
|
||||||
method: &str,
|
|
||||||
ctx: &Arc<AppContext>,
|
|
||||||
) -> (String, Value) {
|
|
||||||
let body_str = String::from_utf8(body_bytes.to_vec()).unwrap_or_default();
|
|
||||||
|
|
||||||
let decrypted_body = ctx.cryptor.decrypt(body_str.clone()).unwrap_or_else(|err| {
|
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to decrypt response body: {}. Assuming it's not encrypted.",
|
"Failed to decrypt response body: {}. Assuming it's not encrypted.",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
body_str
|
body
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut response_value: Value = serde_json::from_str(&decrypted_body).unwrap_or_else(|err| {
|
let mut response_value: Value = serde_json::from_str(&decrypted_body).unwrap_or_else(|err| {
|
||||||
@@ -147,16 +185,23 @@ async fn process_response(
|
|||||||
Value::String(decrypted_body.clone())
|
Value::String(decrypted_body.clone())
|
||||||
});
|
});
|
||||||
|
|
||||||
let decrypted = response_value.clone();
|
let action = match modify_response(&mut response_value, method, ctx).await {
|
||||||
|
Ok(action) => action,
|
||||||
if let Err(e) = modify_response(&mut response_value, method, ctx, &decrypted_body).await {
|
Err(e) => {
|
||||||
warn!("Failed to modify response: {}", e);
|
warn!("Failed to modify response: {}", e);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let modified_body_str = serde_json::to_string(&response_value).unwrap_or_default();
|
let modified_body_str = serde_json::to_string(&response_value).expect("serialization succeeded");
|
||||||
let encrypted = ctx.cryptor.encrypt(modified_body_str);
|
let encrypted = ctx.cryptor.encrypt(modified_body_str.clone());
|
||||||
|
let final_ = action.map(|action| (modified_body_str.clone(), action));
|
||||||
|
|
||||||
(encrypted, decrypted)
|
Processed {
|
||||||
|
original: decrypted_body,
|
||||||
|
final_,
|
||||||
|
encrypted,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Placeholder for request modification logic.
|
/// Placeholder for request modification logic.
|
||||||
@@ -164,9 +209,9 @@ async fn modify_request(
|
|||||||
_request_json: &mut Value,
|
_request_json: &mut Value,
|
||||||
_method: &str,
|
_method: &str,
|
||||||
_ctx: &Arc<AppContext>,
|
_ctx: &Arc<AppContext>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<Option<InterceptionAction>> {
|
||||||
// TODO: Implement request modification logic based on rules or other criteria.
|
// TODO: Implement request modification logic based on rules or other criteria.
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies modification rules to the response.
|
/// Applies modification rules to the response.
|
||||||
@@ -174,10 +219,9 @@ async fn modify_response(
|
|||||||
response_json: &mut Value,
|
response_json: &mut Value,
|
||||||
method: &str,
|
method: &str,
|
||||||
ctx: &Arc<AppContext>,
|
ctx: &Arc<AppContext>,
|
||||||
original_decrypted: &str,
|
) -> anyhow::Result<Option<InterceptionAction>> {
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// Check for generic method interception (e.g., replace response from DB)
|
// Check for generic method interception (e.g., replace response from DB)
|
||||||
if let Some(intercepted) = intercept_response(method, original_decrypted, ctx).await? {
|
if let Some((intercepted, action)) = intercept_response(method, ctx).await? {
|
||||||
debug!("Intercepting response for method: {}", method);
|
debug!("Intercepting response for method: {}", method);
|
||||||
*response_json = serde_json::from_str(&intercepted).unwrap_or_else(|e| {
|
*response_json = serde_json::from_str(&intercepted).unwrap_or_else(|e| {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -186,10 +230,11 @@ async fn modify_response(
|
|||||||
);
|
);
|
||||||
Value::String(intercepted)
|
Value::String(intercepted)
|
||||||
});
|
});
|
||||||
return Ok(());
|
return Ok(Some(action));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for getcommand
|
// Special handling for getcommand
|
||||||
|
// TODO: Return interception rule
|
||||||
if method == "com.linspirer.device.getcommand"
|
if method == "com.linspirer.device.getcommand"
|
||||||
&& let Err(e) = handle_getcommand_response(response_json, ctx).await
|
&& let Err(e) = handle_getcommand_response(response_json, ctx).await
|
||||||
{
|
{
|
||||||
@@ -202,7 +247,7 @@ async fn modify_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the 'getcommand' response by injecting verified commands.
|
/// Handles the 'getcommand' response by injecting verified commands.
|
||||||
@@ -257,15 +302,21 @@ async fn handle_getcommand_response(
|
|||||||
/// Checks for and applies response interception rules.
|
/// Checks for and applies response interception rules.
|
||||||
async fn intercept_response(
|
async fn intercept_response(
|
||||||
method: &str,
|
method: &str,
|
||||||
_orignal_response: &str,
|
|
||||||
ctx: &Arc<AppContext>,
|
ctx: &Arc<AppContext>,
|
||||||
) -> anyhow::Result<Option<String>> {
|
) -> anyhow::Result<Option<(String, InterceptionAction)>> {
|
||||||
// Check if there's an interception rule for this method
|
// Check if there's an interception rule for this method
|
||||||
let rule = crate::db::repositories::rules::find_by_method(&ctx.db, method).await?;
|
let rule = crate::db::repositories::rules::find_by_method(&ctx.db, method).await?;
|
||||||
|
|
||||||
match rule {
|
match rule {
|
||||||
Some(r) if r.action == "replace" => Ok(r.custom_response.as_ref().cloned()),
|
Some(InterceptionRule {
|
||||||
Some(r) if r.action == "modify" => {
|
action: InterceptionAction::Replace,
|
||||||
|
custom_response,
|
||||||
|
..
|
||||||
|
}) => Ok(custom_response.map(|resp| (resp, InterceptionAction::Replace))),
|
||||||
|
Some(InterceptionRule {
|
||||||
|
action: InterceptionAction::Modify,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
// TODO: Apply modifications
|
// TODO: Apply modifications
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user