feat(plugins): hand SOCKS5 transport proxy routes to connections
This commit is contained in:
1 parent
8518a5bd25
commit
2b15327de3
13 files changed
+330
-46
No files matched your search
@@ -23,7 +23,9 @@ pub use filesystem::{
|
||||
PLUGIN_FILESYSTEM_CREATE_DIRECTORY_METHOD, PLUGIN_FILESYSTEM_DELETE_METHOD, PLUGIN_FILESYSTEM_LIST_METHOD,
|
||||
PLUGIN_FILESYSTEM_READ_METHOD, PLUGIN_FILESYSTEM_RENAME_METHOD, PLUGIN_FILESYSTEM_WRITE_METHOD,
|
||||
};
|
||||
pub use host::{ActivePluginSession, PluginConnectionActionResult, PluginConnectionHandle, PluginHost};
|
||||
pub use host::{
|
||||
ActivePluginSession, PluginConnectionActionResult, PluginConnectionHandle, PluginHost, PluginRuntimeProxy,
|
||||
};
|
||||
pub use installer::{
|
||||
PluginInstallPolicy, PluginInstallResponse, PluginInstallResult, PluginPackageInstaller, PluginRollbackResponse,
|
||||
PluginRollbackResult, PluginSignatureStatus, PluginTrustStore, PluginTrustedKey, DBXP_EXTENSION,
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::models::connection::{ConnectionConfig, ConnectionTestResult, DatabaseType};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
|
||||
use super::{
|
||||
@@ -191,13 +191,26 @@ impl PluginHost {
|
||||
/// to an installed connection provider.
|
||||
pub fn connection_params_standalone(&self, config: &ConnectionConfig) -> Result<serde_json::Value, String> {
|
||||
let (_, provider) = self.resolve_connection_provider(config)?;
|
||||
plugin_connection_params(config, &provider, &config.host, config.port)
|
||||
plugin_connection_params(config, &provider, &config.host, config.port, None)
|
||||
}
|
||||
|
||||
/// Reports whether the connection's provider declared
|
||||
/// `proxy_route` (multi-endpoint targets that want a SOCKS5
|
||||
/// runtime route over transport layers instead of a static tunnel).
|
||||
/// Unresolvable configs report `false` so the caller falls back to the
|
||||
/// static-tunnel path (and its empty-endpoint guard).
|
||||
pub async fn wants_proxy_route(&self, config: &ConnectionConfig) -> bool {
|
||||
match self.resolve_connection_provider(config) {
|
||||
Ok((_, provider)) => provider.proxy_route,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
pub async fn test_connection(
|
||||
&self,
|
||||
config: &ConnectionConfig,
|
||||
runtime_host: &str,
|
||||
runtime_port: u16,
|
||||
runtime_proxy: Option<PluginRuntimeProxy>,
|
||||
) -> Result<ConnectionTestResult, String> {
|
||||
let _activity = self
|
||||
.inner
|
||||
@@ -214,7 +227,7 @@ impl PluginHost {
|
||||
let result: serde_json::Value = session
|
||||
.invoke_with_timeout(
|
||||
PLUGIN_CONNECTION_TEST_METHOD,
|
||||
plugin_connection_params(config, &provider, runtime_host, runtime_port)?,
|
||||
plugin_connection_params(config, &provider, runtime_host, runtime_port, runtime_proxy.as_ref())?,
|
||||
None,
|
||||
Some(plugin_connect_deadline(config, &provider)),
|
||||
)
|
||||
@@ -227,6 +240,7 @@ impl PluginHost {
|
||||
config: &ConnectionConfig,
|
||||
runtime_host: &str,
|
||||
runtime_port: u16,
|
||||
runtime_proxy: Option<PluginRuntimeProxy>,
|
||||
) -> Result<PluginConnectionHandle, String> {
|
||||
let activity = self
|
||||
.inner
|
||||
@@ -235,7 +249,7 @@ impl PluginHost {
|
||||
.begin_connection(config.plugin_id.as_deref().unwrap_or_default(), &config.name)?;
|
||||
let (_, provider) = self.resolve_connection_provider(config)?;
|
||||
validate_plugin_connection_values(config, &provider)?;
|
||||
let params = plugin_connection_params(config, &provider, runtime_host, runtime_port)?;
|
||||
let params = plugin_connection_params(config, &provider, runtime_host, runtime_port, runtime_proxy.as_ref())?;
|
||||
let needs_session = provider.has_capability(PluginConnectionCapability::Connect)
|
||||
|| provider.has_capability(PluginConnectionCapability::Disconnect);
|
||||
let session = if needs_session {
|
||||
@@ -274,6 +288,7 @@ impl PluginHost {
|
||||
action_id: &str,
|
||||
runtime_host: &str,
|
||||
runtime_port: u16,
|
||||
runtime_proxy: Option<PluginRuntimeProxy>,
|
||||
) -> Result<PluginConnectionActionResult, String> {
|
||||
let _activity = self
|
||||
.inner
|
||||
@@ -284,7 +299,8 @@ impl PluginHost {
|
||||
let action = plugin_invoke_connection_action(&provider, action_id)?;
|
||||
validate_plugin_connection_values_for_action(config, &provider, action.requires_valid_form)?;
|
||||
let session = self.activate(config.plugin_id.as_deref().unwrap_or_default()).await?;
|
||||
let mut params = plugin_connection_params(config, &provider, runtime_host, runtime_port)?;
|
||||
let mut params =
|
||||
plugin_connection_params(config, &provider, runtime_host, runtime_port, runtime_proxy.as_ref())?;
|
||||
params
|
||||
.as_object_mut()
|
||||
.ok_or("Plugin connection action params must be an object")?
|
||||
@@ -433,21 +449,50 @@ fn plugin_connection_params(
|
||||
provider: &PluginConnectionProviderContribution,
|
||||
runtime_host: &str,
|
||||
runtime_port: u16,
|
||||
runtime_proxy: Option<&PluginRuntimeProxy>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut runtime = serde_json::json!({
|
||||
"host": runtime_host,
|
||||
"port": runtime_port,
|
||||
});
|
||||
if let Some(proxy) = runtime_proxy {
|
||||
runtime["proxy"] = serde_json::to_value(proxy).map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(serde_json::json!({
|
||||
"provider": {
|
||||
"id": provider.id,
|
||||
"databaseType": provider.database_type,
|
||||
},
|
||||
"connection": serde_json::to_value(config).map_err(|error| error.to_string())?,
|
||||
"runtime": {
|
||||
"host": runtime_host,
|
||||
"port": runtime_port,
|
||||
},
|
||||
"runtime": runtime,
|
||||
"operationId": uuid::Uuid::new_v4().to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// A host-managed SOCKS5 route handed to a plugin through
|
||||
/// `runtime.proxy`. Providers declaring `proxy_route` (multi-endpoint
|
||||
/// targets such as Kafka) dial every advertised broker through this route
|
||||
/// instead of a static tunnel, which can only reach a single endpoint.
|
||||
/// Credentials ride the same encrypted lifecycle channel as connection
|
||||
/// secrets and must never be logged by the plugin.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginRuntimeProxy {
|
||||
#[serde(rename = "type")]
|
||||
pub proxy_type: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub username: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl PluginRuntimeProxy {
|
||||
pub fn socks5(host: String, port: u16, username: String, password: String) -> Self {
|
||||
Self { proxy_type: "socks5".to_string(), host, port, username, password }
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_plugin_connection_values(
|
||||
config: &ConnectionConfig,
|
||||
provider: &PluginConnectionProviderContribution,
|
||||
@@ -775,9 +820,9 @@ fn ensure_permission(plugin: &super::InstalledPlugin, required_permission: Optio
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
plugin_connect_deadline, plugin_connection_action_result, plugin_field_is_visible,
|
||||
plugin_connect_deadline, plugin_connection_action_result, plugin_connection_params, plugin_field_is_visible,
|
||||
plugin_invoke_connection_action, validate_plugin_connection_values,
|
||||
validate_plugin_connection_values_for_action,
|
||||
validate_plugin_connection_values_for_action, PluginRuntimeProxy,
|
||||
};
|
||||
use crate::models::connection::ConnectionConfig;
|
||||
use crate::plugins::PluginConnectionProviderContribution;
|
||||
@@ -824,9 +869,9 @@ mod tests {
|
||||
let lifecycle = registry.lifecycle();
|
||||
let host = super::PluginHost::new(registry);
|
||||
let update = lifecycle.begin_update("sample.ui").unwrap();
|
||||
assert!(host.connect_connection(&config, "localhost", 0).await.is_err());
|
||||
assert!(host.connect_connection(&config, "localhost", 0, None).await.is_err());
|
||||
drop(update);
|
||||
let connection = host.connect_connection(&config, "localhost", 0).await.unwrap();
|
||||
let connection = host.connect_connection(&config, "localhost", 0, None).await.unwrap();
|
||||
assert!(lifecycle.begin_update("sample.ui").unwrap_err().contains("Saved UI connection"));
|
||||
connection.disconnect().await.unwrap();
|
||||
drop(connection);
|
||||
@@ -885,6 +930,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_params_embed_optional_socks5_proxy_route() {
|
||||
let config: ConnectionConfig = serde_json::from_value(serde_json::json!({
|
||||
"id": "plugin-connection",
|
||||
"name": "Plugin connection",
|
||||
"db_type": "plugin",
|
||||
"host": "",
|
||||
"port": 0,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"database": null,
|
||||
"plugin_id": "sample",
|
||||
"plugin_connection_provider": "sample.connection",
|
||||
"plugin_connection_type": "sample"
|
||||
}))
|
||||
.unwrap();
|
||||
let provider: PluginConnectionProviderContribution = serde_json::from_value(serde_json::json!({
|
||||
"id": "sample.connection",
|
||||
"label": "Sample",
|
||||
"database_type": "sample",
|
||||
"fields": []
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let without = plugin_connection_params(&config, &provider, "", 0, None).unwrap();
|
||||
assert!(without["runtime"].get("proxy").is_none());
|
||||
|
||||
let proxy = PluginRuntimeProxy::socks5("127.0.0.1".to_string(), 1080, "user".to_string(), "pass".to_string());
|
||||
let with = plugin_connection_params(&config, &provider, "k1", 9092, Some(&proxy)).unwrap();
|
||||
assert_eq!(with["runtime"]["proxy"]["type"], "socks5");
|
||||
assert_eq!(with["runtime"]["proxy"]["host"], "127.0.0.1");
|
||||
assert_eq!(with["runtime"]["proxy"]["port"], 1080);
|
||||
assert_eq!(with["runtime"]["proxy"]["username"], "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_port_for_port_bound_connection_field() {
|
||||
let config: ConnectionConfig = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -560,6 +560,14 @@ pub struct PluginConnectionProviderContribution {
|
||||
pub capabilities: Vec<PluginConnectionCapability>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub actions: Vec<PluginConnectionActionContribution>,
|
||||
/// Providers whose targets have multiple reachable endpoints (Kafka
|
||||
/// bootstrap + advertised listeners) declare this flag so the host hands
|
||||
/// them a SOCKS5 `runtime.proxy` route instead of a static tunnel, which
|
||||
/// can only reach a single endpoint. Without the flag, transport layers
|
||||
/// keep today's static-tunnel behavior (fine for single-endpoint
|
||||
/// providers such as SSH or LDAP, which declare binding host/port fields).
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub proxy_route: bool,
|
||||
}
|
||||
|
||||
impl PluginConnectionProviderContribution {
|
||||
@@ -1470,6 +1478,26 @@ mod tests {
|
||||
PluginManifest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn connection_provider_proxy_route_defaults_false_and_parses() {
|
||||
let provider: PluginConnectionProviderContribution = serde_json::from_value(serde_json::json!({
|
||||
"id": "sample.connection",
|
||||
"database_type": "sample",
|
||||
"fields": []
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(!provider.proxy_route);
|
||||
|
||||
let provider: PluginConnectionProviderContribution = serde_json::from_value(serde_json::json!({
|
||||
"id": "sample.connection",
|
||||
"database_type": "sample",
|
||||
"fields": [],
|
||||
"proxy_route": true
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(provider.proxy_route);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_only_strict_https_network_permissions() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user