From 803f0d6219ae84838cc6884c5a9dec379c6bc1ff Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakhong.chan@aecom.com>
Date: Wed, 21 Jul 2021 12:09:09 +0800
Subject: [PATCH] Feat: Add Barebones certificate info display

---
 server/model/monitor.js | 13 ++++++++++-
 server/util-server.js   | 50 +++++++++++++++++++++++++++++++++++++++++
 src/mixins/socket.js    |  5 +++++
 src/pages/Details.vue   | 40 +++++++++++++++++++++++++++++++++
 4 files changed, 107 insertions(+), 1 deletion(-)

diff --git a/server/model/monitor.js b/server/model/monitor.js
index 13308867..79376c80 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -6,7 +6,7 @@ dayjs.extend(utc)
 dayjs.extend(timezone)
 const axios = require("axios");
 const {UP, DOWN, PENDING} = require("../util");
-const {tcping, ping} = require("../util-server");
+const {tcping, ping, checkCertificate} = require("../util-server");
 const {R} = require("redbean-node");
 const {BeanModel} = require("redbean-node/dist/bean-model");
 const {Notification} = require("../notification")
@@ -79,6 +79,9 @@ class Monitor extends BeanModel {
                     })
                     bean.msg = `${res.status} - ${res.statusText}`
                     bean.ping = dayjs().valueOf() - startTime;
+                    if (this.url.startsWith("https")) {
+                        Monitor.sendCertInfo(checkCertificate(res), io, this.id, this.user_id);
+                    }
 
                     if (this.type === "http") {
                         bean.status = UP;
@@ -218,6 +221,14 @@ class Monitor extends BeanModel {
         io.to(userID).emit("avgPing", monitorID, avgPing);
     }
 
+    /**
+     *
+     * @param checkCertificateResult : Object return result of checkCertificate
+     */
+     static async sendCertInfo(checkCertificateResult, io, monitorID, userID) {
+        io.to(userID).emit("certInfo", monitorID, checkCertificateResult);
+    }
+
     /**
      * Uptime with calculation
      * Calculation based on:
diff --git a/server/util-server.js b/server/util-server.js
index b387f4c7..e0e25534 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -70,3 +70,53 @@ exports.getSettings = async function (type) {
 
     return result;
 }
+
+
+// ssl-checker by @dyaa
+// param: res - response object from axios
+// return an object containing the certificate information
+
+const getDaysBetween = (validFrom, validTo) =>
+    Math.round(Math.abs(+validFrom - +validTo) / 8.64e7);
+
+const getDaysRemaining = (validFrom, validTo) => {
+    const daysRemaining = getDaysBetween(validFrom, validTo);
+    if (new Date(validTo).getTime() < new Date().getTime()) {
+        return -daysRemaining;
+    }
+    return daysRemaining;
+};
+
+exports.checkCertificate = function (res) {
+    const {
+        valid_from,
+        valid_to,
+        subjectaltname,
+        issuer,
+        fingerprint,
+    } = res.request.res.socket.getPeerCertificate(false);
+
+    if (!valid_from || !valid_to || !subjectaltname) {
+        reject(new Error('No certificate'));
+        return;
+    }
+
+    const valid = res.request.res.socket.authorized || false;
+
+    const validTo = new Date(valid_to);
+
+    const validFor = subjectaltname
+        .replace(/DNS:|IP Address:/g, "")
+        .split(", ");
+
+    const daysRemaining = getDaysRemaining(new Date(), validTo);
+
+    return {
+        valid,
+        validFor,
+        validTo,
+        daysRemaining,
+        issuer,
+        fingerprint,
+    };
+}
\ No newline at end of file
diff --git a/src/mixins/socket.js b/src/mixins/socket.js
index 0c4d68e1..612bbadd 100644
--- a/src/mixins/socket.js
+++ b/src/mixins/socket.js
@@ -25,6 +25,7 @@ export default {
             importantHeartbeatList: { },
             avgPingList: { },
             uptimeList: { },
+            certInfoList: {},
             notificationList: [],
             windowWidth: window.innerWidth,
             showListMobile: false,
@@ -114,6 +115,10 @@ export default {
             this.uptimeList[`${monitorID}_${type}`] = data
         });
 
+        socket.on('certInfo', (monitorID, data) => {
+            this.certInfoList[monitorID] = data
+        });
+
         socket.on('importantHeartbeatList', (monitorID, data) => {
             if (! (monitorID in this.importantHeartbeatList)) {
                 this.importantHeartbeatList[monitorID] = data;
diff --git a/src/pages/Details.vue b/src/pages/Details.vue
index f8c4879a..727f0aab 100644
--- a/src/pages/Details.vue
+++ b/src/pages/Details.vue
@@ -54,6 +54,38 @@
         </div>
     </div>
 
+    <div class="shadow-box big-padding text-center stats" v-if="monitor.type === 'http' && monitor.url.startsWith('https') && certInfo != null">
+        <div class="row">
+            <div class="col">
+                <h4>Certificate Info</h4>
+                <table class="text-start">
+                    <tbody>
+                        <tr class="my-3">
+                            <td class="px-3">Valid: </td>
+                            <td>{{ certInfo.valid }}</td>
+                        </tr>
+                        <tr class="my-3">
+                            <td class="px-3">Valid To: </td>
+                            <td>{{ certInfo.validTo ? new Date(certInfo.validTo).toLocaleString() : "" }}</td>
+                        </tr>
+                        <tr class="my-3">
+                            <td class="px-3">Days Remaining: </td>
+                            <td>{{ certInfo.daysRemaining }}</td>
+                        </tr>
+                        <tr class="my-3">
+                            <td class="px-3">Issuer: </td>
+                            <td>{{ certInfo.issuer }}</td>
+                        </tr>
+                        <tr class="my-3">
+                            <td class="px-3">Fingerprint: </td>
+                            <td>{{ certInfo.fingerprint }}</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+
     <div class="shadow-box">
         <table class="table table-borderless table-hover">
             <thead>
@@ -180,6 +212,14 @@ export default {
             }
         },
 
+        certInfo() {
+            if (this.$root.certInfoList[this.monitor.id]) {
+                return this.$root.certInfoList[this.monitor.id]
+            } else {
+                return { }
+            }
+        },
+
         displayedRecords() {
             const startIndex = this.perPage * (this.page - 1);
             const endIndex = startIndex + this.perPage;