Feat: Improve Certificaet Info Display

This commit is contained in:
Nelson Chan 2021-10-01 18:44:32 +08:00
parent 2aaed66b38
commit 13bdfefa9d
7 changed files with 215 additions and 103 deletions

View File

@ -59,7 +59,7 @@ class Prometheus {
} }
try { try {
monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.daysRemaining) monitor_cert_days_remaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }

View File

@ -185,38 +185,42 @@ const getDaysRemaining = (validFrom, validTo) => {
return daysRemaining; return daysRemaining;
}; };
exports.checkCertificate = function (res) { // Fix certificate Info for display
const { // param: info - the chain obtained from getPeerCertificate()
valid_from, const parseCertificateInfo = function (info) {
valid_to, let link = info;
subjectaltname,
issuer,
fingerprint,
} = res.request.res.socket.getPeerCertificate(false);
if (!valid_from || !valid_to || !subjectaltname) { while (link) {
throw { if (!link.valid_from || !link.valid_to) {
message: "No TLS certificate in response", break;
}; }
link.validTo = new Date(link.valid_to);
link.validFor = link.subjectaltname?.replace(/DNS:|IP Address:/g, "").split(", ");
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
// Move up the chain until loop is encountered
if (link.issuerCertificate == null) {
break;
} else if (link.fingerprint == link.issuerCertificate.fingerprint) {
link.issuerCertificate = null;
break;
} else {
link = link.issuerCertificate;
}
} }
return info;
};
exports.checkCertificate = function (res) {
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false; const valid = res.request.res.socket.authorized || false;
const validTo = new Date(valid_to); const parsedInfo = parseCertificateInfo(info);
const validFor = subjectaltname
.replace(/DNS:|IP Address:/g, "")
.split(", ");
const daysRemaining = getDaysRemaining(new Date(), validTo);
return { return {
valid, valid: valid,
validFor, certInfo: parsedInfo
validTo,
daysRemaining,
issuer,
fingerprint,
}; };
}; };

View File

@ -0,0 +1,30 @@
<template>
<div>
<h4>{{ $t("Certificate Info") }}</h4>
{{ $t("Certificate Chain") }}:
<div v-if="valid" class="rounded d-inline-flex ms-2 py-1 px-3 bg-success text-white">{{ $t("Valid") }}</div>
<div v-if="!valid" class="rounded d-inline-flex ms-2 py-1 px-3 bg-danger text-white">{{ $t("Invalid") }}</div>
<certificate-info-row :cert="certInfo" />
</div>
</template>
<script>
import CertificateInfoRow from "./CertificateInfoRow.vue";
export default {
components: {
CertificateInfoRow,
},
props: {
certInfo: {
type: Object,
required: true,
},
valid: {
type: Boolean,
required: true,
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,106 @@
<template>
<div>
<div class="d-flex flex-row align-items-center p-1 overflow-hidden">
<div class="m-3 ps-3">
<font-awesome-icon class="cert-icon" icon="file-contract" />
</div>
<div class="m-3">
<table class="text-start">
<tbody>
<tr class="my-3">
<td class="px-3">Subject:</td>
<td>{{ formatSubject(cert.subject) }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Valid To:</td>
<td><Datetime :value="cert.validTo" /></td>
</tr>
<tr class="my-3">
<td class="px-3">Days Remaining:</td>
<td>{{ cert.daysRemaining }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Issuer:</td>
<td>{{ formatSubject(cert.issuer) }}</td>
</tr>
<tr class="my-3">
<td class="px-3">Fingerprint:</td>
<td>{{ cert.fingerprint }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d-flex">
<font-awesome-icon
v-if="cert.issuerCertificate"
class="m-2 ps-6 link-icon"
icon="link"
/>
</div>
<certificate-info-row
v-if="cert.issuerCertificate"
:cert="cert.issuerCertificate"
/>
</div>
</template>
<script>
import Datetime from "../components/Datetime.vue";
export default {
name: "CertificateInfoRow",
components: {
Datetime,
},
props: {
cert: {
type: Object,
required: true,
},
},
methods: {
formatSubject(subject) {
if (subject.O && subject.CN && subject.C) {
return `${subject.CN} - ${subject.O} (${subject.C})`;
} else if (subject.O && subject.CN) {
return `${subject.CN} - ${subject.O}`;
} else if (subject.CN) {
return subject.CN;
} else {
return "no info";
}
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
table {
overflow: hidden;
}
.cert-icon {
font-size: 70px;
color: $link-color;
opacity: 0.5;
.dark & {
color: $dark-font-color;
opacity: 0.3;
}
}
.link-icon {
font-size: 20px;
margin-left: 50px !important;
color: $link-color;
opacity: 0.5;
.dark & {
color: $dark-font-color;
opacity: 0.3;
}
}
</style>

View File

@ -30,6 +30,8 @@ import {
faUpload, faUpload,
faCopy, faCopy,
faCheck, faCheck,
faFileContract,
faLink,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
@ -59,6 +61,8 @@ library.add(
faUpload, faUpload,
faCopy, faCopy,
faCheck, faCheck,
faFileContract,
faLink,
); );
export { FontAwesomeIcon }; export { FontAwesomeIcon };

View File

@ -30,7 +30,7 @@ export default {
importantHeartbeatList: { }, importantHeartbeatList: { },
avgPingList: { }, avgPingList: { },
uptimeList: { }, uptimeList: { },
certInfoList: {}, tlsInfoList: {},
notificationList: [], notificationList: [],
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...", connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
}; };
@ -154,7 +154,7 @@ export default {
}); });
socket.on("certInfo", (monitorID, data) => { socket.on("certInfo", (monitorID, data) => {
this.certInfoList[monitorID] = JSON.parse(data); this.tlsInfoList[monitorID] = JSON.parse(data);
}); });
socket.on("importantHeartbeatList", (monitorID, data, overwrite) => { socket.on("importantHeartbeatList", (monitorID, data, overwrite) => {

View File

@ -73,11 +73,11 @@
<span class="num"><Uptime :monitor="monitor" type="720" /></span> <span class="num"><Uptime :monitor="monitor" type="720" /></span>
</div> </div>
<div v-if="certInfo" class="col"> <div v-if="tlsInfo" class="col">
<h4>{{ $t("Cert Exp.") }}</h4> <h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="certInfo.validTo" date-only />)</p> <p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num"> <span class="num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ certInfo.daysRemaining }} {{ $t("days") }}</a> <a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a>
</span> </span>
</div> </div>
</div> </div>
@ -87,41 +87,7 @@
<div v-if="showCertInfoBox" class="shadow-box big-padding text-center"> <div v-if="showCertInfoBox" class="shadow-box big-padding text-center">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h4>{{ $t("Certificate Info") }}</h4> <certificate-info :certInfo="tlsInfo.certInfo" :valid="tlsInfo.valid" />
<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><Datetime :value="certInfo.validTo" /></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>
</div> </div>
@ -207,8 +173,8 @@
<script> <script>
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent } from "vue";
import { useToast } from "vue-toastification" import { useToast } from "vue-toastification";
const toast = useToast() const toast = useToast();
import Confirm from "../components/Confirm.vue"; import Confirm from "../components/Confirm.vue";
import HeartbeatBar from "../components/HeartbeatBar.vue"; import HeartbeatBar from "../components/HeartbeatBar.vue";
import Status from "../components/Status.vue"; import Status from "../components/Status.vue";
@ -218,6 +184,7 @@ import Uptime from "../components/Uptime.vue";
import Pagination from "v-pagination-3"; import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue")); const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue";
export default { export default {
components: { components: {
@ -230,6 +197,7 @@ export default {
Pagination, Pagination,
PingChart, PingChart,
Tag, Tag,
CertificateInfo,
}, },
data() { data() {
return { return {
@ -239,32 +207,32 @@ export default {
toggleCertInfoBox: false, toggleCertInfoBox: false,
showPingChartBox: true, showPingChartBox: true,
paginationConfig: { paginationConfig: {
texts:{ texts: {
count:`${this.$t("Showing {from} to {to} of {count} records")}|{count} ${this.$t("records")}|${this.$t("One record")}`, count: `${this.$t("Showing {from} to {to} of {count} records")}|{count} ${this.$t("records")}|${this.$t("One record")}`,
first:this.$t("First"), first: this.$t("First"),
last:this.$t("Last"), last: this.$t("Last"),
nextPage:'>', nextPage: ">",
nextChunk:'>>', nextChunk: ">>",
prevPage:'<', prevPage: "<",
prevChunk:'<<' prevChunk: "<<"
} }
} }
} };
}, },
computed: { computed: {
monitor() { monitor() {
let id = this.$route.params.id let id = this.$route.params.id;
return this.$root.monitorList[id]; return this.$root.monitorList[id];
}, },
lastHeartBeat() { lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) { if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id] return this.$root.lastHeartbeatList[this.monitor.id];
} }
return { return {
status: -1, status: -1,
} };
}, },
ping() { ping() {
@ -272,7 +240,7 @@ export default {
return this.lastHeartBeat.ping; return this.lastHeartBeat.ping;
} }
return this.$t("notAvailableShort") return this.$t("notAvailableShort");
}, },
avgPing() { avgPing() {
@ -280,14 +248,14 @@ export default {
return this.$root.avgPingList[this.monitor.id]; return this.$root.avgPingList[this.monitor.id];
} }
return this.$t("notAvailableShort") return this.$t("notAvailableShort");
}, },
importantHeartBeatList() { importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) { if (this.$root.importantHeartbeatList[this.monitor.id]) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties // eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id]; this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id] return this.$root.importantHeartbeatList[this.monitor.id];
} }
return []; return [];
@ -295,22 +263,22 @@ export default {
status() { status() {
if (this.$root.statusList[this.monitor.id]) { if (this.$root.statusList[this.monitor.id]) {
return this.$root.statusList[this.monitor.id] return this.$root.statusList[this.monitor.id];
} }
return { } return { };
}, },
certInfo() { tlsInfo() {
if (this.$root.certInfoList[this.monitor.id]) { if (this.$root.tlsInfoList[this.monitor.id]) {
return this.$root.certInfoList[this.monitor.id] return this.$root.tlsInfoList[this.monitor.id];
} }
return null return null;
}, },
showCertInfoBox() { showCertInfoBox() {
return this.certInfo != null && this.toggleCertInfoBox; return this.tlsInfo != null && this.toggleCertInfoBox;
}, },
displayedRecords() { displayedRecords() {
@ -324,8 +292,8 @@ export default {
}, },
methods: { methods: {
testNotification() { testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id) this.$root.getSocket().emit("testNotification", this.monitor.id);
toast.success("Test notification is requested.") toast.success("Test notification is requested.");
}, },
pauseDialog() { pauseDialog() {
@ -334,14 +302,14 @@ export default {
resumeMonitor() { resumeMonitor() {
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => { this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res) this.$root.toastRes(res);
}) });
}, },
pauseMonitor() { pauseMonitor() {
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => { this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res) this.$root.toastRes(res);
}) });
}, },
deleteDialog() { deleteDialog() {
@ -360,11 +328,11 @@ export default {
this.$root.deleteMonitor(this.monitor.id, (res) => { this.$root.deleteMonitor(this.monitor.id, (res) => {
if (res.ok) { if (res.ok) {
toast.success(res.msg); toast.success(res.msg);
this.$router.push("/dashboard") this.$router.push("/dashboard");
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
clearEvents() { clearEvents() {
@ -372,7 +340,7 @@ export default {
if (! res.ok) { if (! res.ok) {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
clearHeartbeats() { clearHeartbeats() {
@ -380,13 +348,13 @@ export default {
if (! res.ok) { if (! res.ok) {
toast.error(res.msg); toast.error(res.msg);
} }
}) });
}, },
pingTitle(average = false) { pingTitle(average = false) {
let translationPrefix = "" let translationPrefix = "";
if (average) { if (average) {
translationPrefix = "Avg. " translationPrefix = "Avg. ";
} }
if (this.monitor.type === "http") { if (this.monitor.type === "http") {
@ -396,7 +364,7 @@ export default {
return this.$t(translationPrefix + "Ping"); return this.$t(translationPrefix + "Ping");
}, },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>