Completed
Push — master ( f4336a...a269ea )
by Eric
7s
created

SSLCertificateMonitor::hostCoveredByCertificate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 16
rs 9.2
cc 4
eloc 8
nc 3
nop 3
1
<?php
2
3
namespace EricMakesStuff\ServerMonitor\Monitors;
4
5
use Carbon\Carbon;
6
use EricMakesStuff\ServerMonitor\Events\SSLCertificateExpiring;
7
use EricMakesStuff\ServerMonitor\Events\SSLCertificateInvalid;
8
use EricMakesStuff\ServerMonitor\Events\SSLCertificateValid;
9
use EricMakesStuff\ServerMonitor\Exceptions\InvalidConfiguration;
10
11
class SSLCertificateMonitor extends BaseMonitor
12
{
13
    /**  @var array */
14
    protected $certificateInfo;
15
16
    /**  @var string */
17
    protected $certificateExpiration;
18
19
    /**  @var string */
20
    protected $certificateDomain;
21
22
    /**  @var array */
23
    protected $certificateAdditionalDomains = [];
24
25
    /**  @var int */
26
    protected $certificateDaysUntilExpiration;
27
28
    /**  @var string */
29
    protected $url;
30
31
    /**  @var array */
32
    protected $alarmDaysBeforeExpiration = [28, 14, 7, 3, 2, 1, 0];
33
34
    /**
35
     * @param array $config
36
     */
37
    public function __construct(array $config)
38
    {
39
        if (!empty($config['url'])) {
40
            $this->url = $config['url'];
41
        }
42
43
        if (!empty($config['alarmDaysBeforeExpiration'])) {
44
            $this->alarmDaysBeforeExpiration = $config['alarmDaysBeforeExpiration'];
45
        }
46
    }
47
48
    /**
49
     * @throws InvalidConfiguration
50
     */
51
    public function runMonitor()
52
    {
53
        $urlParts = $this->parseUrl($this->url);
54
55
        try {
56
            $this->certificateInfo = $this->downloadCertificate($urlParts);
57
        } catch (\ErrorException $e) {
58
            event(new SSLCertificateInvalid($this));
59
            return false;
60
        } catch (\Exception $e) {
61
            throw InvalidConfiguration::urlCouldNotBeDownloaded();
62
        }
63
64
        $this->processCertificate($this->certificateInfo);
65
66
        if ($this->certificateDaysUntilExpiration < 0
67
            || ! $this->hostCoveredByCertificate($urlParts['host'], $this->certificateDomain, $this->certificateAdditionalDomains)) {
68
            event(new SSLCertificateInvalid($this));
69
        } elseif (in_array($this->certificateDaysUntilExpiration, $this->alarmDaysBeforeExpiration)) {
70
            event(new SSLCertificateExpiring($this));
71
        } else {
72
            event(new SSLCertificateValid($this));
73
        }
74
    }
75
76
    protected function downloadCertificate($urlParts)
77
    {
78
        $streamContext = stream_context_create([
79
            "ssl" => [
80
                "capture_peer_cert" => TRUE
81
            ]
82
        ]);
83
84
        $streamClient = stream_socket_client("ssl://{$urlParts['host']}:443", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
85
86
        $certificateContext = stream_context_get_params($streamClient);
87
88
        return openssl_x509_parse($certificateContext['options']['ssl']['peer_certificate']);
89
    }
90
91
    public function processCertificate($certificateInfo)
92
    {
93
        if (!empty($certificateInfo['subject']) && !empty($certificateInfo['subject']['CN'])) {
94
            $this->certificateDomain = $certificateInfo['subject']['CN'];
95
        }
96
97
        if (!empty($certificateInfo['validTo_time_t'])) {
98
            $validTo = Carbon::createFromTimestampUTC($certificateInfo['validTo_time_t']);
99
            $this->certificateExpiration = $validTo->toDateString();
100
            $this->certificateDaysUntilExpiration = - $validTo->diffInDays(Carbon::now(), false);
101
        }
102
103
        if (!empty($certificateInfo['extensions']) && !empty($certificateInfo['extensions']['subjectAltName'])) {
104
            $this->certificateAdditionalDomains = [];
105
            $domains = explode(', ', $certificateInfo['extensions']['subjectAltName']);
106
            foreach ($domains as $domain) {
107
                $this->certificateAdditionalDomains[] = str_replace('DNS:', '', $domain);
108
            }
109
        }
110
    }
111
112
    public function hostCoveredByCertificate($host, $certificateHost, array $certificateAdditionalDomains = [])
113
    {
114
        if ($host == $certificateHost) {
115
            return true;
116
        }
117
118
        // Determine if wildcard domain covers the host domain
119
        if ($certificateHost[0] == '*' && substr_count($host, '.') > 1) {
120
            $certificateHost = substr($certificateHost, 1);
121
            $host = substr($host, strpos($host, '.'));
122
            return $certificateHost == $host;
123
        }
124
125
        // Determine if the host domain is in the certificate's additional domains
126
        return in_array($host, $certificateAdditionalDomains);
127
    }
128
129
    protected function parseUrl($url)
130
    {
131
        if (empty($url)) {
132
            throw InvalidConfiguration::noUrlConfigured();
133
        }
134
135
        $urlParts = parse_url($url);
136
137
        if (!$urlParts) {
138
            throw InvalidConfiguration::urlCouldNotBeParsed();
139
        }
140
141
        if (empty($urlParts['scheme']) || $urlParts['scheme'] != 'https') {
142
            throw InvalidConfiguration::urlNotSecure();
143
        }
144
145
        return $urlParts;
146
    }
147
148
    public function getUrl()
149
    {
150
        return $this->url;
151
    }
152
153
    public function getCertificateInfo()
154
    {
155
        return $this->certificateInfo;
156
    }
157
158
    public function getCertificateExpiration()
159
    {
160
        return $this->certificateExpiration;
161
    }
162
163
    public function getCertificateDomain()
164
    {
165
        return $this->certificateDomain;
166
    }
167
168
    public function getCertificateDaysUntilExpiration()
169
    {
170
        return $this->certificateDaysUntilExpiration;
171
    }
172
173
    public function getAlarmDaysBeforeExpiration()
174
    {
175
        return $this->alarmDaysBeforeExpiration;
176
    }
177
178
    public function getCertificateAdditionalDomains()
179
    {
180
        return $this->certificateAdditionalDomains;
181
    }
182
}
183