@@ -130,6 +130,8 @@ final class LocalStaticHTTPServer {
130130 private var rootDirectory : URL ?
131131 private let queue = DispatchQueue ( label: " LocalStaticHTTPServer.queue " )
132132 private var serverStarted = false
133+ private var activeConnections : Set < NWConnection > = [ ]
134+ private let connectionsLock = NSLock ( )
133135
134136 // Start HTTP (or HTTPS if tlsIdentity is provided) on given port. Serves static files from rootDir.
135137 func start( host: NWEndpoint . Host = . ipv4( IPv4Address ( " 127.0.0.1 " ) !) ,
@@ -140,6 +142,11 @@ final class LocalStaticHTTPServer {
140142 InstallLogger . shared. log ( " Starting HTTP server with port: \( port) , tlsIdentity: \( tlsIdentity != nil ? " present " : " nil " ) " )
141143 self . rootDirectory = rootDir
142144 self . serverStarted = false
145+
146+ // Clear any existing connections
147+ connectionsLock. lock ( )
148+ activeConnections. removeAll ( )
149+ connectionsLock. unlock ( )
143150
144151 // Create TCP params and attach TLS options if identity provided
145152 let tcpOptions = NWProtocolTCP . Options ( )
@@ -186,7 +193,6 @@ final class LocalStaticHTTPServer {
186193 listener. newConnectionHandler = { [ weak self] connection in
187194 guard let self = self else { return }
188195 InstallLogger . shared. logDebug ( " New connection received " )
189- connection. start ( queue: self . queue)
190196 self . handleConnection ( connection)
191197 }
192198
@@ -251,13 +257,25 @@ final class LocalStaticHTTPServer {
251257
252258 func stop( ) {
253259 InstallLogger . shared. log ( " Stopping HTTP server " )
260+
261+ connectionsLock. lock ( )
262+ for connection in activeConnections {
263+ connection. cancel ( )
264+ }
265+ activeConnections. removeAll ( )
266+ connectionsLock. unlock ( )
267+
254268 listener? . cancel ( )
255269 listener = nil
256270 serverStarted = false
257271 }
258272
259273 // Very minimal GET-only static file handler.
260274 private func handleConnection( _ connection: NWConnection ) {
275+ connectionsLock. lock ( )
276+ activeConnections. insert ( connection)
277+ connectionsLock. unlock ( )
278+
261279 var received = Data ( )
262280
263281 func receiveMore( ) {
@@ -266,7 +284,6 @@ final class LocalStaticHTTPServer {
266284 received. append ( data)
267285 // If header end reached
268286 if let range = received. range ( of: Data ( " \r \n \r \n " . utf8) ) {
269- // fixed: use half-open Range 0..<range.upperBound
270287 self . processHTTPRequest ( connection: connection, headerData: received. subdata ( in: 0 ..< range. upperBound) )
271288 return
272289 }
@@ -280,14 +297,14 @@ final class LocalStaticHTTPServer {
280297 }
281298 }
282299
300+ connection. start ( queue: queue)
283301 receiveMore ( )
284302 }
285303
286304 private func processHTTPRequest( connection: NWConnection , headerData: Data ) {
287- defer { connection. cancel ( ) }
288-
289305 guard let header = String ( data: headerData, encoding: . utf8) else {
290306 InstallLogger . shared. logDebug ( " Failed to decode HTTP header " )
307+ self . removeConnection ( connection)
291308 return
292309 }
293310
@@ -298,9 +315,15 @@ final class LocalStaticHTTPServer {
298315 }
299316
300317 // parse the request line
301- guard let requestLine = lines. first else { return }
318+ guard let requestLine = lines. first else {
319+ self . removeConnection ( connection)
320+ return
321+ }
302322 let comps = requestLine. components ( separatedBy: " " )
303- guard comps. count >= 2 else { return }
323+ guard comps. count >= 2 else {
324+ self . removeConnection ( connection)
325+ return
326+ }
304327 let method = comps [ 0 ]
305328 var path = comps [ 1 ]
306329 // strip query
@@ -350,13 +373,26 @@ final class LocalStaticHTTPServer {
350373 connection. send ( content: headerDataToSend, completion: . contentProcessed( { _ in
351374 connection. send ( content: data, completion: . contentProcessed( { _ in
352375 InstallLogger . shared. logDebug ( " File sent successfully: \( path) " )
376+ // Don't cancel immediately, let the client close
377+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) {
378+ self . removeConnection ( connection)
379+ }
353380 } ) )
354381 } ) )
355382 } catch {
356383 InstallLogger . shared. logError ( " Read error for file \( path) : \( error. localizedDescription) " )
357384 sendSimpleResponse ( connection: connection, status: 500 , text: " Read error: \( error. localizedDescription) " )
358385 }
359386 }
387+
388+ private func removeConnection( _ connection: NWConnection ) {
389+ connectionsLock. lock ( )
390+ if activeConnections. contains ( connection) {
391+ connection. cancel ( )
392+ activeConnections. remove ( connection)
393+ }
394+ connectionsLock. unlock ( )
395+ }
360396
361397 private func sendSimpleResponse( connection: NWConnection , status: Int , text: String ) {
362398 let body = text + " \n "
@@ -368,6 +404,9 @@ final class LocalStaticHTTPServer {
368404 body ) . utf8)
369405 connection. send ( content: combined, completion: . contentProcessed( { _ in
370406 InstallLogger . shared. logDebug ( " Sent response: \( status) \( self . httpStatusText ( status) ) " )
407+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) {
408+ self . removeConnection ( connection)
409+ }
371410 } ) )
372411 }
373412
@@ -639,45 +678,78 @@ public func installApp(from ipaURL: URL) throws {
639678 var tlsIdentity : sec_identity_t ? = nil
640679 let p12URL = sslDir. appendingPathComponent ( " localhost.p12 " )
641680
681+ // Function to convert OSStatus to readable string
682+ func securityErrorToString( _ status: OSStatus ) -> String {
683+ if let message = SecCopyErrorMessageString ( status, nil ) {
684+ return message as String
685+ }
686+ return " Unknown error: \( status) "
687+ }
688+
642689 if fm. fileExists ( atPath: p12URL. path) {
643690 InstallLogger . shared. log ( " Found PKCS12 file at \( p12URL. path) " )
644- if let pData = try ? Data ( contentsOf: p12URL) {
691+ do {
692+ let pData = try Data ( contentsOf: p12URL)
645693 InstallLogger . shared. log ( " PKCS12 file size: \( pData. count) bytes " )
646- // PKCS#12 has no password; pass empty string
694+
695+ // Try with empty password first
647696 let options : CFDictionary = [ kSecImportExportPassphrase as String : " " ] as CFDictionary
648697 var items : CFArray ? = nil
649698 let status = SecPKCS12Import ( pData as CFData , options, & items)
650- InstallLogger . shared. log ( " SecPKCS12Import status: \( status) " )
651-
699+ InstallLogger . shared. log ( " SecPKCS12Import status: \( status) - \( securityErrorToString ( status ) ) " )
700+
652701 if status == errSecSuccess,
653702 let arr = items as? [ [ String : Any ] ] ,
654703 let first = arr. first {
655704 InstallLogger . shared. log ( " PKCS12 import successful, items count: \( arr. count) " )
656705
657706 // The import dictionary values are Any; safely cast to SecIdentity
658707 if let identityAny = first [ kSecImportItemIdentity as String ] {
659- do {
660- let identityRef = identityAny as! SecIdentity
661- InstallLogger . shared. log ( " SecIdentity cast successful " )
662- // Convert to sec_identity_t for sec_protocol_options_set_local_identity()
663- if let secId = sec_identity_create ( identityRef) {
664- tlsIdentity = secId
665- InstallLogger . shared. logSuccess ( " TLS identity created successfully " )
666- } else {
667- InstallLogger . shared. logWarning ( " sec_identity_create failed; falling back to HTTP " )
668- }
669- } catch {
670- InstallLogger . shared. logError ( " Failed to cast to SecIdentity: \( error) " )
708+ let identityRef = identityAny as! SecIdentity
709+ InstallLogger . shared. log ( " SecIdentity cast successful " )
710+ // Convert to sec_identity_t for sec_protocol_options_set_local_identity()
711+ if let secId = sec_identity_create ( identityRef) {
712+ tlsIdentity = secId
713+ InstallLogger . shared. logSuccess ( " TLS identity created successfully " )
714+ } else {
715+ InstallLogger . shared. logWarning ( " sec_identity_create failed; falling back to HTTP " )
671716 }
672717 } else {
673718 // No identity entry in the import result
674719 InstallLogger . shared. logWarning ( " PKCS#12 import produced no SecIdentity. Will use HTTP only. " )
675720 }
676721 } else {
677- InstallLogger . shared. logWarning ( " PKCS12 import failed (status \( status) ). Will use HTTP only. " )
722+ // Try with common passwords if empty password fails
723+ InstallLogger . shared. log ( " Trying common passwords for PKCS12 file... " )
724+ let commonPasswords = [ " password " , " 123456 " , " admin " , " localhost " , " ssl " , " cert " , " prostore " ]
725+ var foundPassword = false
726+
727+ for password in commonPasswords {
728+ let optionsWithPassword : CFDictionary = [ kSecImportExportPassphrase as String : password] as CFDictionary
729+ var passwordItems : CFArray ? = nil
730+ let passwordStatus = SecPKCS12Import ( pData as CFData , optionsWithPassword, & passwordItems)
731+
732+ if passwordStatus == errSecSuccess,
733+ let arr = passwordItems as? [ [ String : Any ] ] ,
734+ let first = arr. first,
735+ let identityAny = first [ kSecImportItemIdentity as String ] {
736+
737+ let identityRef = identityAny as! SecIdentity
738+ if let secId = sec_identity_create ( identityRef) {
739+ tlsIdentity = secId
740+ InstallLogger . shared. logSuccess ( " TLS identity created successfully with password: ' \( password) ' " )
741+ foundPassword = true
742+ break
743+ }
744+ }
745+ }
746+
747+ if !foundPassword {
748+ InstallLogger . shared. logWarning ( " PKCS12 import failed even with common passwords. Will use HTTP only. " )
749+ }
678750 }
679- } else {
680- InstallLogger . shared. logWarning ( " Failed to read PKCS#12 file at \( p12URL . path ) ; using HTTP only " )
751+ } catch {
752+ InstallLogger . shared. logError ( " Failed to read PKCS#12 file: \( error ) " )
681753 }
682754 } else {
683755 InstallLogger . shared. log ( " No PKCS#12 found at \( p12URL. path) ; using HTTP only " )
@@ -691,6 +763,10 @@ public func installApp(from ipaURL: URL) throws {
691763 do {
692764 startedPort = try LocalStaticHTTPServer . shared. start ( port: chosenPort, rootDir: workDir, tlsIdentity: tlsIdentity)
693765 InstallLogger . shared. logSuccess ( " Server started on port \( startedPort) " )
766+
767+ // Give the server a moment to stabilize
768+ InstallLogger . shared. log ( " Waiting for server to stabilize... " )
769+ Thread . sleep ( forTimeInterval: 0.5 )
694770 } catch {
695771 InstallLogger . shared. logError ( " Failed to start local server: \( error) " )
696772 throw InstallAppError . serverStartFailed ( " Failed to start local server: \( error) " )
0 commit comments