feat(jdbc): support SSH tunnel and proxy for JDBC connections
Parse host:port from JDBC URLs and rewrite them when an SSH or proxy tunnel is active, so the Java plugin connects through the local forwarded port. Enable the SSH/Proxy tabs in the connection dialog for JDBC.
This commit is contained in:
parent
c8d4a04a31
commit
45e67fae6e
|
|
@ -8,7 +8,9 @@ use crate::db;
|
|||
use crate::db::proxy_tunnel::ProxyTunnelManager;
|
||||
use crate::db::ssh_tunnel::TunnelManager;
|
||||
use crate::external;
|
||||
use crate::models::connection::{parse_mongo_first_host, ConnectionConfig, DatabaseType};
|
||||
use crate::models::connection::{
|
||||
parse_jdbc_host_port, parse_mongo_first_host, rewrite_jdbc_url_host, ConnectionConfig, DatabaseType,
|
||||
};
|
||||
use crate::plugins::{PluginDriverSession, PluginRegistry};
|
||||
use crate::query_cancel::RunningQueries;
|
||||
use crate::storage::Storage;
|
||||
|
|
@ -292,7 +294,15 @@ impl AppState {
|
|||
.await?;
|
||||
PoolKind::Gaussdb(Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::Jdbc => self.external_driver_pool("jdbc", &db_config).await?,
|
||||
DatabaseType::Jdbc => {
|
||||
let mut jdbc_config = db_config.clone();
|
||||
if host != config.host || port != config.port {
|
||||
if let Some(ref url) = jdbc_config.connection_string {
|
||||
jdbc_config.connection_string = Some(rewrite_jdbc_url_host(url, &host, port));
|
||||
}
|
||||
}
|
||||
self.external_driver_pool("jdbc", &jdbc_config).await?
|
||||
}
|
||||
};
|
||||
|
||||
self.connections.write().await.insert(pool_key.clone(), pool);
|
||||
|
|
@ -317,6 +327,13 @@ impl AppState {
|
|||
.filter(|s| !s.is_empty())
|
||||
.and_then(parse_mongo_first_host)
|
||||
.unwrap_or_else(|| (config.host.clone(), config.port))
|
||||
} else if config.db_type == DatabaseType::Jdbc {
|
||||
config
|
||||
.connection_string
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.and_then(parse_jdbc_host_port)
|
||||
.unwrap_or_else(|| (config.host.clone(), config.port))
|
||||
} else {
|
||||
(config.host.clone(), config.port)
|
||||
};
|
||||
|
|
@ -350,6 +367,13 @@ impl AppState {
|
|||
.filter(|s| !s.is_empty())
|
||||
.and_then(parse_mongo_first_host)
|
||||
.unwrap_or_else(|| (config.host.clone(), config.port))
|
||||
} else if config.db_type == DatabaseType::Jdbc {
|
||||
config
|
||||
.connection_string
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.and_then(parse_jdbc_host_port)
|
||||
.unwrap_or_else(|| (config.host.clone(), config.port))
|
||||
} else {
|
||||
(config.host.clone(), config.port)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -387,6 +387,53 @@ fn rewrite_mongo_uri_host(uri: &str, new_host: &str, new_port: u16) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn parse_jdbc_host_port(url: &str) -> Option<(String, u16)> {
|
||||
let rest = url.strip_prefix("jdbc:")?;
|
||||
|
||||
// jdbc:oracle:thin:@host:port:SID or jdbc:oracle:thin:@//host:port/service
|
||||
if let Some(after) = rest.strip_prefix("oracle:") {
|
||||
let at_pos = after.find('@')?;
|
||||
let after_at = &after[at_pos + 1..];
|
||||
let after_at = after_at.strip_prefix("//").unwrap_or(after_at);
|
||||
let host_port = after_at.split(&['/', ':', '?'][..]).next()?;
|
||||
let port_str = after_at.strip_prefix(host_port)?.strip_prefix(':')?.split(&[':', '/', ';', '?'][..]).next()?;
|
||||
return Some((host_port.to_string(), port_str.parse().ok()?));
|
||||
}
|
||||
|
||||
// jdbc:sqlserver://host:port;prop=val or jdbc:sqlserver://host\instance:port;...
|
||||
if let Some(after) = rest.strip_prefix("sqlserver://") {
|
||||
let authority = after.split(';').next().unwrap_or(after);
|
||||
let authority = authority.split('\\').next().unwrap_or(authority);
|
||||
return match authority.rsplit_once(':') {
|
||||
Some((h, p)) => Some((h.to_string(), p.parse().ok()?)),
|
||||
None => Some((authority.to_string(), 1433)),
|
||||
};
|
||||
}
|
||||
|
||||
// Generic: jdbc:subprotocol://[user:pass@]host:port[/path][?query]
|
||||
let scheme_end = rest.find("://")?;
|
||||
let after_scheme = &rest[scheme_end + 3..];
|
||||
let authority = after_scheme.split('/').next().unwrap_or(after_scheme);
|
||||
let authority = authority.split('?').next().unwrap_or(authority);
|
||||
let host_port = match authority.rfind('@') {
|
||||
Some(idx) => &authority[idx + 1..],
|
||||
None => authority,
|
||||
};
|
||||
match host_port.rsplit_once(':') {
|
||||
Some((h, p)) => Some((h.to_string(), p.parse().ok()?)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewrite_jdbc_url_host(url: &str, new_host: &str, new_port: u16) -> String {
|
||||
let Some((old_host, old_port)) = parse_jdbc_host_port(url) else {
|
||||
return url.to_string();
|
||||
};
|
||||
let old_authority = format!("{old_host}:{old_port}");
|
||||
let new_authority = format!("{new_host}:{new_port}");
|
||||
url.replacen(&old_authority, &new_authority, 1)
|
||||
}
|
||||
|
||||
fn encode_url_part(value: &str) -> String {
|
||||
utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
|
@ -722,4 +769,91 @@ mod tests {
|
|||
|
||||
assert!(url.matches("directConnection").count() == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_postgresql() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:postgresql://myhost:5432/mydb").unwrap();
|
||||
assert_eq!(h, "myhost");
|
||||
assert_eq!(p, 5432);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_mysql() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:mysql://db.example.com:3306/app?useSSL=false").unwrap();
|
||||
assert_eq!(h, "db.example.com");
|
||||
assert_eq!(p, 3306);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_with_userinfo() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:postgresql://user:pass@pghost:5433/db").unwrap();
|
||||
assert_eq!(h, "pghost");
|
||||
assert_eq!(p, 5433);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_oracle_thin() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:oracle:thin:@orahost:1521:ORCL").unwrap();
|
||||
assert_eq!(h, "orahost");
|
||||
assert_eq!(p, 1521);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_oracle_service() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:oracle:thin:@//orahost:1521/service").unwrap();
|
||||
assert_eq!(h, "orahost");
|
||||
assert_eq!(p, 1521);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_sqlserver() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:sqlserver://mshost:1433;databaseName=master").unwrap();
|
||||
assert_eq!(h, "mshost");
|
||||
assert_eq!(p, 1433);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_sqlserver_no_port() {
|
||||
let (h, p) = super::parse_jdbc_host_port("jdbc:sqlserver://mshost;databaseName=master").unwrap();
|
||||
assert_eq!(h, "mshost");
|
||||
assert_eq!(p, 1433);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_no_port_returns_none() {
|
||||
assert!(super::parse_jdbc_host_port("jdbc:postgresql://myhost/mydb").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jdbc_host_port_invalid_returns_none() {
|
||||
assert!(super::parse_jdbc_host_port("not-a-jdbc-url").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_jdbc_url_postgresql() {
|
||||
let url = "jdbc:postgresql://myhost:5432/mydb";
|
||||
let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 54321);
|
||||
assert_eq!(rewritten, "jdbc:postgresql://127.0.0.1:54321/mydb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_jdbc_url_oracle() {
|
||||
let url = "jdbc:oracle:thin:@orahost:1521:ORCL";
|
||||
let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 54321);
|
||||
assert_eq!(rewritten, "jdbc:oracle:thin:@127.0.0.1:54321:ORCL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_jdbc_url_sqlserver() {
|
||||
let url = "jdbc:sqlserver://mshost:1433;databaseName=master";
|
||||
let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 54321);
|
||||
assert_eq!(rewritten, "jdbc:sqlserver://127.0.0.1:54321;databaseName=master");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_jdbc_url_unparseable_returns_original() {
|
||||
let url = "jdbc:custom:some-opaque-string";
|
||||
let rewritten = super::rewrite_jdbc_url_host(url, "127.0.0.1", 54321);
|
||||
assert_eq!(rewritten, url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,10 +422,8 @@ const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selecte
|
|||
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
|
||||
|
||||
const connectionUrlPlaceholder = computed(() => getUrlPlaceholder(form.value.db_type));
|
||||
const canUseSsh = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "jdbc");
|
||||
const canUseProxy = computed(
|
||||
() => form.value.db_type !== "sqlite" && form.value.db_type !== "duckdb" && form.value.db_type !== "jdbc",
|
||||
);
|
||||
const canUseSsh = computed(() => form.value.db_type !== "sqlite");
|
||||
const canUseProxy = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "duckdb");
|
||||
const testResultMessage = computed(() => {
|
||||
if (!testResult.value) return "";
|
||||
return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message;
|
||||
|
|
|
|||
Loading…
Reference in New Issue