Skip to content

Commit 4e0e778

Browse files
jfuginayclaude
andcommitted
Improve data package import for TAK server configs
- Add nested ZIP extraction for TAK data package format - Parse TAK preference.pref XML to extract server connection details - Extract certificate passwords from preferences before importing P12s - Reorder import flow: preferences → certificates → configs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 32c3779 commit 4e0e778

File tree

1 file changed

+115
-24
lines changed

1 file changed

+115
-24
lines changed

apps/omnitak/OmniTAKMobile/Features/DataPackages/Services/DataPackageImportManager.swift

Lines changed: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,25 @@ class DataPackageImportManager: ObservableObject {
3131

3232
// Extract zip file
3333
await statusCallback(.extracting)
34-
try await extractZipFile(from: url, to: tempDir)
34+
try extractZipFile(from: url, to: tempDir)
3535

3636
// Find and process contents
3737
let contents = try findPackageContents(in: tempDir)
3838

39-
// Import certificates
4039
var importedItems = 0
40+
41+
// Parse preferences FIRST to get passwords for certificate import
42+
await statusCallback(.configuring)
43+
for prefURL in contents.preferences {
44+
do {
45+
try await parsePreferences(from: prefURL)
46+
importedItems += 1
47+
} catch {
48+
print("⚠️ Failed to parse preferences: \(error.localizedDescription)")
49+
}
50+
}
51+
52+
// Import certificates (using passwords extracted from preferences)
4153
for certURL in contents.certificates {
4254
do {
4355
try await importCertificate(from: certURL)
@@ -47,8 +59,7 @@ class DataPackageImportManager: ObservableObject {
4759
}
4860
}
4961

50-
// Parse and apply server configurations
51-
await statusCallback(.configuring)
62+
// Parse and apply additional server configurations
5263
for configURL in contents.serverConfigs {
5364
do {
5465
try await parseServerConfig(from: configURL)
@@ -58,15 +69,6 @@ class DataPackageImportManager: ObservableObject {
5869
}
5970
}
6071

61-
// Parse preferences
62-
for prefURL in contents.preferences {
63-
do {
64-
try await parsePreferences(from: prefURL)
65-
} catch {
66-
print("⚠️ Failed to parse preferences: \(error.localizedDescription)")
67-
}
68-
}
69-
7072
// Record import
7173
let record = ImportRecord(
7274
packageName: url.deletingPathExtension().lastPathComponent,
@@ -83,7 +85,7 @@ class DataPackageImportManager: ObservableObject {
8385

8486
// MARK: - Extract ZIP
8587

86-
private func extractZipFile(from sourceURL: URL, to destinationURL: URL) async throws {
88+
private func extractZipFile(from sourceURL: URL, to destinationURL: URL) throws {
8789
// Read the ZIP data
8890
let zipData = try Data(contentsOf: sourceURL)
8991

@@ -117,6 +119,9 @@ class DataPackageImportManager: ObservableObject {
117119
var serverConfigs: [URL] = []
118120
var preferences: [URL] = []
119121

122+
// First, extract any nested zip files (TAK data packages often have nested zips)
123+
try extractNestedZips(in: directory)
124+
120125
let enumerator = fileManager.enumerator(at: directory, includingPropertiesForKeys: [.isRegularFileKey])
121126

122127
while let fileURL = enumerator?.nextObject() as? URL {
@@ -127,14 +132,14 @@ class DataPackageImportManager: ObservableObject {
127132
if ext == "p12" || ext == "pfx" || ext == "pem" || ext == "crt" || ext == "cer" {
128133
certificates.append(fileURL)
129134
}
130-
// Server config files
131-
else if ext == "xml" || ext == "json" || filename.contains("server") || filename.contains("connection") {
132-
serverConfigs.append(fileURL)
133-
}
134-
// Preference files
135-
else if filename.contains("pref") || filename.contains("config") {
135+
// Preference files (check first - .pref files contain server config)
136+
else if ext == "pref" || filename.contains("preference") {
136137
preferences.append(fileURL)
137138
}
139+
// Server config files (but not manifest.xml)
140+
else if (ext == "xml" && !filename.contains("manifest")) || ext == "json" || filename.contains("server") || filename.contains("connection") {
141+
serverConfigs.append(fileURL)
142+
}
138143
}
139144

140145
return PackageContents(
@@ -144,6 +149,29 @@ class DataPackageImportManager: ObservableObject {
144149
)
145150
}
146151

152+
// MARK: - Extract Nested Zips
153+
154+
private func extractNestedZips(in directory: URL) throws {
155+
let enumerator = fileManager.enumerator(at: directory, includingPropertiesForKeys: [.isRegularFileKey])
156+
157+
while let fileURL = enumerator?.nextObject() as? URL {
158+
if fileURL.pathExtension.lowercased() == "zip" {
159+
let nestedDir = fileURL.deletingPathExtension()
160+
try fileManager.createDirectory(at: nestedDir, withIntermediateDirectories: true)
161+
162+
do {
163+
try extractZipFile(from: fileURL, to: nestedDir)
164+
print("📦 Extracted nested zip: \(fileURL.lastPathComponent)")
165+
166+
// Recursively extract any further nested zips
167+
try extractNestedZips(in: nestedDir)
168+
} catch {
169+
print("⚠️ Failed to extract nested zip: \(error.localizedDescription)")
170+
}
171+
}
172+
}
173+
}
174+
147175
// MARK: - Import Certificate
148176

149177
private func importCertificate(from url: URL) async throws {
@@ -154,8 +182,19 @@ class DataPackageImportManager: ObservableObject {
154182
let ext = url.pathExtension.lowercased()
155183

156184
if ext == "p12" || ext == "pfx" {
157-
// P12/PFX file - try common passwords or prompt user
158-
let passwords = ["atakatak", ""] // Common TAK server passwords
185+
// P12/PFX file - try passwords from preferences first, then common defaults
186+
var passwords = ["atakatak", ""] // Common TAK server passwords
187+
188+
// Check if we have passwords from the preference file
189+
if filename.lowercased().contains("truststore") || filename.lowercased().contains("ca") {
190+
if let caPassword = UserDefaults.standard.string(forKey: "lastImportCAPassword") {
191+
passwords.insert(caPassword, at: 0)
192+
}
193+
} else {
194+
if let clientPassword = UserDefaults.standard.string(forKey: "lastImportClientPassword") {
195+
passwords.insert(clientPassword, at: 0)
196+
}
197+
}
159198

160199
for password in passwords {
161200
do {
@@ -322,9 +361,61 @@ class DataPackageImportManager: ObservableObject {
322361
// MARK: - Parse Preferences
323362

324363
private func parsePreferences(from url: URL) async throws {
325-
// Parse TAK preference files
326-
// This would apply app-wide settings from the package
327364
print("ℹ️ Parsing preferences from: \(url.lastPathComponent)")
365+
366+
let data = try Data(contentsOf: url)
367+
guard let xmlString = String(data: data, encoding: .utf8) else {
368+
throw ImportError.configParsingFailed("Invalid XML encoding")
369+
}
370+
371+
// Parse TAK preference.pref format
372+
// Look for connectString entries like: "public.opentakserver.io:8089:ssl"
373+
if let connectString = extractPreferenceEntry(from: xmlString, key: "connectString0") {
374+
let components = connectString.split(separator: ":")
375+
if components.count >= 2 {
376+
let host = String(components[0])
377+
let port = UInt16(components[1]) ?? 8089
378+
let useTLS = components.count >= 3 && components[2] == "ssl"
379+
380+
// Get server description if available
381+
let description = extractPreferenceEntry(from: xmlString, key: "description0") ?? "Imported Server"
382+
383+
// Get certificate passwords
384+
let clientPassword = extractPreferenceEntry(from: xmlString, key: "clientPassword") ?? "atakatak"
385+
let caPassword = extractPreferenceEntry(from: xmlString, key: "caPassword") ?? "atakatak"
386+
387+
// Store passwords for certificate import
388+
UserDefaults.standard.set(clientPassword, forKey: "lastImportClientPassword")
389+
UserDefaults.standard.set(caPassword, forKey: "lastImportCAPassword")
390+
391+
let server = TAKServer(
392+
name: description,
393+
host: host,
394+
port: port,
395+
protocolType: useTLS ? "ssl" : "tcp",
396+
useTLS: useTLS,
397+
isDefault: false,
398+
certificateName: "administrator"
399+
)
400+
401+
serverManager.addServer(server)
402+
print("✅ Imported server from preferences: \(description) (\(host):\(port), TLS: \(useTLS))")
403+
}
404+
}
405+
}
406+
407+
private func extractPreferenceEntry(from xml: String, key: String) -> String? {
408+
// Match TAK preference format: <entry key="keyName" class="...">value</entry>
409+
let pattern = "key=\"\(key)\"[^>]*>([^<]*)</entry>"
410+
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
411+
412+
let range = NSRange(xml.startIndex..., in: xml)
413+
guard let match = regex.firstMatch(in: xml, range: range),
414+
let valueRange = Range(match.range(at: 1), in: xml) else {
415+
return nil
416+
}
417+
418+
return String(xml[valueRange])
328419
}
329420
}
330421

0 commit comments

Comments
 (0)