From 8c8e59f469f65cc5ad59de5f989f8f5899652fcd Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Wed, 28 Jan 2026 15:53:12 +0100 Subject: [PATCH 1/4] code Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 85 ++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 11 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index 863939ea..c9bac0b4 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -5,25 +5,74 @@ import Foundation import Alamofire -final class NKMonitor: EventMonitor, Sendable { - let nkCommonInstance: NKCommon - let queue = DispatchQueue(label: "com.nextcloud.NKMonitor") +// Description: +// +// NKMonitor is an Alamofire EventMonitor implementation used to observe +// the lifecycle of network requests and responses within the Nextcloud iOS client. +// +// Its primary responsibilities are: +// +// - Logging outgoing requests and incoming responses at different verbosity levels. +// - Tracking server-side error codes per account for diagnostic and recovery purposes. +// - Detecting potential account mismatches between the logical account assigned +// to a request and the user encoded in the WebDAV request path. +// +// Account Safety and Diagnostics: +// +// In a multi-account environment, it is critical to ensure that each request +// is executed using the correct account credentials. +// +// To support this, NKMonitor: +// +// - Extracts the logical account identifier from a custom internal HTTP header +// attached to each request. +// - On authentication failures (HTTP 401), compares the account identifier +// against the username declared in the WebDAV path (e.g. /remote.php/dav/files/). +// - Logs an explicit error when a mismatch is detected, providing deterministic +// evidence of a request executed with inconsistent account context. +// +// This mechanism allows distinguishing between: +// - Legitimate authentication failures for the correct account. +// - Requests accidentally executed using credentials belonging to a different account. +// +// Threading Model: +// +// - All logging operations are performed on a dedicated background DispatchQueue. +// - The monitor does not assume any actor isolation and is intentionally not Sendable. +// - Consumers of delegate callbacks are responsible for ensuring thread safety. +// +// Security Notes: +// +// - Authorization headers are never inspected or decoded. +// - Only application-internal account identifiers are logged. +// - No credentials or sensitive authentication material are exposed. +// +// NKMonitor is intended as an observational and diagnostic component and does not +// modify request execution or response handling. +// + +final class NKMonitor: EventMonitor { + internal let nkCommonInstance: NKCommon + internal let queue = DispatchQueue(label: "com.nextcloud.NKMonitor", qos: .utility) init(nkCommonInstance: NKCommon) { self.nkCommonInstance = nkCommonInstance } func requestDidResume(_ request: Request) { - DispatchQueue.global(qos: .utility).async { + let account = request.request? + .allHTTPHeaderFields?[self.nkCommonInstance.headerAccount] ?? "unknown" + queue.async { switch NKLogFileManager.shared.logLevel { case .normal: // General-purpose log: full Request description - nkLog(info: "Request started: \(request)") + nkLog(info: "User: \(account) - Request started: \(request)") case .verbose: // Full dump: headers + body let headers = request.request?.allHTTPHeaderFields?.description ?? "None" let body = request.request?.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "None" - + + nkLog(debug: "User: \(account)") nkLog(debug: "Request started: \(request)") nkLog(debug: "Headers: \(headers)") nkLog(debug: "Body: \(body)") @@ -35,6 +84,7 @@ final class NKMonitor: EventMonitor, Sendable { func request(_ request: DataRequest, didParseResponse response: AFDataResponse) { nkCommonInstance.delegate?.request(request, didParseResponse: response) + let account = request.request?.allHTTPHeaderFields?[self.nkCommonInstance.headerAccount] ?? "unknown" // Check for header and account error code tracking if let statusCode = response.response?.statusCode, @@ -46,15 +96,27 @@ final class NKMonitor: EventMonitor, Sendable { } } - DispatchQueue.global(qos: .utility).async { + // Check 401 + if response.response?.statusCode == 401 { + let pathUser = request.request?.url? + .path + .components(separatedBy: "/files/") + .dropFirst() + .first + + if let pathUser, pathUser != account { + nkLog(error: "ACCOUNT MISMATCH host=\(request.request?.url?.host ?? "-") pathUser=\(pathUser) headerUser=\(account)") + } + } + + queue.async { switch NKLogFileManager.shared.logLevel { case .normal: let resultString = String(describing: response.result) - if let request = response.request { - nkLog(info: "Network response request: \(request), result: \(resultString)") + nkLog(info: "User: \(account) - Network response request: \(request), result: \(resultString)") } else { - nkLog(info: "Network response result: \(resultString)") + nkLog(info: "User: \(account) - Network response result: \(resultString)") } case .compact: @@ -63,7 +125,7 @@ final class NKMonitor: EventMonitor, Sendable { let code = response.response?.statusCode { let responseStatus = (200..<300).contains(code) ? "RESPONSE: SUCCESS" : "RESPONSE: ERROR" - nkLog(network: "\(code) \(method) \(url) \(responseStatus)") + nkLog(network: "\(account) \(code) \(method) \(url) \(responseStatus)") } case .verbose: @@ -71,6 +133,7 @@ final class NKMonitor: EventMonitor, Sendable { let headerFields = String(describing: response.response?.allHeaderFields ?? [:]) let date = Date().formatted(using: "yyyy-MM-dd' 'HH:mm:ss") + nkLog(debug: "User: \(account)") nkLog(debug: "Network response result: \(date) " + debugDesc) nkLog(debug: "Network response all headers: \(date) " + headerFields) From 8547363b5ba3d018aad27df9b9d3271f79b71a0a Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 29 Jan 2026 08:35:57 +0100 Subject: [PATCH 2/4] clean Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index c9bac0b4..fca7c751 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -60,8 +60,13 @@ final class NKMonitor: EventMonitor { } func requestDidResume(_ request: Request) { + guard let urlRequest = request.request else { + // URLRequest not created yet → skip logging + return + } let account = request.request? .allHTTPHeaderFields?[self.nkCommonInstance.headerAccount] ?? "unknown" + queue.async { switch NKLogFileManager.shared.logLevel { case .normal: From 01dbac88fda2564a8119b6b90509f240460c796f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 29 Jan 2026 08:41:52 +0100 Subject: [PATCH 3/4] info Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index fca7c751..c10b931f 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -130,7 +130,7 @@ final class NKMonitor: EventMonitor { let code = response.response?.statusCode { let responseStatus = (200..<300).contains(code) ? "RESPONSE: SUCCESS" : "RESPONSE: ERROR" - nkLog(network: "\(account) \(code) \(method) \(url) \(responseStatus)") + nkLog(network: "User: \(account) Code: \(code) Method: \(method) Url: \(url) - \(responseStatus)") } case .verbose: From 71ba3b3f818992ea37a62128d72614428584ea62 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 29 Jan 2026 08:48:22 +0100 Subject: [PATCH 4/4] clean Signed-off-by: Marino Faggiana --- Sources/NextcloudKit/NKMonitor.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/NextcloudKit/NKMonitor.swift b/Sources/NextcloudKit/NKMonitor.swift index c10b931f..3080d170 100644 --- a/Sources/NextcloudKit/NKMonitor.swift +++ b/Sources/NextcloudKit/NKMonitor.swift @@ -64,8 +64,7 @@ final class NKMonitor: EventMonitor { // URLRequest not created yet → skip logging return } - let account = request.request? - .allHTTPHeaderFields?[self.nkCommonInstance.headerAccount] ?? "unknown" + let account = urlRequest.allHTTPHeaderFields?[self.nkCommonInstance.headerAccount] ?? "unknown" queue.async { switch NKLogFileManager.shared.logLevel { @@ -74,8 +73,8 @@ final class NKMonitor: EventMonitor { nkLog(info: "User: \(account) - Request started: \(request)") case .verbose: // Full dump: headers + body - let headers = request.request?.allHTTPHeaderFields?.description ?? "None" - let body = request.request?.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "None" + let headers = urlRequest.allHTTPHeaderFields?.description ?? "None" + let body = urlRequest.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? "None" nkLog(debug: "User: \(account)") nkLog(debug: "Request started: \(request)") @@ -129,7 +128,7 @@ final class NKMonitor: EventMonitor { let url = request.request?.url?.absoluteString, let code = response.response?.statusCode { - let responseStatus = (200..<300).contains(code) ? "RESPONSE: SUCCESS" : "RESPONSE: ERROR" + let responseStatus = (200..<300).contains(code) ? "Response: SUCCESS" : "Response: ERROR" nkLog(network: "User: \(account) Code: \(code) Method: \(method) Url: \(url) - \(responseStatus)") }