feat(sync): allow overriding webdav user agent for restricted gateways

This commit is contained in:
t8y2
2026-10-08 22:27:18 +08:00
parent b351c0f3b8
commit 641a7bd71a
22 changed files with 140 additions and 2 deletions
@@ -4432,6 +4432,7 @@ const webdavPassword = ref("");
const webdavRememberPassword = ref(localStorage.getItem("dbx-webdav-remember-password") === "true");
const webdavHasSavedPassword = ref(false);
const webdavRemotePath = ref(localStorage.getItem("dbx-webdav-remote-path") || DEFAULT_WEB_DAV_REMOTE_PATH);
const webdavUserAgent = ref(localStorage.getItem("dbx-webdav-user-agent") || "");
const webdavSyncSecrets = ref(false);
const webdavSecretsPassphrase = ref("");
const webdavHasSavedSecretsPassphrase = ref(false);
@@ -4671,6 +4672,7 @@ function currentWebDavConfig(): WebDavConfig {
username: webdavUsername.value.trim() || undefined,
password: webdavPassword.value || undefined,
remotePath: webdavRemotePath.value.trim() || DEFAULT_WEB_DAV_REMOTE_PATH,
userAgent: webdavUserAgent.value.trim() || undefined,
};
}
@@ -9765,6 +9767,13 @@ LIMIT 100;</pre
{{ t("settings.syncRemotePathDescription") }}
</p>
</div>
<div class="space-y-2 md:col-span-2">
<Label for="webdav-user-agent">{{ t("settings.syncUserAgent") }}</Label>
<Input id="webdav-user-agent" v-model="webdavUserAgent" autocomplete="off" placeholder="Zotero/7.0.15" />
<p class="text-xs text-muted-foreground">
{{ t("settings.syncUserAgentDescription") }}
</p>
</div>
<div class="settings-item space-y-2 md:col-span-2 rounded-md border bg-muted/20 px-3 py-3">
<label class="flex items-center gap-2 text-xs">
<input v-model="webdavAutoUploadEnabled" type="checkbox" class="h-4 w-4 shrink-0 accent-primary" />
+2
View File
@@ -7580,6 +7580,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "Parol şifrələnir və yalnız cari DBX instansiyasında saxlanılır. WebDAV ilə sinxronlaşdırılmır və sinxronlaşdırma parolunu əvəz etmir.",
syncRemotePath: "Uzaq anlıq nüsxənin yolu",
syncRemotePathDescription: "Yükləmə zamanı DBX çatışmayan üst qovluqları yaradacaq.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "İstəyə bağlı. Bəzi WebDAV xidmətləri yalnız müəyyən klient tətbiqlərinə icazə verir; uyğun User-Agent daxil edin (məsələn, Zotero/7.0.15). Boş buraxılsa User-Agent göndərilmir.",
syncSecretNotice: "Standart olaraq DBX yalnız əlaqə məlumatlarını və parametrləri sinxronlaşdırır. Verilənlər bazası parolları, SSH parolları, proksi parolları, HTTP tunel tokenləri, əlaqə sətirləri və AI API açarları yerli olaraq qalır.",
syncSecrets: "Şifrələnmiş məxfi məlumatları sinxronlaşdır",
syncSecretsDescription: "Aktiv olduqda DBX verilənlər bazası parollarını, SSH parollarını, proksi parollarını, HTTP tunel tokenlərini və AI API açarlarını uzaq yaddaşa yükləməzdən əvvəl şifrələyir.",
+2
View File
@@ -8623,6 +8623,8 @@ export default {
syncRememberWebDavPasswordDescription: "The password is encrypted and stored in the current DBX instance only. It is not synced to WebDAV and does not replace the sync password.",
syncRemotePath: "Remote snapshot path",
syncRemotePathDescription: "DBX will create missing parent folders when uploading.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Optional. Some WebDAV services only allow specific client applications; fill in the matching User-Agent (for example Zotero/7.0.15). Leave empty to send no User-Agent.",
syncSecretNotice: "By default, DBX syncs connection details and settings only. Database passwords, SSH passwords, proxy passwords, HTTP tunnel tokens, connection strings, and AI API keys stay local.",
syncSecrets: "Sync encrypted secrets",
syncSecretsDescription: "When enabled, DBX encrypts database passwords, SSH passwords, proxy passwords, HTTP tunnel tokens, and AI API keys before uploading them to remote storage.",
+2
View File
@@ -7876,6 +7876,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "La contraseña se cifra y se guarda solo en la instancia DBX actual. No se sincroniza a WebDAV ni reemplaza la contraseña de sync.",
syncRemotePath: "Ruta remota",
syncRemotePathDescription: "DBX creará las carpetas padre faltantes al subir.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Opcional. Algunos servicios WebDAV solo permiten aplicaciones cliente específicas; introduce el User-Agent correspondiente (por ejemplo, Zotero/7.0.15). Si se deja vacío, no se envía ningún User-Agent.",
syncSecretNotice: "De forma predeterminada, DBX solo sincroniza detalles de conexión y ajustes. Las contraseñas de base de datos, SSH, proxy, cadenas de conexión y API keys de IA quedan locales.",
syncSecrets: "Sincronizar secretos cifrados",
syncSecretsDescription: "Al activarlo, DBX cifra contraseñas de base de datos, contraseñas SSH y API keys de IA antes de subirlas a WebDAV.",
+2
View File
@@ -8379,6 +8379,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "Kata sandi dienkripsi dan disimpan hanya pada instans DBX saat ini. Tidak disinkronkan ke WebDAV dan tidak menggantikan kata sandi sinkronisasi.",
syncRemotePath: "Jalur snapshot jarak jauh",
syncRemotePathDescription: "DBX akan membuat folder induk yang hilang saat mengunggah.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Opsional. Beberapa layanan WebDAV hanya mengizinkan aplikasi klien tertentu; isi User-Agent yang sesuai (misalnya Zotero/7.0.15). Jika dikosongkan, User-Agent tidak dikirim.",
syncSecretNotice: "Secara default, DBX hanya menyinkronkan detail koneksi dan pengaturan. Kata sandi database, kata sandi SSH, kata sandi proxy, token tunnel HTTP, connection string, dan kunci API AI tetap lokal.",
syncSecrets: "Sinkronkan rahasia terenkripsi",
syncSecretsDescription: "Jika diaktifkan, DBX mengenkripsi kata sandi database, kata sandi SSH, kata sandi proxy, token tunnel HTTP, dan kunci API AI sebelum mengunggahnya ke penyimpanan jarak jauh.",
+2
View File
@@ -7654,6 +7654,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "La password viene crittografata e memorizzata solo nell'istanza DBX corrente. Non viene sincronizzata su WebDAV e non sostituisce la password di sincronizzazione.",
syncRemotePath: "Percorso snapshot remoto",
syncRemotePathDescription: "DBX creerà le cartelle superiori mancanti durante il caricamento.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Opzionale. Alcuni servizi WebDAV consentono l'accesso solo ad applicazioni client specifiche; inserisci lo User-Agent corrispondente (ad esempio Zotero/7.0.15). Se lasciato vuoto, non viene inviato alcuno User-Agent.",
syncSecretNotice: "Per impostazione predefinita, DBX sincronizza solo i dettagli e le impostazioni di connessione. Le password dei database, le password SSH, le password proxy, le stringhe di connessione e le chiavi API AI rimangono locali.",
syncSecrets: "Sincronizza i segreti crittografati",
syncSecretsDescription: "Se abilitato, DBX crittografa le password del database, le password SSH e le chiavi API AI prima di caricarle su WebDAV.",
+2
View File
@@ -7668,6 +7668,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "パスワードは暗号化され、現在のDBXインスタンスにのみ保存されます。WebDAVには同期されず、同期パスワードを置き換えるものでもありません。",
syncRemotePath: "リモートスナップショットパス",
syncRemotePathDescription: "アップロード時に不足している親フォルダーをDBXが作成します。",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "省略可。一部の WebDAV サービスは特定のクライアントアプリのみ許可します。該当する User-Agent(例:Zotero/7.0.15)を入力してください。空欄の場合は User-Agent を送信しません。",
syncSecretNotice: "デフォルトでは、DBXは接続詳細と設定のみを同期します。データベースパスワード、SSHパスワード、プロキシパスワード、接続文字列、AI APIキーはローカルに保持されます。",
syncSecrets: "暗号化されたシークレットを同期",
syncSecretsDescription: "有効時、DBXはデータベースパスワード、SSHパスワード、AI APIキーを暗号化してからWebDAVにアップロードします。",
+2
View File
@@ -7488,6 +7488,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "비밀번호는 현재 DBX 인스턴스에만 암호화되어 저장됩니다. WebDAV에 동기화되지 않으며 동기화 비밀번호를 대체하지 않습니다.",
syncRemotePath: "원격 스냅샷 경로",
syncRemotePathDescription: "DBX가 업로드 시 누락된 상위 폴더를 만듭니다.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "선택 사항. 일부 WebDAV 서비스는 특정 클라이언트 앱만 허용합니다. 해당하는 User-Agent(예: Zotero/7.0.15)를 입력하세요. 비워 두면 User-Agent를 전송하지 않습니다.",
syncSecretNotice: "기본적으로 DBX는 연결 세부 정보와 설정만 동기화합니다. 데이터베이스 비밀번호, SSH 비밀번호, 프록시 비밀번호, HTTP 터널 토큰, 연결 문자열 및 AI API 키는 로컬에 유지됩니다.",
syncSecrets: "암호화된 비밀 동기화",
syncSecretsDescription: "활성화하면 DBX가 데이터베이스 비밀번호, SSH 비밀번호, 프록시 비밀번호, HTTP 터널 토큰 및 AI API 키를 암호화하여 원격 저장소에 업로드합니다.",
+2
View File
@@ -7655,6 +7655,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "A senha é criptografada e armazenada apenas na instância DBX atual. Ela não é sincronizada para o WebDAV e não substitui a senha de sincronização.",
syncRemotePath: "Caminho remoto do snapshot",
syncRemotePathDescription: "O DBX criará as pastas pai ausentes ao enviar.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Opcional. Alguns serviços WebDAV permitem apenas aplicativos cliente específicos; preencha o User-Agent correspondente (por exemplo, Zotero/7.0.15). Se deixado vazio, nenhum User-Agent é enviado.",
syncSecretNotice: "Por padrão, o DBX sincroniza apenas detalhes de conexão e configurações. Senhas de banco de dados, senhas SSH, senhas de proxy, strings de conexão e chaves de API de IA permanecem locais.",
syncSecrets: "Sincronizar segredos criptografados",
syncSecretsDescription: "Quando ativado, o DBX criptografa senhas de banco de dados, senhas SSH e chaves de API de IA antes de enviá-las para o WebDAV.",
+2
View File
@@ -8903,6 +8903,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "Пароль шифруется и хранится только в текущем экземпляре DBX. Он не синхронизируется с WebDAV и не заменяет пароль синхронизации.",
syncRemotePath: "Путь к удалённому снимку",
syncRemotePathDescription: "DBX создаст отсутствующие родительские папки при загрузке.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "Необязательно. Некоторые службы WebDAV разрешают доступ только определённым клиентским приложениям; укажите соответствующий User-Agent (например, Zotero/7.0.15). Если оставить пустым, User-Agent отправляться не будет.",
syncSecretNotice: "По умолчанию DBX синхронизирует только параметры подключений и настройки. Пароли баз данных, пароли SSH, пароли прокси, токены HTTP-туннелей, строки подключения и ключи AI API остаются локально.",
syncSecrets: "Синхронизировать зашифрованные секреты",
syncSecretsDescription: "Если включено, DBX шифрует пароли баз данных, пароли SSH, пароли прокси, токены HTTP-туннелей и ключи AI API перед загрузкой в удалённое хранилище.",
+2
View File
@@ -7478,6 +7478,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "Parola şifrelenir ve yalnızca geçerli DBX örneğinde saklanır. WebDAV'a eşitlenmez ve eşitleme parolasının yerine geçmez.",
syncRemotePath: "Uzak anlık görüntü yolu",
syncRemotePathDescription: "DBX, yükleme sırasında eksik üst klasörleri oluşturur.",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "İsteğe bağlı. Bazı WebDAV hizmetleri yalnızca belirli istemci uygulamalarına izin verir; eşleşen User-Agent değerini girin (örneğin Zotero/7.0.15). Boş bırakılırsa User-Agent gönderilmez.",
syncSecretNotice: "DBX varsayılan olarak yalnızca bağlantı ayrıntılarını ve ayarları eşitler. Veritabanı parolaları, SSH parolaları, proxy parolaları, HTTP tünel belirteçleri, bağlantı dizeleri ve yapay zekâ API anahtarları yerelde kalır.",
syncSecrets: "Şifreli gizli bilgileri eşitle",
syncSecretsDescription: "Etkinleştirildiğinde DBX; veritabanı parolalarını, SSH parolalarını, proxy parolalarını, HTTP tünel belirteçlerini ve yapay zekâ API anahtarlarını uzak depolamaya yüklemeden önce şifreler.",
+2
View File
@@ -8608,6 +8608,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "密码会加密保存在当前 DBX 实例中,不会同步到 WebDAV,也不会替代同步密码。",
syncRemotePath: "远端快照路径",
syncRemotePathDescription: "上传时 DBX 会自动创建缺失的父目录。",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "可选。部分 WebDAV 服务仅允许特定客户端应用访问,填写对应的 User-Agent(例如 Zotero/7.0.15)即可。留空则不发送 User-Agent。",
syncSecretNotice: "默认只同步连接信息和设置,不会同步数据库密码、SSH 密码、代理密码、HTTP 隧道 Token、连接串或 AI API Key。",
syncSecrets: "同步加密后的敏感信息",
syncSecretsDescription: "开启后,DBX 会先加密数据库密码、SSH 密码、代理密码、HTTP 隧道 Token 和 AI API Key,再上传到远端存储。",
+2
View File
@@ -7003,6 +7003,8 @@ export default withEnglishFallback({
syncRememberWebDavPasswordDescription: "密碼會加密儲存在目前 DBX 執行個體中,不會同步到 WebDAV,也不會取代同步密碼。",
syncRemotePath: "遠端快照路徑",
syncRemotePathDescription: "上傳時 DBX 會自動建立缺失的父目錄。",
syncUserAgent: "User-Agent",
syncUserAgentDescription: "選填。部分 WebDAV 服務僅允許特定用戶端應用程式存取,填寫對應的 User-Agent(例如 Zotero/7.0.15)即可。留空則不傳送 User-Agent。",
syncSecretNotice: "預設只同步連線詳細資料和設定。資料庫密碼、SSH 密碼、代理伺服器密碼、連線字串和 AI API Key 會保留在本機。",
syncSecrets: "同步加密後的敏感資訊",
syncSecretsDescription: "開啟後,DBX 會先加密資料庫密碼、SSH 密碼和 AI API Key,再上傳到 WebDAV。",
+1
View File
@@ -2489,6 +2489,7 @@ export interface WebDavConfig {
username?: string;
password?: string;
remotePath?: string;
userAgent?: string;
}
export interface WebDavSyncSummary {
+1
View File
@@ -382,6 +382,7 @@ export interface WebDavConfig {
username?: string;
password?: string;
remotePath?: string;
userAgent?: string;
}
export interface WebDavSyncSummary {
@@ -194,7 +194,7 @@ describe("settings search", () => {
it("exposes WebDAV sync in Web settings without exposing snippet sync", () => {
const webEntries = resolveSettingsSearchEntries(SETTINGS_SEARCH_DEFINITIONS, { isWeb: true, visibleCategories: new Set<SettingsCategory>(["sync"]) }, translate, categoryLabels);
expect(webEntries.map((entry) => entry.id)).toEqual(["sync-webdav", "sync-webdav-endpoint", "sync-webdav-username", "sync-webdav-password", "sync-webdav-remote-path", "sync-webdav-auto-upload", "sync-secrets", "sync-secrets-passphrase"]);
expect(webEntries.map((entry) => entry.id)).toEqual(["sync-webdav", "sync-webdav-endpoint", "sync-webdav-username", "sync-webdav-password", "sync-webdav-remote-path", "sync-webdav-user-agent", "sync-webdav-auto-upload", "sync-secrets", "sync-secrets-passphrase"]);
});
it("matches Chinese text as a Unicode substring", () => {
@@ -288,6 +288,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "sync-webdav-username", category: "sync", titleKey: "settings.syncUsername", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-password", category: "sync", titleKey: "settings.syncPassword", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-remote-path", category: "sync", titleKey: "settings.syncRemotePath", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-user-agent", category: "sync", titleKey: "settings.syncUserAgent", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-webdav-auto-upload", category: "sync", titleKey: "settings.syncAutoUploadInterval", targetId: "sync-webdav", route: { syncMethodTab: "webdav" } },
{ id: "sync-snippet", category: "sync", titleKey: "settings.syncSnippetTitle", descriptionKey: "settings.syncSnippetDescription", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
{ id: "sync-snippet-provider", category: "sync", titleKey: "settings.syncSnippetProvider", targetId: "sync-snippet", route: { syncMethodTab: "snippet" }, visible: desktopOnly },
@@ -3,7 +3,7 @@ import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStor
export const WEB_DAV_BACKUP_SELECTION_STORAGE_KEY = "dbx-webdav-backup-selection";
export const SYNC_METHOD_STORAGE_KEY = "dbx-sync-method";
export const WEB_DAV_AUTO_UPLOAD_STORAGE_KEYS = ["dbx-webdav-endpoint", "dbx-webdav-username", "dbx-webdav-remote-path", "dbx-webdav-auto-upload-enabled", "dbx-webdav-auto-upload-interval-minutes", WEB_DAV_BACKUP_SELECTION_STORAGE_KEY] as const;
export const WEB_DAV_AUTO_UPLOAD_STORAGE_KEYS = ["dbx-webdav-endpoint", "dbx-webdav-username", "dbx-webdav-user-agent", "dbx-webdav-remote-path", "dbx-webdav-auto-upload-enabled", "dbx-webdav-auto-upload-interval-minutes", WEB_DAV_BACKUP_SELECTION_STORAGE_KEY] as const;
export const DEFAULT_WEB_DAV_REMOTE_PATH = "DBX/sync/snapshot.json";
export const DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES = 30;
@@ -34,6 +34,7 @@ export function normalizedWebDavAutoUploadInterval(value: unknown): number {
export function readWebDavAutoUploadConfig(): WebDavAutoUploadConfig {
const endpoint = safeLocalStorageGet("dbx-webdav-endpoint")?.trim() || "";
const username = safeLocalStorageGet("dbx-webdav-username")?.trim() || "";
const userAgent = safeLocalStorageGet("dbx-webdav-user-agent")?.trim() || "";
const remotePath = safeLocalStorageGet("dbx-webdav-remote-path")?.trim() || DEFAULT_WEB_DAV_REMOTE_PATH;
return {
@@ -43,6 +44,7 @@ export function readWebDavAutoUploadConfig(): WebDavAutoUploadConfig {
? {
endpoint,
username: username || undefined,
userAgent: userAgent || undefined,
remotePath,
}
: null,
@@ -52,6 +54,7 @@ export function readWebDavAutoUploadConfig(): WebDavAutoUploadConfig {
export function writeWebDavAutoUploadFields(config: WebDavConfig, autoUpload: { enabled: boolean; intervalMinutes: unknown }) {
safeLocalStorageSet("dbx-webdav-endpoint", config.endpoint.trim());
safeLocalStorageSet("dbx-webdav-username", config.username?.trim() || "");
safeLocalStorageSet("dbx-webdav-user-agent", config.userAgent?.trim() || "");
safeLocalStorageSet("dbx-webdav-remote-path", config.remotePath?.trim() || DEFAULT_WEB_DAV_REMOTE_PATH);
safeLocalStorageSet("dbx-webdav-auto-upload-enabled", String(autoUpload.enabled));
safeLocalStorageSet("dbx-webdav-auto-upload-interval-minutes", String(normalizedWebDavAutoUploadInterval(autoUpload.intervalMinutes)));
@@ -94,6 +94,11 @@ pub struct WebDavConfig {
pub username: Option<String>,
pub password: Option<String>,
pub remote_path: Option<String>,
/// Optional User-Agent override for WebDAV gateways that only allow
/// specific client applications. When empty no User-Agent is sent,
/// matching the long-standing default behavior.
#[serde(default)]
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -1789,6 +1794,10 @@ impl WebDavClient {
let builder = Client::builder();
let builder =
if webdav_endpoint_uses_direct_connection(&config.endpoint) { builder.no_proxy() } else { builder };
let builder = match config.user_agent.as_deref().map(str::trim).filter(|ua| !ua.is_empty()) {
Some(user_agent) => builder.user_agent(user_agent),
None => builder,
};
let http = builder.build().expect("failed to build WebDAV HTTP client");
Self { http, config }
}
@@ -3427,6 +3436,29 @@ mod tests {
(format!("http://{address}/"), server)
}
/// Single-request WebDAV server that returns the complete request head
/// (request line plus headers) instead of just the request line.
async fn spawn_webdav_request_capture_server() -> (String, tokio::task::JoinHandle<String>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
let mut chunk = [0_u8; 4096];
while !request.windows(4).any(|window| window == b"\r\n\r\n") {
let read = socket.read(&mut chunk).await.unwrap();
assert!(read > 0, "request ended before headers were complete");
request.extend_from_slice(&chunk[..read]);
}
let response = "HTTP/1.1 207 Multi-Status\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
socket.write_all(response.as_bytes()).await.unwrap();
String::from_utf8(request).unwrap()
});
(format!("http://{address}/"), server)
}
fn github_snippet_response(content: &str) -> String {
serde_json::json!({
"files": { DEFAULT_SNIPPET_FILE_NAME: { "content": content } }
@@ -3682,6 +3714,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("DBX-home/sync/snapshot.json".to_string()),
user_agent: None,
});
client.put_snapshot(&snapshot).await.unwrap();
@@ -3707,6 +3740,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("DBX-home/sync/snapshot.json".to_string()),
user_agent: None,
});
client.put_snapshot(&snapshot).await.unwrap();
@@ -3732,6 +3766,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("DBX-home/sync/snapshot.json".to_string()),
user_agent: None,
});
let error = client.put_snapshot(&snapshot).await.unwrap_err();
@@ -3750,6 +3785,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("snapshot.json".to_string()),
user_agent: None,
});
client.put_snapshot(&snapshot).await.unwrap();
@@ -3765,6 +3801,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("DBX-home/sync/snapshot.json".to_string()),
user_agent: None,
});
client.test().await.unwrap();
@@ -3772,6 +3809,41 @@ mod tests {
assert_eq!(server.await.unwrap(), vec!["PROPFIND / HTTP/1.1"]);
}
#[tokio::test]
async fn webdav_requests_send_configured_user_agent() {
let (endpoint, server) = spawn_webdav_request_capture_server().await;
let client = WebDavClient::new(WebDavConfig {
endpoint,
username: None,
password: None,
remote_path: None,
user_agent: Some("Zotero/7.0.15".to_string()),
});
client.test().await.unwrap();
let request = server.await.unwrap();
assert!(request.starts_with("PROPFIND / "));
assert!(request.lines().any(|line| line.eq_ignore_ascii_case("user-agent: Zotero/7.0.15")));
}
#[tokio::test]
async fn webdav_requests_omit_user_agent_by_default() {
let (endpoint, server) = spawn_webdav_request_capture_server().await;
let client = WebDavClient::new(WebDavConfig {
endpoint,
username: None,
password: None,
remote_path: None,
user_agent: None,
});
client.test().await.unwrap();
let request = server.await.unwrap();
assert!(!request.to_ascii_lowercase().contains("user-agent:"));
}
#[test]
fn scrubs_connection_secret_fields() {
let mut config = ConnectionConfig {
@@ -4670,6 +4742,7 @@ mod tests {
username: Some("alice".to_string()),
password: None,
remote_path: None,
user_agent: None,
};
let snippet = SnippetSyncConfig {
provider: SnippetProvider::GitHub,
@@ -4848,6 +4921,7 @@ mod tests {
username: Some("alice".to_string()),
password: None,
remote_path: Some("DBX/sync/snapshot.json".to_string()),
user_agent: None,
};
save_webdav_password(&source, &webdav, "webdav-secret").await.unwrap();
let snippet = SnippetSyncConfig {
@@ -5287,6 +5361,7 @@ mod tests {
username: None,
password: None,
remote_path: Some("snapshot.json".to_string()),
user_agent: None,
});
client.put_snapshot(&snapshot).await.unwrap();
let (downloaded, _) = client.get_snapshot().await.unwrap();
+2
View File
@@ -163,6 +163,8 @@ GitHub Gist、Gitee 和 GitLab 代码片段始终使用**代码片段加密密
填写 WebDAV 服务地址、用户名、应用密码和远端快照路径。默认路径为 `DBX/sync/snapshot.json`。
部分 WebDAV 服务(如中国科技云)只允许特定客户端应用访问,会按 User-Agent 拦截未知客户端。遇到这类限制时,在 User-Agent 输入框填写服务允许的客户端标识(例如 `Zotero/7.0.15`);默认留空,表示不发送 User-Agent。
Web/Docker 模式下,WebDAV 请求由 DBX 服务端发起,因此不受浏览器 CORS 限制;WebDAV 应用密码会加密保存在当前 DBX 实例中,不会写入同步快照。
WebDAV 支持定时自动上传。自动上传只在对应 DBX 客户端保持打开时执行;Web/Docker 使用时需保持至少一个 DBX 页面打开。下载始终需要手动确认,避免远端快照意外覆盖当前实例的数据。
+2
View File
@@ -163,6 +163,8 @@ Neither password can be recovered. An encrypted snippet cannot be restored witho
Enter the WebDAV endpoint, username, application password, and remote snapshot path. The default path is `DBX/sync/snapshot.json`.
Some WebDAV services (for example CSTCloud) only allow specific client applications and reject unknown clients based on the User-Agent. When you hit such a restriction, fill the User-Agent field with an identifier the service accepts (for example `Zotero/7.0.15`). It stays empty by default, which sends no User-Agent.
In Web/Docker mode, the DBX server sends WebDAV requests, so browser CORS restrictions do not apply. WebDAV application passwords are encrypted in the current DBX instance and are never included in sync snapshots.
WebDAV supports scheduled uploads while the corresponding DBX client remains open. Keep at least one DBX page open when using Web/Docker. Downloads always require confirmation to prevent accidental replacement of data in the current instance.
+20
View File
@@ -110,6 +110,25 @@ test("reads normalized WebDAV auto-upload config from localStorage", () => {
endpoint: "https://dav.example.com/",
username: "alice",
remotePath: "DBX/sync/snapshot.json",
userAgent: undefined,
});
});
test("reads configured WebDAV user agent for client-restricted gateways", () => {
localStorage.setItem("dbx-webdav-endpoint", "https://dav.example.com/");
localStorage.setItem("dbx-webdav-user-agent", " Zotero/7.0.15 ");
localStorage.setItem("dbx-webdav-auto-upload-enabled", "true");
localStorage.setItem("dbx-webdav-auto-upload-interval-minutes", "1");
const config = readWebDavAutoUploadConfig();
assert.equal(config.webDavConfig?.userAgent, "Zotero/7.0.15");
useWebDavAutoUpload();
return vi.advanceTimersByTimeAsync(60_000).then(() => {
assert.equal(webdavSyncUploadMock.mock.calls.length, 1);
assert.equal(webdavSyncUploadMock.mock.calls[0][0].userAgent, "Zotero/7.0.15");
});
});
@@ -127,6 +146,7 @@ test("keeps WebDAV auto-upload running outside the settings dialog", async () =>
endpoint: "https://dav.example.com/",
username: undefined,
remotePath: "DBX/sync/snapshot.json",
userAgent: undefined,
});
assert.equal(webdavSyncUploadMock.mock.calls[0][3], false);