/*
* Copyright (C) 2008-2017, Juick
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package com.juick.components.s2s;
import com.juick.components.XMPPServer;
import com.juick.components.s2s.util.DialbackUtils;
import com.juick.xmpp.extensions.StreamError;
import com.juick.xmpp.extensions.StreamFeatures;
import com.juick.xmpp.utils.XmlUtils;
import org.apache.commons.text.RandomStringGenerator;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.UUID;
/**
* @author ugnich
*/
public class ConnectionOut extends Connection implements Runnable {
public boolean streamReady = false;
public String to;
String checkSID = null;
String dbKey = null;
RandomStringGenerator generator = new RandomStringGenerator.Builder().withinRange('a', 'z').build();
public ConnectionOut(XMPPServer xmpp, String hostname) throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException, XmlPullParserException, KeyManagementException, KeyStoreException, IOException {
super(xmpp);
to = hostname;
dbKey = DialbackUtils.generateDialbackKey(generator.generate(15), to, xmpp.HOSTNAME, streamID);
}
public ConnectionOut(XMPPServer xmpp, String hostname, String checkSID, String dbKey) throws Exception {
super(xmpp);
to = hostname;
this.checkSID = checkSID;
this.dbKey = dbKey;
streamID = UUID.randomUUID().toString();
}
void sendOpenStream() throws IOException {
sendStanza("");
}
void processDialback() throws Exception {
if (checkSID != null) {
sendDialbackVerify(checkSID, dbKey);
}
sendStanza("" +
dbKey + "");
}
@Override
public void run() {
logger.info("stream to {} start", to);
try {
socket = new Socket();
InetSocketAddress address = DNSQueries.getServerAddress(to);
socket.connect(address);
restartParser();
sendOpenStream();
parser.next(); // stream:stream
streamID = parser.getAttributeValue(null, "id");
if (streamID == null || streamID.isEmpty()) {
throw new Exception("stream to " + to + " invalid first packet");
}
logger.info("stream to {} {} open", to, streamID);
xmpp.addConnectionOut(ConnectionOut.this);
boolean xmppversionnew = parser.getAttributeValue(null, "version") != null;
if (!xmppversionnew) {
processDialback();
}
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
logParser();
String tag = parser.getName();
if (tag.equals("result") && parser.getNamespace().equals(NS_DB)) {
String type = parser.getAttributeValue(null, "type");
if (type != null && type.equals("valid")) {
streamReady = true;
logger.info("stream to {} {} ready", to, streamID);
String cache = xmpp.getFromCache(to);
if (cache != null) {
logger.info("stream to {} {} sending cache", to, streamID);
sendStanza(cache);
}
} else {
logger.info("stream to {} {} dialback fail", to, streamID);
}
XmlUtils.skip(parser);
} else if (tag.equals("verify") && parser.getNamespace().equals(NS_DB)) {
String from = parser.getAttributeValue(null, "from");
String type = parser.getAttributeValue(null, "type");
String sid = parser.getAttributeValue(null, "id");
if (from != null && from.equals(to) && sid != null && !sid.isEmpty() && type != null) {
ConnectionIn c = xmpp.getConnectionIn(sid);
if (c != null) {
c.sendDialbackResult(from, type);
}
}
XmlUtils.skip(parser);
} else if (tag.equals("features") && parser.getNamespace().equals(NS_STREAM)) {
StreamFeatures features = StreamFeatures.parse(parser);
if (sc != null && !isSecured() && features.STARTTLS >= 0 && !xmpp.brokenSSLhosts.contains(to)) {
logger.info("stream to {} {} securing", to, streamID);
sendStanza("");
} else {
processDialback();
}
} else if (tag.equals("proceed") && parser.getNamespace().equals(NS_TLS)) {
try {
socket = sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(),
socket.getPort(), true);
((SSLSocket) socket).startHandshake();
setSecured(true);
logger.info("stream {} secured", streamID);
restartParser();
sendOpenStream();
} catch (SSLException sex) {
logger.error("s2s ssl error: {} {}, error {}", to, streamID, sex);
sendStanza("");
xmpp.removeConnectionOut(this);
closeConnection();
}
} else if (isSecured() && tag.equals("stream") && parser.getNamespace().equals(NS_STREAM)) {
streamID = parser.getAttributeValue(null, "id");
} else if (tag.equals("error")) {
StreamError streamError = StreamError.parse(parser);
logger.warn("Stream error from {}: {}", streamID, streamError.getCondition());
xmpp.removeConnectionOut(this);
closeConnection();
} else {
String unhandledStanza = XmlUtils.parseToString(parser, true);
logger.warn("Unhandled stanza from {} {} : {}", to, streamID, unhandledStanza);
}
}
logger.warn("stream to {} {} finished", to, streamID);
xmpp.removeConnectionOut(ConnectionOut.this);
closeConnection();
} catch (EOFException | SocketException eofex) {
logger.info("stream {} {} closed (dirty)", to, streamID);
xmpp.removeConnectionOut(ConnectionOut.this);
closeConnection();
} catch (Exception e) {
logger.error("s2s out exception: {} {}, exception {}", to, streamID, e);
xmpp.removeConnectionOut(ConnectionOut.this);
closeConnection();
}
}
public void sendDialbackVerify(String sid, String key) {
sendStanza("" +
key + "");
}
@Override
public void restartParser() throws XmlPullParserException, IOException {
super.restartParser();
streamID = UUID.randomUUID().toString();
}
}