From 50e6476cb017223729b897dd0fe6f75da26e6b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 17 Aug 2026 01:50:11 +0800 Subject: [PATCH] fix(meilisearch): preserve proxy paths and secure REST requests --- .../connection/ConnectionDialog.vue | 100 +++++++++++++++-- .../connectionUrl.meilisearch.spec.ts | 83 +++++++++++++- .../src/lib/connection/connectionDeepLink.ts | 2 + .../lib/connection/connectionPresentation.ts | 4 +- .../src/lib/connection/connectionUrl.ts | 23 +++- crates/dbx-core/src/connection.rs | 3 +- crates/dbx-core/src/db/meilisearch_driver.rs | 105 +++++++++++++++++- src-tauri/src/commands/connection.rs | 6 +- 8 files changed, 303 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index e8256718ca..086ee290e8 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -33,7 +33,7 @@ import { useToast } from "@/composables/useToast"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/backend/api"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; -import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl"; +import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl"; import { MAX_CONNECT_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits"; import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection"; import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink"; @@ -354,7 +354,6 @@ const elasticsearchConnectionPorts = ref { return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record) } : {}; } +function meilisearchConnectionUrl(config: Pick): string { + const host = config.host.trim(); + if (!host) return ""; + + const scheme = config.ssl ? "https" : "http"; + const endpointHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; + const externalConfig = externalConfigRecord(config.external_config); + const storedBasePath = externalConfig.basePath ?? externalConfig.base_path; + const basePathSegments = typeof storedBasePath === "string" ? storedBasePath.trim().split("/").filter(Boolean) : []; + const basePath = basePathSegments.length ? `/${basePathSegments.join("/")}` : ""; + const urlParams = config.url_params?.trim().replace(/^\?/, "") || ""; + + return `${scheme}://${endpointHost}:${config.port}${basePath}${urlParams ? `?${urlParams}` : ""}`; +} + +function syncMeilisearchHostInput(config: Pick) { + meilisearchHostInput.value = meilisearchConnectionUrl(config); + appliedMeilisearchHostInput.value = meilisearchHostInput.value; +} + +function resetMeilisearchHostInput() { + meilisearchHostInput.value = ""; + appliedMeilisearchHostInput.value = ""; +} + function sqlServerPortExplicitFromConfig(config: Pick): boolean { if (config.db_type !== "sqlserver") return false; const external = externalConfigRecord(config.external_config); @@ -651,6 +675,8 @@ const selectedJdbcDriverPath = ref(""); const jdbcManualClasspathOpen = ref(false); const connectionUrlInput = ref(""); const appliedConnectionUrlInput = ref(""); +const meilisearchHostInput = ref(""); +const appliedMeilisearchHostInput = ref(""); const oracleTnsAdminPath = ref(""); const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql"); const h2ConnectionMode = ref("file"); @@ -2313,13 +2339,13 @@ function applyProfile(val: string, preserveConnectionFields = false) { form.value.db_type = profile.type; form.value.driver_profile = val; form.value.driver_label = isCustomCompatibleProfile() ? customDriverName.value.trim() || profile.label : profile.label; - if (profile.type !== "sqlserver") { + const preserveMeilisearchConfig = preserveConnectionFields && previousDatabaseType === "meilisearch" && profile.type === "meilisearch"; + if (profile.type !== "sqlserver" && !preserveMeilisearchConfig) { form.value.external_config = undefined; } if (profile.type !== "elasticsearch" || previousDatabaseType !== "elasticsearch") { resetElasticsearchProxyFields(); } - if (!preserveConnectionFields) { oracleTnsAdminPath.value = ""; form.value.port = profile.port; @@ -2427,6 +2453,11 @@ function applyProfile(val: string, preserveConnectionFields = false) { } resetHiveKerberosFields(profile.type === "hive" || profile.type === "kyuubi" || profile.type === "impala" ? form.value : undefined); } + if (profile.type === "meilisearch") { + syncMeilisearchHostInput(form.value); + } else { + resetMeilisearchHostInput(); + } } function switchOceanbaseMode(mode: "mysql" | "oracle") { @@ -2520,6 +2551,11 @@ watch( productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0; connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : ""; appliedConnectionUrlInput.value = connectionUrlInput.value.trim(); + if (config.db_type === "meilisearch") { + syncMeilisearchHostInput(config); + } else { + resetMeilisearchHostInput(); + } if (config.db_type === "mq") { hydrateMqFields(config.external_config); } else { @@ -3577,6 +3613,11 @@ function applyConnectionUrlToForm(input: string): boolean { const draft = parseConnectionDeepLink(input) ?? parseServiceConnectionUrl(input); if (draft) { applyConnectionDraftToForm({ ...draft, oneTime: undefined }); + if (form.value.db_type === "meilisearch") { + syncMeilisearchHostInput(form.value); + } else { + resetMeilisearchHostInput(); + } resetTestState(); appliedConnectionUrlInput.value = input.trim(); return true; @@ -3587,6 +3628,11 @@ function applyConnectionUrlToForm(input: string): boolean { if (form.value.db_type === "victoriametrics") { hydrateVictoriaMetricsFields(form.value.external_config); } + if (form.value.db_type === "meilisearch") { + syncMeilisearchHostInput(form.value); + } else { + resetMeilisearchHostInput(); + } oracleTnsAdminPath.value = parseOracleTnsConnectionString(parsed.connectionString)?.tnsAdmin || ""; selectedType.value = parsed.driverProfile; customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : ""; @@ -3611,21 +3657,46 @@ function hasPendingConnectionUrlInput(): boolean { return !!url && url !== appliedConnectionUrlInput.value; } +function hasPendingMeilisearchHostInput(): boolean { + const url = meilisearchHostInput.value.trim(); + return url !== appliedMeilisearchHostInput.value; +} + +function applyMeilisearchHostInput(): boolean { + try { + const input = meilisearchHostInput.value.trim(); + form.value = applyParsedConnectionUrl(form.value, parseConnectionUrl(input, "meilisearch")); + appliedMeilisearchHostInput.value = input; + resetTestState(); + return true; + } catch (e: any) { + toast(t("connection.parseConnectionUrlFailed", { message: e?.message || String(e) }), 5000); + return false; + } +} + function ensureConnectionHostResolvedFromUrl(): boolean { - if (!hasPendingConnectionUrlInput()) return true; - return applyConnectionUrlToForm(connectionUrlInput.value.trim()); + if (hasPendingConnectionUrlInput() && !applyConnectionUrlToForm(connectionUrlInput.value.trim())) return false; + if (form.value.db_type === "meilisearch" && hasPendingMeilisearchHostInput()) return applyMeilisearchHostInput(); + return true; } function formValueForSubmit(): Omit { const url = connectionUrlInput.value.trim(); - if (!url || url === appliedConnectionUrlInput.value) return form.value; + if (url && url !== appliedConnectionUrlInput.value) { + const draft = parseConnectionDeepLink(url); + if (draft) { + return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined }); + } - const draft = parseConnectionDeepLink(url); - if (draft) { - return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined }); + return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value)); } - return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value)); + if (form.value.db_type === "meilisearch" && hasPendingMeilisearchHostInput()) { + return applyParsedConnectionUrl(form.value, parseConnectionUrl(meilisearchHostInput.value.trim(), "meilisearch")); + } + + return form.value; } function applyDremioJdbcMetadata(config: LegacyConnectionConfig) { @@ -3902,7 +3973,6 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo config.username = ""; config.password = config.password.trim(); config.database = undefined; - config.external_config = undefined; } else if (config.db_type === "sqlserver") { config.external_config = sqlServerPortExplicitFromConfig(config) ? { portExplicit: true } : undefined; } else if (supportsGaussdbIdentifierQuoteStyle(config)) { @@ -4754,6 +4824,7 @@ function resetForm() { selectedJdbcDriverPath.value = ""; connectionUrlInput.value = ""; appliedConnectionUrlInput.value = ""; + resetMeilisearchHostInput(); oracleTnsAdminPath.value = ""; dialogStep.value = "select"; dbSearchQuery.value = ""; @@ -4810,6 +4881,7 @@ function applyConnectionDraftToConfig(config: Omit, draf database: draft.database ?? config.database, url_params: draft.urlParams ?? config.url_params, ssl: draft.ssl ?? config.ssl, + external_config: draft.dbType === "meilisearch" && draft.basePath !== undefined ? applyMeilisearchBasePathToExternalConfig(config.external_config, draft.basePath) : config.external_config, connection_string: draft.connectionString ?? config.connection_string, oracle_connection_type: draft.oracleConnectionType ?? config.oracle_connection_type, one_time: draft.oneTime || undefined, @@ -6989,6 +7061,10 @@ function openExternalUrl(url: string) { +
+ + +
diff --git a/apps/desktop/src/lib/__tests__/connection/connectionUrl.meilisearch.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionUrl.meilisearch.spec.ts index 6e279c5e4b..0ae2a7fbdf 100644 --- a/apps/desktop/src/lib/__tests__/connection/connectionUrl.meilisearch.spec.ts +++ b/apps/desktop/src/lib/__tests__/connection/connectionUrl.meilisearch.spec.ts @@ -1,9 +1,15 @@ import { describe, expect, it } from "vitest"; -import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl"; +import { applyParsedConnectionUrl, connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl"; +import { parseConnectionDeepLink } from "@/lib/connection/connectionDeepLink"; +import type { ConnectionConfig } from "@/types/database"; describe("Meilisearch connection URLs", () => { - it("parses the dedicated scheme with the default port and API key", () => { - expect(parseConnectionUrl("meilisearch://:secret@search.example.com")).toMatchObject({ + it("rejects an empty host URL", () => { + expect(() => parseConnectionUrl("", "meilisearch")).toThrow("Connection URL is empty"); + }); + + it("parses the dedicated scheme with the default port, API key, and proxy path", () => { + expect(parseConnectionUrl("meilisearch://:secret@search.example.com/gateway/meili")).toMatchObject({ dbType: "meilisearch", driverProfile: "meilisearch", driverLabel: "Meilisearch", @@ -12,6 +18,7 @@ describe("Meilisearch connection URLs", () => { username: "", password: "secret", ssl: false, + basePath: "/gateway/meili", }); }); @@ -25,6 +32,76 @@ describe("Meilisearch connection URLs", () => { }); }); + it.each([ + ["http://search.example.com/gateway/meili/", 80, false], + ["https://search.example.com/gateway/meili", 443, true], + ["https://search.example.com:8443/gateway/meili", 8443, true], + ])("parses proxy URL %s", (url, port, ssl) => { + expect(parseConnectionUrl(url, "meilisearch")).toMatchObject({ + dbType: "meilisearch", + host: "search.example.com", + port, + ssl, + database: undefined, + basePath: "/gateway/meili", + }); + }); + + it("stores the proxy path in canonical external config without dropping other fields", () => { + const parsed = parseConnectionUrl("http://search.example.com/gateway/meili", "meilisearch"); + const config = applyParsedConnectionUrl( + { + db_type: "meilisearch", + driver_profile: "meilisearch", + driver_label: "Meilisearch", + host: "localhost", + port: 7700, + username: "", + password: "", + url_params: "", + ssl: false, + external_config: { base_path: "/legacy", custom: true }, + } as Omit, + parsed, + ); + + expect(config.external_config).toEqual({ basePath: "/gateway/meili", custom: true }); + }); + + it("clears stale proxy paths when the parsed URL uses the root path", () => { + const parsed = parseConnectionUrl("http://search.example.com/", "meilisearch"); + const config = applyParsedConnectionUrl( + { + db_type: "meilisearch", + driver_profile: "meilisearch", + driver_label: "Meilisearch", + host: "localhost", + port: 7700, + username: "", + password: "", + url_params: "", + ssl: false, + external_config: { basePath: "/old", base_path: "/legacy", custom: true }, + } as Omit, + parsed, + ); + + expect(parsed.basePath).toBe(""); + expect(config.external_config).toEqual({ custom: true }); + }); + + it("preserves the proxy path when a Meilisearch URL arrives through a deep link", () => { + const link = new URL("dbx://connection/new"); + link.searchParams.set("type", "meilisearch"); + link.searchParams.set("url", "https://search.example.com/gateway/meili?insecure=true"); + + expect(parseConnectionDeepLink(link.toString())).toMatchObject({ + dbType: "meilisearch", + basePath: "/gateway/meili", + urlParams: "insecure=true", + }); + }); + it("exposes the Meilisearch scheme profile", () => { expect(connectionProfileForScheme("meilisearch")).toEqual({ type: "meilisearch", diff --git a/apps/desktop/src/lib/connection/connectionDeepLink.ts b/apps/desktop/src/lib/connection/connectionDeepLink.ts index 377d9c8259..6c2d2d4bd5 100644 --- a/apps/desktop/src/lib/connection/connectionDeepLink.ts +++ b/apps/desktop/src/lib/connection/connectionDeepLink.ts @@ -23,6 +23,7 @@ export interface ConnectionDeepLinkDraft { password?: string; database?: string; urlParams?: string; + basePath?: string; ssl?: boolean; connectionString?: string; oracleConnectionType?: "service_name" | "sid"; @@ -188,6 +189,7 @@ function draftFromConnectionUrl(value: string, preferredProfile?: string): Conne password: parsed.password, database: parsed.database, urlParams: parsed.urlParams, + basePath: parsed.basePath, ssl: parsed.ssl, connectionString: parsed.connectionString, oracleConnectionType: parsed.oracleConnectionType, diff --git a/apps/desktop/src/lib/connection/connectionPresentation.ts b/apps/desktop/src/lib/connection/connectionPresentation.ts index be7d08adfb..09f7320cef 100644 --- a/apps/desktop/src/lib/connection/connectionPresentation.ts +++ b/apps/desktop/src/lib/connection/connectionPresentation.ts @@ -208,13 +208,15 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string { case "elasticsearch": case "easysearch": - case "meilisearch": case "qdrant": case "milvus": case "weaviate": case "chromadb": return "http://user:password@host:port"; + case "meilisearch": + return "http://host:port/base/path"; + case "dameng": return "dm://user:password@host:port"; diff --git a/apps/desktop/src/lib/connection/connectionUrl.ts b/apps/desktop/src/lib/connection/connectionUrl.ts index 208516905f..aca657a6c8 100644 --- a/apps/desktop/src/lib/connection/connectionUrl.ts +++ b/apps/desktop/src/lib/connection/connectionUrl.ts @@ -19,6 +19,7 @@ export interface ParsedConnectionUrl { useMongoUrl?: boolean; portExplicit?: boolean; apiPath?: string; + basePath?: string; } export type ConnectionProfile = { @@ -651,20 +652,24 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa }; } + const isMeilisearch = profile.type === "meilisearch"; + const defaultPort = isMeilisearch && scheme === "http" ? 80 : isMeilisearch && scheme === "https" ? 443 : profile.defaultPort; + return { ...(name ? { name } : {}), dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, host: parsed.hostname, - port: parsed.port ? Number(parsed.port) : profile.defaultPort, + port: parsed.port ? Number(parsed.port) : defaultPort, ...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}), username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username), password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password), - database: profile.type === "victoriametrics" ? "metrics" : databaseFromPath(parsed.pathname), + database: profile.type === "victoriametrics" ? "metrics" : isMeilisearch ? undefined : databaseFromPath(parsed.pathname), urlParams: effectiveUrlParams, ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)), ...(profile.type === "victoriametrics" ? { apiPath: parsed.pathname.replace(/\/+$/, "") } : {}), + ...(isMeilisearch ? { basePath: parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "") } : {}), }; } @@ -711,12 +716,26 @@ function isRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +export function applyMeilisearchBasePathToExternalConfig(existing: unknown, basePath: string | undefined): unknown { + const next = isRecord(existing) ? { ...existing } : {}; + delete next.base_path; + if (basePath) { + next.basePath = basePath; + } else { + delete next.basePath; + } + return Object.keys(next).length > 0 ? next : undefined; +} + function parsedExternalConfig(existing: unknown, parsed: ParsedConnectionUrl): unknown { if (parsed.dbType === "victoriametrics") { const next = isRecord(existing) ? { ...existing } : {}; next.apiPath = parsed.apiPath || "/prometheus"; return next; } + if (parsed.dbType === "meilisearch") { + return applyMeilisearchBasePathToExternalConfig(existing, parsed.basePath); + } if (parsed.dbType !== "sqlserver") return existing; const next = isRecord(existing) ? { ...existing } : {}; diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index e71d8c21c7..fa4b6f91cf 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -2064,11 +2064,12 @@ impl AppState { PoolKind::Easysearch(client) } DatabaseType::Meilisearch => { - let client = db::meilisearch_driver::MeilisearchClient::new( + let client = db::meilisearch_driver::MeilisearchClient::new_for_config( &url, Some(&db_config.password), db_config.ssl, db_config.url_params.as_deref(), + db_config.external_config.as_ref(), connect_timeout, )?; db::meilisearch_driver::test_connection(&client, connect_timeout).await?; diff --git a/crates/dbx-core/src/db/meilisearch_driver.rs b/crates/dbx-core/src/db/meilisearch_driver.rs index 94cc30bd00..a3a13fe7d4 100644 --- a/crates/dbx-core/src/db/meilisearch_driver.rs +++ b/crates/dbx-core/src/db/meilisearch_driver.rs @@ -1,4 +1,4 @@ -use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; +use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS}; use reqwest::{Client as HttpClient, Method, RequestBuilder, StatusCode}; use serde::Deserialize; use serde_json::{Map, Value}; @@ -57,6 +57,18 @@ impl MeilisearchClient { Ok(Self { http, base_url: url.trim_end_matches('/').to_string(), api_key }) } + pub fn new_for_config( + url: &str, + api_key: Option<&str>, + tls_enabled: bool, + url_params: Option<&str>, + external_config: Option<&Value>, + timeout: Duration, + ) -> Result { + let base_url = meilisearch_base_url(url, external_config)?; + Self::new(&base_url, api_key, tls_enabled, url_params, timeout) + } + fn request(&self, method: Method, path: &str) -> RequestBuilder { let request = self.http.request(method, format!("{}{}", self.base_url, path)); match self.api_key.as_deref() { @@ -78,6 +90,31 @@ impl MeilisearchClient { } } +fn meilisearch_base_url(url: &str, external_config: Option<&Value>) -> Result { + let url = url.trim_end_matches('/'); + let Some(raw_base_path) = external_config + .and_then(Value::as_object) + .and_then(|config| config.get("basePath").or_else(|| config.get("base_path"))) + .and_then(Value::as_str) + else { + return Ok(url.to_string()); + }; + + let input = raw_base_path.trim(); + if input.is_empty() || input == "/" { + return Ok(url.to_string()); + } + if input.contains(['?', '#']) { + return Err("Meilisearch base path cannot contain a query or fragment".to_string()); + } + + let segments = input.split('/').filter(|segment| !segment.is_empty()).collect::>(); + if segments.iter().any(|segment| matches!(*segment, "." | "..")) { + return Err("Meilisearch base path cannot contain '.' or '..' segments".to_string()); + } + Ok(format!("{url}/{}", segments.join("/"))) +} + fn meilisearch_accept_invalid_certs(tls_enabled: bool, url_params: Option<&str>) -> bool { tls_enabled && url_params.is_some_and(|params| { @@ -732,6 +769,18 @@ fn parse_rest_request_line(line: &str) -> Result<(Method, String), String> { if !path.starts_with('/') { return Err("Meilisearch REST path must start with '/'".to_string()); } + if path.contains('#') { + return Err("Meilisearch REST path must not contain a fragment".to_string()); + } + let raw_path = path.split_once('?').map_or(path, |(path, _)| path); + for segment in raw_path.split('/') { + let decoded = percent_decode_str(segment) + .decode_utf8() + .map_err(|_| "Meilisearch REST path contains invalid UTF-8 escaping".to_string())?; + if matches!(decoded.as_ref(), "." | "..") || decoded.contains(['/', '\\']) { + return Err("Meilisearch REST path cannot contain traversal segments".to_string()); + } + } Ok((method, path.to_string())) } @@ -762,9 +811,54 @@ fn raw_response_result(status: u16, body: String, start: Instant, truncated: boo #[cfg(test)] mod tests { - use super::{decode_bounded_rest_body, filter_expression, meilisearch_sort_from_request, parse_rest_request_line}; + use super::{ + decode_bounded_rest_body, filter_expression, meilisearch_base_url, meilisearch_sort_from_request, + parse_rest_request_line, MeilisearchClient, + }; use reqwest::Method; + use serde_json::json; + use std::time::Duration; + #[test] + fn normalizes_canonical_and_legacy_proxy_base_paths() { + assert_eq!( + meilisearch_base_url("http://search.example.com:7700/", Some(&json!({ "basePath": "/gateway/meili/" }))) + .unwrap(), + "http://search.example.com:7700/gateway/meili" + ); + assert_eq!( + meilisearch_base_url("https://search.example.com", Some(&json!({ "base_path": "gateway//meili" }))) + .unwrap(), + "https://search.example.com/gateway/meili" + ); + } + + #[test] + fn keeps_the_origin_unchanged_for_empty_proxy_base_paths() { + for external_config in + [None, Some(json!({})), Some(json!({ "basePath": "" })), Some(json!({ "basePath": "/" }))] + { + assert_eq!( + meilisearch_base_url("http://search.example.com:7700/", external_config.as_ref()).unwrap(), + "http://search.example.com:7700" + ); + } + } + + #[test] + fn config_aware_client_uses_proxy_base_path() { + let client = MeilisearchClient::new_for_config( + "https://search.example.com:8443/", + None, + true, + None, + Some(&json!({ "basePath": "/gateway/meili/" })), + Duration::from_secs(1), + ) + .unwrap(); + + assert_eq!(client.base_url, "https://search.example.com:8443/gateway/meili"); + } #[test] fn translates_document_filters_to_meilisearch_syntax() { let filter = serde_json::json!({ @@ -808,6 +902,13 @@ mod tests { fn parses_rest_request_lines() { assert_eq!(parse_rest_request_line("GET /version").unwrap(), (Method::GET, "/version".to_string())); assert_eq!(parse_rest_request_line("/health").unwrap(), (Method::GET, "/health".to_string())); + assert_eq!( + parse_rest_request_line("GET /indexes/movies?limit=1").unwrap(), + (Method::GET, "/indexes/movies?limit=1".to_string()) + ); + assert!(parse_rest_request_line("GET /../version").unwrap_err().contains("traversal")); + assert!(parse_rest_request_line("GET /gateway/%2e%2e/version").unwrap_err().contains("traversal")); + assert!(parse_rest_request_line("GET /gateway/%2e%2e%2fversion").unwrap_err().contains("traversal")); assert!(parse_rest_request_line("CONNECT /health") .unwrap_err() .contains("Unsupported Meilisearch HTTP method")); diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index c01902a569..84a4829684 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -1158,11 +1158,12 @@ async fn test_connection_with_info_inner( .map(|_| "Connection successful".to_string()) } DatabaseType::Meilisearch => { - let client = db::meilisearch_driver::MeilisearchClient::new( + let client = db::meilisearch_driver::MeilisearchClient::new_for_config( &url, Some(&config.password), config.ssl, config.url_params.as_deref(), + config.external_config.as_ref(), connect_timeout, )?; db::meilisearch_driver::test_connection(&client, connect_timeout) @@ -1560,11 +1561,12 @@ pub async fn connect_db( PoolKind::Easysearch(client) } DatabaseType::Meilisearch => { - let client = db::meilisearch_driver::MeilisearchClient::new( + let client = db::meilisearch_driver::MeilisearchClient::new_for_config( &url, Some(&db_config.password), db_config.ssl, db_config.url_params.as_deref(), + db_config.external_config.as_ref(), connect_timeout, )?; db::meilisearch_driver::test_connection(&client, connect_timeout).await?;