mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-12-21 11:10:08 +01:00
✨ Check certificate expiry
This commit is contained in:
@@ -60,6 +60,10 @@
|
||||
036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */; };
|
||||
036C39142E5CB822008DAEDF /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39132E5CB820008DAEDF /* TestBundle.swift */; };
|
||||
036C3A212E5CBBAA008DAEDF /* ValetConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */; };
|
||||
0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
|
||||
0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
|
||||
0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
|
||||
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
|
||||
0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
|
||||
0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
|
||||
0396160F2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
|
||||
@@ -998,6 +1002,7 @@
|
||||
036C39092E5C8CBD008DAEDF /* PackagistP2Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistP2Response.swift; sourceTree = "<group>"; };
|
||||
036C390E2E5C8D3B008DAEDF /* PackagistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistError.swift; sourceTree = "<group>"; };
|
||||
036C39132E5CB820008DAEDF /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = "<group>"; };
|
||||
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
|
||||
0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggableEvent.swift; sourceTree = "<group>"; };
|
||||
039C29122E8AA15F007F5FAB /* ActiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveApi.swift; sourceTree = "<group>"; };
|
||||
039C29172E8AA311007F5FAB /* TestableApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApi.swift; sourceTree = "<group>"; };
|
||||
@@ -2193,6 +2198,7 @@
|
||||
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
|
||||
C41C02A827E61A65009F26CB /* FakeValetSite.swift */,
|
||||
C4BB39382981AFC700F8E797 /* PhpVersionSource.swift */,
|
||||
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */,
|
||||
);
|
||||
path = Sites;
|
||||
sourceTree = "<group>";
|
||||
@@ -2758,6 +2764,7 @@
|
||||
C456A0C62AA614BD0080144F /* PhpPreference.swift in Sources */,
|
||||
C4B585442770FE3900DA4FBE /* RealCommand.swift in Sources */,
|
||||
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */,
|
||||
0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */,
|
||||
C40C5C9C2846A40600E28255 /* Preset.swift in Sources */,
|
||||
C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */,
|
||||
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
|
||||
@@ -2926,6 +2933,7 @@
|
||||
C471E83928F9BB650021E251 /* ValetSite.swift in Sources */,
|
||||
C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */,
|
||||
C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */,
|
||||
0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */,
|
||||
033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
|
||||
C4E2E86928FC3002003B070C /* Utility.swift in Sources */,
|
||||
C471E83D28F9BB650021E251 /* FakeDomainScanner.swift in Sources */,
|
||||
@@ -3160,6 +3168,7 @@
|
||||
C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
|
||||
C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */,
|
||||
C471E8A628F9BB8F0021E251 /* App.swift in Sources */,
|
||||
0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */,
|
||||
C4513F912B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */,
|
||||
C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */,
|
||||
C45B914C295607F400F4EC78 /* Service.swift in Sources */,
|
||||
@@ -3413,6 +3422,7 @@
|
||||
C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */,
|
||||
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
|
||||
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
|
||||
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */,
|
||||
C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */,
|
||||
C463E381284930EE00422731 /* PresetHelper.swift in Sources */,
|
||||
C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
|
||||
|
||||
@@ -116,6 +116,11 @@
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "SKIP_UPDATE_CHECK"
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// CertificateValidator.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Assistant on 29/10/2025.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
/**
|
||||
A utility class for validating SSL certificates, including checking expiration dates.
|
||||
*/
|
||||
class CertificateValidator {
|
||||
|
||||
/// The dependency container for file system access
|
||||
private let container: Container
|
||||
|
||||
init(_ container: Container) {
|
||||
self.container = container
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a certificate file exists and returns its expiration date.
|
||||
- Parameter certificatePath: Path to the certificate file (supports ~ for home directory)
|
||||
- Returns: A tuple containing (exists: Bool, expirationDate: Date?)
|
||||
*/
|
||||
func validateCertificate(at certificatePath: String) -> (exists: Bool, expirationDate: Date?) {
|
||||
let exists = container.filesystem.fileExists(certificatePath)
|
||||
|
||||
guard exists else {
|
||||
return (exists: false, expirationDate: nil)
|
||||
}
|
||||
|
||||
let expirationDate = getCertificateExpirationDate(at: certificatePath)
|
||||
return (exists: true, expirationDate: expirationDate)
|
||||
}
|
||||
|
||||
/**
|
||||
Loads certificate data from a file path using the filesystem abstraction.
|
||||
- Parameter path: The file path to the certificate
|
||||
- Returns: Certificate data as CFData, or nil if loading fails
|
||||
*/
|
||||
private func loadCertificateData(from path: String) -> CFData? {
|
||||
do {
|
||||
let certificateString = try container.filesystem.getStringFromFile(path)
|
||||
|
||||
// Remove PEM headers and footers, and whitespace
|
||||
let cleanedCertificate = certificateString
|
||||
.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
|
||||
.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.replacingOccurrences(of: "\r", with: "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
guard let certificateData = Data(base64Encoded: cleanedCertificate) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return certificateData as CFData
|
||||
} catch {
|
||||
Log.err("Failed to read certificate file at \(path): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets detailed information about a certificate.
|
||||
- Parameter certificatePath: Path to the certificate file
|
||||
- Returns: A dictionary containing certificate details, or nil if the certificate couldn't be read
|
||||
*/
|
||||
func getCertificateInfo(at certificatePath: String) -> [String: Any]? {
|
||||
guard let certificateData = loadCertificateData(from: certificatePath),
|
||||
let certificate = SecCertificateCreateWithData(nil, certificateData) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let certDict = SecCertificateCopyValues(certificate, nil, nil) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var info: [String: Any] = [:]
|
||||
|
||||
// Extract common name
|
||||
if let subjectDict = certDict[kSecOIDX509V1SubjectName as String] as? [String: Any],
|
||||
let subjectArray = subjectDict[kSecPropertyKeyValue as String] as? [[String: Any]] {
|
||||
for item in subjectArray {
|
||||
if let label = item[kSecPropertyKeyLabel as String] as? String,
|
||||
label == "Common Name",
|
||||
let value = item[kSecPropertyKeyValue as String] as? String {
|
||||
info["commonName"] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract expiration date
|
||||
if let validityDict = certDict[kSecOIDX509V1ValidityNotAfter as String] as? [String: Any],
|
||||
let validityValue = validityDict[kSecPropertyKeyValue as String] as? NSNumber {
|
||||
let expirationDate = Date(timeIntervalSinceReferenceDate: validityValue.doubleValue)
|
||||
info["expirationDate"] = expirationDate
|
||||
}
|
||||
|
||||
// Extract issue date
|
||||
if let validityDict = certDict[kSecOIDX509V1ValidityNotBefore as String] as? [String: Any],
|
||||
let validityValue = validityDict[kSecPropertyKeyValue as String] as? NSNumber {
|
||||
let issueDate = Date(timeIntervalSinceReferenceDate: validityValue.doubleValue)
|
||||
info["issueDate"] = issueDate
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the expiration date of a certificate.
|
||||
- Parameter certificatePath: Path to the certificate file
|
||||
- Returns: The expiration date, or nil if the certificate couldn't be read
|
||||
*/
|
||||
func getCertificateExpirationDate(at certificatePath: String) -> Date? {
|
||||
guard let info = getCertificateInfo(at: certificatePath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return info["expirationDate"] as? Date
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ class ValetSite: ValetListable {
|
||||
/// Whether the site has been secured.
|
||||
var secured: Bool!
|
||||
|
||||
/// When the certificate expires.
|
||||
var expiryDate: Date?
|
||||
|
||||
/// What driver is currently in use. If not detected, defaults to nil.
|
||||
var driver: String?
|
||||
|
||||
@@ -122,13 +125,23 @@ class ValetSite: ValetListable {
|
||||
|
||||
/**
|
||||
Checks if a certificate file can be found in the `valet/Certificates` directory.
|
||||
- Note: The file is not validated, only its presence is checked.
|
||||
Also tracks the expiry date of the certificate if it exists.
|
||||
*/
|
||||
public func determineSecured() {
|
||||
secured = container.filesystem
|
||||
.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
|
||||
let certificatePath = "~/.config/valet/Certificates/\(self.name).\(self.tld).crt"
|
||||
|
||||
// TODO: Also verify the certificate hasn't expired
|
||||
let (exists, expiryDate) = CertificateValidator(container)
|
||||
.validateCertificate(at: certificatePath)
|
||||
|
||||
if exists, let expiryDate {
|
||||
Log.info("Certificate for \(self.name).\(self.tld) expires at: \(expiryDate).")
|
||||
} else {
|
||||
Log.info("No certificate for \(self.name).\(self.tld).")
|
||||
}
|
||||
|
||||
// Persist the information for the list
|
||||
self.secured = exists
|
||||
self.expiryDate = expiryDate
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user