|
5 | 5 | @_exported import AndroidLooper
|
6 | 6 | @_exported import AndroidChoreographer
|
7 | 7 | @_exported import AndroidContext
|
| 8 | + |
| 9 | +#if canImport(Android) |
| 10 | +import Android |
| 11 | +import Foundation |
| 12 | + |
| 13 | +/// Utilities for setting up Android compatibility with Foundation |
| 14 | +public class AndroidBootstrap { |
| 15 | + /// Collects all the certificate files from the Android certificate store and writes them to a single `cacerts.pem` file that can be used by libcurl, |
| 16 | + /// which is communicated through the `URLSessionCertificateAuthorityInfoFile` environment property |
| 17 | + /// |
| 18 | + /// See https://android.googlesource.com/platform/frameworks/base/+/8b192b19f264a8829eac2cfaf0b73f6fc188d933%5E%21/#F0 |
| 19 | + /// See https://github.com/apple/swift-nio-ssl/blob/d1088ebe0789d9eea231b40741831f37ab654b61/Sources/NIOSSL/AndroidCABundle.swift#L30 |
| 20 | + @available(macOS 13.0, iOS 16.0, *) |
| 21 | + public static func setupCACerts(force: Bool = false, fromCertficateFolders certsFolders: [String] = ["/system/etc/security/cacerts", "/apex/com.android.conscrypt/cacerts"]) throws { |
| 22 | + // if someone else has already set URLSessionCertificateAuthorityInfoFile then do not override unless forced |
| 23 | + if !force && getenv("URLSessionCertificateAuthorityInfoFile") != nil { |
| 24 | + return |
| 25 | + } |
| 26 | + |
| 27 | + //let cacheFolder = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // file:////.cache/ (unwritable) |
| 28 | + let cacheFolder = URL.temporaryDirectory |
| 29 | + //logger.debug("setupCACerts: \(cacheFolder)") |
| 30 | + let generatedCacertsURL = cacheFolder.appendingPathComponent("cacerts-aggregate.pem") |
| 31 | + //logger.debug("setupCACerts: generatedCacertsURL=\(generatedCacertsURL)") |
| 32 | + |
| 33 | + let contents = try FileManager.default.contentsOfDirectory(at: cacheFolder, includingPropertiesForKeys: nil) |
| 34 | + //logger.debug("setupCACerts: cacheFolder=\(cacheFolder) contents=\(contents)") |
| 35 | + |
| 36 | + // clear any previous generated certificates file that may have been created by this app |
| 37 | + if FileManager.default.fileExists(atPath: generatedCacertsURL.path) { |
| 38 | + try FileManager.default.removeItem(atPath: generatedCacertsURL.path) |
| 39 | + } |
| 40 | + |
| 41 | + let created = FileManager.default.createFile(atPath: generatedCacertsURL.path, contents: nil) |
| 42 | + //logger.debug("setupCACerts: created file: \(created): \(generatedCacertsURL.path)") |
| 43 | + |
| 44 | + let fs = try FileHandle(forWritingTo: generatedCacertsURL) |
| 45 | + defer { try? fs.close() } |
| 46 | + |
| 47 | + // write a header |
| 48 | + fs.write(""" |
| 49 | + ## Bundle of CA Root Certificates |
| 50 | + ## Auto-generated on \(Date()) |
| 51 | + ## by aggregating certificates from: \(certsFolders) |
| 52 | +
|
| 53 | + """.data(using: .utf8)!) |
| 54 | + |
| 55 | + // Go through each folder and load each certificate file (ending with ".0"), |
| 56 | + // and smash them together into a single aggreagate file tha curl can load. |
| 57 | + // The .0 files will contain some extra metadata, but libcurl only cares about the |
| 58 | + // -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- sections, |
| 59 | + // so we can naïvely concatenate them all and libcurl will understand the bundle. |
| 60 | + for certsFolder in certsFolders { |
| 61 | + let certsFolderURL = URL(fileURLWithPath: certsFolder) |
| 62 | + if (try? certsFolderURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) != true { continue } |
| 63 | + let certURLs = try FileManager.default.contentsOfDirectory(at: certsFolderURL, includingPropertiesForKeys: [.isRegularFileKey, .isReadableKey]) |
| 64 | + for certURL in certURLs { |
| 65 | + //logger.debug("setupCACerts: certURL=\(certURL)") |
| 66 | + // certificate files have names like "53a1b57a.0" |
| 67 | + if certURL.pathExtension != "0" { continue } |
| 68 | + do { |
| 69 | + if try certURL.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile == false { continue } |
| 70 | + if try certURL.resourceValues(forKeys: [.isReadableKey]).isReadable == false { continue } |
| 71 | + try fs.write(contentsOf: try Data(contentsOf: certURL)) |
| 72 | + } catch { |
| 73 | + //logger.warning("setupCACerts: error reading certificate file \(certURL.path): \(error)") |
| 74 | + continue |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + |
| 80 | + //setenv("URLSessionCertificateAuthorityInfoFile", "INSECURE_SSL_NO_VERIFY", 1) // disables all certificate verification |
| 81 | + //setenv("URLSessionCertificateAuthorityInfoFile", "/system/etc/security/cacerts/", 1) // doesn't work for directories |
| 82 | + setenv("URLSessionCertificateAuthorityInfoFile", generatedCacertsURL.path, 1) |
| 83 | + //logger.debug("setupCACerts: set URLSessionCertificateAuthorityInfoFile=\(generatedCacertsURL.path)") |
| 84 | + } |
| 85 | +} |
| 86 | +#endif |
| 87 | + |
0 commit comments