Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Use keylog event to obtain TLS certificate for better reliability [1.23.X] #4630

Merged
merged 2 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,12 @@ class Monitor extends BeanModel {
}
}

let tlsInfo;
// Store tlsInfo when key material is received
options.httpsAgent.on("keylog", (line, tlsSocket) => {
tlsInfo = checkCertificate(tlsSocket);
});

log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);

Expand All @@ -521,29 +527,20 @@ class Monitor extends BeanModel {
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;

// Check certificate if https is used
let certInfoStartTime = dayjs().valueOf();
// Store certificate and check for expiry if https is used
if (this.getUrl()?.protocol === "https:") {
log.debug("monitor", `[${this.name}] Check cert`);
try {
let tlsInfoObject = checkCertificate(res);
tlsInfo = await this.updateTlsInfo(tlsInfoObject);

if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfoObject);
}
// No way to listen for the `secureConnection` event, so we do it here
const tlssocket = res.request.res.socket;

} catch (e) {
if (e.message !== "No TLS certificate in response") {
log.error("monitor", "Caught error");
log.error("monitor", e.message);
}
if (tlssocket) {
tlsInfo.valid = tlssocket.authorized || false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chakflying
I think this might be the source of #4693

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I had that problem with 1.23.12, too and when I comment line 536 in the container and restart it, everything is back to normal as with 1.23.11.

I have done no deep-dive into the code, but I think that the root cause for the problem when connecting over a proxy could be that it is not the target connection TLS validity, but the connection socket to the proxy here. The latter can be unencrypted, despite the target URL being HTTPS - consider the case when you have a proxy "http://192.168.4.5:3128" and a monitoring target of "https://github.com".

Whatever the case, "tlsInfo.valid" can be "undefined" here in these cases and thus the assignment fails.

}
}

if (process.env.TIMELOGGER === "1") {
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
await this.updateTlsInfo(tlsInfo);
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
await this.checkCertExpiryNotifications(tlsInfo);
}
}

if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
Expand Down
19 changes: 13 additions & 6 deletions server/util-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,20 +716,27 @@ const parseCertificateInfo = function (info) {

/**
* Check if certificate is valid
* @param {Object} res Response object from axios
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
if (!res.request.res.socket) {
throw new Error("No socket found");
exports.checkCertificate = function (socket) {
let certInfoStartTime = dayjs().valueOf();

// Return null if there is no socket
if (socket === undefined || socket == null) {
return null;
}

const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
const info = socket.getPeerCertificate(true);
const valid = socket.authorized || false;

log.debug("cert", "Parsing Certificate Info");
const parsedInfo = parseCertificateInfo(info);

if (process.env.TIMELOGGER === "1") {
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
}

return {
valid: valid,
certInfo: parsedInfo
Expand Down