mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-10 23:02:46 +02:00
feat: Add DNS timings and resolution overrides (#360)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ use yaak_models::util::UpdateSource;
|
|||||||
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
use yaak_plugins::events::{PluginContext, RenderPurpose};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_templates::{parse_and_render, render_json_value_raw, RenderOptions};
|
use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaakcli")]
|
#[command(name = "yaakcli")]
|
||||||
@@ -149,14 +149,7 @@ async fn render_http_request(
|
|||||||
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
// Apply path placeholders (e.g., /users/:id -> /users/123)
|
||||||
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
let (url, url_parameters) = apply_path_placeholders(&url, &url_parameters);
|
||||||
|
|
||||||
Ok(HttpRequest {
|
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() })
|
||||||
url,
|
|
||||||
url_parameters,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
authentication,
|
|
||||||
..r.to_owned()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -169,16 +162,10 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the same app_id for both data directory and keyring
|
// Use the same app_id for both data directory and keyring
|
||||||
let app_id = if cfg!(debug_assertions) {
|
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||||
"app.yaak.desktop.dev"
|
|
||||||
} else {
|
|
||||||
"app.yaak.desktop"
|
|
||||||
};
|
|
||||||
|
|
||||||
let data_dir = cli.data_dir.unwrap_or_else(|| {
|
let data_dir = cli.data_dir.unwrap_or_else(|| {
|
||||||
dirs::data_dir()
|
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||||
.expect("Could not determine data directory")
|
|
||||||
.join(app_id)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let db_path = data_dir.join("db.sqlite");
|
let db_path = data_dir.join("db.sqlite");
|
||||||
@@ -191,9 +178,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Initialize encryption manager for secure() template function
|
// Initialize encryption manager for secure() template function
|
||||||
// Use the same app_id as the Tauri app for keyring access
|
// Use the same app_id as the Tauri app for keyring access
|
||||||
let encryption_manager = Arc::new(
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
EncryptionManager::new(query_manager.clone(), app_id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize plugin manager for template functions
|
// Initialize plugin manager for template functions
|
||||||
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||||
@@ -203,9 +188,8 @@ async fn main() {
|
|||||||
let node_bin_path = PathBuf::from("node");
|
let node_bin_path = PathBuf::from("node");
|
||||||
|
|
||||||
// Find the plugin runtime - check YAAK_PLUGIN_RUNTIME env var, then fallback to development path
|
// Find the plugin runtime - check YAAK_PLUGIN_RUNTIME env var, then fallback to development path
|
||||||
let plugin_runtime_main = std::env::var("YAAK_PLUGIN_RUNTIME")
|
let plugin_runtime_main =
|
||||||
.map(PathBuf::from)
|
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
// Development fallback: look relative to crate root
|
// Development fallback: look relative to crate root
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
||||||
@@ -226,14 +210,10 @@ async fn main() {
|
|||||||
// Initialize plugins from database
|
// Initialize plugins from database
|
||||||
let plugins = db.list_plugins().unwrap_or_default();
|
let plugins = db.list_plugins().unwrap_or_default();
|
||||||
if !plugins.is_empty() {
|
if !plugins.is_empty() {
|
||||||
let errors = plugin_manager
|
let errors =
|
||||||
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
plugin_manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
||||||
.await;
|
|
||||||
for (plugin_dir, error_msg) in errors {
|
for (plugin_dir, error_msg) in errors {
|
||||||
eprintln!(
|
eprintln!("Warning: Failed to initialize plugin '{}': {}", plugin_dir, error_msg);
|
||||||
"Warning: Failed to initialize plugin '{}': {}",
|
|
||||||
plugin_dir, error_msg
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +229,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Requests { workspace_id } => {
|
Commands::Requests { workspace_id } => {
|
||||||
let requests = db
|
let requests = db.list_http_requests(&workspace_id).expect("Failed to list requests");
|
||||||
.list_http_requests(&workspace_id)
|
|
||||||
.expect("Failed to list requests");
|
|
||||||
if requests.is_empty() {
|
if requests.is_empty() {
|
||||||
println!("No requests found in workspace {}", workspace_id);
|
println!("No requests found in workspace {}", workspace_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -261,9 +239,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Send { request_id } => {
|
Commands::Send { request_id } => {
|
||||||
let request = db
|
let request = db.get_http_request(&request_id).expect("Failed to get request");
|
||||||
.get_http_request(&request_id)
|
|
||||||
.expect("Failed to get request");
|
|
||||||
|
|
||||||
// Resolve environment chain for variable substitution
|
// Resolve environment chain for variable substitution
|
||||||
let environment_chain = db
|
let environment_chain = db
|
||||||
@@ -318,18 +294,13 @@ async fn main() {
|
|||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Drain events silently
|
// Drain events silently
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||||
while event_rx.recv().await.is_some() {}
|
|
||||||
});
|
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||||
let response = sender
|
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||||
.send(sendable, event_tx)
|
|
||||||
.await
|
|
||||||
.expect("Failed to send request");
|
|
||||||
|
|
||||||
// Wait for event handler to finish
|
// Wait for event handler to finish
|
||||||
if let Some(handle) = verbose_handle {
|
if let Some(handle) = verbose_handle {
|
||||||
@@ -383,18 +354,13 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
||||||
while event_rx.recv().await.is_some() {}
|
|
||||||
});
|
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the request
|
// Send the request
|
||||||
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
let sender = ReqwestSender::new().expect("Failed to create HTTP client");
|
||||||
let response = sender
|
let response = sender.send(sendable, event_tx).await.expect("Failed to send request");
|
||||||
.send(sendable, event_tx)
|
|
||||||
.await
|
|
||||||
.expect("Failed to send request");
|
|
||||||
|
|
||||||
if let Some(handle) = verbose_handle {
|
if let Some(handle) = verbose_handle {
|
||||||
let _ = handle.await;
|
let _ = handle.await;
|
||||||
@@ -421,12 +387,7 @@ async fn main() {
|
|||||||
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
let (body, _stats) = response.text().await.expect("Failed to read response body");
|
||||||
println!("{}", body);
|
println!("{}", body);
|
||||||
}
|
}
|
||||||
Commands::Create {
|
Commands::Create { workspace_id, name, method, url } => {
|
||||||
workspace_id,
|
|
||||||
name,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
} => {
|
|
||||||
let request = HttpRequest {
|
let request = HttpRequest {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::error::Result;
|
|
||||||
use crate::PluginContextExt;
|
use crate::PluginContextExt;
|
||||||
|
use crate::error::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||||
@@ -54,7 +54,12 @@ pub(crate) async fn cmd_secure_template<R: Runtime>(
|
|||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let plugin_context = window.plugin_context();
|
let plugin_context = window.plugin_context();
|
||||||
Ok(encrypt_secure_template_function(plugin_manager, encryption_manager, &plugin_context, template)?)
|
Ok(encrypt_secure_template_function(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
template,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::PluginContextExt;
|
use crate::PluginContextExt;
|
||||||
|
use crate::error::Result;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use KeyAndValueRef::{Ascii, Binary};
|
use KeyAndValueRef::{Ascii, Binary};
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||||
use yaak_models::models::GrpcRequest;
|
use yaak_models::models::GrpcRequest;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tauri::{AppHandle, Runtime};
|
use tauri::{AppHandle, Runtime};
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
const NAMESPACE: &str = "analytics";
|
const NAMESPACE: &str = "analytics";
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
use crate::PluginContextExt;
|
||||||
use crate::error::Error::GenericError;
|
use crate::error::Error::GenericError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::models_ext::BlobManagerExt;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::render::render_http_request;
|
use crate::render::render_http_request;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
|
||||||
use tokio::fs::{File, create_dir_all};
|
use tokio::fs::{File, create_dir_all};
|
||||||
@@ -15,22 +19,19 @@ use yaak_http::client::{
|
|||||||
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
HttpConnectionOptions, HttpConnectionProxySetting, HttpConnectionProxySettingAuth,
|
||||||
};
|
};
|
||||||
use yaak_http::cookies::CookieStore;
|
use yaak_http::cookies::CookieStore;
|
||||||
use yaak_http::manager::HttpConnectionManager;
|
use yaak_http::manager::{CachedClient, HttpConnectionManager};
|
||||||
use yaak_http::sender::ReqwestSender;
|
use yaak_http::sender::ReqwestSender;
|
||||||
use yaak_http::tee_reader::TeeReader;
|
use yaak_http::tee_reader::TeeReader;
|
||||||
use yaak_http::transaction::HttpTransaction;
|
use yaak_http::transaction::HttpTransaction;
|
||||||
use yaak_http::types::{
|
use yaak_http::types::{
|
||||||
SendableBody, SendableHttpRequest, SendableHttpRequestOptions, append_query_params,
|
SendableBody, SendableHttpRequest, SendableHttpRequestOptions, append_query_params,
|
||||||
};
|
};
|
||||||
use crate::models_ext::BlobManagerExt;
|
|
||||||
use yaak_models::blob_manager::BodyChunk;
|
use yaak_models::blob_manager::BodyChunk;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseHeader,
|
CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseHeader,
|
||||||
HttpResponseState, ProxySetting, ProxySettingAuth,
|
HttpResponseState, ProxySetting, ProxySettingAuth,
|
||||||
};
|
};
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use crate::PluginContextExt;
|
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
|
CallHttpAuthenticationRequest, HttpHeader, PluginContext, RenderPurpose,
|
||||||
};
|
};
|
||||||
@@ -173,7 +174,12 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
let environment_id = environment.map(|e| e.id);
|
let environment_id = environment.map(|e| e.id);
|
||||||
let workspace = window.db().get_workspace(workspace_id)?;
|
let workspace = window.db().get_workspace(workspace_id)?;
|
||||||
let (resolved, auth_context_id) = resolve_http_request(window, unrendered_request)?;
|
let (resolved, auth_context_id) = resolve_http_request(window, unrendered_request)?;
|
||||||
let cb = PluginTemplateCallback::new(plugin_manager.clone(), encryption_manager.clone(), &plugin_context, RenderPurpose::Send);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
plugin_manager.clone(),
|
||||||
|
encryption_manager.clone(),
|
||||||
|
&plugin_context,
|
||||||
|
RenderPurpose::Send,
|
||||||
|
);
|
||||||
let env_chain =
|
let env_chain =
|
||||||
window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?;
|
window.db().resolve_environments(&workspace.id, folder_id, environment_id.as_deref())?;
|
||||||
let request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?;
|
let request = render_http_request(&resolved, env_chain, &cb, &RenderOptions::throw()).await?;
|
||||||
@@ -228,12 +234,13 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = connection_manager
|
let cached_client = connection_manager
|
||||||
.get_client(&HttpConnectionOptions {
|
.get_client(&HttpConnectionOptions {
|
||||||
id: plugin_context.id.clone(),
|
id: plugin_context.id.clone(),
|
||||||
validate_certificates: workspace.setting_validate_certificates,
|
validate_certificates: workspace.setting_validate_certificates,
|
||||||
proxy: proxy_setting,
|
proxy: proxy_setting,
|
||||||
client_certificate,
|
client_certificate,
|
||||||
|
dns_overrides: workspace.setting_dns_overrides.clone(),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -250,7 +257,7 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
|
|
||||||
let cookie_store = maybe_cookie_store.as_ref().map(|(cs, _)| cs.clone());
|
let cookie_store = maybe_cookie_store.as_ref().map(|(cs, _)| cs.clone());
|
||||||
let result = execute_transaction(
|
let result = execute_transaction(
|
||||||
client,
|
cached_client,
|
||||||
sendable_request,
|
sendable_request,
|
||||||
response_ctx,
|
response_ctx,
|
||||||
cancelled_rx.clone(),
|
cancelled_rx.clone(),
|
||||||
@@ -310,7 +317,7 @@ pub fn resolve_http_request<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_transaction<R: Runtime>(
|
async fn execute_transaction<R: Runtime>(
|
||||||
client: reqwest::Client,
|
cached_client: CachedClient,
|
||||||
mut sendable_request: SendableHttpRequest,
|
mut sendable_request: SendableHttpRequest,
|
||||||
response_ctx: &mut ResponseContext<R>,
|
response_ctx: &mut ResponseContext<R>,
|
||||||
mut cancelled_rx: Receiver<bool>,
|
mut cancelled_rx: Receiver<bool>,
|
||||||
@@ -321,7 +328,10 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let workspace_id = response_ctx.response().workspace_id.clone();
|
let workspace_id = response_ctx.response().workspace_id.clone();
|
||||||
let is_persisted = response_ctx.is_persisted();
|
let is_persisted = response_ctx.is_persisted();
|
||||||
|
|
||||||
let sender = ReqwestSender::with_client(client);
|
// Keep a reference to the resolver for DNS timing events
|
||||||
|
let resolver = cached_client.resolver.clone();
|
||||||
|
|
||||||
|
let sender = ReqwestSender::with_client(cached_client.client);
|
||||||
let transaction = match cookie_store {
|
let transaction = match cookie_store {
|
||||||
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
Some(cs) => HttpTransaction::with_cookie_store(sender, cs),
|
||||||
None => HttpTransaction::new(sender),
|
None => HttpTransaction::new(sender),
|
||||||
@@ -346,21 +356,39 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
let (event_tx, mut event_rx) =
|
let (event_tx, mut event_rx) =
|
||||||
tokio::sync::mpsc::channel::<yaak_http::sender::HttpResponseEvent>(100);
|
tokio::sync::mpsc::channel::<yaak_http::sender::HttpResponseEvent>(100);
|
||||||
|
|
||||||
|
// Set the event sender on the DNS resolver so it can emit DNS timing events
|
||||||
|
resolver.set_event_sender(Some(event_tx.clone())).await;
|
||||||
|
|
||||||
|
// Shared state to capture DNS timing from the event processing task
|
||||||
|
let dns_elapsed = Arc::new(AtomicI32::new(0));
|
||||||
|
|
||||||
// Write events to DB in a task (only for persisted responses)
|
// Write events to DB in a task (only for persisted responses)
|
||||||
if is_persisted {
|
if is_persisted {
|
||||||
let response_id = response_id.clone();
|
let response_id = response_id.clone();
|
||||||
let app_handle = app_handle.clone();
|
let app_handle = app_handle.clone();
|
||||||
let update_source = response_ctx.update_source.clone();
|
let update_source = response_ctx.update_source.clone();
|
||||||
let workspace_id = workspace_id.clone();
|
let workspace_id = workspace_id.clone();
|
||||||
|
let dns_elapsed = dns_elapsed.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
|
// Capture DNS timing when we see a DNS event
|
||||||
|
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
||||||
|
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
||||||
|
}
|
||||||
let db_event = HttpResponseEvent::new(&response_id, &workspace_id, event.into());
|
let db_event = HttpResponseEvent::new(&response_id, &workspace_id, event.into());
|
||||||
let _ = app_handle.db().upsert_http_response_event(&db_event, &update_source);
|
let _ = app_handle.db().upsert_http_response_event(&db_event, &update_source);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// For ephemeral responses, just drain the events
|
// For ephemeral responses, just drain the events but still capture DNS timing
|
||||||
tokio::spawn(async move { while event_rx.recv().await.is_some() {} });
|
let dns_elapsed = dns_elapsed.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(event) = event_rx.recv().await {
|
||||||
|
if let yaak_http::sender::HttpResponseEvent::DnsResolved { duration, .. } = &event {
|
||||||
|
dns_elapsed.store(*duration as i32, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture request body as it's sent (only for persisted responses)
|
// Capture request body as it's sent (only for persisted responses)
|
||||||
@@ -528,10 +556,14 @@ async fn execute_transaction<R: Runtime>(
|
|||||||
// Final update with closed state and accurate byte count
|
// Final update with closed state and accurate byte count
|
||||||
response_ctx.update(|r| {
|
response_ctx.update(|r| {
|
||||||
r.elapsed = start.elapsed().as_millis() as i32;
|
r.elapsed = start.elapsed().as_millis() as i32;
|
||||||
|
r.elapsed_dns = dns_elapsed.load(Ordering::SeqCst);
|
||||||
r.content_length = Some(written_bytes as i32);
|
r.content_length = Some(written_bytes as i32);
|
||||||
r.state = HttpResponseState::Closed;
|
r.state = HttpResponseState::Closed;
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Clear the event sender from the resolver since this request is done
|
||||||
|
resolver.set_event_sender(None).await;
|
||||||
|
|
||||||
Ok((response_ctx.response().clone(), maybe_blob_write_handle))
|
Ok((response_ctx.response().clone(), maybe_blob_write_handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
use crate::PluginContextExt;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::PluginContextExt;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
|
||||||
use yaak_core::WorkspaceContext;
|
use yaak_core::WorkspaceContext;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, maybe_gen_id, maybe_gen_id_opt};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||||
|
|
||||||
pub(crate) async fn import_data<R: Runtime>(
|
pub(crate) async fn import_data<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::history::get_or_upsert_launch_info;
|
use crate::history::get_or_upsert_launch_info;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
@@ -8,9 +9,8 @@ use std::time::Instant;
|
|||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
|
|
||||||
// Check for updates every hour
|
// Check for updates every hour
|
||||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::http_request::send_http_request_with_context;
|
use crate::http_request::send_http_request_with_context;
|
||||||
|
use crate::models_ext::BlobManagerExt;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::render::{render_grpc_request, render_http_request, render_json_value};
|
use crate::render::{render_grpc_request, render_http_request, render_json_value};
|
||||||
use crate::window::{CreateWindowConfig, create_window};
|
use crate::window::{CreateWindowConfig, create_window};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,11 +16,8 @@ use tauri::{AppHandle, Emitter, Manager, Runtime};
|
|||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
|
||||||
use crate::models_ext::BlobManagerExt;
|
|
||||||
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::error::Error::PluginErr;
|
use yaak_plugins::error::Error::PluginErr;
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
@@ -32,6 +31,7 @@ use yaak_plugins::events::{
|
|||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::plugin_handle::PluginHandle;
|
use yaak_plugins::plugin_handle::PluginHandle;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
|
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||||
|
|
||||||
pub(crate) async fn handle_plugin_event<R: Runtime>(
|
pub(crate) async fn handle_plugin_event<R: Runtime>(
|
||||||
@@ -170,7 +170,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
req.purpose,
|
||||||
|
);
|
||||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let grpc_request =
|
let grpc_request =
|
||||||
render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
|
render_grpc_request(&req.grpc_request, environment_chain, &cb, &opt).await?;
|
||||||
@@ -191,7 +196,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
req.purpose,
|
||||||
|
);
|
||||||
let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = &RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let http_request =
|
let http_request =
|
||||||
render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
|
render_http_request(&req.http_request, environment_chain, &cb, &opt).await?;
|
||||||
@@ -222,7 +232,12 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
)?;
|
)?;
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
let cb = PluginTemplateCallback::new(plugin_manager, encryption_manager, &plugin_context, req.purpose);
|
let cb = PluginTemplateCallback::new(
|
||||||
|
plugin_manager,
|
||||||
|
encryption_manager,
|
||||||
|
&plugin_context,
|
||||||
|
req.purpose,
|
||||||
|
);
|
||||||
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
let opt = RenderOptions { error_behavior: RenderErrorBehavior::Throw };
|
||||||
let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
|
let data = render_json_value(req.data, environment_chain, &cb, &opt).await?;
|
||||||
Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
|
Ok(Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data })))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||||
@@ -11,7 +12,6 @@ use tauri_plugin_updater::{Update, UpdaterExt};
|
|||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
use crate::PluginContextExt;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::import::import_data;
|
use crate::import::import_data;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::PluginContextExt;
|
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::events::{Color, ShowToastRequest};
|
use yaak_plugins::events::{Color, ShowToastRequest};
|
||||||
use yaak_plugins::install::download_and_install;
|
use yaak_plugins::install::download_and_install;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
|
|
||||||
pub(crate) async fn handle_deep_link<R: Runtime>(
|
pub(crate) async fn handle_deep_link<R: Runtime>(
|
||||||
app_handle: &AppHandle<R>,
|
app_handle: &AppHandle<R>,
|
||||||
@@ -55,7 +55,8 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
&plugin_context,
|
&plugin_context,
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
app_handle.emit(
|
app_handle.emit(
|
||||||
"show_toast",
|
"show_toast",
|
||||||
ShowToastRequest {
|
ShowToastRequest {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::window_menu::app_menu;
|
use crate::window_menu::app_menu;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
@@ -8,7 +9,6 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use crate::models_ext::QueryManagerExt;
|
|
||||||
|
|
||||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! WebSocket Tauri command wrappers
|
//! WebSocket Tauri command wrappers
|
||||||
//! These wrap the core yaak-ws functionality for Tauri IPC.
|
//! These wrap the core yaak-ws functionality for Tauri IPC.
|
||||||
|
|
||||||
|
use crate::PluginContextExt;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models_ext::QueryManagerExt;
|
use crate::models_ext::QueryManagerExt;
|
||||||
use crate::PluginContextExt;
|
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -56,9 +56,10 @@ pub async fn cmd_ws_delete_request<R: Runtime>(
|
|||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> Result<WebsocketRequest> {
|
) -> Result<WebsocketRequest> {
|
||||||
Ok(app_handle
|
Ok(app_handle.db().delete_websocket_request_by_id(
|
||||||
.db()
|
request_id,
|
||||||
.delete_websocket_request_by_id(request_id, &UpdateSource::from_window_label(window.label()))?)
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -67,12 +68,10 @@ pub async fn cmd_ws_delete_connection<R: Runtime>(
|
|||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> Result<WebsocketConnection> {
|
) -> Result<WebsocketConnection> {
|
||||||
Ok(app_handle
|
Ok(app_handle.db().delete_websocket_connection_by_id(
|
||||||
.db()
|
connection_id,
|
||||||
.delete_websocket_connection_by_id(
|
&UpdateSource::from_window_label(window.label()),
|
||||||
connection_id,
|
)?)
|
||||||
&UpdateSource::from_window_label(window.label()),
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -296,8 +295,10 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for header in plugin_result.set_headers.unwrap_or_default() {
|
for header in plugin_result.set_headers.unwrap_or_default() {
|
||||||
match (http::HeaderName::from_str(&header.name), HeaderValue::from_str(&header.value))
|
match (
|
||||||
{
|
http::HeaderName::from_str(&header.name),
|
||||||
|
HeaderValue::from_str(&header.value),
|
||||||
|
) {
|
||||||
(Ok(name), Ok(value)) => {
|
(Ok(name), Ok(value)) => {
|
||||||
headers.insert(name, value);
|
headers.insert(name, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use std::time::Duration;
|
|||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
use yaak_tauri_utils::api_client::yaak_api_client;
|
|
||||||
use yaak_models::db_context::DbContext;
|
use yaak_models::db_context::DbContext;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
use yaak_tauri_utils::api_client::yaak_api_client;
|
||||||
|
|
||||||
/// Extension trait for accessing the QueryManager from Tauri Manager types.
|
/// Extension trait for accessing the QueryManager from Tauri Manager types.
|
||||||
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
/// This is needed temporarily until all crates are refactored to not use Tauri.
|
||||||
|
|||||||
Generated
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
@@ -18,4 +20,4 @@ export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environ
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use crate::dns::LocalhostResolver;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use reqwest::{Client, Proxy, redirect};
|
use reqwest::{Client, Proxy, redirect};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use yaak_models::models::DnsOverride;
|
||||||
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
use yaak_tls::{ClientCertificateConfig, get_tls_config};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -28,10 +30,14 @@ pub struct HttpConnectionOptions {
|
|||||||
pub validate_certificates: bool,
|
pub validate_certificates: bool,
|
||||||
pub proxy: HttpConnectionProxySetting,
|
pub proxy: HttpConnectionProxySetting,
|
||||||
pub client_certificate: Option<ClientCertificateConfig>,
|
pub client_certificate: Option<ClientCertificateConfig>,
|
||||||
|
pub dns_overrides: Vec<DnsOverride>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpConnectionOptions {
|
impl HttpConnectionOptions {
|
||||||
pub(crate) fn build_client(&self) -> Result<Client> {
|
/// Build a reqwest Client and return it along with the DNS resolver.
|
||||||
|
/// The resolver is returned separately so it can be configured per-request
|
||||||
|
/// to emit DNS timing events to the appropriate channel.
|
||||||
|
pub(crate) fn build_client(&self) -> Result<(Client, Arc<LocalhostResolver>)> {
|
||||||
let mut client = Client::builder()
|
let mut client = Client::builder()
|
||||||
.connection_verbose(true)
|
.connection_verbose(true)
|
||||||
.redirect(redirect::Policy::none())
|
.redirect(redirect::Policy::none())
|
||||||
@@ -40,15 +46,19 @@ impl HttpConnectionOptions {
|
|||||||
.no_brotli()
|
.no_brotli()
|
||||||
.no_deflate()
|
.no_deflate()
|
||||||
.referer(false)
|
.referer(false)
|
||||||
.tls_info(true);
|
.tls_info(true)
|
||||||
|
// Disable connection pooling to ensure DNS resolution happens on each request
|
||||||
|
// This is needed so we can emit DNS timing events for each request
|
||||||
|
.pool_max_idle_per_host(0);
|
||||||
|
|
||||||
// Configure TLS with optional client certificate
|
// Configure TLS with optional client certificate
|
||||||
let config =
|
let config =
|
||||||
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
get_tls_config(self.validate_certificates, true, self.client_certificate.clone())?;
|
||||||
client = client.use_preconfigured_tls(config);
|
client = client.use_preconfigured_tls(config);
|
||||||
|
|
||||||
// Configure DNS resolver
|
// Configure DNS resolver - keep a reference to configure per-request
|
||||||
client = client.dns_resolver(LocalhostResolver::new());
|
let resolver = LocalhostResolver::new(self.dns_overrides.clone());
|
||||||
|
client = client.dns_resolver(resolver.clone());
|
||||||
|
|
||||||
// Configure proxy
|
// Configure proxy
|
||||||
match self.proxy.clone() {
|
match self.proxy.clone() {
|
||||||
@@ -69,7 +79,7 @@ impl HttpConnectionOptions {
|
|||||||
self.client_certificate.is_some()
|
self.client_certificate.is_some()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(client.build()?)
|
Ok((client.build()?, resolver))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+143
-11
@@ -1,53 +1,185 @@
|
|||||||
|
use crate::sender::HttpResponseEvent;
|
||||||
use hyper_util::client::legacy::connect::dns::{
|
use hyper_util::client::legacy::connect::dns::{
|
||||||
GaiResolver as HyperGaiResolver, Name as HyperName,
|
GaiResolver as HyperGaiResolver, Name as HyperName,
|
||||||
};
|
};
|
||||||
|
use log::info;
|
||||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio::sync::{RwLock, mpsc};
|
||||||
use tower_service::Service;
|
use tower_service::Service;
|
||||||
|
use yaak_models::models::DnsOverride;
|
||||||
|
|
||||||
|
/// Stores resolved addresses for a hostname override
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ResolvedOverride {
|
||||||
|
pub ipv4: Vec<Ipv4Addr>,
|
||||||
|
pub ipv6: Vec<Ipv6Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LocalhostResolver {
|
pub struct LocalhostResolver {
|
||||||
fallback: HyperGaiResolver,
|
fallback: HyperGaiResolver,
|
||||||
|
event_tx: Arc<RwLock<Option<mpsc::Sender<HttpResponseEvent>>>>,
|
||||||
|
overrides: Arc<HashMap<String, ResolvedOverride>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalhostResolver {
|
impl LocalhostResolver {
|
||||||
pub fn new() -> Arc<Self> {
|
pub fn new(dns_overrides: Vec<DnsOverride>) -> Arc<Self> {
|
||||||
let resolver = HyperGaiResolver::new();
|
let resolver = HyperGaiResolver::new();
|
||||||
Arc::new(Self { fallback: resolver })
|
|
||||||
|
// Pre-parse DNS overrides into a lookup map
|
||||||
|
let mut overrides = HashMap::new();
|
||||||
|
for o in dns_overrides {
|
||||||
|
if !o.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hostname = o.hostname.to_lowercase();
|
||||||
|
|
||||||
|
let ipv4: Vec<Ipv4Addr> =
|
||||||
|
o.ipv4.iter().filter_map(|s| s.parse::<Ipv4Addr>().ok()).collect();
|
||||||
|
|
||||||
|
let ipv6: Vec<Ipv6Addr> =
|
||||||
|
o.ipv6.iter().filter_map(|s| s.parse::<Ipv6Addr>().ok()).collect();
|
||||||
|
|
||||||
|
// Only add if at least one address is valid
|
||||||
|
if !ipv4.is_empty() || !ipv6.is_empty() {
|
||||||
|
overrides.insert(hostname, ResolvedOverride { ipv4, ipv6 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Arc::new(Self {
|
||||||
|
fallback: resolver,
|
||||||
|
event_tx: Arc::new(RwLock::new(None)),
|
||||||
|
overrides: Arc::new(overrides),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the event sender for the current request.
|
||||||
|
/// This should be called before each request to direct DNS events
|
||||||
|
/// to the appropriate channel.
|
||||||
|
pub async fn set_event_sender(&self, tx: Option<mpsc::Sender<HttpResponseEvent>>) {
|
||||||
|
let mut guard = self.event_tx.write().await;
|
||||||
|
*guard = tx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resolve for LocalhostResolver {
|
impl Resolve for LocalhostResolver {
|
||||||
fn resolve(&self, name: Name) -> Resolving {
|
fn resolve(&self, name: Name) -> Resolving {
|
||||||
let host = name.as_str().to_lowercase();
|
let host = name.as_str().to_lowercase();
|
||||||
|
let event_tx = self.event_tx.clone();
|
||||||
|
let overrides = self.overrides.clone();
|
||||||
|
|
||||||
|
info!("DNS resolve called for: {}", host);
|
||||||
|
|
||||||
|
// Check for DNS override first
|
||||||
|
if let Some(resolved) = overrides.get(&host) {
|
||||||
|
log::debug!("DNS override found for: {}", host);
|
||||||
|
let hostname = host.clone();
|
||||||
|
let mut addrs: Vec<SocketAddr> = Vec::new();
|
||||||
|
|
||||||
|
// Add IPv4 addresses
|
||||||
|
for ip in &resolved.ipv4 {
|
||||||
|
addrs.push(SocketAddr::new(IpAddr::V4(*ip), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IPv6 addresses
|
||||||
|
for ip in &resolved.ipv6 {
|
||||||
|
addrs.push(SocketAddr::new(IpAddr::V6(*ip), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
||||||
|
|
||||||
|
return Box::pin(async move {
|
||||||
|
// Emit DNS event for override
|
||||||
|
let guard = event_tx.read().await;
|
||||||
|
if let Some(tx) = guard.as_ref() {
|
||||||
|
let _ = tx
|
||||||
|
.send(HttpResponseEvent::DnsResolved {
|
||||||
|
hostname,
|
||||||
|
addresses,
|
||||||
|
duration: 0,
|
||||||
|
overridden: true,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for .localhost suffix
|
||||||
let is_localhost = host.ends_with(".localhost");
|
let is_localhost = host.ends_with(".localhost");
|
||||||
if is_localhost {
|
if is_localhost {
|
||||||
|
let hostname = host.clone();
|
||||||
// Port 0 is fine; reqwest replaces it with the URL's explicit
|
// Port 0 is fine; reqwest replaces it with the URL's explicit
|
||||||
// port or the scheme’s default (80/443, etc.).
|
// port or the scheme's default (80/443, etc.).
|
||||||
// (See docs note below.)
|
|
||||||
let addrs: Vec<SocketAddr> = vec![
|
let addrs: Vec<SocketAddr> = vec![
|
||||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let addresses: Vec<String> = addrs.iter().map(|a| a.ip().to_string()).collect();
|
||||||
|
|
||||||
return Box::pin(async move {
|
return Box::pin(async move {
|
||||||
|
// Emit DNS event for localhost resolution
|
||||||
|
let guard = event_tx.read().await;
|
||||||
|
if let Some(tx) = guard.as_ref() {
|
||||||
|
let _ = tx
|
||||||
|
.send(HttpResponseEvent::DnsResolved {
|
||||||
|
hostname,
|
||||||
|
addresses,
|
||||||
|
duration: 0,
|
||||||
|
overridden: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
Ok::<Addrs, Box<dyn std::error::Error + Send + Sync>>(Box::new(addrs.into_iter()))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to system DNS
|
||||||
let mut fallback = self.fallback.clone();
|
let mut fallback = self.fallback.clone();
|
||||||
let name_str = name.as_str().to_string();
|
let name_str = name.as_str().to_string();
|
||||||
|
let hostname = host.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match HyperName::from_str(&name_str) {
|
let start = Instant::now();
|
||||||
Ok(n) => fallback
|
|
||||||
.call(n)
|
let result = match HyperName::from_str(&name_str) {
|
||||||
.await
|
Ok(n) => fallback.call(n).await,
|
||||||
.map(|addrs| Box::new(addrs) as Addrs)
|
Err(e) => return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
||||||
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
};
|
||||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>),
|
|
||||||
|
let duration = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(addrs) => {
|
||||||
|
// Collect addresses for event emission
|
||||||
|
let addr_vec: Vec<SocketAddr> = addrs.collect();
|
||||||
|
let addresses: Vec<String> =
|
||||||
|
addr_vec.iter().map(|a| a.ip().to_string()).collect();
|
||||||
|
|
||||||
|
// Emit DNS event
|
||||||
|
let guard = event_tx.read().await;
|
||||||
|
if let Some(tx) = guard.as_ref() {
|
||||||
|
let _ = tx
|
||||||
|
.send(HttpResponseEvent::DnsResolved {
|
||||||
|
hostname,
|
||||||
|
addresses,
|
||||||
|
duration,
|
||||||
|
overridden: false,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Box::new(addr_vec.into_iter()) as Addrs)
|
||||||
|
}
|
||||||
|
Err(err) => Err(Box::new(err) as Box<dyn std::error::Error + Send + Sync>),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::client::HttpConnectionOptions;
|
use crate::client::HttpConnectionOptions;
|
||||||
|
use crate::dns::LocalhostResolver;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::info;
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
@@ -7,8 +8,15 @@ use std::sync::Arc;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
/// A cached HTTP client along with its DNS resolver.
|
||||||
|
/// The resolver is needed to set the event sender per-request.
|
||||||
|
pub struct CachedClient {
|
||||||
|
pub client: Client,
|
||||||
|
pub resolver: Arc<LocalhostResolver>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HttpConnectionManager {
|
pub struct HttpConnectionManager {
|
||||||
connections: Arc<RwLock<BTreeMap<String, (Client, Instant)>>>,
|
connections: Arc<RwLock<BTreeMap<String, (CachedClient, Instant)>>>,
|
||||||
ttl: Duration,
|
ttl: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,21 +28,26 @@ impl HttpConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<Client> {
|
pub async fn get_client(&self, opt: &HttpConnectionOptions) -> Result<CachedClient> {
|
||||||
let mut connections = self.connections.write().await;
|
let mut connections = self.connections.write().await;
|
||||||
let id = opt.id.clone();
|
let id = opt.id.clone();
|
||||||
|
|
||||||
// Clean old connections
|
// Clean old connections
|
||||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||||
|
|
||||||
if let Some((c, last_used)) = connections.get_mut(&id) {
|
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
||||||
info!("Re-using HTTP client {id}");
|
info!("Re-using HTTP client {id}");
|
||||||
*last_used = Instant::now();
|
*last_used = Instant::now();
|
||||||
return Ok(c.clone());
|
return Ok(CachedClient {
|
||||||
|
client: cached.client.clone(),
|
||||||
|
resolver: cached.resolver.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let c = opt.build_client()?;
|
let (client, resolver) = opt.build_client()?;
|
||||||
connections.insert(id.into(), (c.clone(), Instant::now()));
|
let cached = CachedClient { client: client.clone(), resolver: resolver.clone() };
|
||||||
Ok(c)
|
connections.insert(id.into(), (cached, Instant::now()));
|
||||||
|
|
||||||
|
Ok(CachedClient { client, resolver })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ pub enum HttpResponseEvent {
|
|||||||
ChunkReceived {
|
ChunkReceived {
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
},
|
},
|
||||||
|
DnsResolved {
|
||||||
|
hostname: String,
|
||||||
|
addresses: Vec<String>,
|
||||||
|
duration: u64,
|
||||||
|
overridden: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for HttpResponseEvent {
|
impl Display for HttpResponseEvent {
|
||||||
@@ -67,6 +73,19 @@ impl Display for HttpResponseEvent {
|
|||||||
HttpResponseEvent::HeaderDown(name, value) => write!(f, "< {}: {}", name, value),
|
HttpResponseEvent::HeaderDown(name, value) => write!(f, "< {}: {}", name, value),
|
||||||
HttpResponseEvent::ChunkSent { bytes } => write!(f, "> [{} bytes sent]", bytes),
|
HttpResponseEvent::ChunkSent { bytes } => write!(f, "> [{} bytes sent]", bytes),
|
||||||
HttpResponseEvent::ChunkReceived { bytes } => write!(f, "< [{} bytes received]", bytes),
|
HttpResponseEvent::ChunkReceived { bytes } => write!(f, "< [{} bytes received]", bytes),
|
||||||
|
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
||||||
|
if *overridden {
|
||||||
|
write!(f, "* DNS override {} -> {}", hostname, addresses.join(", "))
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"* DNS resolved {} to {} ({}ms)",
|
||||||
|
hostname,
|
||||||
|
addresses.join(", "),
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +112,9 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
HttpResponseEvent::HeaderDown(name, value) => D::HeaderDown { name, value },
|
HttpResponseEvent::HeaderDown(name, value) => D::HeaderDown { name, value },
|
||||||
HttpResponseEvent::ChunkSent { bytes } => D::ChunkSent { bytes },
|
HttpResponseEvent::ChunkSent { bytes } => D::ChunkSent { bytes },
|
||||||
HttpResponseEvent::ChunkReceived { bytes } => D::ChunkReceived { bytes },
|
HttpResponseEvent::ChunkReceived { bytes } => D::ChunkReceived { bytes },
|
||||||
|
HttpResponseEvent::DnsResolved { hostname, addresses, duration, overridden } => {
|
||||||
|
D::DnsResolved { hostname, addresses, duration, overridden }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -12,6 +12,8 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
|||||||
|
|
||||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||||
|
|
||||||
|
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||||
|
|
||||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
@@ -38,7 +40,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -91,6 +93,6 @@ export type WebsocketMessageType = "text" | "binary";
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|
||||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add DNS resolution timing to http_responses
|
||||||
|
ALTER TABLE http_responses ADD COLUMN elapsed_dns INTEGER DEFAULT 0 NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add DNS overrides setting to workspaces
|
||||||
|
ALTER TABLE workspaces ADD COLUMN setting_dns_overrides TEXT DEFAULT '[]' NOT NULL;
|
||||||
@@ -73,6 +73,20 @@ pub struct ClientCertificate {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
|
pub struct DnsOverride {
|
||||||
|
pub hostname: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ipv4: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ipv6: Vec<String>,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
#[ts(optional, as = "Option<bool>")]
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
@@ -303,6 +317,8 @@ pub struct Workspace {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub setting_follow_redirects: bool,
|
pub setting_follow_redirects: bool,
|
||||||
pub setting_request_timeout: i32,
|
pub setting_request_timeout: i32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub setting_dns_overrides: Vec<DnsOverride>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Workspace {
|
impl UpsertModelInfo for Workspace {
|
||||||
@@ -343,6 +359,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||||
|
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +376,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
WorkspaceIden::SettingFollowRedirects,
|
WorkspaceIden::SettingFollowRedirects,
|
||||||
WorkspaceIden::SettingRequestTimeout,
|
WorkspaceIden::SettingRequestTimeout,
|
||||||
WorkspaceIden::SettingValidateCertificates,
|
WorkspaceIden::SettingValidateCertificates,
|
||||||
|
WorkspaceIden::SettingDnsOverrides,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +386,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
{
|
{
|
||||||
let headers: String = row.get("headers")?;
|
let headers: String = row.get("headers")?;
|
||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
|
let setting_dns_overrides: String = row.get("setting_dns_overrides")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -382,6 +401,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||||
|
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1333,6 +1353,7 @@ pub struct HttpResponse {
|
|||||||
pub content_length_compressed: Option<i32>,
|
pub content_length_compressed: Option<i32>,
|
||||||
pub elapsed: i32,
|
pub elapsed: i32,
|
||||||
pub elapsed_headers: i32,
|
pub elapsed_headers: i32,
|
||||||
|
pub elapsed_dns: i32,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub headers: Vec<HttpResponseHeader>,
|
pub headers: Vec<HttpResponseHeader>,
|
||||||
pub remote_addr: Option<String>,
|
pub remote_addr: Option<String>,
|
||||||
@@ -1381,6 +1402,7 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
(ContentLengthCompressed, self.content_length_compressed.into()),
|
(ContentLengthCompressed, self.content_length_compressed.into()),
|
||||||
(Elapsed, self.elapsed.into()),
|
(Elapsed, self.elapsed.into()),
|
||||||
(ElapsedHeaders, self.elapsed_headers.into()),
|
(ElapsedHeaders, self.elapsed_headers.into()),
|
||||||
|
(ElapsedDns, self.elapsed_dns.into()),
|
||||||
(Error, self.error.into()),
|
(Error, self.error.into()),
|
||||||
(Headers, serde_json::to_string(&self.headers)?.into()),
|
(Headers, serde_json::to_string(&self.headers)?.into()),
|
||||||
(RemoteAddr, self.remote_addr.into()),
|
(RemoteAddr, self.remote_addr.into()),
|
||||||
@@ -1402,6 +1424,7 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
HttpResponseIden::ContentLengthCompressed,
|
HttpResponseIden::ContentLengthCompressed,
|
||||||
HttpResponseIden::Elapsed,
|
HttpResponseIden::Elapsed,
|
||||||
HttpResponseIden::ElapsedHeaders,
|
HttpResponseIden::ElapsedHeaders,
|
||||||
|
HttpResponseIden::ElapsedDns,
|
||||||
HttpResponseIden::Error,
|
HttpResponseIden::Error,
|
||||||
HttpResponseIden::Headers,
|
HttpResponseIden::Headers,
|
||||||
HttpResponseIden::RemoteAddr,
|
HttpResponseIden::RemoteAddr,
|
||||||
@@ -1435,6 +1458,7 @@ impl UpsertModelInfo for HttpResponse {
|
|||||||
version: r.get("version")?,
|
version: r.get("version")?,
|
||||||
elapsed: r.get("elapsed")?,
|
elapsed: r.get("elapsed")?,
|
||||||
elapsed_headers: r.get("elapsed_headers")?,
|
elapsed_headers: r.get("elapsed_headers")?,
|
||||||
|
elapsed_dns: r.get("elapsed_dns").unwrap_or_default(),
|
||||||
remote_addr: r.get("remote_addr")?,
|
remote_addr: r.get("remote_addr")?,
|
||||||
status: r.get("status")?,
|
status: r.get("status")?,
|
||||||
status_reason: r.get("status_reason")?,
|
status_reason: r.get("status_reason")?,
|
||||||
@@ -1491,6 +1515,12 @@ pub enum HttpResponseEventData {
|
|||||||
ChunkReceived {
|
ChunkReceived {
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
},
|
},
|
||||||
|
DnsResolved {
|
||||||
|
hostname: String,
|
||||||
|
addresses: Vec<String>,
|
||||||
|
duration: u64,
|
||||||
|
overridden: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HttpResponseEventData {
|
impl Default for HttpResponseEventData {
|
||||||
|
|||||||
+5
-3
@@ -12,6 +12,8 @@ export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
|||||||
|
|
||||||
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
|
||||||
|
|
||||||
|
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||||
|
|
||||||
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
@@ -38,7 +40,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -77,6 +79,6 @@ export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping"
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|
||||||
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, };
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ pub async fn check_plugin_updates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Search for plugins in the registry.
|
/// Search for plugins in the registry.
|
||||||
pub async fn search_plugins(
|
pub async fn search_plugins(http_client: &Client, query: &str) -> Result<PluginSearchResponse> {
|
||||||
http_client: &Client,
|
|
||||||
query: &str,
|
|
||||||
) -> Result<PluginSearchResponse> {
|
|
||||||
let mut url = build_url("/search");
|
let mut url = build_url("/search");
|
||||||
{
|
{
|
||||||
let mut query_pairs = url.query_pairs_mut();
|
let mut query_pairs = url.query_pairs_mut();
|
||||||
|
|||||||
@@ -196,7 +196,11 @@ pub fn decrypt_secure_template_function(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_tokens.push(Token::Raw {
|
new_tokens.push(Token::Raw {
|
||||||
text: template_function_secure_run(encryption_manager, args_map, plugin_context)?,
|
text: template_function_secure_run(
|
||||||
|
encryption_manager,
|
||||||
|
args_map,
|
||||||
|
plugin_context,
|
||||||
|
)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
t => {
|
t => {
|
||||||
@@ -216,7 +220,8 @@ pub fn encrypt_secure_template_function(
|
|||||||
plugin_context: &PluginContext,
|
plugin_context: &PluginContext,
|
||||||
template: &str,
|
template: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
let decrypted =
|
||||||
|
decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
||||||
let tokens = Tokens {
|
let tokens = Tokens {
|
||||||
tokens: vec. Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DnsOverride = { hostname: string, ipv4: Array<string>, ipv6: Array<string>, enabled?: boolean, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
@@ -20,4 +22,4 @@ export type SyncState = { model: "sync_state", id: string, workspaceId: string,
|
|||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|||||||
@@ -296,11 +296,7 @@ pub fn compute_sync_ops(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_models(
|
fn workspace_models(db: &DbContext, version: &str, workspace_id: &str) -> Result<Vec<SyncModel>> {
|
||||||
db: &DbContext,
|
|
||||||
version: &str,
|
|
||||||
workspace_id: &str,
|
|
||||||
) -> Result<Vec<SyncModel>> {
|
|
||||||
// We want to include private environments here so that we can take them into account during
|
// We want to include private environments here so that we can take them into account during
|
||||||
// the sync process. Otherwise, they would be treated as deleted.
|
// the sync process. Otherwise, they would be treated as deleted.
|
||||||
let include_private_environments = true;
|
let include_private_environments = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::connect::ws_connect;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use futures_util::stream::SplitSink;
|
use futures_util::stream::SplitSink;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use http::HeaderMap;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -10,7 +11,6 @@ use tokio::net::TcpStream;
|
|||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio_tungstenite::tungstenite::Message;
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
||||||
use http::HeaderMap;
|
|
||||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||||
use yaak_tls::ClientCertificateConfig;
|
use yaak_tls::ClientCertificateConfig;
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ export type HttpRequest = { model: "http_request", id: string, createdAt: string
|
|||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, elapsedDns: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array<HttpResponseHeader>, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
|
||||||
|
|
||||||
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, };
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import type { DnsOverride, Workspace } from '@yaakapp-internal/models';
|
||||||
|
import { patchModel } from '@yaakapp-internal/models';
|
||||||
|
import { useCallback, useId, useMemo } from 'react';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { Checkbox } from './core/Checkbox';
|
||||||
|
import { IconButton } from './core/IconButton';
|
||||||
|
import { PlainInput } from './core/PlainInput';
|
||||||
|
import { HStack, VStack } from './core/Stacks';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from './core/Table';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
workspace: Workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DnsOverrideWithId extends DnsOverride {
|
||||||
|
_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DnsOverridesEditor({ workspace }: Props) {
|
||||||
|
const reactId = useId();
|
||||||
|
|
||||||
|
// Ensure each override has an internal ID for React keys
|
||||||
|
const overridesWithIds = useMemo<DnsOverrideWithId[]>(() => {
|
||||||
|
return workspace.settingDnsOverrides.map((override, index) => ({
|
||||||
|
...override,
|
||||||
|
_id: `${reactId}-${index}`,
|
||||||
|
}));
|
||||||
|
}, [workspace.settingDnsOverrides, reactId]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(overrides: DnsOverride[]) => {
|
||||||
|
patchModel(workspace, { settingDnsOverrides: overrides });
|
||||||
|
},
|
||||||
|
[workspace],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAdd = useCallback(() => {
|
||||||
|
const newOverride: DnsOverride = {
|
||||||
|
hostname: '',
|
||||||
|
ipv4: [''],
|
||||||
|
ipv6: [],
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
handleChange([...workspace.settingDnsOverrides, newOverride]);
|
||||||
|
}, [workspace.settingDnsOverrides, handleChange]);
|
||||||
|
|
||||||
|
const handleUpdate = useCallback(
|
||||||
|
(index: number, update: Partial<DnsOverride>) => {
|
||||||
|
const updated = workspace.settingDnsOverrides.map((o, i) =>
|
||||||
|
i === index ? { ...o, ...update } : o,
|
||||||
|
);
|
||||||
|
handleChange(updated);
|
||||||
|
},
|
||||||
|
[workspace.settingDnsOverrides, handleChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const updated = workspace.settingDnsOverrides.filter((_, i) => i !== index);
|
||||||
|
handleChange(updated);
|
||||||
|
},
|
||||||
|
[workspace.settingDnsOverrides, handleChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={3} className="pb-3">
|
||||||
|
<div className="text-text-subtle text-sm">
|
||||||
|
Override DNS resolution for specific hostnames. This works like{' '}
|
||||||
|
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code>{' '}
|
||||||
|
but only for requests made from this workspace.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{overridesWithIds.length > 0 && (
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell className="w-8" />
|
||||||
|
<TableHeaderCell>Hostname</TableHeaderCell>
|
||||||
|
<TableHeaderCell>IPv4 Address</TableHeaderCell>
|
||||||
|
<TableHeaderCell>IPv6 Address</TableHeaderCell>
|
||||||
|
<TableHeaderCell className="w-10" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{overridesWithIds.map((override, index) => (
|
||||||
|
<DnsOverrideRow
|
||||||
|
key={override._id}
|
||||||
|
override={override}
|
||||||
|
onUpdate={(update) => handleUpdate(index, update)}
|
||||||
|
onDelete={() => handleDelete(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
<Button size="xs" color="secondary" variant="border" onClick={handleAdd}>
|
||||||
|
Add DNS Override
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DnsOverrideRowProps {
|
||||||
|
override: DnsOverride;
|
||||||
|
onUpdate: (update: Partial<DnsOverride>) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DnsOverrideRow({ override, onUpdate, onDelete }: DnsOverrideRowProps) {
|
||||||
|
const ipv4Value = override.ipv4.join(', ');
|
||||||
|
const ipv6Value = override.ipv6.join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
hideLabel
|
||||||
|
title={override.enabled ? 'Disable override' : 'Enable override'}
|
||||||
|
checked={override.enabled ?? true}
|
||||||
|
onChange={(enabled) => onUpdate({ enabled })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PlainInput
|
||||||
|
size="sm"
|
||||||
|
hideLabel
|
||||||
|
label="Hostname"
|
||||||
|
placeholder="api.example.com"
|
||||||
|
defaultValue={override.hostname}
|
||||||
|
onChange={(hostname) => onUpdate({ hostname })}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PlainInput
|
||||||
|
size="sm"
|
||||||
|
hideLabel
|
||||||
|
label="IPv4 addresses"
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
defaultValue={ipv4Value}
|
||||||
|
onChange={(value) =>
|
||||||
|
onUpdate({
|
||||||
|
ipv4: value
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PlainInput
|
||||||
|
size="sm"
|
||||||
|
hideLabel
|
||||||
|
label="IPv6 addresses"
|
||||||
|
placeholder="::1"
|
||||||
|
defaultValue={ipv6Value}
|
||||||
|
onChange={(value) =>
|
||||||
|
onUpdate({
|
||||||
|
ipv6: value
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
iconSize="sm"
|
||||||
|
icon="trash"
|
||||||
|
title="Delete override"
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -188,6 +188,35 @@ function EventDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DNS Resolution - show hostname, addresses, and timing
|
||||||
|
if (e.type === 'dns_resolved') {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<EventDetailHeader
|
||||||
|
title={e.overridden ? 'DNS Override' : 'DNS Resolution'}
|
||||||
|
timestamp={event.createdAt}
|
||||||
|
actions={actions}
|
||||||
|
/>
|
||||||
|
<KeyValueRows>
|
||||||
|
<KeyValueRow label="Hostname">{e.hostname}</KeyValueRow>
|
||||||
|
<KeyValueRow label="Addresses">{e.addresses.join(', ')}</KeyValueRow>
|
||||||
|
<KeyValueRow label="Duration">
|
||||||
|
{e.overridden ? (
|
||||||
|
<span className="text-text-subtlest">--</span>
|
||||||
|
) : (
|
||||||
|
`${String(e.duration)}ms`
|
||||||
|
)}
|
||||||
|
</KeyValueRow>
|
||||||
|
</KeyValueRows>
|
||||||
|
{e.overridden && (
|
||||||
|
<KeyValueRows>
|
||||||
|
<KeyValueRow label="Source">Workspace Override</KeyValueRow>
|
||||||
|
</KeyValueRows>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Default - use summary
|
// Default - use summary
|
||||||
const { summary } = getEventDisplay(event.event);
|
const { summary } = getEventDisplay(event.event);
|
||||||
return (
|
return (
|
||||||
@@ -219,6 +248,11 @@ function formatEventRaw(event: HttpResponseEventData): string {
|
|||||||
return `[${formatBytes(event.bytes)} sent]`;
|
return `[${formatBytes(event.bytes)} sent]`;
|
||||||
case 'chunk_received':
|
case 'chunk_received':
|
||||||
return `[${formatBytes(event.bytes)} received]`;
|
return `[${formatBytes(event.bytes)} received]`;
|
||||||
|
case 'dns_resolved':
|
||||||
|
if (event.overridden) {
|
||||||
|
return `DNS override ${event.hostname} → ${event.addresses.join(', ')}`;
|
||||||
|
}
|
||||||
|
return `DNS resolved ${event.hostname} → ${event.addresses.join(', ')} (${event.duration}ms)`;
|
||||||
default:
|
default:
|
||||||
return '[unknown event]';
|
return '[unknown event]';
|
||||||
}
|
}
|
||||||
@@ -297,6 +331,15 @@ function getEventDisplay(event: HttpResponseEventData): EventDisplay {
|
|||||||
label: 'Chunk',
|
label: 'Chunk',
|
||||||
summary: `${formatBytes(event.bytes)} chunk received`,
|
summary: `${formatBytes(event.bytes)} chunk received`,
|
||||||
};
|
};
|
||||||
|
case 'dns_resolved':
|
||||||
|
return {
|
||||||
|
icon: 'globe',
|
||||||
|
color: event.overridden ? 'success' : 'secondary',
|
||||||
|
label: event.overridden ? 'DNS Override' : 'DNS',
|
||||||
|
summary: event.overridden
|
||||||
|
? `${event.hostname} → ${event.addresses.join(', ')} (overridden)`
|
||||||
|
: `${event.hostname} → ${event.addresses.join(', ')} (${event.duration}ms)`,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
icon: 'info',
|
icon: 'info',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { InlineCode } from './core/InlineCode';
|
|||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { HStack, VStack } from './core/Stacks';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||||
|
import { DnsOverridesEditor } from './DnsOverridesEditor';
|
||||||
import { HeadersEditor } from './HeadersEditor';
|
import { HeadersEditor } from './HeadersEditor';
|
||||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||||
import { MarkdownEditor } from './MarkdownEditor';
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
@@ -27,11 +28,13 @@ interface Props {
|
|||||||
|
|
||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
const TAB_DATA = 'data';
|
const TAB_DATA = 'data';
|
||||||
|
const TAB_DNS = 'dns';
|
||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
const TAB_GENERAL = 'general';
|
const TAB_GENERAL = 'general';
|
||||||
|
|
||||||
export type WorkspaceSettingsTab =
|
export type WorkspaceSettingsTab =
|
||||||
| typeof TAB_AUTH
|
| typeof TAB_AUTH
|
||||||
|
| typeof TAB_DNS
|
||||||
| typeof TAB_HEADERS
|
| typeof TAB_HEADERS
|
||||||
| typeof TAB_GENERAL
|
| typeof TAB_GENERAL
|
||||||
| typeof TAB_DATA;
|
| typeof TAB_DATA;
|
||||||
@@ -75,6 +78,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
value: TAB_DATA,
|
value: TAB_DATA,
|
||||||
label: 'Storage',
|
label: 'Storage',
|
||||||
},
|
},
|
||||||
|
{ value: TAB_DNS, label: 'DNS' },
|
||||||
...headersTab,
|
...headersTab,
|
||||||
...authTab,
|
...authTab,
|
||||||
]}
|
]}
|
||||||
@@ -153,6 +157,9 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
<WorkspaceEncryptionSetting size="xs" />
|
<WorkspaceEncryptionSetting size="xs" />
|
||||||
</VStack>
|
</VStack>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value={TAB_DNS} className="overflow-y-auto h-full px-4">
|
||||||
|
<DnsOverridesEditor workspace={workspace} />
|
||||||
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export function HttpResponseDurationTag({ response }: Props) {
|
|||||||
return () => clearInterval(timeout.current);
|
return () => clearInterval(timeout.current);
|
||||||
}, [response.createdAt, response.state]);
|
}, [response.createdAt, response.state]);
|
||||||
|
|
||||||
const title = `HEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
|
const dnsValue = response.elapsedDns > 0 ? formatMillis(response.elapsedDns) : '--';
|
||||||
|
const title = `DNS: ${dnsValue}\nHEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
|
||||||
|
|
||||||
const elapsed = response.state === 'closed' ? response.elapsed : fallbackElapsed;
|
const elapsed = response.state === 'closed' ? response.elapsed : fallbackElapsed;
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import {
|
|||||||
GitCommitVerticalIcon,
|
GitCommitVerticalIcon,
|
||||||
GitForkIcon,
|
GitForkIcon,
|
||||||
GitPullRequestIcon,
|
GitPullRequestIcon,
|
||||||
|
GlobeIcon,
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
HandIcon,
|
HandIcon,
|
||||||
HardDriveDownloadIcon,
|
HardDriveDownloadIcon,
|
||||||
@@ -212,6 +213,7 @@ const icons = {
|
|||||||
git_commit_vertical: GitCommitVerticalIcon,
|
git_commit_vertical: GitCommitVerticalIcon,
|
||||||
git_fork: GitForkIcon,
|
git_fork: GitForkIcon,
|
||||||
git_pull_request: GitPullRequestIcon,
|
git_pull_request: GitPullRequestIcon,
|
||||||
|
globe: GlobeIcon,
|
||||||
grip_vertical: GripVerticalIcon,
|
grip_vertical: GripVerticalIcon,
|
||||||
circle_off: CircleOffIcon,
|
circle_off: CircleOffIcon,
|
||||||
hand: HandIcon,
|
hand: HandIcon,
|
||||||
|
|||||||
Reference in New Issue
Block a user