From aa803a414938e6d10f861c2392251d29e9797c8f Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:35:40 +0200 Subject: [PATCH 01/11] 0.9.7.1: add generic Nextcloud transport resolver --- include/nc_transport.inc.php | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 include/nc_transport.inc.php diff --git a/include/nc_transport.inc.php b/include/nc_transport.inc.php new file mode 100644 index 0000000..b9eba6a --- /dev/null +++ b/include/nc_transport.inc.php @@ -0,0 +1,96 @@ +'dns.google', 'ips'=>array('8.8.8.8','8.8.4.4'), 'url'=>'https://dns.google/resolve?name='.rawurlencode($host).'&type=A'), + array('host'=>'cloudflare-dns.com', 'ips'=>array('1.1.1.1','1.0.0.1'), 'url'=>'https://cloudflare-dns.com/dns-query?name='.rawurlencode($host).'&type=A'), + ); + + foreach ($providers as $provider) + { + foreach ($provider['ips'] as $resolver_ip) + { + $ch = curl_init($provider['url']); + $options = array( + CURLOPT_RETURNTRANSFER=>true, + CURLOPT_FOLLOWLOCATION=>false, + CURLOPT_CONNECTTIMEOUT=>5, + CURLOPT_TIMEOUT=>10, + CURLOPT_HTTPHEADER=>array('Accept: application/dns-json'), + CURLOPT_USERAGENT=>'Bratonien-Tools-DNS/0.9.7.1', + ); + if (defined('CURLOPT_RESOLVE')) + { + $options[CURLOPT_RESOLVE] = array($provider['host'].':443:'.$resolver_ip); + } + curl_setopt_array($ch, $options); + $body = curl_exec($ch); + $errno = curl_errno($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($body === false || $errno !== 0 || $status < 200 || $status >= 300) continue; + + $decoded = json_decode((string)$body, true); + if (!is_array($decoded) || !isset($decoded['Answer']) || !is_array($decoded['Answer'])) continue; + foreach ($decoded['Answer'] as $answer) + { + if ((int)($answer['type'] ?? 0) !== 1) continue; + $ip = trim((string)($answer['data'] ?? '')); + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) continue; + return $cache[$host] = $ip; + } + } + } + + throw new RuntimeException('Für '.$host.' konnte keine öffentliche IPv4-Adresse ermittelt werden.'); +} + +function bratonien_tools_nc_transport_resolve_entry($url) +{ + $scheme = bratonien_tools_nc_transport_scheme($url); + $host = bratonien_tools_nc_transport_host($url); + if (bratonien_tools_nc_transport_is_ip($host)) return null; + + $port = (int)parse_url((string)$url, PHP_URL_PORT); + if ($port < 1) $port = $scheme === 'https' ? 443 : 80; + $ip = bratonien_tools_nc_transport_public_ip($host); + return $host.':'.$port.':'.$ip; +} + +function bratonien_tools_nc_transport_apply_curl(array &$options, $url) +{ + $entry = bratonien_tools_nc_transport_resolve_entry($url); + if ($entry !== null) + { + if (!defined('CURLOPT_RESOLVE')) throw new RuntimeException('Diese cURL-Version unterstützt keine direkte Host-zu-IP-Zuordnung.'); + $options[CURLOPT_RESOLVE] = array($entry); + } +} From aa5c2f7f0c9a6f25a77814662fba7bfb8bfd1a11 Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:37:23 +0200 Subject: [PATCH 02/11] 0.9.7.1: add runtime Nextcloud target resolver --- runtime/lib/resolve-nextcloud-target.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 runtime/lib/resolve-nextcloud-target.php diff --git a/runtime/lib/resolve-nextcloud-target.php b/runtime/lib/resolve-nextcloud-target.php new file mode 100644 index 0000000..52b4d1b --- /dev/null +++ b/runtime/lib/resolve-nextcloud-target.php @@ -0,0 +1,24 @@ +#!/usr/bin/env php +'); + $url = rtrim(trim((string)$argv[1]), '/'); + bratonien_tools_nc_transport_scheme($url); + $host = bratonien_tools_nc_transport_host($url); + echo bratonien_tools_nc_transport_public_ip($host)."\n"; + exit(0); +} +catch (Throwable $e) +{ + fwrite(STDERR, $e->getMessage()."\n"); + exit(1); +} From bb434b9b027114e30689115d2075358c148d5fc9 Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:37:59 +0200 Subject: [PATCH 03/11] 0.9.7.1: route wizard and WebDAV calls through resolved target IP --- include/nc_transport.inc.php | 244 +++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/include/nc_transport.inc.php b/include/nc_transport.inc.php index b9eba6a..6791ee1 100644 --- a/include/nc_transport.inc.php +++ b/include/nc_transport.inc.php @@ -94,3 +94,247 @@ function bratonien_tools_nc_transport_apply_curl(array &$options, $url) $options[CURLOPT_RESOLVE] = array($entry); } } + +function bratonien_tools_nc_transport_http($url, $username = '', $password = '', array $headers = array()) +{ + if (!function_exists('curl_init')) throw new RuntimeException('Der Server kann Nextcloud derzeit nicht per HTTP prüfen.'); + + $ch = curl_init($url); + $options = array( + CURLOPT_RETURNTRANSFER=>true, + CURLOPT_FOLLOWLOCATION=>true, + CURLOPT_MAXREDIRS=>3, + CURLOPT_CONNECTTIMEOUT=>8, + CURLOPT_TIMEOUT=>15, + CURLOPT_HTTPHEADER=>array_merge(array('Accept: application/json'), $headers), + CURLOPT_USERAGENT=>'Bratonien-Tools-NC-Wizard/0.9.7.1', + ); + bratonien_tools_nc_transport_apply_curl($options, $url); + + if ($username !== '') + { + $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; + $options[CURLOPT_USERPWD] = $username.':'.$password; + } + + curl_setopt_array($ch, $options); + $body = curl_exec($ch); + $errno = curl_errno($ch); + $error = curl_error($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($body === false || $errno !== 0) + { + throw new RuntimeException('Verbindung fehlgeschlagen'.($error !== '' ? ': '.$error : '.')); + } + + return array('status'=>$status, 'body'=>(string)$body); +} + +function bratonien_tools_nc_transport_webdav_list(array $state, $path = '') +{ + if (empty($state['scan_ok']) || trim((string)$state['base_url']) === '' || trim((string)$state['username']) === '' || (string)$state['_password'] === '') + { + throw new RuntimeException('Die Nextcloud-Sitzung des Assistenten ist nicht vollständig.'); + } + if (!function_exists('curl_init')) throw new RuntimeException('cURL ist für die Verzeichnisauswahl nicht verfügbar.'); + + $path = trim((string)$path, '/'); + if ($path !== '' && preg_match('#(^|/)\.\.(/|$)#', $path)) throw new RuntimeException('Ungültiger Verzeichnispfad.'); + + $segments = $path === '' ? array() : array_map('rawurlencode', explode('/', $path)); + $user = rawurlencode((string)$state['username']); + $url = rtrim((string)$state['base_url'], '/').'/remote.php/dav/files/'.$user.'/'.implode('/', $segments); + if (substr($url, -1) !== '/') $url .= '/'; + + $body = ''; + $ch = curl_init($url); + $options = array( + CURLOPT_RETURNTRANSFER=>true, + CURLOPT_CUSTOMREQUEST=>'PROPFIND', + CURLOPT_POSTFIELDS=>$body, + CURLOPT_HTTPHEADER=>array('Depth: 1','Content-Type: application/xml; charset=utf-8'), + CURLOPT_HTTPAUTH=>CURLAUTH_BASIC, + CURLOPT_USERPWD=>(string)$state['username'].':'.(string)$state['_password'], + CURLOPT_CONNECTTIMEOUT=>8, + CURLOPT_TIMEOUT=>20, + ); + bratonien_tools_nc_transport_apply_curl($options, $url); + curl_setopt_array($ch, $options); + $response = curl_exec($ch); + $errno = curl_errno($ch); + $error = curl_error($ch); + $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($response === false || $errno !== 0) throw new RuntimeException('Nextcloud-Verzeichnisse konnten nicht geladen werden'.($error !== '' ? ': '.$error : '.')); + if ($status === 401 || $status === 403) throw new RuntimeException('Nextcloud hat den Zugriff auf dieses Verzeichnis abgelehnt.'); + if ($status !== 207) throw new RuntimeException('Nextcloud-Verzeichnisabfrage antwortete mit HTTP '.$status.'.'); + + libxml_use_internal_errors(true); + $xml = simplexml_load_string((string)$response); + if ($xml === false) throw new RuntimeException('Nextcloud hat eine ungültige WebDAV-Antwort geliefert.'); + $xml->registerXPathNamespace('d', 'DAV:'); + $xml->registerXPathNamespace('oc', 'http://owncloud.org/ns'); + + $children = array(); + $fileids = array(); + $current_fileid = 0; + $base_path = (string)parse_url($url, PHP_URL_PATH); + + foreach ($xml->xpath('//d:response') as $item) + { + $item->registerXPathNamespace('d', 'DAV:'); + $item->registerXPathNamespace('oc', 'http://owncloud.org/ns'); + $hrefs = $item->xpath('d:href'); + $collections = $item->xpath('d:propstat/d:prop/d:resourcetype/d:collection'); + $ids = $item->xpath('d:propstat/d:prop/oc:fileid'); + if (!$hrefs || !$collections || !$ids) continue; + + $fileid = (int)trim((string)$ids[0]); + if ($fileid < 1) continue; + $href = rawurldecode((string)$hrefs[0]); + $href_path = (string)parse_url($href, PHP_URL_PATH); + + if (rtrim($href_path, '/') === rtrim($base_path, '/')) + { + $current_fileid = $fileid; + continue; + } + + $name = basename(rtrim($href_path, '/')); + if ($name === '') continue; + $child_path = $path === '' ? $name : $path.'/'.$name; + $children[$child_path] = $name; + $fileids[$child_path] = $fileid; + } + natcasesort($children); + + if ($current_fileid < 1) throw new RuntimeException('Nextcloud hat für das aktuelle Verzeichnis keine eindeutige Datei-ID geliefert.'); + + $parent = ''; + if ($path !== '') + { + $parts = explode('/', $path); + array_pop($parts); + $parent = implode('/', $parts); + } + + return array( + 'current'=>$path, + 'parent'=>$parent, + 'children'=>$children, + 'current_fileid'=>$current_fileid, + 'fileids'=>$fileids, + ); +} + +function bratonien_tools_nc_transport_refresh_directory_state(array &$state, $path = null) +{ + if ($path === null) $path = (string)($state['directory_path'] ?? ''); + $listing = bratonien_tools_nc_transport_webdav_list($state, $path); + $state['directory_path'] = (string)$listing['current']; + $state['directory_parent'] = (string)$listing['parent']; + $state['directory_children'] = (array)$listing['children']; + $state['directory_current_fileid'] = (int)$listing['current_fileid']; + $state['directory_fileids'] = (array)$listing['fileids']; + if (!isset($state['directory_selected']) || !is_array($state['directory_selected'])) $state['directory_selected'] = array(); + if (!isset($state['directory_selected_fileids']) || !is_array($state['directory_selected_fileids'])) $state['directory_selected_fileids'] = array(); +} + +function bratonien_tools_nc_transport_wizard_directory_browse() +{ + $state = bratonien_tools_nc_wizard_state(); + if ((int)$state['step'] !== 2 || (string)$state['technical_stage'] !== 'mounts' || empty($state['directory_selection_ready'])) throw new RuntimeException('Die Verzeichnisauswahl ist in diesem Fenster nicht verfügbar.'); + $path = trim((string)($_POST['nc_wizard_directory_path'] ?? ''), '/'); + bratonien_tools_nc_transport_refresh_directory_state($state, $path); + bratonien_tools_nc_wizard_store($state); + return array('message'=>'Verzeichnis geöffnet.'); +} + +function bratonien_tools_nc_transport_edit_start() +{ + $id = (int)($_POST['connection_id'] ?? 0); + $connection = bratonien_tools_nc_connector_connection($id, true); + if (!$connection) throw new RuntimeException('Connector-Verbindung wurde nicht gefunden.'); + + $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); + $is_webdav = (string)$connection['adapter'] === 'remote' + && (string)($config['source_mode'] ?? '') === 'webdav-placeholder'; + if (!$is_webdav) return bratonien_tools_nc_connector_edit_start(); + + $credentials = bratonien_tools_nc_connector_scoped_secret($connection); + $base_url = trim((string)($config['nextcloud_url'] ?? '')); + $username = trim((string)($credentials['nextcloud_user'] ?? '')); + if ($username === '') $username = trim((string)($config['nextcloud_access_user'] ?? $config['access_user'] ?? '')); + $password = (string)($credentials['nextcloud_password'] ?? ''); + + $roots = isset($config['roots']) && is_array($config['roots']) ? array_values($config['roots']) : array(); + $selected = array(); + $selected_ids = array(); + foreach ($roots as $root) + { + $path = trim((string)($root['webdav_path'] ?? ''), '/'); + $fileid = (int)($root['fileid'] ?? 0); + if ($fileid < 1) continue; + $selected[] = $path; + $selected_ids[$path] = $fileid; + } + + $state = bratonien_tools_nc_wizard_state(); + $state = array_merge($state, array( + 'editing_connection_id'=>$id, + 'editing_adapter'=>(string)$connection['adapter'], + 'editing_mode'=>'update', + 'connection_name'=>(string)$connection['name'], + 'host_input'=>$base_url, + 'base_url'=>$base_url, + 'username'=>$username, + '_password'=>$password, + '_fallback_user'=>(string)($credentials['piwigo_user'] ?? ''), + '_fallback_password'=>(string)($credentials['piwigo_password'] ?? ''), + '_api_key_id'=>(string)($credentials['api_key_id'] ?? ''), + '_api_key_secret'=>(string)($credentials['api_key_secret'] ?? ''), + 'api_status'=>trim((string)($credentials['api_key_id'] ?? '')) !== '' && trim((string)($credentials['api_key_secret'] ?? '')) !== '' ? 'ok' : 'pending', + 'roots'=>$roots, + 'directory_selected'=>$selected, + 'directory_selected_fileids'=>$selected_ids, + 'source_mode'=>'webdav-placeholder', + 'transport'=>'webdav', + )); + + if ($base_url !== '' && $username !== '' && $password !== '') + { + $state['step'] = 2; + $state['scan_ok'] = true; + $state['technical_stage'] = 'mounts'; + $state['technical_source'] = 'WebDAV'; + $state['technical_error'] = ''; + $state['technical_complete'] = false; + $state['directory_selection_ready'] = true; + $state['directory_path'] = ''; + $state['directory_parent'] = ''; + $state['directory_children'] = array(); + $state['directory_current_fileid'] = 0; + + try + { + bratonien_tools_nc_transport_refresh_directory_state($state, ''); + } + catch (Throwable $e) + { + $state['step'] = 1; + $state['scan_ok'] = false; + $state['technical_error'] = $e->getMessage(); + } + } + else + { + $state['step'] = 1; + $state['scan_ok'] = false; + } + + bratonien_tools_nc_wizard_store($state); + return array('message'=>'Verbindung #'.$id.' wurde zum Bearbeiten geöffnet.'); +} From b9adcbddac8d0b0d8a509bd172b845f54b88fa8f Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:38:25 +0200 Subject: [PATCH 04/11] 0.9.7.1: use transport-aware WebDAV wizard handlers --- include/tool_registry.inc.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/tool_registry.inc.php b/include/tool_registry.inc.php index b89c5fc..b49f033 100644 --- a/include/tool_registry.inc.php +++ b/include/tool_registry.inc.php @@ -6,7 +6,7 @@ if (!defined('PHPWG_ROOT_PATH')) if (isset($GLOBALS['template']) && is_object($GLOBALS['template']) && method_exists($GLOBALS['template'], 'func_combine_script')) { - $script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.6.30'; + $script_version = function_exists('bratonien_tools_current_version') ? bratonien_tools_current_version() : '0.9.7.1'; $GLOBALS['template']->func_combine_script(array( 'id'=>'bratonien_nc_connector_edit_v2', 'path'=>BRATONIEN_TOOLS_PATH.'js/nc_connector_edit_v2.js', @@ -41,6 +41,7 @@ require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_delete_safe.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_webdav.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_wizard_webdav_flow.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_edit.inc.php'); +require_once(BRATONIEN_TOOLS_PATH . 'include/nc_transport.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_scheduler.inc.php'); require_once(BRATONIEN_TOOLS_PATH . 'include/nc_connector_system.inc.php'); @@ -74,7 +75,7 @@ function bratonien_tools_get_tools() 'album_share_regenerate_link' => array('handler' => 'bratonien_tools_regenerate_album_share_link'), 'album_share_revoke' => array('handler' => 'bratonien_tools_revoke_album_share'), 'nc_connector_create_webdav_parallel' => array('handler' => 'bratonien_tools_nc_connector_create_webdav_placeholder_from_wizard'), - 'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_connector_edit_start'), + 'nc_connector_edit_start' => array('handler' => 'bratonien_tools_nc_transport_edit_start'), 'nc_connector_update_local' => array('handler' => 'bratonien_tools_nc_connector_update_local_friendly'), 'nc_connector_delete' => array('handler' => 'bratonien_tools_nc_connector_delete_safe'), 'nc_connector_update_name' => array('handler' => 'bratonien_tools_nc_connector_update_name'), @@ -82,7 +83,7 @@ function bratonien_tools_get_tools() 'nc_connector_run_now' => array('handler' => 'bratonien_tools_nc_connector_run_now'), 'nc_connector_wizard_scan' => array('handler' => 'bratonien_tools_nc_wizard_scan_webdav_first'), 'nc_connector_wizard_save_technical' => array('handler' => 'bratonien_tools_nc_wizard_save_technical_flow'), - 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_wizard_directory_browse'), + 'nc_connector_wizard_directory_browse' => array('handler' => 'bratonien_tools_nc_transport_wizard_directory_browse'), 'nc_connector_wizard_directory_add' => array('handler' => 'bratonien_tools_nc_wizard_directory_add'), 'nc_connector_wizard_directory_remove' => array('handler' => 'bratonien_tools_nc_wizard_directory_remove'), 'nc_connector_wizard_save_mounts' => array('handler' => 'bratonien_tools_nc_wizard_save_sources_dispatch'), From 986eb530d705b5029d4efa89034a7e6bbb203a3d Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:38:52 +0200 Subject: [PATCH 05/11] 0.9.7.1: scan remote Nextcloud through resolved IP transport --- include/nc_connector_wizard_webdav_flow.inc.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/nc_connector_wizard_webdav_flow.inc.php b/include/nc_connector_wizard_webdav_flow.inc.php index 84a72b9..297c57e 100644 --- a/include/nc_connector_wizard_webdav_flow.inc.php +++ b/include/nc_connector_wizard_webdav_flow.inc.php @@ -28,7 +28,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first() { try { - $response = bratonien_tools_nc_wizard_http($candidate_url.'/status.php'); + $response = bratonien_tools_nc_transport_http($candidate_url.'/status.php'); if ($response['status'] < 200 || $response['status'] >= 300) continue; $candidate_status = json_decode($response['body'], true); if (!is_array($candidate_status) || empty($candidate_status['installed'])) continue; @@ -44,7 +44,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first() throw new RuntimeException('Unter dieser Adresse konnte keine Nextcloud erreicht werden. HTTP und HTTPS wurden automatisch geprüft.'); } - $user_response = bratonien_tools_nc_wizard_http( + $user_response = bratonien_tools_nc_transport_http( $base_url.'/ocs/v2.php/cloud/user?format=json', $username, $password, @@ -104,7 +104,7 @@ function bratonien_tools_nc_wizard_scan_webdav_first() 'api_error'=>'', )); - bratonien_tools_nc_wizard_refresh_directory_state($state, ''); + bratonien_tools_nc_transport_refresh_directory_state($state, ''); bratonien_tools_nc_wizard_store($state); return array('message'=>'Nextcloud und WebDAV wurden bestätigt. Jetzt können die Verzeichnisse des angemeldeten Benutzers ausgewählt werden.'); From a54df74e678e03760b83c69e5c12ed0402a971ae Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:39:27 +0200 Subject: [PATCH 06/11] 0.9.7.1: pin WebDAV runtime to resolved target IP --- .../lib/build_webdav_placeholder_source.py | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/runtime/lib/build_webdav_placeholder_source.py b/runtime/lib/build_webdav_placeholder_source.py index 0bcd803..132d791 100755 --- a/runtime/lib/build_webdav_placeholder_source.py +++ b/runtime/lib/build_webdav_placeholder_source.py @@ -14,6 +14,7 @@ import getpass import json import os import shutil +import socket import ssl import sys import tempfile @@ -21,6 +22,7 @@ import urllib.error import urllib.parse import urllib.request import xml.etree.ElementTree as ET +from contextlib import contextmanager from pathlib import Path, PurePosixPath DAV = "DAV:" @@ -52,9 +54,37 @@ def safe_local_name(name: str) -> str: return name +@contextmanager +def pinned_resolution(host: str, ip: str): + host = host.strip("[]").lower() + ip = ip.strip("[]") + if not host or not ip or host == ip: + yield + return + + original = socket.getaddrinfo + + def resolve(name, port, family=0, type=0, proto=0, flags=0): + normalized = str(name).strip("[]").lower() + if normalized == host: + return original(ip, port, family, type, proto, flags) + return original(name, port, family, type, proto, flags) + + socket.getaddrinfo = resolve + try: + yield + finally: + socket.getaddrinfo = original + + class WebDavClient: - def __init__(self, base_url: str, user: str, password: str, timeout: int = 30) -> None: + def __init__(self, base_url: str, user: str, password: str, timeout: int = 30, connect_ip: str = "") -> None: self.base_url = base_url.rstrip("/") + parsed = urllib.parse.urlparse(self.base_url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + fail("Nextcloud base URL must use HTTP or HTTPS and contain a host") + self.host = parsed.hostname.strip("[]") + self.connect_ip = connect_ip.strip("[]") or self.host self.user = user self.password = password self.timeout = timeout @@ -84,9 +114,10 @@ class WebDavClient: request.add_header("Depth", "1") request.add_header("Content-Type", "application/xml; charset=utf-8") try: - with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response: - status = response.status - payload = response.read() + with pinned_resolution(self.host, self.connect_ip): + with urllib.request.urlopen(request, timeout=self.timeout, context=self.context) as response: + status = response.status + payload = response.read() except urllib.error.HTTPError as error: if error.code in {401, 403}: fail("Nextcloud rejected the WebDAV credentials or directory access") @@ -96,14 +127,16 @@ class WebDavClient: if status != 207: fail(f"Nextcloud PROPFIND returned HTTP {status}") + base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/") + current: dict[str, object] | None = None + children: list[dict[str, object]] = [] + for response in root.findall(f"{{{DAV}}}response") if False else []: + pass try: root = ET.fromstring(payload) except ET.ParseError as error: raise RuntimeError("Nextcloud returned invalid WebDAV XML") from error - base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/") - current: dict[str, object] | None = None - children: list[dict[str, object]] = [] for response in root.findall(f"{{{DAV}}}response"): href = response.findtext(f"{{{DAV}}}href", default="") href_path = urllib.parse.unquote(urllib.parse.urlparse(href).path).rstrip("/") @@ -213,6 +246,7 @@ def atomic_text(path: Path, text: str) -> None: def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--base-url", required=True) + parser.add_argument("--connect-ip", default="", help="IP address used for the TCP connection while preserving the URL host for HTTP Host and TLS SNI") parser.add_argument("--user", required=True) parser.add_argument("--password-file", type=Path) parser.add_argument("--root", action="append", required=True, help="WebDAV path relative to the authenticated user's files root") @@ -241,7 +275,7 @@ def main() -> int: shutil.rmtree(staging) staging.mkdir(parents=True) - client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout)) + client = WebDavClient(args.base_url, args.user, password, max(1, args.timeout), args.connect_ip) mapping: dict[str, dict[str, object]] = {} manifest: list[str] = [] total_files = total_folders = total_skipped = 0 @@ -303,6 +337,7 @@ def main() -> int: atomic_json(args.mapping, { "version": 1, "base_url": args.base_url.rstrip("/"), + "connect_ip": client.connect_ip, "user": args.user, "files": final_mapping, }) @@ -311,6 +346,7 @@ def main() -> int: "files": total_files, "folders": total_folders, "skipped": total_skipped, + "connect_ip": client.connect_ip, "source_dir": str(source_dir), "manifest": str(args.manifest), "mapping": str(args.mapping), From 5396f50bc0d30e8dbade235b027b01ba5e637139 Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:39:54 +0200 Subject: [PATCH 07/11] 0.9.7.1: resolve remote Nextcloud target before WebDAV scan --- runtime/sync-webdav.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/runtime/sync-webdav.sh b/runtime/sync-webdav.sh index c0b8c06..b5372f8 100644 --- a/runtime/sync-webdav.sh +++ b/runtime/sync-webdav.sh @@ -75,8 +75,12 @@ done < "$WEBDAV_ROOTS_FILE" [[ ${#ROOT_ARGS[@]} -gt 0 ]] || { write_status error "Keine WebDAV-Wurzeln konfiguriert"; exit 1; } +WEBDAV_CONNECT_IP="$(php "$SCRIPT_DIR/lib/resolve-nextcloud-target.php" "$WEBDAV_BASE_URL")" +[[ -n "$WEBDAV_CONNECT_IP" ]] || { write_status error "Nextcloud-Zieladresse konnte nicht ermittelt werden"; exit 1; } + python3 "$SCRIPT_DIR/lib/build_webdav_placeholder_source.py" \ --base-url "$WEBDAV_BASE_URL" \ + --connect-ip "$WEBDAV_CONNECT_IP" \ --user "$WEBDAV_USER" \ --password-file "$WEBDAV_PASSWORD_FILE" \ "${ROOT_ARGS[@]}" \ From 6b082ba8b50240884324b8f3a6ee77b944eb3b8b Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:40:19 +0200 Subject: [PATCH 08/11] 0.9.7.1: fetch WebDAV previews through resolved target IP --- runtime/lib/precache-webdav-previews.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/runtime/lib/precache-webdav-previews.php b/runtime/lib/precache-webdav-previews.php index 48683bc..f586e36 100644 --- a/runtime/lib/precache-webdav-previews.php +++ b/runtime/lib/precache-webdav-previews.php @@ -6,6 +6,8 @@ if (PHP_SAPI !== 'cli') exit(1); } +require_once(dirname(__DIR__, 2).'/include/nc_transport.inc.php'); + const BRATONIEN_WEBDAV_PREVIEW_VERSION = 2; const BRATONIEN_WEBDAV_PREVIEW_MAX_EDGE = 4096; const BRATONIEN_WEBDAV_PREVIEW_JPEG_QUALITY = 88; @@ -24,7 +26,7 @@ function quote_webdav_path($path) function fetch_remote_blob($url, $user, $password) { $ch = curl_init($url); - curl_setopt_array($ch, array( + $options = array( CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, @@ -32,8 +34,10 @@ function fetch_remote_blob($url, $user, $password) CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $user.':'.$password, CURLOPT_FAILONERROR => false, - CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.6.1', - )); + CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Precache/0.9.7.1', + ); + bratonien_tools_nc_transport_apply_curl($options, $url); + curl_setopt_array($ch, $options); $body = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); From a5be16f88e1fcce8fc5fd4178bf2dce31f322c7a Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:40:40 +0200 Subject: [PATCH 09/11] 0.9.7.1: stream WebDAV originals through resolved target IP --- webdav-image.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webdav-image.php b/webdav-image.php index dad21cc..c0bdcbe 100644 --- a/webdav-image.php +++ b/webdav-image.php @@ -8,6 +8,7 @@ if (!defined('BRATONIEN_TOOLS_PATH')) define('BRATONIEN_TOOLS_PATH', PHPWG_ROOT_PATH.'plugins/'.BRATONIEN_TOOLS_ID.'/'); } require_once(BRATONIEN_TOOLS_PATH.'include/webdav_image_runtime.inc.php'); +require_once(BRATONIEN_TOOLS_PATH.'include/nc_transport.inc.php'); function bratonien_tools_webdav_image_abort($status, $message) { @@ -120,7 +121,7 @@ $options = array( CURLOPT_USERPWD => $user.':'.$password, CURLOPT_RETURNTRANSFER => false, CURLOPT_FAILONERROR => false, - CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.6.1', + CURLOPT_USERAGENT => 'Bratonien-Tools-WebDAV-Image/0.9.7.1', CURLOPT_HEADERFUNCTION => function($ch, $line) { $length = strlen($line); @@ -147,6 +148,7 @@ $options = array( return strlen($data); }, ); +bratonien_tools_nc_transport_apply_curl($options, $url); if (!empty($_SERVER['HTTP_RANGE'])) { $range = trim((string)$_SERVER['HTTP_RANGE']); From 5f81cc9ce9377eb2aa3143df51e36cb1caca101c Mon Sep 17 00:00:00 2001 From: Terranom674 Date: Wed, 19 Aug 2026 20:40:57 +0200 Subject: [PATCH 10/11] 0.9.7.1: bump plugin version --- main.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.inc.php b/main.inc.php index e6caca7..53c06dc 100644 --- a/main.inc.php +++ b/main.inc.php @@ -1,7 +1,7 @@ Date: Wed, 19 Aug 2026 20:41:46 +0200 Subject: [PATCH 11/11] 0.9.7.1: clean pinned WebDAV resolver implementation --- runtime/lib/build_webdav_placeholder_source.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime/lib/build_webdav_placeholder_source.py b/runtime/lib/build_webdav_placeholder_source.py index 132d791..a383e4a 100755 --- a/runtime/lib/build_webdav_placeholder_source.py +++ b/runtime/lib/build_webdav_placeholder_source.py @@ -130,8 +130,6 @@ class WebDavClient: base_path = urllib.parse.unquote(urllib.parse.urlparse(url).path).rstrip("/") current: dict[str, object] | None = None children: list[dict[str, object]] = [] - for response in root.findall(f"{{{DAV}}}response") if False else []: - pass try: root = ET.fromstring(payload) except ET.ParseError as error: