capscan.lua

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 = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
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 = ("&nbsp;<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>");