aboutsummaryrefslogtreecommitdiffstats
path: root/main.lua
blob: cb6e03fd591d3da714db353d3928c4ee8e74a043 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
require "luarocks.require"

require "copas"
require "socket"
require "ssl"
require "lxp"

function log(type, area, message)
	print(type, area, message);
end

require "core.stanza_dispatch"
require "core.rostermanager"
require "core.offlinemessage"
require "util.stanza"
require "util.jid"

-- Locals for faster access --
local t_insert = table.insert;
local t_concat = table.concat;
local t_concatall = function (t, sep) local tt = {}; for _, s in ipairs(t) do t_insert(tt, tostring(s)); end return t_concat(tt, sep); end
local m_random = math.random;
local format = string.format;
local st = stanza;
------------------------------

users = {};
hosts = 	{ 
			["localhost"] = 	{
							type = "local";
							connected = true;
							sessions = {};
						};
			["getjabber.ath.cx"] = 	{
							type = "local";
							connected = true;
							sessions = {};
						};
		}

local hosts, users = hosts, users;

local ssl_ctx, msg = ssl.newcontext { mode = "server", protocol = "sslv23", key = "/home/matthew/ssl_cert/server.key",
    certificate = "/home/matthew/ssl_cert/server.crt", capath = "/etc/ssl/certs", verify = "none", }
        
if not ssl_ctx then error("Failed to initialise SSL/TLS support: "..tostring(msg)); end


function connect_host(host)
	hosts[host] = { type = "remote", sendbuffer = {} };
end

function handler(conn)
	local copas_receive, copas_send = copas.receive, copas.send;
	local reqdata, sktmsg;
	local session = { sendbuffer = { external = {} }, conn = conn, notopen = true, priority = 0 }


	-- Logging functions --

	local mainlog, log = log;
	do
		local conn_name = tostring(conn):match("%w+$");
		log = function (type, area, message) mainlog(type, conn_name, message); end
	end
	local print = function (...) log("info", "core", t_concatall({...}, "\t")); end
	session.log = log;

	--	--	--

	-- Send buffers --

	local sendbuffer = session.sendbuffer;
	local send = function (data) return t_insert(sendbuffer, tostring(data)); end;
	local send_to = 	function (to, stanza)
					local node, host, resource = jid.split(to);
					print("Routing stanza to "..to..":", node, host, resource);
					if not hosts[host] then
						print("   ...but host offline, establishing connection");
						connect_host(host);
						t_insert(hosts[host].sendbuffer, stanza); -- This will be sent when s2s connection succeeds					
					elseif hosts[host].connected then
						print("   ...putting in our external send buffer");
						t_insert(sendbuffer.external, { node = node, host = host, resource = resource, data = stanza});
						print("   ...there are now "..tostring(#sendbuffer.external).." stanzas in the external send buffer");
					end
				end
	session.send, session.send_to = send, send_to;

	--	--	--
	print("Client connected");
	conn = ssl.wrap(copas.wrap(conn), ssl_ctx);
	
	do
		local succ, msg
		conn:settimeout(15)
		while not succ do
			succ, msg = conn:dohandshake()
			if not succ then
				print("SSL: "..tostring(msg));
				if msg == 'wantread' then
					socket.select({conn}, nil)
				elseif msg == 'wantwrite' then
					socket.select(nil, {conn})
				else
					-- other error
				end
			end
		end
	end
	print("SSL handshake complete");
	-- XML parser initialisation --

	local parser;
	local stanza;
	
	local stanza_dispatch = init_stanza_dispatcher(session);

	local xml_handlers = {};
	
	do
		local ns_stack = { "" };
		local curr_ns = "";
		local curr_tag;
		function xml_handlers:StartElement(name, attr)
			curr_ns,name = name:match("^(.+):(%w+)$");
			print("Tag received:", name, tostring(curr_ns));
			if not stanza then
				if session.notopen then
					if name == "stream" then
						session.host = attr.to or error("Client failed to specify destination hostname");
			                        session.version = attr.version or 0;
			                        session.streamid = m_random(1000000, 99999999);
			                        print(session, session.host, "Client opened stream");
			                        send("<?xml version='1.0'?>");
			                        send(format("<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s' >", session.streamid, session.host));
			                        --send("<stream:features>");
			                        --send("<mechanism>PLAIN</mechanism>");
        			                --send [[<register xmlns="http://jabber.org/features/iq-register"/> ]]
        			                --send("</stream:features>");
						log("info", "core", "Stream opened successfully");
						session.notopen = nil;
						return;
					end
					error("Client failed to open stream successfully");
				end
				if name ~= "iq" and name ~= "presence" and name ~= "message" then
					error("Client sent invalid top-level stanza");
				end
				stanza = st.stanza(name, { to = attr.to, type = attr.type, id = attr.id, xmlns = curr_ns });
				curr_tag = stanza;
			else
				attr.xmlns = curr_ns;
				stanza:tag(name, attr);
			end
		end
		function xml_handlers:CharacterData(data)
			if data:match("%S") then
				stanza:text(data);
			end
		end
		function xml_handlers:EndElement(name)
			curr_ns,name = name:match("^(.+):(%w+)$");
			--print("<"..name.."/>", tostring(stanza), tostring(#stanza.last_add < 1), tostring(stanza.last_add[#stanza.last_add].name));
			if (not stanza) or #stanza.last_add < 0 or (#stanza.last_add > 0 and name ~= stanza.last_add[#stanza.last_add].name) then error("XML parse error in client stream"); end
			-- Complete stanza
			print(name, tostring(#stanza.last_add));
			if #stanza.last_add == 0 then
				stanza_dispatch(stanza);
				stanza = nil;
			else
				stanza:up();
			end
		end
--[[		function xml_handlers:StartNamespaceDecl(namespace)
			table.insert(ns_stack, namespace);
			curr_ns = namespace;
			log("debug", "parser", "Entering namespace "..tostring(curr_ns));
		end
		function xml_handlers:EndNamespaceDecl(namespace)
			table.remove(ns_stack);
			log("debug", "parser", "Leaving namespace "..tostring(curr_ns));
			curr_ns = ns_stack[#ns_stack];
			log("debug", "parser", "Entering namespace "..tostring(curr_ns));
		end
]]
	end
	parser = lxp.new(xml_handlers, ":");

	--	--	--

	-- Main loop --
	print "Receiving..."
	reqdata = copas_receive(conn, 1);
	print "Received"
	while reqdata do
		parser:parse(reqdata);
		if #sendbuffer.external > 0 then
			-- Stanzas queued to go to other places, from us
			-- ie. other local users, or remote hosts that weren't connected before
			print(#sendbuffer.external.." stanzas queued for other recipients, sending now...");
			for n, packet in pairs(sendbuffer.external) do
				if not hosts[packet.host] then
					connect_host(packet.host);
					t_insert(hosts[packet.host].sendbuffer, packet.data);
				elseif hosts[packet.host].type == "local" then
					print("   ...is to a local user")
					local destuser = hosts[packet.host].sessions[packet.node];
					if destuser and destuser.sessions then
						if not destuser.sessions[packet.resource] then
							local best_resource;
							for resource, session in pairs(destuser.sessions) do
								if not best_session then best_session = session;
								elseif session.priority >= best_session.priority and session.priority >= 0 then
									best_session = session;
								end
							end
							if not best_session then
								offlinemessage.new(packet.node, packet.host, packet.data);
							else
								print("resource '"..packet.resource.."' was not online, have chosen to send to '"..best_session.username.."@"..best_session.host.."/"..best_session.resource.."'");
								packet.resource = best_session.resource;
							end
						end
						if destuser.sessions[packet.resource] == session then
							log("warn", "core", "Attempt to send stanza to self, dropping...");
						else
							print("...sending...");
							copas_send(destuser.sessions[packet.resource].conn, tostring(packet.data));
							print("...sent")
						end
					elseif packet.data.name == "message" then
						print("   ...will be stored offline");
						offlinemessage.new(packet.node, packet.host, packet.data);
					elseif packet.data.name == "iq" then
						print("   ...is an iq");
						send(st.reply(packet.data)
							:tag("error", { type = "cancel" })
								:tag("service-unavailable", { xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas" }));
					end
					print("   ...removing from send buffer");
					sendbuffer.external[n] = nil;
				end
			end
		end
		
		if #sendbuffer > 0 then 
			for n, data in ipairs(sendbuffer) do
				print "Sending..."
				copas_send(conn, data);
				print "Sent"
				sendbuffer[n] = nil;
			end
		end
		print "Receiving..."
		repeat
			reqdata, sktmsg = copas_receive(conn, 1);
			if sktmsg == 'wantread' then
				print("Received... wantread");
				--socket.select({conn}, nil)
				--print("Socket ready now...");
			elseif sktmsg then
				print("Received socket message:", sktmsg);
			end
		until reqdata or sktmsg == "closed";
		print("Received", tostring(reqdata));
	end
	log("info", "core", "Client disconnected, connection closed");
end

server = socket.bind("*", 5223)
assert(server, "Failed to bind to socket")
copas.addserver(server, handler)

copas.loop();