package chatty;
import static chatty.Irc.SSL_ERROR;
import chatty.util.StringUtil;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.logging.Logger;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* A single connection to a server that can receive and send data.
*
* @author tduva
*/
public class Connection implements Runnable {
private static final Logger LOGGER = Logger.getLogger(Connection.class.getName());
private final InetSocketAddress address;
private final Irc irc;
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private boolean connected = false;
private int disconnectReason = -1;
private String disconnectMessage = null;
private int connectionCheckedCount;
private static final int CONNECT_TIMEOUT = 10*1000; // 10 seconds timeout
private static final int SOCKET_BLOCK_TIMEOUT = 15*1000; // 15 seconds
private static final int PING_AFTER_CHECKS = 3; // 45 seconds (3*SOCKET_BLOCK_TIMEOUT)
private final String idPrefix;
private final boolean secured;
public Connection(Irc irc, InetSocketAddress address, String id, boolean secured) {
this.irc = irc;
this.address = address;
this.idPrefix = "["+id+"] ";
this.secured = secured;
}
private final void info(String message) {
LOGGER.info(idPrefix+message);
}
private final void warning(String message) {
LOGGER.warning(idPrefix+message);
}
public InetSocketAddress getAddress() {
return address;
}
/**
* Thread that opens the connection and receives data from the connection.
*/
@Override
public void run() {
Charset charset = Charset.forName("UTF-8");
try {
info("Trying to connect to "+address+(secured ? " (secured)" : ""));
// Try to connect and open streams
if (secured) {
SSLSocketFactory sf = (SSLSocketFactory)SSLSocketFactory.getDefault();
socket = sf.createSocket();
((SSLSocket) socket).setUseClientMode(true);
/**
* Workaround for "Could not generate DH keypair" exception
* http://stackoverflow.com/a/6862383
*
* Maybe not be necessary anymore for now
*/
// List<String> limited = new LinkedList<>();
// for (String suite : ((SSLSocket) socket).getEnabledCipherSuites()) {
// if (!suite.contains("_DHE_")) {
// limited.add(suite);
// }
// }
// ((SSLSocket) socket).setEnabledCipherSuites(limited.toArray(new String[limited.size()]));
} else {
socket = new Socket();
}
socket.connect(address, CONNECT_TIMEOUT);
// info("Connecting to "+socket.getRemoteSocketAddress().toString());
out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(),charset)
);
in = new BufferedReader(
new InputStreamReader(socket.getInputStream(),charset)
);
socket.setSoTimeout(SOCKET_BLOCK_TIMEOUT);
} catch (UnknownHostException ex) {
irc.disconnected(Irc.ERROR_UNKNOWN_HOST);
warning("Error opening connection to "+address+": "+ex);
return;
} catch (SocketTimeoutException ex) {
warning("Error opening connection: "+ex);
irc.disconnected(Irc.ERROR_SOCKET_TIMEOUT);
return;
} catch (IOException ex) {
warning("Error opening connection: "+ex);
irc.disconnected(Irc.ERROR_SOCKET_ERROR, ex.getLocalizedMessage());
return;
}
// At this point the connection succeeded, but not registered with the
// IRC server (wich is often called "connected" in this context)
info("Connected to "+socket.getRemoteSocketAddress().toString());
connected = true;
irc.connected(socket.getInetAddress().toString(),address.getPort());
StringBuilder b = new StringBuilder();
boolean previousWasCR = false;
String receivedLine = null;
while (true) {
try {
/**
* Read line ending with \r\n (blocks, but has a timeout set).
*
* This also filters \r and \n characters from the parsed
* messages, because they are not added to the buffer.
*/
int c = in.read();
if (c == -1) {
// End of stream
break;
}
if (c == '\r') {
previousWasCR = true;
//System.out.print("\\r");
} else if (c == '\n') {
//System.out.println("\\n");
if (previousWasCR) {
// Take buffer as line and reset
receivedLine = b.toString();
b.setLength(0);
previousWasCR = false;
}
} else {
b.append((char)c);
previousWasCR = false;
//System.out.print((char)c);
}
if (receivedLine == null) {
// Read more characters to get to end of line
continue;
}
// Line was received
irc.received(receivedLine);
receivedLine = null;
activity();
} catch (SocketTimeoutException ex) {
checkConnection();
} catch (SSLException ex) {
warning("SSL Error reading from socket: "+ex);
disconnectReason = SSL_ERROR;
disconnectMessage = ex.getLocalizedMessage();
break;
} catch (IOException ex) {
info("Error reading from socket: "+ex);
break;
}
}
// No longer receiving data, so properly close connection if necessary.
close();
}
/**
* Notifies the activity tracker that there was activity on the connection.
*/
private void activity() {
connectionCheckedCount = 0;
}
/**
* Checks if the server should be pinged. Takes into account the approximate
* passed time and how active the connection was before it stopped being
* active.
*/
private void checkConnection() {
connectionCheckedCount++;
if (connectionCheckedCount == PING_AFTER_CHECKS) {
//LOGGER.info("Pinging server to check connection..");
send("PING");
connectionCheckedCount = 0;
}
}
/**
* Closes the connection if still connected and cleans up.
*/
synchronized public void close() {
if (connected) {
info("Closing socket.");
try {
out.close();
in.close();
socket.close();
} catch (IOException ex) {
warning("Error closing socket: "+ex);
}
if (disconnectReason == -1) {
disconnectReason = Irc.ERROR_CONNECTION_CLOSED;
}
if (disconnectMessage == null) {
disconnectMessage = "";
}
irc.disconnected(disconnectReason, disconnectMessage);
}
connected = false;
}
/**
* Send a line of data to the server.
*
* @param data
*/
synchronized public void send(String data) {
data = StringUtil.removeLinebreakCharacters(data);
irc.sent(data);
out.print(data+"\r\n");
out.flush();
activity();
}
}