Skip to content

Commit

Permalink
Fix log redaction loading issue
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Nov 25, 2024
1 parent b77e3fa commit 5e62584
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 93 deletions.
16 changes: 16 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@
F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; };
F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; };
F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01DAE322C2B032A00521E46 /* RelaySelection.swift */; };
F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; };
F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; };
F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */; };
Expand Down Expand Up @@ -948,6 +949,8 @@
F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; };
F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; };
F0A1638A2C47B77300592300 /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */; };
F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; };
F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
F0ACE30D2BE4E478006D5333 /* MullvadMockData.h in Headers */ = {isa = PBXBuildFile; fileRef = F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */; settings = {ATTRIBUTES = (Public, ); }; };
F0ACE3102BE4E478006D5333 /* MullvadMockData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; };
F0ACE3112BE4E478006D5333 /* MullvadMockData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -2170,6 +2173,7 @@
F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = "<group>"; };
F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = "<group>"; };
F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = "<group>"; };
F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = "<group>"; };
F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadMockData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadMockData.h; sourceTree = "<group>"; };
F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2429,6 +2433,7 @@
440E9EF02BDA93CB00B1FD11 /* MullvadVPN */ = {
isa = PBXGroup;
children = (
F0A7EBB02CEF6C5F005BB671 /* Log */,
440E9EF62BDA957300B1FD11 /* Classes */,
440E9F002BDA997C00B1FD11 /* Extensions */,
440E9EFB2BDA97C600B1FD11 /* GeneralAPIs */,
Expand Down Expand Up @@ -4211,6 +4216,14 @@
path = GeneralAPIs;
sourceTree = "<group>";
};
F0A7EBB02CEF6C5F005BB671 /* Log */ = {
isa = PBXGroup;
children = (
F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */,
);
path = Log;
sourceTree = "<group>";
};
F0ACE3092BE4E478006D5333 /* MullvadMockData */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5388,6 +5401,7 @@
A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */,
58B07C182AEFDD6C00A09625 /* StoreTransactionLog.swift in Sources */,
A9A5FA382ACB05600083449F /* InputTextFormatter.swift in Sources */,
F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */,
A9A5FA372ACB052D0083449F /* ApplicationTarget.swift in Sources */,
A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */,
A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */,
Expand Down Expand Up @@ -5498,6 +5512,7 @@
A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */,
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */,
A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */,
F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */,
A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */,
7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */,
A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */,
Expand Down Expand Up @@ -5525,6 +5540,7 @@
A9A5FA342ACB05160083449F /* StringTests.swift in Sources */,
7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */,
A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */,
F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */,
7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */,
A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */,
);
Expand Down
86 changes: 52 additions & 34 deletions ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private let kRedactedContainerPlaceholder = "[REDACTED CONTAINER PATH]"

class ConsolidatedApplicationLog: TextOutputStreamable {
typealias Metadata = KeyValuePairs<MetadataKey, String>
private let bufferSize: UInt64 = 131_072

enum MetadataKey: String {
case id, os
Expand All @@ -30,6 +31,10 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
let applicationGroupContainers: [URL]
let metadata: Metadata

private let logQueue = DispatchQueue(
label: "com.mullvad.consolidation.logs.queue",
attributes: .concurrent
)
private var logs: [LogAttachment] = []

init(
Expand All @@ -46,37 +51,50 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
}
}

func addLogFiles(fileURLs: [URL]) {
for fileURL in fileURLs {
addSingleLogFile(fileURL)
func addLogFiles(fileURLs: [URL], completion: @escaping () -> Void = {}) {
logQueue.async(flags: .barrier) {
for fileURL in fileURLs {
self.addSingleLogFile(fileURL)
}
completion()
}
}

func addError(message: String, error: String) {
func addError(message: String, error: String, completion: (() -> Void)? = nil) {
let redactedError = redact(string: error)

logs.append(LogAttachment(label: message, content: redactedError))
logQueue.async(flags: .barrier) {
self.logs.append(LogAttachment(label: message, content: redactedError))
completion?()
}
}

var string: String {
var body = ""
write(to: &body)
return body
var result = ""
logQueue.sync {
var body = ""
self.write(to: &body)
result = body
}
return result
}

func write(to stream: inout some TextOutputStream) {
print("System information:", to: &stream)
for (key, value) in metadata {
print("\(key.rawValue): \(value)", to: &stream)
}
print("", to: &stream)

for attachment in logs {
print(kLogDelimiter, to: &stream)
print(attachment.label, to: &stream)
print(kLogDelimiter, to: &stream)
print(attachment.content, to: &stream)
print("", to: &stream)
logQueue.sync {
var localOutput = ""

localOutput += "System information:\n"
for (key, value) in metadata {
localOutput += "\(key.rawValue): \(value)\n"
}
localOutput += "\n"
for attachment in logs {
localOutput += "\(kLogDelimiter)\n"
localOutput += "\(attachment.label)\n"
localOutput += "\(kLogDelimiter)\n"
localOutput += "\(attachment.content)\n\n"
}

stream.write(localOutput)
}
}

Expand All @@ -92,10 +110,11 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
let path = fileURL.path
let redactedPath = redact(string: path)

if let lossyString = Self.readFileLossy(path: path, maxBytes: ApplicationConfiguration.logMaximumFileSize) {
if let lossyString = readFileLossy(path: path, maxBytes: bufferSize) {
let redactedString = redact(string: lossyString)

logs.append(LogAttachment(label: redactedPath, content: redactedString))
logQueue.async(flags: .barrier) {
self.logs.append(LogAttachment(label: redactedPath, content: redactedString))
}
} else {
addError(message: redactedPath, error: "Log file does not exist: \(path).")
}
Expand All @@ -113,7 +132,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
]
}

private static func readFileLossy(path: String, maxBytes: UInt64) -> String? {
private func readFileLossy(path: String, maxBytes: UInt64) -> String? {
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
return nil
}
Expand All @@ -125,12 +144,11 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
fileHandle.seek(toFileOffset: 0)
}

let data = fileHandle.readData(ofLength: Int(ApplicationConfiguration.logMaximumFileSize))
let data = fileHandle.readData(ofLength: Int(bufferSize))
let replacementCharacter = Character(UTF8.decode(UTF8.encodedReplacementCharacter))
let lossyString = String(
String(decoding: data, as: UTF8.self)
.drop { ch in
// Drop leading replacement characters produced when decoding data
ch == replacementCharacter
}
)
Expand All @@ -147,9 +165,9 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
private func redact(string: String) -> String {
[
redactContainerPaths,
Self.redactAccountNumber,
Self.redactIPv4Address,
Self.redactIPv6Address,
redactAccountNumber,
redactIPv4Address,
redactIPv6Address,
redactCustomStrings,
].reduce(string) { resultString, transform -> String in
transform(resultString)
Expand All @@ -165,7 +183,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
}
}

private static func redactAccountNumber(string: String) -> String {
private func redactAccountNumber(string: String) -> String {
redact(
// swiftlint:disable:next force_try
regularExpression: try! NSRegularExpression(pattern: #"\d{16}"#),
Expand All @@ -174,23 +192,23 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
)
}

private static func redactIPv4Address(string: String) -> String {
private func redactIPv4Address(string: String) -> String {
redact(
regularExpression: NSRegularExpression.ipv4RegularExpression,
string: string,
replacementString: kRedactedPlaceholder
)
}

private static func redactIPv6Address(string: String) -> String {
private func redactIPv6Address(string: String) -> String {
redact(
regularExpression: NSRegularExpression.ipv6RegularExpression,
string: string,
replacementString: kRedactedPlaceholder
)
}

private static func redact(
private func redact(
regularExpression: NSRegularExpression,
string: String,
replacementString: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,73 @@ import Operations
final class ProblemReportInteractor {
private let apiProxy: APIQuerying
private let tunnelManager: TunnelManager
private let consolidatedLog: ConsolidatedApplicationLog

private lazy var consolidatedLog: ConsolidatedApplicationLog = {
let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier
let redactStrings = [tunnelManager.deviceState.accountData?.number].compactMap { $0 }

let report = ConsolidatedApplicationLog(
redactCustomStrings: redactStrings,
redactContainerPathsForSecurityGroupIdentifiers: [securityGroupIdentifier]
init(apiProxy: APIQuerying, tunnelManager: TunnelManager) {
self.apiProxy = apiProxy
self.tunnelManager = tunnelManager
self.consolidatedLog = ConsolidatedApplicationLog(
redactCustomStrings: [tunnelManager.deviceState.accountData?.number].compactMap { $0 },
redactContainerPathsForSecurityGroupIdentifiers: [ApplicationConfiguration.securityGroupIdentifier]
)
}

let logFileURLs = ApplicationTarget.allCases.flatMap {
ApplicationConfiguration.logFileURLs(for: $0, in: ApplicationConfiguration.containerURL)
}
report.addLogFiles(fileURLs: logFileURLs)

return report
}()

var reportString: String {
consolidatedLog.string
func loadLogFiles(completion: @escaping () -> Void) {
DispatchQueue
.global()
.async { [weak self] in
guard let self else { return }
let logFileURLs = ApplicationTarget.allCases.flatMap {
ApplicationConfiguration.logFileURLs(for: $0, in: ApplicationConfiguration.containerURL)
}
consolidatedLog.addLogFiles(fileURLs: logFileURLs) {
DispatchQueue.main.async {
completion()
}
}
}
}

init(apiProxy: APIQuerying, tunnelManager: TunnelManager) {
self.apiProxy = apiProxy
self.tunnelManager = tunnelManager
func fetchReportString(completion: @escaping (String) -> Void) {
DispatchQueue
.global()
.async { [weak self] in
guard let self else { return }
let result = self.consolidatedLog.string
DispatchQueue.main.async {
completion(result)
}
}
}

func sendReport(
email: String,
message: String,
completion: @escaping (Result<Void, Error>) -> Void
) -> Cancellable {
let request = REST.ProblemReportRequest(
address: email,
message: message,
log: consolidatedLog.string,
metadata: consolidatedLog.metadata.reduce(into: [:]) { output, entry in
output[entry.key.rawValue] = entry.value
}
)
) {
DispatchQueue
.global()
.async { [weak self] in
guard let self else { return }
let logString = self.consolidatedLog.string
let metadataDict = self.consolidatedLog.metadata.reduce(into: [:]) { output, entry in
output[entry.key.rawValue] = entry.value
}
let request = REST.ProblemReportRequest(
address: email,
message: message,
log: logString,
metadata: metadataDict
)

return apiProxy.sendProblemReport(
request,
retryStrategy: .default,
completionHandler: completion
)
_ = self.apiProxy.sendProblemReport(
request,
retryStrategy: .default
) { result in
DispatchQueue.main.async {
completion(result)
}
}
}
}
}
Loading

0 comments on commit 5e62584

Please sign in to comment.