| 1 | require "verse" |
| 2 | require "verse.client" |
| 3 | local array = require "array"; |
| 4 | local calculate_hash = require "caps".calculate_hash; |
| 5 | |
| 6 | -- Configurable: |
| 7 | local jid, pass = arg[1], nil; |
| 8 | --- -- -- -- --- |
| 9 | |
| 10 | local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; |
| 11 | local function escape(str) return (string.gsub(str, "['&<>\"]", escape_table)); end |
| 12 | |
| 13 | local xmlns_caps = "http://jabber.org/protocol/caps"; |
| 14 | local xmlns_disco = "http://jabber.org/protocol/disco#info"; |
| 15 | |
| 16 | if not jid then |
| 17 | io.write("Please give a JID as the first argument\n"); |
| 18 | os.exit(1); |
| 19 | end |
| 20 | |
| 21 | if not pass then |
| 22 | io.write("Password (not blanked): "); |
| 23 | pass = io.read("*l"); |
| 24 | end |
| 25 | |
| 26 | local conn = verse.new(verse.logger()); |
| 27 | |
| 28 | conn:add_plugin("version"); |
| 29 | |
| 30 | conn:connect_client(jid, pass); |
| 31 | |
| 32 | local contacts = {}; |
| 33 | |
| 34 | conn:hook("ready", function () |
| 35 | conn:hook("presence", function (presence) |
| 36 | if contacts[presence.attr.from] or presence.attr.type then return; end; |
| 37 | |
| 38 | local contact = { jid = presence.attr.from }; |
| 39 | contacts[contact.jid] = contact; |
| 40 | |
| 41 | local caps_tag = presence:get_child("c", xmlns_caps); |
| 42 | if not caps_tag then |
| 43 | conn:debug("No caps from %s: %s", presence.attr.from, tostring(presence)); |
| 44 | return; |
| 45 | end |
| 46 | |
| 47 | contact.hash_type = caps_tag.attr.hash; |
| 48 | contact.ver = caps_tag.attr.ver; |
| 49 | contact.node = caps_tag.attr.node; |
| 50 | |
| 51 | local node_string = contact.node.."#"..contact.ver; |
| 52 | |
| 53 | conn:query_version(contact.jid, function (version) |
| 54 | contact.version = ("%s%s%s"):format( |
| 55 | version.name or "[no name]", |
| 56 | version.version and (" "..version.version) or "", |
| 57 | version.platform and (" ("..version.platform..")") or "" |
| 58 | ); |
| 59 | end); |
| 60 | |
| 61 | conn:send_iq(verse.iq({ to = contact.jid, type = "get" }) |
| 62 | :tag("query", { xmlns = xmlns_disco, node = node_string }), |
| 63 | function (result) |
| 64 | if result.attr.type == "error" then |
| 65 | contact.error = { result:get_error() }; |
| 66 | return; |
| 67 | end |
| 68 | contact.calculated_ver, contact.calculated_S |
| 69 | = calculate_hash(result.tags[1]); |
| 70 | if contact.calculated_ver ~= contact.ver then |
| 71 | conn:warn("Invalid caps hash: %s", contact.jid); |
| 72 | conn:warn("Received: %s Calculated: %s", contact.ver, contact.calculated_ver); |
| 73 | conn:warn("Received stanza: %s", tostring(result)); |
| 74 | conn:warn("Calculated S: %s", contact.calculated_S); |
| 75 | else |
| 76 | conn:warn("Valid caps hash: %s", contact.jid); |
| 77 | end |
| 78 | end); |
| 79 | end, 1000); |
| 80 | conn:send(verse.presence():tag("priority"):text("-1")); |
| 81 | end); |
| 82 | |
| 83 | verse.loop(); |
| 84 | |
| 85 | --- Write report |
| 86 | |
| 87 | local report = io.open("report.html", "w+"); |
| 88 | report:write[[<html> |
| 89 | <head> |
| 90 | <title>]]; |
| 91 | report:write("Entity capabilities validity for contacts of ", jid); |
| 92 | report:write[[</title> |
| 93 | </head> |
| 94 | <body> |
| 95 | ]]; |
| 96 | |
| 97 | local contact_jids = array.collect(keys(contacts)):sort(); |
| 98 | |
| 99 | report:write("<h1>Entity capabilities report for contacts of ", jid, "</h1>\n"); |
| 100 | report:write("<p>", tostring(#contact_jids), " contacts</p>\n"); |
| 101 | |
| 102 | local function write_section(title, jids, show) |
| 103 | report:write("\n<h1>", title, "</h1>\n"); |
| 104 | report:write("<p>", tostring(#jids), " contacts</p>\n"); |
| 105 | report:write("<ul>\n"); |
| 106 | for _, jid in ipairs(jids) do |
| 107 | local contact = contacts[jid]; |
| 108 | local client_link = (" <a href='%s'>%s</a>"):format( |
| 109 | escape(contacts[jid].node or "#"), |
| 110 | escape(contacts[jid].version or "Unknown client") |
| 111 | ); |
| 112 | report:write(" <li>", escape(jid), client_link); |
| 113 | |
| 114 | if show then |
| 115 | report:write("\n <ul>\n"); |
| 116 | for _, field in ipairs(show) do |
| 117 | local friendly_field = field:gsub("^.", string.upper):gsub("_", " "); |
| 118 | local value = escape(contacts[jid][field] or ""); |
| 119 | report:write((" "):rep(12), "<li>", friendly_field, ": ", value, "</li>\n"); |
| 120 | end |
| 121 | report:write(" </ul>\n"); |
| 122 | end |
| 123 | |
| 124 | report:write("</li>\n"); |
| 125 | end |
| 126 | report:write("</ul>\n"); |
| 127 | end |
| 128 | |
| 129 | local function no_caps_filter(jid) |
| 130 | return not contacts[jid].ver; |
| 131 | end |
| 132 | |
| 133 | local function legacy_caps_filter(jid) |
| 134 | return contacts[jid].ver and not contacts[jid].hash_type; |
| 135 | end |
| 136 | |
| 137 | local function valid_caps_filter(jid) |
| 138 | return not(legacy_caps_filter(jid)) |
| 139 | and (contacts[jid].ver == contacts[jid].calculated_ver); |
| 140 | end |
| 141 | |
| 142 | local function invalid_caps_filter(jid) |
| 143 | return not(legacy_caps_filter(jid)) and not(no_caps_filter(jid)) and not(valid_caps_filter(jid)); |
| 144 | end |
| 145 | |
| 146 | write_section("Valid caps", array.filter(contact_jids, valid_caps_filter)); |
| 147 | write_section("Invalid caps", array.filter(contact_jids, invalid_caps_filter), { "ver", "calculated_ver", "calculated_S" }); |
| 148 | write_section("No caps", array.filter(contact_jids, no_caps_filter)); |
| 149 | write_section("Legacy caps", array.filter(contact_jids, legacy_caps_filter), { "ver" }); |
| 150 | |
| 151 | report:write("</body></html>"); |