From fc33fb01a5800561bba486e4e2d4215a6ccc83d8 Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Tue, 22 Oct 2024 15:44:56 -0700 Subject: [PATCH] update --- .codeclimate.yml | 8 +- .eslintrc.yaml | 4 +- .github/workflows/codeql.yml | 2 +- .prettierrc | 2 + README.md | 3 +- html/client.js | 442 ++++++++--------- html/index.html | 32 +- html/jquery.tipsy.js | 334 ++++++------- html/watch.css | 2 +- index.js | 902 +++++++++++++++++------------------ test/watch.js | 68 +-- 11 files changed, 899 insertions(+), 900 deletions(-) create mode 100644 .prettierrc diff --git a/.codeclimate.yml b/.codeclimate.yml index 261f7f0..b43f449 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,16 +1,16 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-8' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' ratings: paths: - - "**.js" + - '**.js' exclude_patterns: - - "html/jquery.tipsy.js" + - 'html/jquery.tipsy.js' checks: return-statements: diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 0fcbe84..7c9f547 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -6,7 +6,7 @@ env: browser: true es2022: true -extends: "@haraka" +extends: '@haraka' rules: - no-unused-vars: ["error", { args: none }] + no-unused-vars: ['error', { args: none }] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7e97bed..8314a66 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,7 +7,7 @@ on: # The branches below must be a subset of the branches above branches: [master] schedule: - - cron: "18 7 * * 4" + - cron: '18 7 * * 4' jobs: codeql: diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/README.md b/README.md index 529a220..f33e4b1 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,7 @@ Enjoy the blinky lights. ## Tips -- Hover your mouse pointer or tap (with touch devices) on table data to see more - details. +- Hover your mouse pointer or tap (with touch devices) on table data to see more details. - Copy that connection UUID at left and use it to grep your logs for even more. - Edit the files in watch/html and play with the appearance. If you make it better, post a screen shot somewhere and create an Issue or PR. diff --git a/html/client.js b/html/client.js index 155ac43..ca8a0bc 100644 --- a/html/client.js +++ b/html/client.js @@ -1,36 +1,36 @@ -"use strict"; - -let ws; -let connect_cols; -let helo_cols; -let mail_from_cols; -let rcpt_to_cols; -let data_cols; -let total_cols; -let cxn_cols; -let txn_cols; - -const connect_plugins = ["geoip", "asn", "p0f", "dns-list", "access", "fcrdns"]; -const helo_plugins = ["helo.checks", "tls", "auth", "relay", "spf"]; -const mail_from_plugins = ["spf", "mail_from.is_resolvable", "known-senders"]; +'use strict' + +let ws +let connect_cols +let helo_cols +let mail_from_cols +let rcpt_to_cols +let data_cols +let total_cols +let cxn_cols +let txn_cols + +const connect_plugins = ['geoip', 'asn', 'p0f', 'dns-list', 'access', 'fcrdns'] +const helo_plugins = ['helo.checks', 'tls', 'auth', 'relay', 'spf'] +const mail_from_plugins = ['spf', 'mail_from.is_resolvable', 'known-senders'] const rcpt_to_plugins = [ - "queue/smtp_forward", - "rcpt_to.in_host_list", - "qmail-deliverable", -]; + 'queue/smtp_forward', + 'rcpt_to.in_host_list', + 'qmail-deliverable', +] const data_plugins = [ - "early_talker", - "bounce", - "headers", - "karma", - "spamassassin", - "rspamd", - "clamd", - "uribl", - "limit", - "dkim", - "attachment", -]; + 'early_talker', + 'bounce', + 'headers', + 'karma', + 'spamassassin', + 'rspamd', + 'clamd', + 'uribl', + 'limit', + 'dkim', + 'attachment', +] // 'seen' plugins are ones we've seen data reported for. When data from a new // plugin arrives, it gets added to one of the sections above and the table is // redrawn. @@ -39,28 +39,28 @@ const seen_plugins = connect_plugins.concat( mail_from_plugins, rcpt_to_plugins, data_plugins, -); +) const ignore_seen = [ - "local_port", - "remote_host", - "helo", - "mail_from", - "rcpt_to", - "queue", -]; + 'local_port', + 'remote_host', + 'helo', + 'mail_from', + 'rcpt_to', + 'queue', +] -let rows_showing = 0; +let rows_showing = 0 function newRowConnectRow1(data, uuid, txnId) { - const host = data.remote_host || { title: "", newval: "" }; - const port = data.local_port ? data.local_port.newval || "25" : "25"; + const host = data.remote_host || { title: '', newval: '' } + const port = data.local_port ? data.local_port.newval || '25' : '25' if (txnId > 1) { return [ ``, `${txnId}`, ``, - ]; + ] } return [ @@ -70,359 +70,359 @@ function newRowConnectRow1(data, uuid, txnId) { `${host.newval}`, `${port}`, ``, - ]; + ] } function newRowConnectRow2(data, uuid, txnId) { - if (txnId > 1) return ""; + if (txnId > 1) return '' - const res = []; + const res = [] connect_plugins.forEach((plugin) => { - let nv = shorten_pi(plugin); - let newc = ""; - let tit = ""; + let nv = shorten_pi(plugin) + let newc = '' + let tit = '' if (data[plugin]) { // not always updated - if (data[plugin].classy) newc = data[plugin].classy; - if (data[plugin].newval) nv = data[plugin].newval; - if (data[plugin].title) tit = data[plugin].title; + if (data[plugin].classy) newc = data[plugin].classy + if (data[plugin].newval) nv = data[plugin].newval + if (data[plugin].title) tit = data[plugin].title } res.push( `${nv}`, - ); - }); - return res.join(""); + ) + }) + return res.join('') } function newRowHelo(data, uuid, txnId) { - if (txnId > 1) return ""; + if (txnId > 1) return '' - const cols = []; + const cols = [] helo_plugins.forEach((plugin) => { - cols.push(`${shorten_pi(plugin)}`); - }); - return cols.join("\n"); + cols.push(`${shorten_pi(plugin)}`) + }) + return cols.join('\n') } function newRow(data, uuid) { - const txnId = uuid.split("_").pop(); - const rowResult = newRowConnectRow1(data, uuid, txnId); + const txnId = uuid.split('_').pop() + const rowResult = newRowConnectRow1(data, uuid, txnId) rowResult.push( ``, ``, - ); + ) data_plugins.slice(0, data_cols).forEach((plugin) => { - rowResult.push(`${shorten_pi(plugin)}`); - }); + rowResult.push(`${shorten_pi(plugin)}`) + }) rowResult.push( '', ``, - ); + ) - rowResult.push(newRowConnectRow2(data, uuid, txnId)); - rowResult.push(newRowHelo(data, uuid, txnId)); + rowResult.push(newRowConnectRow2(data, uuid, txnId)) + rowResult.push(newRowHelo(data, uuid, txnId)) // transaction data mail_from_plugins.forEach((plugin) => { - rowResult.push(`${shorten_pi(plugin)}`); - }); + rowResult.push(`${shorten_pi(plugin)}`) + }) rcpt_to_plugins.forEach((plugin) => { - rowResult.push(`${shorten_pi(plugin)}`); - }); + rowResult.push(`${shorten_pi(plugin)}`) + }) data_plugins.slice(data_cols, data_plugins.length).forEach((plugin) => { - rowResult.push(`${shorten_pi(plugin)}`); - }); - rowResult.push(""); + rowResult.push(`${shorten_pi(plugin)}`) + }) + rowResult.push('') if (txnId > 1) { - const prevUuid = `${uuid.split("_").slice(0, 2).join("_")}_${txnId - 1}`; - const lastRow = $(`#connections > tbody > tr.${prevUuid}`).last(); + const prevUuid = `${uuid.split('_').slice(0, 2).join('_')}_${txnId - 1}` + const lastRow = $(`#connections > tbody > tr.${prevUuid}`).last() if (lastRow) { lastRow .hide() - .after($(rowResult.join("\n"))) - .fadeIn("slow"); + .after($(rowResult.join('\n'))) + .fadeIn('slow') } } else { - $(rowResult.join("\n")) + $(rowResult.join('\n')) .hide() - .prependTo("table#connections > tbody") - .fadeIn(800); + .prependTo('table#connections > tbody') + .fadeIn(800) } - connect_plugins.concat(["remote_host", "local_port"]).forEach((plugin) => { - $(`table#connections > tbody > tr.${uuid}> td.${css_safe(plugin)}`).tipsy(); - }); + connect_plugins.concat(['remote_host', 'local_port']).forEach((plugin) => { + $(`table#connections > tbody > tr.${uuid}> td.${css_safe(plugin)}`).tipsy() + }) } function updateRow(row_data, selector) { // each bit of data in the WSS sent object represents a TD in the table for (const td_name in row_data) { - const td = row_data[td_name]; - if (typeof td !== "object") continue; + const td = row_data[td_name] + if (typeof td !== 'object') continue - const td_name_css = css_safe(td_name); - let td_sel = `${selector} > td.${td_name_css}`; + const td_name_css = css_safe(td_name) + let td_sel = `${selector} > td.${td_name_css}` - if (td_name === "spf") { - if (td.scope === "helo") { - td_sel = `${td_sel}:first`; + if (td_name === 'spf') { + if (td.scope === 'helo') { + td_sel = `${td_sel}:first` } else { - td_sel = `${td_sel}:last`; + td_sel = `${td_sel}:last` } } - update_seen(td_name); + update_seen(td_name) // $('#messages').append(`, ${td_name}: `); if (td.classy) { $(td_sel) - .attr("class", td_name_css) // reset class + .attr('class', td_name_css) // reset class .addClass(td.classy) - .tipsy(); + .tipsy() } if (td.title) { $(td_sel) - .attr("title", `${$(td_sel).attr("title") || ""} ${td.title}`) - .tipsy(); + .attr('title', `${$(td_sel).attr('title') || ''} ${td.title}`) + .tipsy() } - if (td.newval) $(td_sel).html(td.newval).tipsy(); + if (td.newval) $(td_sel).html(td.newval).tipsy() } - $(`${selector} > td`).tipsy(); + $(`${selector} > td`).tipsy() } function httpGetJSON(theUrl) { - let xmlHttp = null; - xmlHttp = new XMLHttpRequest(); - xmlHttp.open("GET", theUrl, false); - xmlHttp.send(null); - return JSON.parse(xmlHttp.responseText); + let xmlHttp = null + xmlHttp = new XMLHttpRequest() + xmlHttp.open('GET', theUrl, false) + xmlHttp.send(null) + return JSON.parse(xmlHttp.responseText) } function ws_connect() { if (!window.location.origin) { - window.location.origin = `${window.location.protocol}//${window.location.hostname}`; + window.location.origin = `${window.location.protocol}//${window.location.hostname}` if (window.location.port) - window.location.origin += `:${window.location.port}`; + window.location.origin += `:${window.location.port}` } - const config = httpGetJSON(`${window.location.origin}/watch/wss_conf`); + const config = httpGetJSON(`${window.location.origin}/watch/wss_conf`) if (!config.wss_url) { - config.wss_url = `wss://${window.location.hostname}`; - if (window.location.port) config.wss_url += `:${window.location.port}`; + config.wss_url = `wss://${window.location.hostname}` + if (window.location.port) config.wss_url += `:${window.location.port}` } - ws = new WebSocket(config.wss_url); + ws = new WebSocket(config.wss_url) ws.onopen = function () { // ws.send('something'); // send a message to the server // $('#messages').append("connected "); - $("span#connect_state").removeClass().addClass("green"); + $('span#connect_state').removeClass().addClass('green') if (config.sampling) { - $("#messages").append("sampling"); + $('#messages').append('sampling') } - }; + } ws.onerror = function (err) { - $("#messages").append(`${err}, ${err.message}`); - }; + $('#messages').append(`${err}, ${err.message}`) + } ws.onclose = function () { // $('#messages').append('closed '); - $("span#connect_state").removeClass().addClass("red"); - reconnect(); - }; + $('span#connect_state').removeClass().addClass('red') + reconnect() + } - let last_insert = 0; + let last_insert = 0 // let sampled_out = 0; ws.onmessage = function (event, flags) { // flags.binary will be set if a binary data is received // flags.masked will be set if the data was masked - const data = JSON.parse(event.data); + const data = JSON.parse(event.data) if (data.msg) { - $("#messages").append(`${data.msg} `); - return; + $('#messages').append(`${data.msg} `) + return } if (data.watchers) { - $("span#watchers").html(data.watchers); - return; + $('span#watchers').html(data.watchers) + return } if (data.uuid === undefined) { - $("#messages").append(" ERROR, no uuid: "); - return; + $('#messages').append(' ERROR, no uuid: ') + return } - const css_valid_uuid = get_css_safe_uuid(data.uuid); - const selector = `table#connections > tbody > tr.${css_valid_uuid}`; + const css_valid_uuid = get_css_safe_uuid(data.uuid) + const selector = `table#connections > tbody > tr.${css_valid_uuid}` if ($(selector).length) { // if the row exists - updateRow(data, selector); - return; + updateRow(data, selector) + return } // row doesn't exist (yet) - let now; + let now if (config.sampling) { - now = new Date().getTime(); + now = new Date().getTime() if (now - last_insert < 1000) { // sampled_out++; // $('#messages').append("so:" + sampled_out); - return; + return } } // time to send a new row - newRow(data, css_valid_uuid); - prune_table(); - last_insert = now; - }; + newRow(data, css_valid_uuid) + prune_table() + last_insert = now + } } function reconnect() { setTimeout(function () { - ws_connect(); - }, 3 * 1000); + ws_connect() + }, 3 * 1000) } function update_seen(plugin) { - if (seen_plugins.indexOf(plugin) !== -1) return; - if (ignore_seen.indexOf(plugin) !== -1) return; + if (seen_plugins.indexOf(plugin) !== -1) return + if (ignore_seen.indexOf(plugin) !== -1) return - seen_plugins.push(plugin); + seen_plugins.push(plugin) - let bits = plugin.split("."); + let bits = plugin.split('.') if (bits.length === 2) { switch ( bits[0] // phase prefix ) { - case "connect": - connect_plugins.push(plugin); - break; - case "helo": - helo_plugins.push(plugin); - break; - case "mail_from": - mail_from_plugins.push(plugin); - break; - case "rcpt_to": - rcpt_to_plugins.push(plugin); - break; - case "data": - data_plugins.push(plugin); - break; + case 'connect': + connect_plugins.push(plugin) + break + case 'helo': + helo_plugins.push(plugin) + break + case 'mail_from': + mail_from_plugins.push(plugin) + break + case 'rcpt_to': + rcpt_to_plugins.push(plugin) + break + case 'data': + data_plugins.push(plugin) + break } - $("#messages").append(`, refresh(${plugin}) `); - return reset_table(); + $('#messages').append(`, refresh(${plugin}) `) + return reset_table() } - bits = plugin.split("/"); + bits = plugin.split('/') if (bits.length === 2) { switch (bits[0]) { - case "auth": // gets coalesced under the 'HELO auth' box - return; + case 'auth': // gets coalesced under the 'HELO auth' box + return } } - $("#messages").append(`, uncategorized(${plugin}) `); - data_plugins.push(plugin); - return reset_table(); + $('#messages').append(`, uncategorized(${plugin}) `) + data_plugins.push(plugin) + return reset_table() } function prune_table() { - rows_showing++; - const max = 200; - if (rows_showing < max) return; + rows_showing++ + const max = 200 + if (rows_showing < max) return $(`table#connections > tbody > tr:gt(${max * 3})`).fadeOut(2000, () => { - $(this).remove(); - }); - rows_showing = $("table#connections > tbody > tr").length; + $(this).remove() + }) + rows_showing = $('table#connections > tbody > tr').length } function reset_table() { // after results for a 'new' plugin that we've never seen arrives, remove // the old rows so the table formatting isn't b0rked - $("table#connections > tbody > tr").fadeOut(5000, () => { - $(this).remove(); - }); - countPhaseCols(); - display_th(); + $('table#connections > tbody > tr').fadeOut(5000, () => { + $(this).remove() + }) + countPhaseCols() + display_th() } function display_th() { - $("table#connections > thead > tr#labels") + $('table#connections > thead > tr#labels') .html( [ - "ID", + 'ID', `CONNECT`, `HELO`, `MAIL FROM`, `RCPT TO`, `DATA`, 'QUEUE', - ].join("\n\t"), + ].join('\n\t'), ) - .tipsy(); - $("table#connections > thead > tr#labels > th").tipsy(); - $("table#connections > tfoot > tr#helptext").html( + .tipsy() + $('table#connections > thead > tr#labels > th').tipsy() + $('table#connections > tfoot > tr#helptext').html( `For a good time: nc ${window.location.hostname} 587`, - ); + ) } function countPhaseCols() { - connect_cols = connect_plugins.length; - helo_cols = helo_plugins.length; - mail_from_cols = mail_from_plugins.length; - rcpt_to_cols = rcpt_to_plugins.length; - data_cols = Math.ceil(data_plugins.length / 2); - cxn_cols = connect_cols + helo_cols; - txn_cols = mail_from_cols + rcpt_to_cols + data_cols; - total_cols = cxn_cols + txn_cols + 3; + connect_cols = connect_plugins.length + helo_cols = helo_plugins.length + mail_from_cols = mail_from_plugins.length + rcpt_to_cols = rcpt_to_plugins.length + data_cols = Math.ceil(data_plugins.length / 2) + cxn_cols = connect_cols + helo_cols + txn_cols = mail_from_cols + rcpt_to_cols + data_cols + total_cols = cxn_cols + txn_cols + 3 } function css_safe(str) { - return str.replace(/([^0-9a-zA-Z\-_])/g, "_"); + return str.replace(/([^0-9a-zA-Z\-_])/g, '_') // http://www.w3.org/TR/CSS21/syndata.html#characters // identifiers can contain only [a-zA-Z0-9] plus - and _ } function shorten_pi(name) { const trims = { - spamassassin: "spam", - early_talker: "early", - "rcpt_to.qmail_deliverable": "qmd", - "qmail-deliverable": "qmd", - "rcpt_to.in_host_list": "host_list", - "mail_from.is_resolvable": "dns", - "known-senders": "known", - "queue/smtp_forward": "forward", - smtp_forward: "forward", - attachment: "attach", - }; - - if (trims[name]) return trims[name]; - - const parts = name.split("."); + spamassassin: 'spam', + early_talker: 'early', + 'rcpt_to.qmail_deliverable': 'qmd', + 'qmail-deliverable': 'qmd', + 'rcpt_to.in_host_list': 'host_list', + 'mail_from.is_resolvable': 'dns', + 'known-senders': 'known', + 'queue/smtp_forward': 'forward', + smtp_forward: 'forward', + attachment: 'attach', + } + + if (trims[name]) return trims[name] + + const parts = name.split('.') switch (parts[0]) { - case "helo": - case "connect": - case "mail_from": - case "rcpt_to": - case "data": - case "queue": - return parts.slice(1).join("."); + case 'helo': + case 'connect': + case 'mail_from': + case 'rcpt_to': + case 'data': + case 'queue': + return parts.slice(1).join('.') } - return name; + return name } function get_css_safe_uuid(uuid) { @@ -431,12 +431,12 @@ function get_css_safe_uuid(uuid) { // CAF2B05E-5382-4E65-A51E-7DEE6EF31F80.1 // bits.length=2 // CAF2B05E-5382-4E65-A51E-7DEE6EF31F80.2 - const bits = uuid.split("."); + const bits = uuid.split('.') if (bits.length === 1) { - bits[1] = 1; + bits[1] = 1 } - return `aa_${bits[0].replace(/[_-]/g, "")}_${bits[1]}`; + return `aa_${bits[0].replace(/[_-]/g, '')}_${bits[1]}` } -countPhaseCols(); +countPhaseCols() diff --git a/html/index.html b/html/index.html index 44d72a7..9ad131a 100644 --- a/html/index.html +++ b/html/index.html @@ -47,14 +47,14 @@
@@ -132,29 +132,29 @@

diff --git a/html/jquery.tipsy.js b/html/jquery.tipsy.js index 3f1e19c..a307f46 100644 --- a/html/jquery.tipsy.js +++ b/html/jquery.tipsy.js @@ -6,170 +6,170 @@ // Modified by Atlassian // https://github.com/atlassian/tipsy -(function ($) { +;(function ($) { var liveBindingWarning = - "To be compatible with jQuery 1.9 and higher," + + 'To be compatible with jQuery 1.9 and higher,' + " You must pass a selector to tipsy's live argument." + - " For instance, `$(document).tipsy({live: 'a.live'});`"; + " For instance, `$(document).tipsy({live: 'a.live'});`" function maybeCall(thing, ctx) { - return typeof thing == "function" ? thing.call(ctx) : thing; + return typeof thing == 'function' ? thing.call(ctx) : thing } function isElementInDOM(ele) { - var el = ele && ele.jquery ? ele.get(0) : ele; - return $.contains(document.documentElement, el); + var el = ele && ele.jquery ? ele.get(0) : ele + return $.contains(document.documentElement, el) } - var tipsyIDcounter = 0; + var tipsyIDcounter = 0 function tipsyID() { - var tipsyID = tipsyIDcounter++; - return "tipsyuid" + tipsyID; + var tipsyID = tipsyIDcounter++ + return 'tipsyuid' + tipsyID } function Tipsy(element, options) { - this.$element = $(element); - this.options = options; - this.enabled = true; - this.fixTitle(); + this.$element = $(element) + this.options = options + this.enabled = true + this.fixTitle() } Tipsy.prototype = { show: function () { // if element is not in the DOM then don't show the Tipsy and return early if (!isElementInDOM(this.$element)) { - return; + return } - var title = this.getTitle(); + var title = this.getTitle() if (!title || !this.enabled) { - return; + return } - var $tip = this.tip(); + var $tip = this.tip() - $tip.find(".tipsy-inner")[this.options.html ? "html" : "text"](title); - $tip[0].className = "tipsy"; // reset classname in case of dynamic gravity + $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title) + $tip[0].className = 'tipsy' // reset classname in case of dynamic gravity $tip .remove() .css({ top: 0, left: 0, - visibility: "hidden", - display: "block", + visibility: 'hidden', + display: 'block', }) - .appendTo(document.body); + .appendTo(document.body) - var that = this; + var that = this function tipOver() { - that.hoverTooltip = true; + that.hoverTooltip = true } function tipOut() { - if (that.hoverState == "in") return; // If field is still focused. - that.hoverTooltip = false; - if (that.options.trigger != "manual") { + if (that.hoverState == 'in') return // If field is still focused. + that.hoverTooltip = false + if (that.options.trigger != 'manual') { var eventOut = - that.options.trigger == "hover" ? "mouseleave.tipsy" : "blur.tipsy"; - that.$element.trigger(eventOut); + that.options.trigger == 'hover' ? 'mouseleave.tipsy' : 'blur.tipsy' + that.$element.trigger(eventOut) } } if (this.options.hoverable) { - $tip.hover(tipOver, tipOut); + $tip.hover(tipOver, tipOut) } if (this.options.className) { - $tip.addClass(maybeCall(this.options.className, this.$element[0])); + $tip.addClass(maybeCall(this.options.className, this.$element[0])) } var pos = $.extend({}, this.$element.offset(), { width: this.$element[0].getBoundingClientRect().width, height: this.$element[0].getBoundingClientRect().height, - }); + }) - var tipCss = {}; + var tipCss = {} var actualWidth = $tip[0].offsetWidth, - actualHeight = $tip[0].offsetHeight; - var gravity = maybeCall(this.options.gravity, this.$element[0]); + actualHeight = $tip[0].offsetHeight + var gravity = maybeCall(this.options.gravity, this.$element[0]) if (gravity.length === 2) { - if (gravity.charAt(1) === "w") { - tipCss.left = pos.left + pos.width / 2 - 15; + if (gravity.charAt(1) === 'w') { + tipCss.left = pos.left + pos.width / 2 - 15 } else { - tipCss.left = pos.left + pos.width / 2 - actualWidth + 15; + tipCss.left = pos.left + pos.width / 2 - actualWidth + 15 } } switch (gravity.charAt(0)) { - case "n": + case 'n': // left could already be set if gravity is 'nw' or 'ne' - if (typeof tipCss.left === "undefined") { - tipCss.left = pos.left + pos.width / 2 - actualWidth / 2; + if (typeof tipCss.left === 'undefined') { + tipCss.left = pos.left + pos.width / 2 - actualWidth / 2 } - tipCss.top = pos.top + pos.height + this.options.offset; - break; - case "s": + tipCss.top = pos.top + pos.height + this.options.offset + break + case 's': // left could already be set if gravity is 'sw' or 'se' - if (typeof tipCss.left === "undefined") { - tipCss.left = pos.left + pos.width / 2 - actualWidth / 2; + if (typeof tipCss.left === 'undefined') { + tipCss.left = pos.left + pos.width / 2 - actualWidth / 2 // We need to apply the left positioning and then recalculate the tooltip height // If the tooltip is positioned close to the right edge of the window, it could cause // the tooltip text to overflow and change height. - $tip.css(tipCss); - actualHeight = $tip[0].offsetHeight; + $tip.css(tipCss) + actualHeight = $tip[0].offsetHeight } - tipCss.top = pos.top - actualHeight - this.options.offset; - break; - case "e": - tipCss.left = pos.left - actualWidth - this.options.offset; - tipCss.top = pos.top + pos.height / 2 - actualHeight / 2; - break; - case "w": - tipCss.left = pos.left + pos.width + this.options.offset; - tipCss.top = pos.top + pos.height / 2 - actualHeight / 2; - break; + tipCss.top = pos.top - actualHeight - this.options.offset + break + case 'e': + tipCss.left = pos.left - actualWidth - this.options.offset + tipCss.top = pos.top + pos.height / 2 - actualHeight / 2 + break + case 'w': + tipCss.left = pos.left + pos.width + this.options.offset + tipCss.top = pos.top + pos.height / 2 - actualHeight / 2 + break } - $tip.css(tipCss).addClass("tipsy-" + gravity); - $tip.find(".tipsy-arrow")[0].className = - "tipsy-arrow tipsy-arrow-" + gravity.charAt(0); + $tip.css(tipCss).addClass('tipsy-' + gravity) + $tip.find('.tipsy-arrow')[0].className = + 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0) if (this.options.fade) { $tip .stop() .css({ opacity: 0, - display: "block", - visibility: "visible", + display: 'block', + visibility: 'visible', }) - .animate({ opacity: this.options.opacity }); + .animate({ opacity: this.options.opacity }) } else { $tip.css({ - visibility: "visible", + visibility: 'visible', opacity: this.options.opacity, - }); + }) } if (this.options.aria) { - var $tipID = tipsyID(); - $tip.attr("id", $tipID); - this.$element.attr("aria-describedby", $tipID); + var $tipID = tipsyID() + $tip.attr('id', $tipID) + this.$element.attr('aria-describedby', $tipID) } }, destroy: function () { - this.$element.removeData("tipsy"); + this.$element.removeData('tipsy') - this.unbindHandlers(); - this.hide(); + this.unbindHandlers() + this.hide() }, unbindHandlers: function () { if (this.options.live) { - $(document).off(".tipsy", this.options.live); + $(document).off('.tipsy', this.options.live) } else { - this.$element.off(".tipsy"); + this.$element.off('.tipsy') } }, @@ -178,37 +178,37 @@ this.tip() .stop() .fadeOut(function () { - $(this).remove(); - }); + $(this).remove() + }) } else { - this.tip().remove(); + this.tip().remove() } if (this.options.aria) { - this.$element.removeAttr("aria-describedby"); + this.$element.removeAttr('aria-describedby') } }, fixTitle: function () { - var $e = this.$element; - if ($e.attr("title") || typeof $e.attr("original-title") != "string") { - $e.attr("original-title", $e.attr("title") || "").removeAttr("title"); + var $e = this.$element + if ($e.attr('title') || typeof $e.attr('original-title') != 'string') { + $e.attr('original-title', $e.attr('title') || '').removeAttr('title') } }, getTitle: function () { var title, $e = this.$element, - o = this.options; - this.fixTitle(); + o = this.options + this.fixTitle() var title, - o = this.options; - if (typeof o.title == "string") { - title = $e.attr(o.title == "title" ? "original-title" : o.title); - } else if (typeof o.title == "function") { - title = o.title.call($e[0]); + o = this.options + if (typeof o.title == 'string') { + title = $e.attr(o.title == 'title' ? 'original-title' : o.title) + } else if (typeof o.title == 'function') { + title = o.title.call($e[0]) } - title = ("" + title).replace(/(^\s*|\s*$)/, ""); - return title || o.fallback; + title = ('' + title).replace(/(^\s*|\s*$)/, '') + return title || o.fallback }, tip: function () { @@ -217,92 +217,92 @@ .html( '
', ) - .attr("role", "tooltip"); - this.$tip.data("tipsy-pointee", this.$element[0]); + .attr('role', 'tooltip') + this.$tip.data('tipsy-pointee', this.$element[0]) } - return this.$tip; + return this.$tip }, validate: function () { if (!this.$element[0].parentNode) { - this.hide(); - this.$element = null; - this.options = null; + this.hide() + this.$element = null + this.options = null } }, enable: function () { - this.enabled = true; + this.enabled = true }, disable: function () { - this.enabled = false; + this.enabled = false }, toggleEnabled: function () { - this.enabled = !this.enabled; + this.enabled = !this.enabled }, - }; + } $.fn.tipsy = function (options) { if (options === true) { - return this.data("tipsy"); - } else if (typeof options == "string") { - var tipsy = this.data("tipsy"); - if (tipsy) tipsy[options](); - return this; + return this.data('tipsy') + } else if (typeof options == 'string') { + var tipsy = this.data('tipsy') + if (tipsy) tipsy[options]() + return this } - options = $.extend({}, $.fn.tipsy.defaults, options); + options = $.extend({}, $.fn.tipsy.defaults, options) if (options.hoverable) { - options.delayOut = options.delayOut || 20; + options.delayOut = options.delayOut || 20 } // Check for jQuery support and patch live binding for jQuery 3 compat. if (options.live === true) { if (!this.selector) { // No more jQuery support! - throw new Error(liveBindingWarning); + throw new Error(liveBindingWarning) } else { // Deprecated behaviour - console && console.warn && console.warn(liveBindingWarning); - options.live = this.selector; + console && console.warn && console.warn(liveBindingWarning) + options.live = this.selector } } function get(ele) { - var tipsy = $.data(ele, "tipsy"); + var tipsy = $.data(ele, 'tipsy') if (!tipsy) { - tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); - $.data(ele, "tipsy", tipsy); + tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)) + $.data(ele, 'tipsy', tipsy) } - return tipsy; + return tipsy } function enter() { - var tipsy = get(this); - tipsy.hoverState = "in"; + var tipsy = get(this) + tipsy.hoverState = 'in' if (options.delayIn == 0) { - tipsy.show(); + tipsy.show() } else { - tipsy.fixTitle(); + tipsy.fixTitle() setTimeout(function () { - if (tipsy.hoverState == "in" && isElementInDOM(tipsy.$element)) { - tipsy.show(); + if (tipsy.hoverState == 'in' && isElementInDOM(tipsy.$element)) { + tipsy.show() } - }, options.delayIn); + }, options.delayIn) } } function leave() { - var tipsy = get(this); - tipsy.hoverState = "out"; + var tipsy = get(this) + tipsy.hoverState = 'out' if (options.delayOut == 0) { - tipsy.hide(); + tipsy.hide() } else { setTimeout(function () { - if (tipsy.hoverState == "out" && !tipsy.hoverTooltip) { - tipsy.hide(); + if (tipsy.hoverState == 'out' && !tipsy.hoverTooltip) { + tipsy.hide() } - }, options.delayOut); + }, options.delayOut) } } @@ -310,29 +310,29 @@ // even when the events are delegated. // this allows destruction to occur. this.each(function () { - get(this); - }); + get(this) + }) - if (options.trigger != "manual") { + if (options.trigger != 'manual') { var eventIn = - options.trigger == "hover" - ? "mouseenter.tipsy focus.tipsy" - : "focus.tipsy", + options.trigger == 'hover' + ? 'mouseenter.tipsy focus.tipsy' + : 'focus.tipsy', eventOut = - options.trigger == "hover" - ? "mouseleave.tipsy blur.tipsy" - : "blur.tipsy"; + options.trigger == 'hover' + ? 'mouseleave.tipsy blur.tipsy' + : 'blur.tipsy' if (options.live) { $(document) .on(eventIn, options.live, enter) - .on(eventOut, options.live, leave); + .on(eventOut, options.live, leave) } else { - this.on(eventIn, enter).on(eventOut, leave); + this.on(eventIn, enter).on(eventOut, leave) } } - return this; - }; + return this + } $.fn.tipsy.defaults = { aria: false, @@ -340,47 +340,47 @@ delayIn: 0, delayOut: 0, fade: false, - fallback: "", - gravity: "n", + fallback: '', + gravity: 'n', html: false, live: false, hoverable: false, offset: 0, opacity: 0.8, - title: "title", - trigger: "hover", - }; + title: 'title', + trigger: 'hover', + } $.fn.tipsy.revalidate = function () { - $(".tipsy").each(function () { - var pointee = $.data(this, "tipsy-pointee"); + $('.tipsy').each(function () { + var pointee = $.data(this, 'tipsy-pointee') if (!pointee || !isElementInDOM(pointee)) { - $(this).remove(); + $(this).remove() } - }); - }; + }) + } // Overwrite this method to provide options on a per-element basis. // For example, you could store the gravity in a 'tipsy-gravity' attribute: // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); // (remember - do not modify 'options' in place!) $.fn.tipsy.elementOptions = function (ele, options) { - return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; - }; + return $.metadata ? $.extend({}, options, $(ele).metadata()) : options + } $.fn.tipsy.autoNS = function () { return $(this).offset().top > $(document).scrollTop() + $(window).height() / 2 - ? "s" - : "n"; - }; + ? 's' + : 'n' + } $.fn.tipsy.autoWE = function () { return $(this).offset().left > $(document).scrollLeft() + $(window).width() / 2 - ? "e" - : "w"; - }; + ? 'e' + : 'w' + } /** * yields a closure of the supplied parameters, producing a function that takes @@ -405,22 +405,22 @@ }, boundTop = $(document).scrollTop() + margin, boundLeft = $(document).scrollLeft() + margin, - $this = $(this); + $this = $(this) - if ($this.offset().top < boundTop) dir.ns = "n"; - if ($this.offset().left < boundLeft) dir.ew = "w"; + if ($this.offset().top < boundTop) dir.ns = 'n' + if ($this.offset().left < boundLeft) dir.ew = 'w' if ( $(window).width() + $(document).scrollLeft() - $this.offset().left < margin ) - dir.ew = "e"; + dir.ew = 'e' if ( $(window).height() + $(document).scrollTop() - $this.offset().top < margin ) - dir.ns = "s"; + dir.ns = 's' - return dir.ns + (dir.ew ? dir.ew : ""); - }; - }; -})(jQuery); + return dir.ns + (dir.ew ? dir.ew : '') + } + } +})(jQuery) diff --git a/html/watch.css b/html/watch.css index c2dd343..900104d 100644 --- a/html/watch.css +++ b/html/watch.css @@ -159,7 +159,7 @@ nav ul { display: inline-table; } nav ul:after { - content: ""; + content: ''; clear: both; display: block; } diff --git a/index.js b/index.js index 2b0b4e6..ee2f017 100644 --- a/index.js +++ b/index.js @@ -1,136 +1,134 @@ -"use strict"; +'use strict' -const path = require("path"); -const redis = require("redis"); -const WebSocket = require("ws"); +const path = require('path') +const redis = require('redis') +const WebSocket = require('ws') -let wss = { broadcast() {} }; -let watchers = 0; +let wss = { broadcast() {} } +let watchers = 0 exports.register = function () { - this.inherits("haraka-plugin-redis"); + this.inherits('haraka-plugin-redis') - this.load_watch_ini(); + this.load_watch_ini() - this.register_hook("init_master", "redis_subscribe_all_results"); - this.register_hook("init_child", "redis_subscribe_all_results"); + this.register_hook('init_master', 'redis_subscribe_all_results') + this.register_hook('init_child', 'redis_subscribe_all_results') - this.register_hook("deny", "w_deny"); - this.register_hook("queue_ok", "queue_ok"); -}; + this.register_hook('deny', 'w_deny') + this.register_hook('queue_ok', 'queue_ok') +} exports.load_watch_ini = function () { this.cfg = this.config.get( - "watch.ini", + 'watch.ini', { - booleans: ["-main.sampling"], + booleans: ['-main.sampling'], }, () => { - this.load_watch_ini(); + this.load_watch_ini() }, - ); + ) - if (this.cfg.ignore === undefined) this.cfg.ignore = {}; -}; + if (this.cfg.ignore === undefined) this.cfg.ignore = {} +} exports.hook_init_http = function (next, server) { - server.http.app.use("/watch/wss_conf", (req, res) => { + server.http.app.use('/watch/wss_conf', (req, res) => { // app.use args: request, response, app_next // pass config information to the WS client - const client = { sampling: this.cfg.main.sampling }; + const client = { sampling: this.cfg.main.sampling } if (this.cfg.wss && this.cfg.wss.url) { - client.wss_url = this.cfg.wss.url; + client.wss_url = this.cfg.wss.url } - res.end(JSON.stringify(client)); - }); + res.end(JSON.stringify(client)) + }) - let htdocs = path.join(__dirname, "html"); + let htdocs = path.join(__dirname, 'html') if (this.cfg.wss && this.cfg.wss.htdocs) { - htdocs = this.cfg.wss.htdocs; + htdocs = this.cfg.wss.htdocs } - server.http.app.use("/watch/", server.http.express.static(htdocs)); + server.http.app.use('/watch/', server.http.express.static(htdocs)) - this.loginfo("watch init_http done"); - next(); -}; + this.loginfo('watch init_http done') + next() +} exports.hook_init_wss = function (next, server) { - const plugin = this; - plugin.loginfo("watch init_wss"); + const plugin = this + plugin.loginfo('watch init_wss') - wss = server.http.wss; + wss = server.http.wss - wss.on("error", (error) => { - plugin.loginfo(`server error: ${error}`); - }); + wss.on('error', (error) => { + plugin.loginfo(`server error: ${error}`) + }) - wss.on("connection", (ws) => { - watchers++; + wss.on('connection', (ws) => { + watchers++ // broadcast updated watcher count - wss.broadcast({ watchers }); + wss.broadcast({ watchers }) - plugin.logdebug(`wss client connected: ${Object.keys(ws)}`); + plugin.logdebug(`wss client connected: ${Object.keys(ws)}`) // send message to just this websocket // ws.send(JSON.stringify({ msg: 'welcome!' }); - ws.on("error", (error) => { - plugin.logerror(`client error: ${error}`); - }); + ws.on('error', (error) => { + plugin.logerror(`client error: ${error}`) + }) - ws.on("close", (code, message) => { - plugin.loginfo(`client closed: ${message.toString()} (${code})`); - watchers--; - }); + ws.on('close', (code, message) => { + plugin.loginfo(`client closed: ${message.toString()} (${code})`) + watchers-- + }) - ws.on("message", (message, isBinary) => { - plugin.logdebug( - `from client: ${isBinary ? message.toString() : message}`, - ); - }); - }); + ws.on('message', (message, isBinary) => { + plugin.logdebug(`from client: ${isBinary ? message.toString() : message}`) + }) + }) wss.broadcast = function broadcast(data) { - const msg = JSON.stringify(data); + const msg = JSON.stringify(data) wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { - client.send(msg); + client.send(msg) } - }); - }; + }) + } - plugin.loginfo("watch init_wss done"); - next(); -}; + plugin.loginfo('watch init_wss done') + next() +} exports.w_deny = function (next, connection, params) { - const pi_code = params[0]; + const pi_code = params[0] // let pi_msg = params[1]; - const pi_name = params[2]; + const pi_name = params[2] // let pi_function = params[3]; // let pi_params = params[4]; - const pi_hook = params[5]; + const pi_hook = params[5] - connection.logdebug(this, `watch deny saw: ${pi_name} deny from ${pi_hook}`); + connection.logdebug(this, `watch deny saw: ${pi_name} deny from ${pi_hook}`) const req = { uuid: connection.transaction ? connection.transaction.uuid : connection.uuid, - local_port: { classy: "bg_white", title: "disconnected" }, + local_port: { classy: 'bg_white', title: 'disconnected' }, remote_host: get_remote_host(connection), - }; + } - connection.logdebug(this, `watch sending dark red to ${pi_name}`); - const bg_class = pi_code === DENYSOFT ? "bg_dyellow" : "bg_dred"; - const report_as = this.get_plugin_name(pi_name); - if (req[report_as]) req[report_as].classy = bg_class; - if (!req[report_as]) req[report_as] = { classy: bg_class }; + connection.logdebug(this, `watch sending dark red to ${pi_name}`) + const bg_class = pi_code === DENYSOFT ? 'bg_dyellow' : 'bg_dred' + const report_as = this.get_plugin_name(pi_name) + if (req[report_as]) req[report_as].classy = bg_class + if (!req[report_as]) req[report_as] = { classy: bg_class } - wss.broadcast(req); - next(); -}; + wss.broadcast(req) + next() +} exports.queue_ok = function (next, connection, msg) { // ok 1390590369 qp 634 (F82E2DD5-9238-41DC-BC95-9C3A02716AD2.1) @@ -138,459 +136,459 @@ exports.queue_ok = function (next, connection, msg) { wss.broadcast({ uuid: connection.transaction.uuid, queue: { - classy: "bg_green", + classy: 'bg_green', title: msg, }, - }); - next(); -}; + }) + next() +} exports.redis_subscribe_all_results = async function (next) { - const plugin = this; + const plugin = this - if (this.pubsub) return; // already subscribed? + if (this.pubsub) return // already subscribed? - this.pubsub = redis.createClient(this.redisCfg.pubsub); - await this.pubsub.connect(); + this.pubsub = redis.createClient(this.redisCfg.pubsub) + await this.pubsub.connect() - await this.pubsub.pSubscribe("result-*", (message, channel) => { - const match = /result-([A-F0-9\-.]+)$/.exec(channel); // uuid + await this.pubsub.pSubscribe('result-*', (message, channel) => { + const match = /result-([A-F0-9\-.]+)$/.exec(channel) // uuid if (!match) { - plugin.logerror("pattern: result-*"); + plugin.logerror('pattern: result-*') } - const m = JSON.parse(message); + const m = JSON.parse(message) - if (typeof m.result !== "object") { - plugin.logerror(`garbage was published on ${channel}: ${m.result}`); - return; + if (typeof m.result !== 'object') { + plugin.logerror(`garbage was published on ${channel}: ${m.result}`) + return } - if (this.cfg.ignore[m.result.ip] !== undefined) return; + if (this.cfg.ignore[m.result.ip] !== undefined) return switch (m.plugin) { - case "local": + case 'local': if (m.result.port) { wss.broadcast({ uuid: match[1], local_port: { newval: m.result.port }, - }); + }) if (m.result.port === 465) { - wss.broadcast({ uuid: match[1], tls: { classy: "bg_green" } }); + wss.broadcast({ uuid: match[1], tls: { classy: 'bg_green' } }) } - return; + return } - break; - case "remote": + break + case 'remote': if (m.result.ip) { - wss.broadcast(exports.format_remote_host(match[1], m.result)); - return; + wss.broadcast(exports.format_remote_host(match[1], m.result)) + return } - break; - case "helo": + break + case 'helo': if (m.result.host) { - wss.broadcast(exports.format_helo(match[1], m.result)); - return; + wss.broadcast(exports.format_helo(match[1], m.result)) + return } - break; - case "reset": + break + case 'reset': if (m.result.duration) { wss.broadcast({ uuid: match[1], queue: { newval: m.result.duration.toFixed(1), }, - }); - return; + }) + return } - break; - case "disconnect": + break + case 'disconnect': if (m.result.duration) { wss.broadcast({ uuid: match[1], queue: { newval: m.result.duration.toFixed(1) }, - local_port: { classy: "bg_white", title: "disconnected" }, - }); - return; + local_port: { classy: 'bg_white', title: 'disconnected' }, + }) + return } - break; - case "access": - case "tls": - if (m.result.msg) return; - break; - case "dnsbl": - case "dns-list": - if (m.result.emit) return; - if (m.result.pass) return; - break; - case "early_talker": - case "helo.checks": - if (m.result.pass) return; - if (m.result.skip) return; - if (m.result.ips) return; - if (m.result.multi) return; - if (m.result.helo_host) return; - break; - case "data.uribl": - case "uribl": - if (m.result.pass) return; - if (m.result.skip) return; - break; - case "karma": - if (m.result.awards) return; - if (m.result.msg) return; - if (m.result.todo) return; + break + case 'access': + case 'tls': + if (m.result.msg) return + break + case 'dnsbl': + case 'dns-list': + if (m.result.emit) return + if (m.result.pass) return + break + case 'early_talker': + case 'helo.checks': + if (m.result.pass) return + if (m.result.skip) return + if (m.result.ips) return + if (m.result.multi) return + if (m.result.helo_host) return + break + case 'data.uribl': + case 'uribl': + if (m.result.pass) return + if (m.result.skip) return + break + case 'karma': + if (m.result.awards) return + if (m.result.msg) return + if (m.result.todo) return // if (m.result.emit) return; - break; - case "mail_from.is_resolvable": - if (m.result.msg) return; - break; - case "known-senders": - if (m.result.rcpt_ods) return; - if (m.result.sender) return; - break; - case "rcpt_to.in_host_list": - case "rcpt_to.qmail_deliverable": - case "qmail-deliverable": - if (m.result.msg) return; - if (m.result.skip) return; - break; - case "limit": - if (m.result.concurrent_count !== undefined) return; - if (m.result.rate_rcpt) return; - if (m.result.rate_rcpt_sender) return; - if (m.result.concurrent) return; - if (m.result.rate_conn) return; - if (m.result.msg) return; - break; - case "relay": - if (m.result.skip) return; - break; - case "headers": - case "data.headers": - if (m.result.pass) return; - if (m.result.msg) return; - if (m.result.skip) return; + break + case 'mail_from.is_resolvable': + if (m.result.msg) return + break + case 'known-senders': + if (m.result.rcpt_ods) return + if (m.result.sender) return + break + case 'rcpt_to.in_host_list': + case 'rcpt_to.qmail_deliverable': + case 'qmail-deliverable': + if (m.result.msg) return + if (m.result.skip) return + break + case 'limit': + if (m.result.concurrent_count !== undefined) return + if (m.result.rate_rcpt) return + if (m.result.rate_rcpt_sender) return + if (m.result.concurrent) return + if (m.result.rate_conn) return + if (m.result.msg) return + break + case 'relay': + if (m.result.skip) return + break + case 'headers': + case 'data.headers': + if (m.result.pass) return + if (m.result.msg) return + if (m.result.skip) return if (m.result.fail) { - if (m.result.fail == "UA") return; + if (m.result.fail == 'UA') return } - break; - case "queue/smtp_forward": + break + case 'queue/smtp_forward': if (m.result.pass) wss.broadcast({ uuid: match[1], - "queue/smtp_forward": { classy: "bg_green" }, - }); - return; - case "outbound": - wss.broadcast({ uuid: match[1], queue: { classy: "bg_green" } }); - return; + 'queue/smtp_forward': { classy: 'bg_green' }, + }) + return + case 'outbound': + wss.broadcast({ uuid: match[1], queue: { classy: 'bg_green' } }) + return } - const req = { uuid: match[1] }; + const req = { uuid: match[1] } req[plugin.get_plugin_name(m.plugin)] = plugin.format_any( m.plugin, m.result, - ); - wss.broadcast(req); - }); + ) + wss.broadcast(req) + }) - this.logdebug(this, `pSubscribed to result-*`); - next(); -}; + this.logdebug(this, `pSubscribed to result-*`) + next() +} exports.get_plugin_name = function (pi_name) { // coalesce auth/* and queue/* plugins to 'auth' and 'queue' if (/^(queue|auth)\//.test(pi_name)) { - return pi_name.split("/").shift(); + return pi_name.split('/').shift() } switch (pi_name) { - case "connect.fcrdns": - return "fcrdns"; - case "connect.p0f": - return "p0f"; - case "dkim_verify": - case "dkim_sign": - return "dkim"; - case "dmarc-perl": - case "data.dmarc": - return "dmarc"; - case "data.headers": - return "headers"; - case "outbound": - return "queue"; + case 'connect.fcrdns': + return 'fcrdns' + case 'connect.p0f': + return 'p0f' + case 'dkim_verify': + case 'dkim_sign': + return 'dkim' + case 'dmarc-perl': + case 'data.dmarc': + return 'dmarc' + case 'data.headers': + return 'headers' + case 'outbound': + return 'queue' } - return pi_name; -}; + return pi_name +} exports.format_any = function (pi_name, r) { // title: the value shown in the HTML tooltip // classy: color of the square switch (pi_name) { - case "access": - if (r.whitelist) return { classy: "bg_green", title: r.pass }; - if (r.fail) return { classy: "bg_red", title: r.fail }; - if (r.skip) return {}; - break; - case "bounce": - return this.format_bounce(r); - case "connect.fcrdns": - case "fcrdns": - return this.format_fcrdns(r); - case "connect.asn": - case "asn": - return this.format_asn(r); - case "connect.geoip": - case "geoip": { - const f = {}; + case 'access': + if (r.whitelist) return { classy: 'bg_green', title: r.pass } + if (r.fail) return { classy: 'bg_red', title: r.fail } + if (r.skip) return {} + break + case 'bounce': + return this.format_bounce(r) + case 'connect.fcrdns': + case 'fcrdns': + return this.format_fcrdns(r) + case 'connect.asn': + case 'asn': + return this.format_asn(r) + case 'connect.geoip': + case 'geoip': { + const f = {} if (r.human) { - f.title = r.human; - f.newval = r.human.substring(0, 6); + f.title = r.human + f.newval = r.human.substring(0, 6) } if (r.distance) { if (parseInt(r.distance, 10) > 4000) { - f.classy = "bg_red"; + f.classy = 'bg_red' } else { - f.classy = "bg_green"; + f.classy = 'bg_green' } } - return f; + return f } - case "connect.p0f": - case "p0f": - return this.format_p0f(r); - case "tls": + case 'connect.p0f': + case 'p0f': + return this.format_p0f(r) + case 'tls': if (r.enabled) { if (r.verified) { - return { classy: "bg_green", title: JSON.stringify(r) }; + return { classy: 'bg_green', title: JSON.stringify(r) } } - return { classy: "bg_lgreen", title: JSON.stringify(r) }; + return { classy: 'bg_lgreen', title: JSON.stringify(r) } } - break; - case "data.uribl": - case "uribl": - case "dnsbl": - case "dns-list": - if (r.fail) return { title: r.fail, classy: "bg_lred" }; - break; - case "karma": + break + case 'data.uribl': + case 'uribl': + case 'dnsbl': + case 'dns-list': + if (r.fail) return { title: r.fail, classy: 'bg_lred' } + break + case 'karma': if (r.score !== undefined) { - if (r.score < -8) return { classy: "bg_red", title: r.score }; - if (r.score < -3) return { classy: "bg_lred", title: r.score }; - if (r.score < 0) return { classy: "bg_yellow", title: r.score }; - if (r.score > 3) return { classy: "bg_green", title: r.score }; - if (r.score >= 0) return { classy: "bg_lgreen", title: r.score }; + if (r.score < -8) return { classy: 'bg_red', title: r.score } + if (r.score < -3) return { classy: 'bg_lred', title: r.score } + if (r.score < 0) return { classy: 'bg_yellow', title: r.score } + if (r.score > 3) return { classy: 'bg_green', title: r.score } + if (r.score >= 0) return { classy: 'bg_lgreen', title: r.score } } - if (r.fail) return { title: r.fail }; + if (r.fail) return { title: r.fail } - if (r.err) return { title: r.err, classy: "bg_yellow" }; - if (r.emit) return {}; - if (r.pass) return { classy: "bg_green" }; - break; - case "mail_from": + if (r.err) return { title: r.err, classy: 'bg_yellow' } + if (r.emit) return {} + if (r.pass) return { classy: 'bg_green' } + break + case 'mail_from': if (r.address) return { newval: r.address && r.address.length > 22 ? `..${r.address.substring(r.address.length - 22)}` : r.address, - classy: "black", + classy: 'black', title: r.address, - }; - break; - case "spf": + } + break + case 'spf': if (r.scope) { - const res = { title: r.result, scope: r.scope }; + const res = { title: r.result, scope: r.scope } switch (r.result) { - case "None": - res.classy = "bg_lgrey"; - break; - case "Pass": - res.classy = "bg_green"; - break; - case "Fail": - res.classy = "bg_red"; - break; - case "SoftFail": - res.classy = "bg_yellow"; - break; + case 'None': + res.classy = 'bg_lgrey' + break + case 'Pass': + res.classy = 'bg_green' + break + case 'Fail': + res.classy = 'bg_red' + break + case 'SoftFail': + res.classy = 'bg_yellow' + break } - return res; + return res } - if (r.skip) return { classy: "bg_yellow" }; - break; - case "recipient": - case "rcpt_to": + if (r.skip) return { classy: 'bg_yellow' } + break + case 'recipient': + case 'rcpt_to': if (r.recipient) { - return exports.format_recipient(r.recipient); + return exports.format_recipient(r.recipient) } - break; - case "auth": - case "auth/auth_vpopmaild": - case "helo.checks": - case "mail_from.is_resolvable": - case "rcpt_to.in_host_list": - case "rcpt_to.qmail_deliverable": - case "qmail-deliverable": - case "avg": - case "clamd": - case "relay": - case "known-senders": - case "limit": - if (r.pass || r.fail) return this.format_default(r); - if (r.skip) return {}; - break; - case "headers": - case "data.headers": + break + case 'auth': + case 'auth/auth_vpopmaild': + case 'helo.checks': + case 'mail_from.is_resolvable': + case 'rcpt_to.in_host_list': + case 'rcpt_to.qmail_deliverable': + case 'qmail-deliverable': + case 'avg': + case 'clamd': + case 'relay': + case 'known-senders': + case 'limit': + if (r.pass || r.fail) return this.format_default(r) + if (r.skip) return {} + break + case 'headers': + case 'data.headers': if (r.fail) { - if (/^direct/.test(r.fail)) return { classy: "bg_lred" }; - if (/^from_match/.test(r.fail)) return { classy: "bg_yellow" }; + if (/^direct/.test(r.fail)) return { classy: 'bg_lred' } + if (/^from_match/.test(r.fail)) return { classy: 'bg_yellow' } } - break; - case "dkim_sign": - case "dkim_verify": + break + case 'dkim_sign': + case 'dkim_verify': if (r.pass || r.fail) { - return this.format_default(r); + return this.format_default(r) } if (r.err) { - return { classy: "bg_yellow", title: r.err }; + return { classy: 'bg_yellow', title: r.err } } - break; - case "rspamd": + break + case 'rspamd': if (r.score !== undefined) return { classy: r.is_spam === true - ? "bg_red" - : r.action === "greylist" - ? "bg_grey" + ? 'bg_red' + : r.action === 'greylist' + ? 'bg_grey' : r.is_skipped === true - ? "" + ? '' : r.score > 5 - ? "bg_lred" + ? 'bg_lred' : r.score < 0 - ? "bg_green" + ? 'bg_green' : r.score < 3 - ? "bg_lgreen" - : "bg_yellow", + ? 'bg_lgreen' + : 'bg_yellow', title: JSON.stringify(r), - }; - break; - case "spamassassin": + } + break + case 'spamassassin': if (r.hits !== undefined) { - const hits = parseFloat(r.hits); + const hits = parseFloat(r.hits) return { classy: hits > 5 - ? "bg_red" + ? 'bg_red' : hits > 2 - ? "bg_yellow" + ? 'bg_yellow' : hits < 0 - ? "bg_green" - : "bg_lgreen", + ? 'bg_green' + : 'bg_lgreen', title: JSON.stringify(r), // title: `${r.flag}, ${hits} hits, time: ${r.time}`, - }; + } } - break; - case "dmarc": - case "data.dmarc": - case "dmarc-perl": - if (r.pass) return { classy: "bg_green", title: r.pass }; - if (r.fail) return { classy: "bg_red", title: r.fail }; - if (r.dmarc === "none") return { classy: "bg_grey", title: r.dmarc }; - if (r.dmarc === "other") return {}; - break; - case "queue": - if (r.pass) return { classy: "bg_green", title: r.pass }; - if (r.fail) return { classy: "bg_red", title: r.fail }; - if (r.msg === "") return {}; - break; + break + case 'dmarc': + case 'data.dmarc': + case 'dmarc-perl': + if (r.pass) return { classy: 'bg_green', title: r.pass } + if (r.fail) return { classy: 'bg_red', title: r.fail } + if (r.dmarc === 'none') return { classy: 'bg_grey', title: r.dmarc } + if (r.dmarc === 'other') return {} + break + case 'queue': + if (r.pass) return { classy: 'bg_green', title: r.pass } + if (r.fail) return { classy: 'bg_red', title: r.fail } + if (r.msg === '') return {} + break } - this.logdebug(pi_name); - this.logdebug(r); + this.logdebug(pi_name) + this.logdebug(r) return { title: this.get_title(pi_name, r), classy: this.get_class(pi_name, r), - }; -}; + } +} exports.format_recipient = function (r) { const rcpt = r.address.length > 22 ? `..${r.address.substring(r.address.length - 22)}` - : r.address; + : r.address - if (r.action === "reject") - return { newval: rcpt, classy: "bg_red", title: r.address }; - if (r.action === "accept") - return { newval: rcpt, classy: "bg_green", title: r.address }; - return { newval: rcpt, classy: "black", title: r.address }; -}; + if (r.action === 'reject') + return { newval: rcpt, classy: 'bg_red', title: r.address } + if (r.action === 'accept') + return { newval: rcpt, classy: 'bg_green', title: r.address } + return { newval: rcpt, classy: 'black', title: r.address } +} exports.format_default = function (r) { - if (r.pass) return { classy: "bg_green", title: r.pass }; - if (r.fail) return { classy: "bg_red", title: r.fail }; - if (r.err) return { classy: "bg_yellow", title: r.err }; -}; + if (r.pass) return { classy: 'bg_green', title: r.pass } + if (r.fail) return { classy: 'bg_red', title: r.fail } + if (r.err) return { classy: 'bg_yellow', title: r.err } +} exports.format_fcrdns = function (r) { - if (r.pass) return { classy: "bg_green" }; - if (r.fail) return { title: r.fail, classy: "bg_lred" }; + if (r.pass) return { classy: 'bg_green' } + if (r.fail) return { title: r.fail, classy: 'bg_lred' } if (r.fcrdns) { - if (typeof r.fcrdns === "string") return { title: r.fcrdns }; + if (typeof r.fcrdns === 'string') return { title: r.fcrdns } if (Array.isArray(r.fcrdns) && r.fcrdns.length) { - return { title: r.fcrdns.join(" ") }; + return { title: r.fcrdns.join(' ') } } } // this.loginfo(r); - return {}; -}; + return {} +} exports.format_asn = function (r) { - if (r.pass) return { classy: "bg_green" }; - if (r.fail) return { title: r.fail, classy: "bg_lred" }; - if (r.asn) return { newval: r.asn }; - if (r.asn_score) return {}; // extra - this.loginfo(r); - return {}; -}; + if (r.pass) return { classy: 'bg_green' } + if (r.fail) return { title: r.fail, classy: 'bg_lred' } + if (r.asn) return { newval: r.asn } + if (r.asn_score) return {} // extra + this.loginfo(r) + return {} +} exports.format_p0f = function (r) { - if (!r || !r.os_name) return {}; + if (!r || !r.os_name) return {} const f = { title: `${r.os_name} ${r.os_flavor}, ${r.distance} hops`, newval: r.os_name, - }; + } if (r.os_name) { - if (/freebsd|mac|ios/i.test(r.os_name)) r.classy = "bg_green"; - if (/windows/i.test(r.os_name)) r.classy = "bg_red"; + if (/freebsd|mac|ios/i.test(r.os_name)) r.classy = 'bg_green' + if (/windows/i.test(r.os_name)) r.classy = 'bg_red' } - return f; -}; + return f +} exports.format_bounce = function (r) { - if (!r) return {}; - if (r.isa === "no") return { classy: "bg_lgreen", title: "not a bounce" }; - if (r.fail && r.fail.length) return { classy: "bg_red", title: r.human }; - return { classy: "bg_green" }; -}; + if (!r) return {} + if (r.isa === 'no') return { classy: 'bg_lgreen', title: 'not a bounce' } + if (r.fail && r.fail.length) return { classy: 'bg_red', title: r.human } + return { classy: 'bg_green' } +} exports.format_results = function (pi_name, r) { const s = { title: this.get_title(pi_name, r), classy: this.get_class(pi_name, r), - }; + } - if (pi_name === "spf") { - s.scope = r.scope; + if (pi_name === 'spf') { + s.scope = r.scope } - return s; -}; + return s +} exports.format_helo = function (uuid, r) { return { @@ -601,115 +599,115 @@ exports.format_helo = function (uuid, r) { ? `...${r.host.substring(r.host.length - 22)}` : r.host, title: r.host, - classy: "bg_white", + classy: 'bg_white', }, - }; -}; + } +} exports.get_class = function (pi_name, r) { - if (!r.pass) r.pass = []; - if (!r.fail) r.fail = []; - if (!r.err) r.err = []; + if (!r.pass) r.pass = [] + if (!r.fail) r.fail = [] + if (!r.err) r.err = [] switch (pi_name) { - case "dmarc": - case "dmarc-perl": - case "data.dmarc": { - if (!r.result) return "got"; - const comment = r.reason && r.reason.length ? r.reason[0].comment : ""; - return r.result === "pass" - ? "bg_green" - : comment === "no policy" - ? "bg_yellow" - : "bg_red"; + case 'dmarc': + case 'dmarc-perl': + case 'data.dmarc': { + if (!r.result) return 'got' + const comment = r.reason && r.reason.length ? r.reason[0].comment : '' + return r.result === 'pass' + ? 'bg_green' + : comment === 'no policy' + ? 'bg_yellow' + : 'bg_red' } - case "karma": { + case 'karma': { if (r.score === undefined) { - const history = parseFloat(r.history) || 0; - return history > 2 ? "bg_green" : history < -1 ? "bg_red" : "bg_yellow"; + const history = parseFloat(r.history) || 0 + return history > 2 ? 'bg_green' : history < -1 ? 'bg_red' : 'bg_yellow' } - const score = parseFloat(r.score) || 0; + const score = parseFloat(r.score) || 0 return score > 3 - ? "bg_green" + ? 'bg_green' : score > 0 - ? "bg_lgreen" + ? 'bg_lgreen' : score < -3 - ? "bg_red" + ? 'bg_red' : score < 0 - ? "bg_lred" - : "bg_yellow"; + ? 'bg_lred' + : 'bg_yellow' } - case "relay": + case 'relay': return r.pass.length && r.fail.length === 0 - ? "bg_green" + ? 'bg_green' : r.pass.length - ? "bg_lgreen" + ? 'bg_lgreen' : r.fail.length - ? "bg_red" + ? 'bg_red' : r.err.length - ? "bg_yellow" - : ""; - case "rcpt_to.in_host_list": + ? 'bg_yellow' + : '' + case 'rcpt_to.in_host_list': return r.pass.length && r.fail.length === 0 - ? "bg_green" + ? 'bg_green' : r.pass.length - ? "bg_lgreen" - : ""; - case "spf": - return r.result === "Pass" - ? "bg_green" - : r.result === "Neutral" - ? "bg_lgreen" + ? 'bg_lgreen' + : '' + case 'spf': + return r.result === 'Pass' + ? 'bg_green' + : r.result === 'Neutral' + ? 'bg_lgreen' : /fail/i.test(r.result) - ? "bg_red" + ? 'bg_red' : /error/i.test(r.result) - ? "bg_yellow" - : ""; + ? 'bg_yellow' + : '' default: return r.pass.length && r.fail.length === 0 - ? "bg_green" + ? 'bg_green' : r.pass.length - ? "bg_lgreen" + ? 'bg_lgreen' : r.fail.length - ? "bg_red" + ? 'bg_red' : r.err.length - ? "bg_yellow" - : "bg_lgrey"; + ? 'bg_yellow' + : 'bg_lgrey' } -}; +} exports.get_title = function (pi_name, r) { // title: the value shown in the HTML tooltip switch (pi_name) { - case "dmarc": - case "dmarc-perl": - case "data.dmarc": { - const comment = r.reason && r.reason.length ? r.reason[0].comment : ""; - return r.result === "pass" + case 'dmarc': + case 'dmarc-perl': + case 'data.dmarc': { + const comment = r.reason && r.reason.length ? r.reason[0].comment : '' + return r.result === 'pass' ? r.result - : [r.result, r.disposition, comment].join(", "); + : [r.result, r.disposition, comment].join(', ') } - case "queue": - return r.human; + case 'queue': + return r.human default: - return r.human_html; + return r.human_html } -}; +} exports.format_remote_host = function (uuid, r) { - let host = r.host || ""; - const ip = r.ip || ""; - let hostShort = host; + let host = r.host || '' + const ip = r.ip || '' + let hostShort = host if (host) { switch (host) { - case "DNSERROR": - case "Unknown": - host = ""; - break; + case 'DNSERROR': + case 'Unknown': + host = '' + break } if (host.length > 22) { - hostShort = `...${host.substring(host.length - 20)}`; + hostShort = `...${host.substring(host.length - 20)}` } } @@ -719,32 +717,32 @@ exports.format_remote_host = function (uuid, r) { newval: host ? `${hostShort} / ${ip}` : ip, title: host ? `${host} / ${ip}` : ip, }, - }; -}; + } +} function get_remote_host(connection) { - let host = ""; - let ip = ""; + let host = '' + let ip = '' if (connection.remote) { - if (connection.remote.host) host = connection.remote.host; - if (connection.remote.ip) ip = connection.remote.ip; + if (connection.remote.host) host = connection.remote.host + if (connection.remote.ip) ip = connection.remote.ip } - let hostShort = host; + let hostShort = host if (host) { switch (host) { - case "DNSERROR": - case "Unknown": - host = ""; - break; + case 'DNSERROR': + case 'Unknown': + host = '' + break } if (host.length > 22) { - hostShort = `...${host.substring(host.length - 20)}`; + hostShort = `...${host.substring(host.length - 20)}` } } return { newval: host ? `${hostShort} / ${ip}` : ip, title: host ? `${host} / ${ip}` : ip, - }; + } } diff --git a/test/watch.js b/test/watch.js index 96214b2..af87a75 100644 --- a/test/watch.js +++ b/test/watch.js @@ -1,43 +1,43 @@ -const assert = require("assert"); -const fixtures = require("haraka-test-fixtures"); - -describe("watch", function () { - it("register", function () { - const plugin = new fixtures.plugin("watch"); - plugin.server = { notes: {} }; - plugin.register(); - assert.ok(plugin.cfg.main); +const assert = require('assert') +const fixtures = require('haraka-test-fixtures') + +describe('watch', function () { + it('register', function () { + const plugin = new fixtures.plugin('watch') + plugin.server = { notes: {} } + plugin.register() + assert.ok(plugin.cfg.main) // console.log(plugin.cfg); - }); - - it("loads watch.ini", function () { - const plugin = new fixtures.plugin("watch"); - plugin.server = { notes: {} }; - plugin.load_watch_ini(); - assert.equal(plugin.cfg.main.sampling, false); - }); - - it("inherits from haraka-plugin-redis", function () { - const plugin = new fixtures.plugin("watch"); - plugin.inherits("haraka-plugin-redis"); - assert.ok(plugin.get_redis_sub_channel); + }) + + it('loads watch.ini', function () { + const plugin = new fixtures.plugin('watch') + plugin.server = { notes: {} } + plugin.load_watch_ini() + assert.equal(plugin.cfg.main.sampling, false) + }) + + it('inherits from haraka-plugin-redis', function () { + const plugin = new fixtures.plugin('watch') + plugin.inherits('haraka-plugin-redis') + assert.ok(plugin.get_redis_sub_channel) // console.log(plugin); - }); + }) - it("ignores results that are not objects", function () { - const plugin = new fixtures.plugin("watch"); - plugin.server = { notes: {} }; - plugin.register(); - plugin.load_redis_ini(); + it('ignores results that are not objects', function () { + const plugin = new fixtures.plugin('watch') + plugin.server = { notes: {} } + plugin.register() + plugin.load_redis_ini() // TODO - }); + }) - it.skip("mocks up a wss socket", function () {}); + it.skip('mocks up a wss socket', function () {}) - it.skip("mocks up a connection", function () {}); + it.skip('mocks up a connection', function () {}) - it.skip("saves some results", function () {}); + it.skip('saves some results', function () {}) - it.skip("sees those results via redis connection subscription", function () {}); -}); + it.skip('sees those results via redis connection subscription', function () {}) +})