/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.dcc;
import java.io.*;
import java.net.*;
import org.w3c.dom.Element;
import util.xml.*;
import com.leafdigital.irc.api.*;
import com.leafdigital.ircui.api.*;
import com.leafdigital.logs.api.Logger;
import com.leafdigital.net.api.Network;
import leafchat.core.api.*;
/**
* Implements the DCC chat window.
*/
public class DCCChatWindow implements GeneralChatWindow.Handler
{
private GeneralChatWindow w;
private PluginContext context;
private String nick;
private Server server;
private OutputStream output;
private boolean cancelled=false;
private InetAddress remoteAddress;
private int remotePort;
/**
* Constructs window (outgoing).
* @param context Plugin context
* @param s Server
* @param nick Nickname
*/
public DCCChatWindow(PluginContext context, Server s, String nick)
{
this(context, s, nick, null, 0);
}
/**
* Constructs window (incoming).
* @param context Plugin context
* @param s Server
* @param nick Nickname
* @param address Address for connection
* @param port Port for connection
*/
public DCCChatWindow(PluginContext context, Server s, String nick, InetAddress address, int port)
{
this.context = context;
this.nick = nick;
this.server = s;
// Create window, minimised if this is newly-received message
IRCUI ircui = context.getSingle(IRCUI.class);
w = ircui.createGeneralChatWindow(context, this, "DCC",
Logger.CATEGORY_USER, nick, 510, s.getOurNick(), nick, address!=null);
w.setTitle(nick + " - DCC chat");
w.setEnabled(false);
// Listen or connect?
if(address == null)
{
(new Thread(new Runnable()
{
@Override
public void run()
{
runServer();
}
}, "DCC chat server thread: " + nick)).start();
}
else
{
this.remoteAddress = address;
this.remotePort = port;
w.addLine("<nick>" + XML.esc(nick) + "</nick> ("+
"<key>" + remoteAddress.getHostAddress() + "</key>:<key>" + remotePort + "</key>) " +
"has requested that you connect directly for a conversation that " +
"doesn't go via the IRC server. If you weren't expecting it, close " +
"this window.");
w.addLine("<internalaction id='connect'>Accept and connect</internalaction>.");
}
}
/** Just used to track whether user has clicked the Connect link or not */
private boolean clicked;
@Override
public void internalAction(Element e) throws GeneralException
{
try
{
if(XML.getRequiredAttribute(e, "id").equals("connect") && !clicked)
{
clicked = true;
connect();
}
}
catch(XMLException ex)
{
throw new BugException(ex);
}
}
private void connect()
{
(new Thread(new Runnable()
{
@Override
public void run()
{
runClient();
}
}, "DCC chat client thread: "+nick)).start();
}
private void error(String xml)
{
w.addLine(xml);
w.setEnabled(false);
cancelled=true;
}
private void runClient()
{
context.log("DCC chat: connecting to " + remoteAddress.getHostAddress() + ":"+remotePort);
w.addLine("Connecting to <key>" + remoteAddress.getHostAddress() + "</key>:<key>"+remotePort+"</key>...");
Network n = context.getSingle(Network.class);
Socket s;
InputStream input;
try
{
s = n.connect(remoteAddress.getHostAddress(), remotePort, 30000);
output = s.getOutputStream();
input = s.getInputStream();
}
catch(IOException e)
{
error("Connection failed: <error>" + XML.esc(e.getMessage()) + "</error>");
return;
}
try
{
connected(input);
}
finally
{
try
{
s.close();
}
catch(IOException e)
{
}
}
}
private void runServer()
{
Network.Port p = null;
Socket s = null;
try
{
InetAddress ia;
long ipnumber;
try
{
p = ((DCCPlugin)context.getPlugin()).getDCCListenPort(nick);
ia = p.getPublicAddress();
ipnumber = ((DCCPlugin)context.getPlugin()).getStupidIPNumber(ia);
}
catch(GeneralException e)
{
error("Connection setup failed: <error>" + XML.esc(e.getMessage()) + "</error>");
return;
}
int port = p.getPublicPort();
// OK, we have a port and stupid-version IP, let's send the DCC message.
try
{
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IRCMsg.constructBytes(
"PRIVMSG " + nick + " :\u0001DCC CHAT chat " + ipnumber + " " + port + "\u0001"));
server.sendLine(out.toByteArray());
context.logDebug("Sent DCC CHAT: " + IRCMsg.convertISO(out.toByteArray()));
}
catch(IOException e)
{
error("Failed to send chat request: <error>" + XML.esc(e.getMessage()) + "</error>");
return;
}
// That's great, now let's listen
InputStream input;
try
{
context.log("DCC chat: listening on " + ia.getHostAddress() + ":" + port);
w.addLine("Waiting for a connection on <key>" + ia.getHostAddress() + "</key>:<key>" + port + "</key>...");
while(true)
{
try
{
s = p.accept(1000);
s.setSoTimeout(1000);
context.logDebug("Got connection on server socket");
context.log("DCC send: connected from " + s.getInetAddress().getHostAddress());
break;
}
catch(SocketTimeoutException e)
{
}
if(cancelled)
{
return;
}
}
output = s.getOutputStream();
input = s.getInputStream();
}
catch(IOException e)
{
error("Problem with local socket: <error>" + XML.esc(e.getMessage()) + "</error>");
return;
}
// Now do connected bit
connected(input);
}
finally
{
try
{
if(p != null)
{
p.close();
}
if(s!=null)
{
s.close();
}
}
catch(IOException e)
{
}
}
}
private void connected(InputStream input)
{
w.addLine("Connected and ready.");
w.setEnabled(true);
byte[] buffer = new byte[1024];
byte[] leftovers = new byte[0];
while(true)
{
try
{
int read = input.read(buffer);
if(read == -1)
{
error("Connection closed.");
return;
}
leftovers = handleBuffer(buffer, read, leftovers);
}
catch(SocketTimeoutException e)
{
}
catch(IOException e)
{
error("Error reading data: <error>" + XML.esc(e.getMessage()) + "</error>");
return;
}
if(cancelled)
{
return;
}
}
}
private byte[] handleBuffer(byte[] buffer, int read, byte[] leftovers)
{
for(int i=0; i<read; i++)
{
int foundsize = 0;
if(i < read - 1 && buffer[i] == '\r' && buffer[i + 1] == '\n')
{
foundsize = 2;
}
else if(buffer[i] == '\n')
{
foundsize = 1;
}
if(foundsize>0)
{
byte[] line = new byte[leftovers.length + i];
System.arraycopy(leftovers, 0, line, 0, leftovers.length);
System.arraycopy(buffer, 0, line, leftovers.length, i);
handleLine(line);
System.arraycopy(buffer, i + foundsize, buffer, 0, read - (i + foundsize));
read -= (i + foundsize);
leftovers = new byte[0];
return handleBuffer(buffer, read, leftovers);
}
}
byte[] newLeftovers = new byte[read + leftovers.length];
System.arraycopy(leftovers, 0, newLeftovers, 0, leftovers.length);
System.arraycopy(buffer, 0, newLeftovers, leftovers.length, read);
return newLeftovers;
}
private void handleLine(byte[] line)
{
IRCEncoding.EncodingInfo encoding = getEncoding();
// Is it a /me?
if(line[0] == 1 && line.length > 7 && (new String(line, 1, 7)).equals("ACTION "))
{
int length = line.length - 8;
if(line[line.length - 1] == 1)
{
length--; // Optional ^A at end
}
byte[] data = new byte[length];
System.arraycopy(line, 8, data, 0, length);
String text = encoding.convertIncoming(data);
w.showRemoteText(MessageDisplay.TYPE_ACTION, nick, text);
}
else
{
w.showRemoteText(MessageDisplay.TYPE_MSG, nick, encoding.convertIncoming(line));
}
}
private IRCEncoding.EncodingInfo getEncoding()
{
IRCEncoding encoding = context.getSingle(IRCEncoding.class);
IRCEncoding.EncodingInfo ei = encoding.getEncoding(
null, null, new IRCUserAddress(nick, "", ""));
return ei;
}
private final static byte[] CRLF={'\r', '\n'};
private final static byte[] ACTIONSTART={1, 'A', 'C', 'T', 'I', 'O', 'N', ' '};
private final static byte[] ACTIONEND={1, '\r', '\n'};
@Override
public void doCommand(Commands c, String line)
{
if(line.length()==0)
{
return; // May already have been checked but
}
IRCEncoding.EncodingInfo ei = getEncoding();
try
{
if(!c.isCommandCharacter(line.charAt(0)))
{
// Send line, CRLF terminated
output.write(ei.convertOutgoing(line));
output.write(CRLF);
output.flush();
w.showOwnText(MessageDisplay.TYPE_MSG, nick, line);
}
else if(line.length() >= 4 && line.substring(1, 4).equalsIgnoreCase("me "))
{
output.write(ACTIONSTART);
output.write(ei.convertOutgoing(line.substring(4)));
output.write(ACTIONEND);
output.flush();
w.showOwnText(MessageDisplay.TYPE_ACTION, nick, line.substring(4));
}
else // Run as normal command
{
c.doCommand(line, null, new IRCUserAddress(nick, "", ""), null, w.getMessageDisplay(), false);
}
}
catch(IOException e)
{
error("Error sending data: <error>" + e.getMessage() + "</error>");
}
}
@Override
public void windowClosed()
{
cancelled = true; // If still waiting for connection, this stops it
}
}