Add SSL certificate checker

Implement SSL certificate checking functionality
This commit is contained in:
Tola Leng
2025-06-20 19:42:24 +07:00
parent 1eabf66311
commit 5ceaa09710
12 changed files with 913 additions and 6 deletions
+1 -1
View File
@@ -8,10 +8,10 @@ A Go-based microservice for service operations including ICMP ping, DNS resoluti
- **ICMP Ping**: Full ping functionality with packet statistics
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
- **TCP Connectivity**: Port connectivity testing
- **SSL Certificate**: SSL Certificate Check
- REST API endpoints
- Health check endpoint
- Configurable via environment variables
- Docker support
- Comprehensive operation statistics
## API Endpoints
+1 -1
View File
@@ -21,7 +21,7 @@ type Config struct {
func Load() *Config {
cfg := &Config{
Port: getEnv("PORT", "8091"),
Port: getEnv("PORT", "8092"),
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 3*time.Second),
MaxCount: getEnvInt("MAX_COUNT", 20),
@@ -79,6 +79,10 @@ func (h *OperationHandler) HandleOperation(w http.ResponseWriter, r *http.Reques
}
result, err = httpOp.Execute(url, method)
case types.OperationSSL:
sslOp := operations.NewSSLOperation(timeout)
result, err = sslOp.Execute(req.Host)
default:
http.Error(w, "Invalid operation type", http.StatusBadRequest)
return
+15 -3
View File
@@ -21,6 +21,7 @@ func main() {
// Initialize PocketBase client (no credentials required)
var pbClient *pocketbase.PocketBaseClient
var monitoringService *monitoring.MonitoringService
var sslMonitoringService *monitoring.SSLMonitoringService
if cfg.PocketBaseEnabled {
var err error
@@ -35,6 +36,11 @@ func main() {
monitoringService = monitoring.NewMonitoringService(pbClient)
go monitoringService.Start()
log.Println("Monitoring service started (public access mode)")
// Initialize and start SSL monitoring service
sslMonitoringService = monitoring.NewSSLMonitoringService(pbClient)
go sslMonitoringService.Start()
log.Println("SSL monitoring service started")
}
}
}
@@ -63,13 +69,16 @@ func main() {
if monitoringService != nil {
log.Printf("Automatic service monitoring enabled")
}
if sslMonitoringService != nil {
log.Printf("SSL certificate monitoring enabled")
}
log.Printf("Endpoints:")
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http)")
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http, ssl)")
log.Printf(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
log.Printf(" POST /ping - Legacy ping endpoint")
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
log.Printf(" GET /health - Health check")
log.Printf("Supported operations: ping, dns, tcp, http")
log.Printf("Supported operations: ping, dns, tcp, http, ssl")
// Setup graceful shutdown
c := make(chan os.Signal, 1)
@@ -77,10 +86,13 @@ func main() {
go func() {
<-c
log.Println("Shutting down monitoring service...")
log.Println("Shutting down monitoring services...")
if monitoringService != nil {
monitoringService.Stop()
}
if sslMonitoringService != nil {
sslMonitoringService.Stop()
}
log.Println("Service stopped")
os.Exit(0)
}()
@@ -0,0 +1,314 @@
package monitoring
import (
"fmt"
"log"
"time"
"service-operation/operations"
"service-operation/pocketbase"
"service-operation/types"
)
type SSLMonitoringService struct {
pbClient *pocketbase.PocketBaseClient
stopChan chan bool
retryQueue map[string]int // Track retry count per certificate
maxRetries int
}
func NewSSLMonitoringService(pbClient *pocketbase.PocketBaseClient) *SSLMonitoringService {
return &SSLMonitoringService{
pbClient: pbClient,
stopChan: make(chan bool),
retryQueue: make(map[string]int),
maxRetries: 3,
}
}
func (s *SSLMonitoringService) Start() {
ticker := time.NewTicker(1 * time.Minute) // Check every minute for scheduling
defer ticker.Stop()
log.Println("SSL monitoring service started with interval and check_at scheduling")
for {
select {
case <-ticker.C:
s.checkSSLCertificates()
case <-s.stopChan:
log.Println("SSL monitoring service stopped")
return
}
}
}
func (s *SSLMonitoringService) Stop() {
s.stopChan <- true
}
func (s *SSLMonitoringService) checkSSLCertificates() {
//log.Println("Fetching SSL certificates from PocketBase...")
certificates, err := s.pbClient.GetSSLCertificates()
if err != nil {
log.Printf("Failed to fetch SSL certificates: %v", err)
return
}
//log.Printf("Found %d SSL certificates to check", len(certificates))
for _, cert := range certificates {
if s.shouldCheckCertificate(cert) {
go s.checkSingleCertificateWithRetry(cert)
}
}
}
func (s *SSLMonitoringService) shouldCheckCertificate(cert types.SSLCertificate) bool {
now := time.Now()
// Priority 1: Check if check_at is set and is due
if cert.CheckAt != "" {
if checkAt, err := s.parseFlexibleTime(cert.CheckAt); err == nil {
if now.After(checkAt) || now.Equal(checkAt) {
log.Printf("Certificate %s is due for manual check (check_at: %s)",
cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
return true
} else {
//log.Printf("Certificate %s scheduled for later check (check_at: %s)",
//cert.Domain, checkAt.Format("2006-01-02 15:04:05"))
//return false
}
} else {
log.Printf("Error parsing check_at for %s: %v", cert.Domain, err)
}
}
// Priority 2: Check based on check_interval (in days) from last update
if cert.Updated == "" {
log.Printf("Certificate %s has never been checked, scheduling check", cert.Domain)
return true
}
// Parse last check time from updated field
lastCheck, err := s.parseFlexibleTime(cert.Updated)
if err != nil {
log.Printf("Error parsing last check time for %s, scheduling check: %v", cert.Domain, err)
return true
}
// Get check interval in days (default to 1 day if not set or invalid)
checkIntervalDays := cert.CheckInterval
if checkIntervalDays <= 0 {
checkIntervalDays = 1 // Default to 1 day
}
// Adjust check interval based on certificate status for critical certificates
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
// Calculate next check time based on days
nextCheck := lastCheck.Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
shouldCheck := now.After(nextCheck)
if shouldCheck {
log.Printf("Certificate %s is due for interval check (last: %s, interval: %d days)",
cert.Domain, lastCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
} else {
//log.Printf("Certificate %s not due yet (next check: %s, interval: %d days)",
//cert.Domain, nextCheck.Format("2006-01-02 15:04:05"), adjustedIntervalDays)
}
return shouldCheck
}
// parseFlexibleTime tries multiple time formats to parse timestamps
func (s *SSLMonitoringService) parseFlexibleTime(timeStr string) (time.Time, error) {
formats := []string{
time.RFC3339,
time.RFC3339Nano,
"2006-01-02 15:04:05.999Z", // ISO 8601 with milliseconds (PocketBase format)
"2006-01-02 15:04:05.999999Z", // ISO 8601 with microseconds
"2006-01-02 15:04:05Z", // ISO 8601 without milliseconds
"2006-01-02T15:04:05.999Z", // RFC3339 with milliseconds
"2006-01-02T15:04:05.999999Z", // RFC3339 with microseconds
"2006-01-02T15:04:05Z", // RFC3339 without milliseconds
"2006-01-02 15:04:05.999999999 -0700 MST",
"2006-01-02 15:04:05.999999 -0700 MST",
"2006-01-02 15:04:05 -0700 MST",
"2006-01-02 15:04:05.999999999",
"2006-01-02 15:04:05.999999",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05.999999999Z",
}
for _, format := range formats {
if t, err := time.Parse(format, timeStr); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time string: %s", timeStr)
}
// adjustCheckIntervalDays adjusts the check interval based on certificate status and days left
func (s *SSLMonitoringService) adjustCheckIntervalDays(cert types.SSLCertificate, defaultIntervalDays int) int {
// Check more frequently for certificates that are expiring soon or have errors
if cert.DaysLeft <= 7 {
return 1 // Check daily for certificates expiring within 7 days
} else if cert.DaysLeft <= 30 {
// Check every 2 days for certificates expiring within 30 days
if defaultIntervalDays > 2 {
return 2
}
} else if cert.Status == "error" {
return 1 // Check daily for certificates with errors
}
return defaultIntervalDays
}
func (s *SSLMonitoringService) checkSingleCertificateWithRetry(cert types.SSLCertificate) {
retryCount := s.retryQueue[cert.ID]
log.Printf("🔍 Checking SSL certificate for domain: %s (attempt %d/%d)",
cert.Domain, retryCount+1, s.maxRetries+1)
result, err := s.performSSLCheck(cert.Domain)
if err != nil && retryCount < s.maxRetries {
// Increment retry count and schedule retry
s.retryQueue[cert.ID] = retryCount + 1
log.Printf("SSL check failed for %s, will retry (%d/%d): %v",
cert.Domain, retryCount+1, s.maxRetries, err)
// Schedule retry with exponential backoff
go func() {
backoffDuration := time.Duration((retryCount + 1) * 30) * time.Second
time.Sleep(backoffDuration)
s.checkSingleCertificateWithRetry(cert)
}()
return
}
// Reset retry count on success or max retries reached
delete(s.retryQueue, cert.ID)
if err != nil {
log.Printf("❌ SSL check failed for domain %s after %d attempts: %v",
cert.Domain, s.maxRetries+1, err)
s.updateCertificateWithError(cert, err)
return
}
// Update certificate with successful results
s.updateCertificateWithResults(cert, result)
}
func (s *SSLMonitoringService) performSSLCheck(domain string) (*types.OperationResult, error) {
log.Printf("Performing SSL check for domain: %s", domain)
sslOp := operations.NewSSLOperation(30 * time.Second)
result, err := sslOp.Execute(domain)
if err != nil {
log.Printf("SSL operation failed for %s: %v", domain, err)
return nil, err
}
if result == nil {
log.Printf("SSL operation returned nil result for %s", domain)
return nil, fmt.Errorf("SSL check returned nil result")
}
log.Printf("SSL check completed for %s: success=%v, days_left=%d",
domain, result.Success, result.SSLDaysLeft)
return result, nil
}
func (s *SSLMonitoringService) updateCertificateWithError(cert types.SSLCertificate, err error) {
log.Printf("Updating certificate %s with error status", cert.Domain)
updateData := map[string]interface{}{
"status": "error",
"updated": time.Now().Format(time.RFC3339),
"error_message": err.Error(),
}
// Calculate next check time based on check_interval (in days) with shorter interval for errors
checkIntervalDays := cert.CheckInterval
if checkIntervalDays <= 0 {
checkIntervalDays = 1
}
// For errors, check again in half the normal interval (minimum 1 day)
errorIntervalDays := checkIntervalDays / 2
if errorIntervalDays < 1 {
errorIntervalDays = 1
}
nextCheck := time.Now().Add(time.Duration(errorIntervalDays) * 24 * time.Hour)
updateData["check_at"] = nextCheck.Format(time.RFC3339)
if updateErr := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); updateErr != nil {
log.Printf("Failed to update SSL certificate %s with error status: %v", cert.ID, updateErr)
} else {
log.Printf("📝 Updated certificate %s with error status (next check in %d days)",
cert.Domain, errorIntervalDays)
}
}
func (s *SSLMonitoringService) updateCertificateWithResults(cert types.SSLCertificate, result *types.OperationResult) {
status := getSSLStatus(result)
log.Printf("Updating certificate %s with results: status=%s, days_left=%d, issuer=%s",
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer)
updateData := map[string]interface{}{
"status": status,
"valid_from": result.SSLValidFrom.Format(time.RFC3339),
"valid_till": result.SSLValidTill.Format(time.RFC3339),
"days_left": result.SSLDaysLeft,
"valid_days_to_expire": result.SSLDaysLeft,
"resolved_ip": result.SSLResolvedIP,
"issuer_cn": result.SSLIssuer, // Now contains organization name like "Google Trust Services"
"issued_to": result.SSLSubject, // Now contains organization name
"serial_number": result.SSLSerialNumber,
"cert_alg": result.SSLAlgorithm,
"cert_sans": result.SSLSANs,
"updated": time.Now().Format(time.RFC3339),
"error_message": "", // Clear any previous error
}
// Calculate next check time based on check_interval (in days) and certificate status
checkIntervalDays := cert.CheckInterval
if checkIntervalDays <= 0 {
checkIntervalDays = 1 // Default to 1 day
}
adjustedIntervalDays := s.adjustCheckIntervalDays(cert, checkIntervalDays)
nextCheck := time.Now().Add(time.Duration(adjustedIntervalDays) * 24 * time.Hour)
updateData["check_at"] = nextCheck.Format(time.RFC3339)
if err := s.pbClient.UpdateSSLCertificate(cert.ID, updateData); err != nil {
log.Printf("Failed to update SSL certificate %s: %v", cert.ID, err)
} else {
log.Printf("✅ SSL certificate updated for %s: %s (%d days left, issuer: %s, next check in %d days)",
cert.Domain, status, result.SSLDaysLeft, result.SSLIssuer, adjustedIntervalDays)
}
}
func getSSLStatus(result *types.OperationResult) string {
if !result.Success {
return "error"
}
if result.SSLDaysLeft <= 0 {
return "expired"
} else if result.SSLDaysLeft <= 7 {
return "critical" // Very urgent
} else if result.SSLDaysLeft <= 30 {
return "expiring_soon"
}
return "valid"
}
@@ -0,0 +1,57 @@
package operations
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
"net"
)
// extractSANs extracts Subject Alternative Names from certificate
func (op *SSLOperation) extractSANs(cert *x509.Certificate) []string {
sans := make([]string, 0)
// Add DNS names
sans = append(sans, cert.DNSNames...)
// Add IP addresses
for _, ip := range cert.IPAddresses {
sans = append(sans, ip.String())
}
// Add email addresses
sans = append(sans, cert.EmailAddresses...)
// Add URIs
for _, uri := range cert.URIs {
sans = append(sans, uri.String())
}
return sans
}
// getResolvedIP resolves the domain to its IP address
func (op *SSLOperation) getResolvedIP(hostname string) string {
ips, err := net.LookupIP(hostname)
if err != nil || len(ips) == 0 {
return ""
}
return ips[0].String()
}
// getCertificateAlgorithm returns detailed algorithm information
func (op *SSLOperation) getCertificateAlgorithm(cert *x509.Certificate) string {
algorithm := cert.SignatureAlgorithm.String()
// Add key size information if available
switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey:
algorithm += fmt.Sprintf(" (RSA %d-bit)", pub.N.BitLen())
case *ecdsa.PublicKey:
algorithm += fmt.Sprintf(" (ECDSA %d-bit)", pub.Curve.Params().BitSize)
}
return algorithm
}
@@ -0,0 +1,159 @@
package operations
import (
"crypto/tls"
"fmt"
"net"
"strings"
"time"
"service-operation/types"
)
type SSLOperation struct {
timeout time.Duration
}
func NewSSLOperation(timeout time.Duration) *SSLOperation {
return &SSLOperation{
timeout: timeout,
}
}
func (op *SSLOperation) Execute(domain string) (*types.OperationResult, error) {
startTime := time.Now()
// Clean and normalize domain
domain = op.normalizeDomain(domain)
// Validate domain format
if domain == "" {
return op.createErrorResult(domain, startTime, "domain cannot be empty")
}
// Add port if not present
host := domain
if !strings.Contains(host, ":") {
host = host + ":443"
}
// Set up TLS connection with timeout
dialer := &net.Dialer{
Timeout: op.timeout,
}
// Create TLS config with proper verification
tlsConfig := &tls.Config{
ServerName: strings.Split(host, ":")[0],
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
}
// Attempt TLS connection
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
endTime := time.Now()
responseTime := endTime.Sub(startTime)
if err != nil {
return &types.OperationResult{
Type: types.OperationSSL,
Host: strings.Split(host, ":")[0],
Success: false,
ResponseTime: responseTime,
Error: fmt.Sprintf("TLS connection failed: %v", err),
StartTime: startTime,
EndTime: endTime,
}, nil
}
defer conn.Close()
// Get certificate chain information
state := conn.ConnectionState()
if len(state.PeerCertificates) == 0 {
return &types.OperationResult{
Type: types.OperationSSL,
Host: strings.Split(host, ":")[0],
Success: false,
ResponseTime: responseTime,
Error: "No certificates found in chain",
StartTime: startTime,
EndTime: endTime,
}, nil
}
cert := state.PeerCertificates[0]
hostname := strings.Split(host, ":")[0]
// Perform comprehensive certificate validation
validationError := op.validateCertificate(cert, hostname)
// Calculate days left until expiration
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
// Extract Subject Alternative Names
sans := op.extractSANs(cert)
// Get resolved IP address
resolvedIP := op.getResolvedIP(hostname)
// Extract certificate algorithm information
algorithm := op.getCertificateAlgorithm(cert)
// Extract issuer organization (O=) instead of full distinguished name
issuerOrganization := op.extractIssuerOrganization(cert.Issuer)
// Extract subject organization (O=) instead of full distinguished name
subjectOrganization := op.extractSubjectOrganization(cert.Subject)
// Check if certificate is valid (not expired and passes validation)
isValid := validationError == nil && time.Now().Before(cert.NotAfter) && time.Now().After(cert.NotBefore)
// Build detailed error message if validation failed
errorMsg := ""
if validationError != nil {
errorMsg = validationError.Error()
} else if time.Now().After(cert.NotAfter) {
errorMsg = "Certificate has expired"
} else if time.Now().Before(cert.NotBefore) {
errorMsg = "Certificate is not yet valid"
}
// Create comprehensive result
result := &types.OperationResult{
Type: types.OperationSSL,
Host: hostname,
Success: isValid,
ResponseTime: responseTime,
Error: errorMsg,
StartTime: startTime,
EndTime: endTime,
// SSL specific fields - using organization instead of full DN
SSLValidFrom: cert.NotBefore,
SSLValidTill: cert.NotAfter,
SSLDaysLeft: daysLeft,
SSLIssuer: issuerOrganization, // Now shows "Google Trust Services" instead of full DN
SSLSubject: subjectOrganization, // Now shows organization instead of full DN
SSLSerialNumber: cert.SerialNumber.String(),
SSLAlgorithm: algorithm,
SSLSANs: strings.Join(sans, ","),
SSLResolvedIP: resolvedIP,
}
return result, nil
}
// createErrorResult creates a standardized error result
func (op *SSLOperation) createErrorResult(domain string, startTime time.Time, errorMsg string) (*types.OperationResult, error) {
return &types.OperationResult{
Type: types.OperationSSL,
Host: domain,
Success: false,
ResponseTime: time.Since(startTime),
Error: errorMsg,
StartTime: startTime,
EndTime: time.Now(),
}, nil
}
@@ -0,0 +1,88 @@
package operations
import (
"crypto/x509/pkix"
"fmt"
"strings"
)
// normalizeDomain cleans and normalizes the domain input
func (op *SSLOperation) normalizeDomain(domain string) string {
// Remove protocol prefixes
domain = strings.Replace(domain, "https://", "", 1)
domain = strings.Replace(domain, "http://", "", 1)
// Remove trailing slash and path
if idx := strings.Index(domain, "/"); idx != -1 {
domain = domain[:idx]
}
// Trim whitespace
domain = strings.TrimSpace(domain)
return domain
}
// formatDistinguishedName formats the certificate distinguished name
func (op *SSLOperation) formatDistinguishedName(name pkix.Name) string {
var parts []string
if name.CommonName != "" {
parts = append(parts, fmt.Sprintf("CN=%s", name.CommonName))
}
for _, org := range name.Organization {
parts = append(parts, fmt.Sprintf("O=%s", org))
}
for _, orgUnit := range name.OrganizationalUnit {
parts = append(parts, fmt.Sprintf("OU=%s", orgUnit))
}
for _, country := range name.Country {
parts = append(parts, fmt.Sprintf("C=%s", country))
}
for _, locality := range name.Locality {
parts = append(parts, fmt.Sprintf("L=%s", locality))
}
for _, province := range name.Province {
parts = append(parts, fmt.Sprintf("ST=%s", province))
}
return strings.Join(parts, ", ")
}
// extractIssuerOrganization extracts only the organization (O=) from the issuer distinguished name
func (op *SSLOperation) extractIssuerOrganization(name pkix.Name) string {
// Return the first organization if available
if len(name.Organization) > 0 {
return name.Organization[0]
}
// Fallback to Common Name if no organization
if name.CommonName != "" {
return name.CommonName
}
// Last resort fallback
return "Unknown"
}
// extractSubjectOrganization extracts only the organization (O=) from the subject distinguished name
func (op *SSLOperation) extractSubjectOrganization(name pkix.Name) string {
// Return the first organization if available
if len(name.Organization) > 0 {
return name.Organization[0]
}
// Fallback to Common Name if no organization
if name.CommonName != "" {
return name.CommonName
}
// Last resort fallback
return "Unknown"
}
@@ -0,0 +1,65 @@
package operations
import (
"crypto/x509"
"fmt"
"time"
)
// validateCertificate performs comprehensive certificate validation
func (op *SSLOperation) validateCertificate(cert *x509.Certificate, hostname string) error {
now := time.Now()
// Check if certificate is expired or not yet valid
if now.Before(cert.NotBefore) {
return fmt.Errorf("certificate is not yet valid (valid from: %v)", cert.NotBefore.Format("2006-01-02 15:04:05"))
}
if now.After(cert.NotAfter) {
return fmt.Errorf("certificate has expired (expired on: %v)", cert.NotAfter.Format("2006-01-02 15:04:05"))
}
// Verify hostname matches certificate
if err := cert.VerifyHostname(hostname); err != nil {
return fmt.Errorf("hostname verification failed: %v", err)
}
// Check key usage - certificates should have digital signature capability
if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return fmt.Errorf("certificate missing required digital signature key usage")
}
// Check if certificate is self-signed (basic check)
if cert.Issuer.CommonName == cert.Subject.CommonName && len(cert.Subject.Organization) == 0 {
return fmt.Errorf("certificate appears to be self-signed")
}
// Validate certificate chain if intermediate certificates are present
if err := op.validateCertificateChain(cert); err != nil {
return fmt.Errorf("certificate chain validation failed: %v", err)
}
return nil
}
// validateCertificateChain performs basic certificate chain validation
func (op *SSLOperation) validateCertificateChain(cert *x509.Certificate) error {
// Check if the certificate has proper extensions for SSL/TLS
hasServerAuth := false
for _, usage := range cert.ExtKeyUsage {
if usage == x509.ExtKeyUsageServerAuth {
hasServerAuth = true
break
}
}
if !hasServerAuth {
return fmt.Errorf("certificate does not have server authentication extension")
}
// Check certificate version (should be v3 for modern certificates)
if cert.Version < 3 {
return fmt.Errorf("certificate version %d is outdated (should be v3)", cert.Version)
}
return nil
}
@@ -0,0 +1,89 @@
package pocketbase
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"service-operation/types"
)
type SSLCertificatesResponse struct {
Page int `json:"page"`
PerPage int `json:"perPage"`
TotalItems int `json:"totalItems"`
TotalPages int `json:"totalPages"`
Items []types.SSLCertificate `json:"items"`
}
func (c *PocketBaseClient) GetSSLCertificates() ([]types.SSLCertificate, error) {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records", c.baseURL)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch SSL certificates: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
}
var response SSLCertificatesResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode SSL certificates response: %v", err)
}
return response.Items, nil
}
func (c *PocketBaseClient) UpdateSSLCertificate(id string, data map[string]interface{}) error {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal SSL certificate data: %v", err)
}
req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create SSL certificate update request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to update SSL certificate: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to update SSL certificate, status: %d", resp.StatusCode)
}
return nil
}
func (c *PocketBaseClient) GetSSLCertificateByID(id string) (*types.SSLCertificate, error) {
url := fmt.Sprintf("%s/api/collections/ssl_certificates/records/%s", c.baseURL, id)
resp, err := c.httpClient.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch SSL certificate: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("PocketBase returned status %d", resp.StatusCode)
}
var cert types.SSLCertificate
if err := json.NewDecoder(resp.Body).Decode(&cert); err != nil {
return nil, fmt.Errorf("failed to decode SSL certificate response: %v", err)
}
return &cert, nil
}
+12 -1
View File
@@ -1,4 +1,3 @@
package types
import "time"
@@ -10,6 +9,7 @@ const (
OperationDNS OperationType = "dns"
OperationTCP OperationType = "tcp"
OperationHTTP OperationType = "http"
OperationSSL OperationType = "ssl"
)
type OperationRequest struct {
@@ -56,6 +56,17 @@ type OperationResult struct {
ContentLength int64 `json:"content_length,omitempty"`
ResponseBody string `json:"response_body,omitempty"`
// SSL specific fields
SSLValidFrom time.Time `json:"ssl_valid_from,omitempty"`
SSLValidTill time.Time `json:"ssl_valid_till,omitempty"`
SSLDaysLeft int `json:"ssl_days_left,omitempty"`
SSLIssuer string `json:"ssl_issuer,omitempty"`
SSLSubject string `json:"ssl_subject,omitempty"`
SSLSerialNumber string `json:"ssl_serial_number,omitempty"`
SSLAlgorithm string `json:"ssl_algorithm,omitempty"`
SSLSANs string `json:"ssl_sans,omitempty"`
SSLResolvedIP string `json:"ssl_resolved_ip,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
}
+108
View File
@@ -0,0 +1,108 @@
package types
import (
"encoding/json"
"strconv"
"time"
)
type SSLCertificate struct {
ID string `json:"id"`
CollectionID string `json:"collectionId"`
CollectionName string `json:"collectionName"`
Domain string `json:"domain"`
IssuerO string `json:"issuer_o"`
Status string `json:"status"`
LastNotified string `json:"last_notified"`
WarningThreshold int `json:"warning_threshold"`
ExpiryThreshold int `json:"expiry_threshold"`
NotificationChannel string `json:"notification_channel"`
ValidFrom string `json:"valid_from"`
SerialNumber string `json:"serial_number"` // Changed to string to handle large numbers
IssuedTo string `json:"issued_to"`
ValidTill string `json:"valid_till"`
ValidityDays int `json:"validity_days"`
DaysLeft int `json:"days_left"`
ValidDaysToExpire int `json:"valid_days_to_expire"`
ResolvedIP string `json:"resolved_ip"`
IssuerCN string `json:"issuer_cn"`
CertAlg string `json:"cert_alg"`
CertSans string `json:"cert_sans"`
CheckInterval int `json:"check_interval"`
CheckAt string `json:"check_at"`
Created string `json:"created"`
Updated string `json:"updated"`
}
// Custom unmarshaler to handle check_interval as both string and int, and serial_number as string
func (s *SSLCertificate) UnmarshalJSON(data []byte) error {
// Create a temporary struct with flexible types
type Alias SSLCertificate
aux := &struct {
CheckInterval interface{} `json:"check_interval"`
SerialNumber interface{} `json:"serial_number"`
*Alias
}{
Alias: (*Alias)(s),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Handle check_interval conversion
switch v := aux.CheckInterval.(type) {
case string:
if v == "" {
s.CheckInterval = 1440 // Default 24 hours in minutes
} else {
interval, err := strconv.Atoi(v)
if err != nil {
s.CheckInterval = 1440 // Default on error
} else {
s.CheckInterval = interval
}
}
case float64:
s.CheckInterval = int(v)
case int:
s.CheckInterval = v
default:
s.CheckInterval = 1440 // Default 24 hours in minutes
}
// Handle serial_number conversion to string
switch v := aux.SerialNumber.(type) {
case string:
s.SerialNumber = v
case float64:
// Handle scientific notation by converting to string
s.SerialNumber = strconv.FormatFloat(v, 'f', 0, 64)
case int64:
s.SerialNumber = strconv.FormatInt(v, 10)
case int:
s.SerialNumber = strconv.Itoa(v)
default:
s.SerialNumber = ""
}
return nil
}
type SSLCheckResult struct {
Domain string `json:"domain"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ValidFrom time.Time `json:"valid_from"`
ValidTill time.Time `json:"valid_till"`
DaysLeft int `json:"days_left"`
Issuer string `json:"issuer"`
Subject string `json:"subject"`
SerialNumber string `json:"serial_number"`
Algorithm string `json:"algorithm"`
SANs []string `json:"sans"`
ResponseTime time.Duration `json:"response_time"`
ResolvedIP string `json:"resolved_ip"`
CheckedAt time.Time `json:"checked_at"`
}