1 module irc.dcc;
2 
3 import std.algorithm;
4 import std.array;
5 import std.exception;
6 import std.random : uniform;
7 import std.range;
8 import std.socket;
9 import std.typecons;
10 
11 import irc.protocol;
12 import irc.client;
13 import irc.eventloop;
14 
15 //ffs
16 version(Windows)
17 	import std.c.windows.winsock;
18 else version(Posix)
19 	import core.sys.posix.netinet.in_;
20 else
21 	static assert(false, "ffff");
22 
23 enum DccChatType
24 {
25 	plain, ///
26 	secure ///
27 }
28 
29 /// Thrown when a DCC error occurs.
30 class DccException : Exception
31 {
32 	this(string msg, Throwable next = null, string file = __FILE__, size_t line = __LINE__)
33 	{
34 		super(msg, file, line, next);
35 	}
36 }
37 
38 /**
39  * Hub for new DCC connections.
40  */
41 class DccServer
42 {
43 	private:
44 	IrcEventLoop eventLoop;
45 	IrcClient client;
46 	uint clientAddress_ = 0;
47 
48 	ushort portStart, portEnd;
49 	ushort lastPort;
50 
51 	string queriedNick;
52 
53 	static struct Offer
54 	{
55 		enum State { pendingAddress, listening, sending, declined, finished }
56 		State state;
57 	}
58 
59 	Offer[] sendQueue;
60 
61 	Tuple!(Socket, ushort) allocatePort()
62 	{
63 		auto portRange = iota(portStart, portEnd + 1);
64 
65 		auto ports = portRange.cycle(lastPort);
66 		ports.popFront(); // skip last used port
67 
68 		size_t tries = 0;
69 		auto socket = new TcpSocket();
70 		foreach(lport; ports)
71 		{
72 			auto port = cast(ushort)lport;
73 			try
74 			{
75 				auto addr = new InternetAddress(port); // ew :<
76 				socket.bind(addr);
77 				lastPort = port;
78 				return tuple(cast(Socket)socket, port);
79 			}
80 			catch(SocketOSException e)
81 			{
82 				if(++tries >= portRange.length)
83 					throw new DccException("no available port in assigned range", e);
84 			}
85 		}
86 
87 		assert(false);
88 	}
89 
90 	// Figure out the client address by getting
91 	// its hostname from the IRC server
92 	void queryUserhost()
93 	{
94 		queriedNick = client.nickName;
95 		client.queryUserhost(queriedNick);
96 		client.onUserhostReply ~= &onUserhostReply;
97 	}
98 
99 	void onUserhostReply(in IrcUser[] users)
100 	{
101 		// If a client address has ben set
102 		// by this point, don't overwrite it.
103 		if(clientAddress != 0)
104 		{
105 			client.onUserhostReply.unsubscribeHandler(&onUserhostReply);
106 			return;
107 		}
108 
109 		foreach(ref user; users)
110 		{
111 			if(user.nickName == queriedNick)
112 			{
113 				clientAddress = user.hostName;
114 				client.onUserhostReply.unsubscribeHandler(&onUserhostReply);
115 				break;
116 			}
117 		}
118 	}
119 
120 	public:
121 	/**
122 	 * Create a new DCC server given the event loop
123 	 * and IRC _client to be associated with this
124 	 * server.
125 	 *
126 	 * The event loop is used to schedule reads and
127 	 * writes for open DCC connections.
128 	 *
129 	 * The associated IRC _client is used to send
130 	 * DCC/CTCP notifications as well as to look up
131 	 * the Internet address to advertise for this
132 	 * server.
133 	 */
134 	this(IrcEventLoop eventLoop, IrcClient client)
135 	{
136 		this.eventLoop = eventLoop;
137 		this.client = client;
138 
139 		setPortRange(49152, 65535);
140 
141 		if(client.connected)
142 			queryUserhost();
143 		else
144 		{
145 			void onConnect()
146 			{
147 				if(clientAddress != 0)
148 					queryUserhost();
149 
150 				client.onConnect.unsubscribeHandler(&onConnect);
151 			}
152 
153 			client.onConnect ~= &onConnect;
154 		}
155 	}
156 
157 	/**
158 	 * The IP address of the DCC server in network byte order.
159 	 *
160 	 * If not explicitly provided, this defaults to the result of
161 	 * looking up the hostname for the associated IRC client.
162 	 */
163 	uint clientAddress() const pure @property
164 	{
165 		return clientAddress_;
166 	}
167 
168 	/// Ditto
169 	void clientAddress(uint addr) pure @property
170 	{
171 		clientAddress_ = addr;
172 	}
173 
174 	/// Ditto
175 	void clientAddress(in char[] hostName) @property
176 	{
177 		auto addresses = getAddress(hostName);
178 
179 		if(addresses.empty)
180 			return;
181 
182 		auto address = addresses[0];
183 		clientAddress = htonl((cast(sockaddr_in*)address.name).sin_addr.s_addr);
184 	}
185 
186 	/// Ditto
187 	void clientAddress(Address address) @property
188 	{
189 		clientAddress = htonl((cast(sockaddr_in*)address.name).sin_addr.s_addr);
190 	}
191 
192 	/**
193 	 * Set the port range for accepting connections.
194 	 *
195 	 * The server selects a port in this range when
196 	 * initiating connections. The default range is
197 	 * 49152–65535. The range is inclusive on both
198 	 * ends.
199 	 */
200 	void setPortRange(ushort lower, ushort upper)
201 	{
202 		enforce(lower < upper);
203 
204 		portStart = lower;
205 		portEnd = upper;
206 
207 		lastPort = uniform(portStart, portEnd);
208 	}
209 
210 	/+/**
211 	 * Send a resource (typically a file) to the given user.
212 	 *
213 	 * The associated IRC client must be connected.
214 	 */
215 	void send(in char[] nick, DccConnection resource)
216 	{
217 		enforce(client.connected, "client must be connected before using DCC SEND");
218 
219 		auto port = 0;
220 		auto len = 0;
221 
222 		auto query = format("SEND %s %d %d %d",
223 		    resource.name, clientAddress, port, len);
224 
225 		client.ctcpQuery(nick, "DCC", query);
226 	}+/
227 
228 	/**
229 	 * Invite the given user to a DCC chat session.
230 	 *
231 	 * The associated IRC client must be connected.
232 	 * Params:
233 	 *   nick = _nick of user to invite
234 	 *   timeout = time in seconds to wait for the
235 	 *   invitation to be accepted
236 	 * Returns:
237 	 *   A listening DCC chat session object
238 	 */
239 	DccChat inviteChat(in char[] nick, uint timeout = 10)
240 	{
241 		enforce(client.connected, "client must be connected before using DCC CHAT");
242 
243 		auto results = allocatePort();
244 		auto socket = results[0];
245 		auto port = results[1];
246 
247 		client.ctcpQuery(nick, "DCC",
248 		    format("CHAT chat %d %d", clientAddress, port));
249 
250 		socket.listen(1);
251 
252 		auto dccChat = new DccChat(socket, timeout);
253 		dccChat.eventLoop = eventLoop;
254 		dccChat.eventIndex = eventLoop.add(dccChat);
255 		return dccChat;
256 	}
257 
258 	void closeConnection(DccConnection conn)
259 	{
260 		eventLoop.remove(conn.eventIndex);
261 		conn.socket.close();
262 	}
263 }
264 
265 /// Represents a DCC connection.
266 abstract class DccConnection
267 {
268 	public:
269 	/// Current state of the connection.
270 	enum State
271 	{
272 		preConnect, /// This session is waiting for a connection.
273 		timedOut, /// This session timed out when waiting for a connection.
274 		connected, /// This is an active connection.
275 		closed /// This DCC session has ended.
276 	}
277 
278 	/// Ditto
279 	State state = State.preConnect;
280 
281 	private:
282 	IrcEventLoop eventLoop;
283 
284 	package:
285 	Socket socket; // Refers to either a server or client
286 	DccEventIndex eventIndex;
287 
288 	enum Event { none, connectionEstablished, finished }
289 
290 	final Event read()
291 	{
292 		final switch(state) with(State)
293 		{
294 			case preConnect:
295 				auto conn = socket.accept();
296 				socket.close();
297 
298 				socket = conn;
299 				state = connected;
300 
301 				onConnected();
302 
303 				return Event.connectionEstablished;
304 			case connected:
305 				static ubyte[1024] buffer; // TODO
306 
307 				auto received = socket.receive(buffer);
308 				if(received <= 0)
309 				{
310 					state = closed;
311 					onDisconnected();
312 					return Event.finished;
313 				}
314 
315 				auto data = buffer[0 .. received];
316 
317 				bool finished = onRead(data);
318 
319 				if(finished)
320 				{
321 					state = closed;
322 					socket.close();
323 					onDisconnected();
324 					return Event.finished;
325 				}
326 				break;
327 			case closed, timedOut:
328 				assert(false);
329 		}
330 
331 		return Event.none;
332 	}
333 
334 	final void doTimeout()
335 	{
336 		state = State.timedOut;
337 		socket.close();
338 
339 		foreach(callback; onTimeout)
340 			callback();
341 	}
342 
343 	protected:
344 	/**
345 	 * Initialize a DCC resource with the given _socket, timeout value and state.
346 	 */
347 	this(Socket socket, uint timeout, State initialState)
348 	{
349 		this.state = initialState;
350 		this.timeout = timeout;
351 		this.socket = socket;
352 	}
353 
354 	/**
355 	 * Write to this connection.
356 	 */
357 	final void write(in void[] data)
358 	{
359 		socket.send(data);
360 	}
361 
362 	/**
363 	 * Invoked when the connection has been established.
364 	 */
365 	abstract void onConnected();
366 
367 	/**
368 	 * Invoked when the connection was closed cleanly.
369 	 */
370 	abstract void onDisconnected();
371 
372 	/**
373 	 * Invoked when _data was received.
374 	 */
375 	abstract bool onRead(in void[] data);
376 
377 	public:
378 	/// The _timeout value of this connection in seconds.
379 	immutable uint timeout;
380 
381 	/// Name of this resource.
382 	abstract string name() @property;
383 
384 	/**
385 	 * Invoked when an error occurs.
386 	 */
387 	void delegate(Exception e)[] onError;
388 
389 	/**
390 	 * Invoked when a listening connection has timed out.
391 	 */
392 	 void delegate()[] onTimeout;
393 }
394 
395 /// Represents a DCC chat session.
396 class DccChat : DccConnection
397 {
398 	private:
399 	import irc.linebuffer;
400 
401 	char[] buffer; // TODO: use dynamically expanding buffer?
402 	LineBuffer lineBuffer;
403 
404 	this(Socket server, uint timeout)
405 	{
406 		super(server, timeout, State.preConnect);
407 
408 		buffer = new char[2048];
409 		lineBuffer = LineBuffer(buffer, &handleLine);
410 	}
411 
412 	protected:
413 	void handleLine(in char[] line)
414 	{
415 		foreach(callback; onMessage)
416 			callback(line);
417 	}
418 
419 	override void onConnected()
420 	{
421 		foreach(callback; onConnect)
422 			callback();
423 	}
424 
425 	override void onDisconnected()
426 	{
427 		foreach(callback; onFinish)
428 			callback();
429 	}
430 
431 	override bool onRead(in void[] data)
432 	{
433 		auto remaining = cast(const(char)[])data;
434 
435 		while(!remaining.empty)
436 		{
437 			auto space = buffer[lineBuffer.position .. $];
438 
439 			auto len = min(remaining.length, space.length);
440 
441 			space[0 .. len] = remaining[0 .. len];
442 
443 			lineBuffer.commit(len);
444 
445 			remaining = remaining[len .. $];
446 		}
447 
448 		return false;
449 	}
450 
451 	public:
452 	/// Always the string "chat".
453 	override string name() @property { return "chat"; }
454 
455 	/// Invoked when the session has started.
456 	void delegate()[] onConnect;
457 
458 	/// Invoked when the session has cleanly ended.
459 	void delegate()[] onFinish;
460 
461 	/// Invoked when a line of text has been received.
462 	void delegate(in char[] line)[] onMessage;
463 
464 	/**
465 	 * Send a single chat _message.
466 	 * Params:
467 	 *   message = _message to _send. Must not contain newlines.
468 	 */
469 	void send(in char[] message)
470 	{
471 		write(message);
472 		write("\n"); // TODO: worth avoiding?
473 	}
474 
475 	/**
476 	 * Send a single, formatted chat message.
477 	 * Params:
478 	 *   fmt = format of message to send. Must not contain newlines.
479 	 *   fmtArgs = $(D fmt) is formatted with these arguments.
480 	 * See_Also:
481 	 *   $(STDREF format, formattedWrite)
482 	 */
483 	void sendf(FmtArgs...)(in char[] fmt, FmtArgs fmtArgs)
484 	{
485 		write(format(fmt, fmtArgs)); // TODO: reusable buffer
486 		write("\n"); // TODO: worth avoiding?
487 	}
488 
489 	/**
490 	 * Send chat _messages.
491 	 * Each message must be terminated with the character $(D \n).
492 	 */
493 	void sendMultiple(in char[] messages)
494 	{
495 		write(messages);
496 	}
497 
498 	/**
499 	 * End the chat session.
500 	 */
501 	void finish()
502 	{
503 		socket.close();
504 		eventLoop.remove(eventIndex);
505 
506 		foreach(callback; onFinish)
507 			callback();
508 	}
509 }