1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4
5/**
6 * DokuWiki Plugin dig (Syntax Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  Andreas Gohr <andi@splitbrain.org>
10 */
11class syntax_plugin_dig extends SyntaxPlugin
12{
13    /** @inheritdoc */
14    public function getType()
15    {
16        return 'substition';
17    }
18
19    /** @inheritdoc */
20    public function getPType()
21    {
22        return 'block';
23    }
24
25    /** @inheritdoc */
26    public function getSort()
27    {
28        return 333;
29    }
30
31    /** @inheritdoc */
32    public function connectTo($mode)
33    {
34        $this->Lexer->addSpecialPattern('<dig>\n.*?\n</dig>', $mode, 'plugin_dig');
35    }
36
37    /** @inheritdoc */
38    public function handle($match, $state, $pos, Doku_Handler $handler)
39    {
40        $lines = explode("\n", $match);
41        array_shift($lines); // skip opening tag
42        array_pop($lines);   // skip closing tag
43
44        return linesToHash($lines);
45    }
46
47    /** @inheritdoc */
48    public function render($mode, Doku_Renderer $R, $data)
49    {
50        if ($mode == 'metadata') return false;
51
52        $R->table_open();
53        $R->tablethead_open();
54        $R->tablerow_open();
55        $R->tableheader_open();
56        $R->cdata($this->getLang('host'));
57        $R->tableheader_close();
58        $R->tableheader_open();
59        $R->cdata($this->getLang('ip'));
60        $R->tableheader_close();
61        $R->tableheader_open();
62        $R->cdata($this->getLang('ns'));
63        $R->tableheader_close();
64        $R->tableheader_open();
65        $R->cdata($this->getLang('ssl'));
66        $R->tableheader_close();
67        $R->tablerow_close();
68        $R->tablethead_close();
69
70        $R->tabletbody_open();
71
72        foreach ($data as $domain => $comment) {
73            $R->tablerow_open();
74            $this->renderCell($R, [$domain, $comment]);
75
76            try {
77                $dns = $this->getDNS($domain);
78                $this->renderCell($R, $dns['ip']);
79                $this->renderCell($R, $dns['ns']);
80            } catch (Exception $e) {
81                $this->renderCell($R, $e->getMessage(), 2);
82            }
83
84            try {
85                $ssl = $this->getSSL($domain);
86
87                $this->renderCell(
88                    $R,
89                    [
90                        'valid' => $ssl['date_valid'] && $ssl['domain_valid'] ? '✔️' : '❌',
91                        'until' => date('Y-m-d', $ssl['date_to']),
92                        'days' => floor(($ssl['date_to'] - time()) / (24 * 60 * 60)),
93                        'issuer' => $ssl['issuer'],
94                        'domains' => count($ssl['domains']),
95                    ]
96                );
97            } catch (Exception $e) {
98                $this->renderCell($R, $e->getMessage());
99            }
100
101            $R->tablerow_close();
102        }
103        $R->tabletbody_close();
104
105        $R->tabletfoot_open();
106        $R->tablerow_open();
107        $R->tablecell_open(4);
108        $R->cdata($this->getLang('lastupdate') . ' ' . date('Y-m-d H:i:s'));
109        $R->tablecell_close();
110        $R->tablerow_close();
111        $R->tabletfoot_close();
112
113        $R->table_close();
114
115        return true;
116    }
117
118    /**
119     * Get the DNS information for a domain
120     *
121     * @param string $domain
122     * @return array
123     * @throws Exception if the DNS lookup fails
124     */
125    protected function getDNS($domain)
126    {
127        $info = [
128            'ip' => [],
129            'ns' => [],
130        ];
131
132        $record = dns_get_record($domain, DNS_ALL);
133        if (!$record) throw new Exception($this->getLang('dnsfail'));
134
135        foreach ($record as $r) {
136            switch ($r['type']) {
137                case 'A':
138                    $info['ip']['IP'] = $r['ip'];
139                    break;
140                case 'AAAA':
141                    $info['ip']['IPv6'] = $r['ipv6'];
142                    break;
143                case 'CNAME':
144                    $info['ip']['CNAME'] = $r['target'];
145                    break;
146                case 'SOA':
147                    $info['ns']['NS'] = $r['mname'];
148                    break;
149                case 'MX':
150                    $info['ns']['MX'] = $r['target'];
151                    break;
152            }
153        }
154        return $info;
155    }
156
157    /**
158     * Get the certificate information for a domain
159     *
160     * @param string $domain
161     * @return array
162     * @throws Exception if the SSL lookup fails
163     */
164    protected function getSSL($domain)
165    {
166        if (!function_exists('openssl_x509_parse')) {
167            throw new Exception($this->getLang('sslfail') . ': ' . 'openssl not available in your PHP installation');
168        }
169
170        $context = stream_context_create(
171            ["ssl" => ["capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]]
172        );
173        $read = @stream_socket_client(
174            "ssl://" . $domain . ":443",
175            $errno,
176            $errstr,
177            15,
178            STREAM_CLIENT_CONNECT,
179            $context
180        );
181        if ($errstr) throw new Exception($this->getLang('sslfail') . ': ' . $errstr, $errno);
182
183
184        $cert = stream_context_get_params($read);
185        $certinfo = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
186
187        $issuer = $certinfo['issuer']['O'] ?? implode(' ', $certinfo['issuer']);
188
189        $domains = array_merge(
190            [$certinfo['subject']['CN']],
191            explode(',', $certinfo['extensions']['subjectAltName'])
192        );
193        $domains = array_map(static fn($d) => preg_replace('/^DNS:/', '', trim($d)), $domains);
194
195        return [
196            'date_from' => $certinfo['validFrom_time_t'],
197            'date_to' => $certinfo['validTo_time_t'],
198            'date_valid' => $certinfo['validFrom_time_t'] < time() && time() < $certinfo['validTo_time_t'],
199            'issuer' => $issuer,
200            'domains' => $domains,
201            'domain_valid' => in_array($domain, $domains),
202        ];
203    }
204
205    /**
206     * @param Doku_Renderer $R
207     * @param string|array $data
208     * @param int $colspan
209     */
210    protected function renderCell($R, $data, $colspan = 1)
211    {
212        $R->tablecell_open($colspan);
213        if (is_array($data)) {
214            foreach ($data as $key => $item) {
215                if (!is_numeric($key)) {
216                    $R->cdata($key . ': ');
217                }
218                $R->cdata($item);
219                $R->linebreak();
220            }
221        } else {
222            $R->cdata($data);
223        }
224        $R->tablecell_close();
225    }
226}
227