1
0
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:
2025-10-29 13:34:39 +01:00
parent 16522ddc60
commit 46766de1a6
4 changed files with 159 additions and 4 deletions

View File

@@ -60,6 +60,10 @@
036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */; }; 036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */; };
036C39142E5CB822008DAEDF /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39132E5CB820008DAEDF /* TestBundle.swift */; }; 036C39142E5CB822008DAEDF /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39132E5CB820008DAEDF /* TestBundle.swift */; };
036C3A212E5CBBAA008DAEDF /* ValetConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.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 */; }; 0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
0396160E2E74A61E002DD7F6 /* 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 039C29172E8AA311007F5FAB /* TestableApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApi.swift; sourceTree = "<group>"; };
@@ -2193,6 +2198,7 @@
C4E4404527C56F4700D225E1 /* ValetSite.swift */, C4E4404527C56F4700D225E1 /* ValetSite.swift */,
C41C02A827E61A65009F26CB /* FakeValetSite.swift */, C41C02A827E61A65009F26CB /* FakeValetSite.swift */,
C4BB39382981AFC700F8E797 /* PhpVersionSource.swift */, C4BB39382981AFC700F8E797 /* PhpVersionSource.swift */,
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */,
); );
path = Sites; path = Sites;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2758,6 +2764,7 @@
C456A0C62AA614BD0080144F /* PhpPreference.swift in Sources */, C456A0C62AA614BD0080144F /* PhpPreference.swift in Sources */,
C4B585442770FE3900DA4FBE /* RealCommand.swift in Sources */, C4B585442770FE3900DA4FBE /* RealCommand.swift in Sources */,
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */, C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */,
0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C40C5C9C2846A40600E28255 /* Preset.swift in Sources */, C40C5C9C2846A40600E28255 /* Preset.swift in Sources */,
C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */, C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
@@ -2926,6 +2933,7 @@
C471E83928F9BB650021E251 /* ValetSite.swift in Sources */, C471E83928F9BB650021E251 /* ValetSite.swift in Sources */,
C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */, C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */,
C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */, C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */,
0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */,
033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */, 033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C4E2E86928FC3002003B070C /* Utility.swift in Sources */, C4E2E86928FC3002003B070C /* Utility.swift in Sources */,
C471E83D28F9BB650021E251 /* FakeDomainScanner.swift in Sources */, C471E83D28F9BB650021E251 /* FakeDomainScanner.swift in Sources */,
@@ -3160,6 +3168,7 @@
C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */, C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */,
C471E8A628F9BB8F0021E251 /* App.swift in Sources */, C471E8A628F9BB8F0021E251 /* App.swift in Sources */,
0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C4513F912B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */, C4513F912B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */,
C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */, C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */,
C45B914C295607F400F4EC78 /* Service.swift in Sources */, C45B914C295607F400F4EC78 /* Service.swift in Sources */,
@@ -3413,6 +3422,7 @@
C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */, C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */, C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */,
C463E381284930EE00422731 /* PresetHelper.swift in Sources */, C463E381284930EE00422731 /* PresetHelper.swift in Sources */,
C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */, C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,

View File

@@ -116,6 +116,11 @@
value = "" value = ""
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "SKIP_UPDATE_CHECK"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction

View File

@@ -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
}
}

View File

@@ -38,6 +38,9 @@ class ValetSite: ValetListable {
/// Whether the site has been secured. /// Whether the site has been secured.
var secured: Bool! var secured: Bool!
/// When the certificate expires.
var expiryDate: Date?
/// What driver is currently in use. If not detected, defaults to nil. /// What driver is currently in use. If not detected, defaults to nil.
var driver: String? var driver: String?
@@ -122,13 +125,23 @@ class ValetSite: ValetListable {
/** /**
Checks if a certificate file can be found in the `valet/Certificates` directory. 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() { public func determineSecured() {
secured = container.filesystem let certificatePath = "~/.config/valet/Certificates/\(self.name).\(self.tld).crt"
.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
// 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
} }
/** /**