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 IncomingLineBuffer lineBuffer; 403 404 this(Socket server, uint timeout) 405 { 406 super(server, timeout, State.preConnect); 407 408 buffer = new char[2048]; 409 lineBuffer = IncomingLineBuffer(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 }