Commit f9e1ffd8 authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gaston

Initial version. JM-6


git-svn-id: http://svn.igniterealtime.org/svn/repos/messenger/trunk@1368 b35dd754-fafc-0310-a699-88a17e54d16e
parent 3693743a
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger;
/**
* Thrown when something failed verifying the key of a Originating Server with an Authoritative
* Server in a dialback operation.
*
* @author Gaston Dombiak
*/
public class RemoteConnectionFailedException extends Exception {
public RemoteConnectionFailedException() {
super();
}
public RemoteConnectionFailedException(String msg) {
super(msg);
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import org.dom4j.Element;
import org.jivesoftware.messenger.ClientSession;
import org.jivesoftware.messenger.PacketRouter;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Presence;
import java.io.IOException;
import java.net.Socket;
/**
* A SocketReader specialized for client connections. This reader will be used when the open
* stream contains a jabber:client namespace. Received packet will have their FROM attribute
* overriden to avoid spoofing.
*
* @author Gaston Dombiak
*/
public class ClientSocketReader extends SocketReader {
public ClientSocketReader(PacketRouter router, String serverName, Socket socket,
SocketConnection connection) {
super(router, serverName, socket, connection);
}
protected void processIQ(IQ packet) throws UnauthorizedException {
// Overwrite the FROM attribute to avoid spoofing
packet.setFrom(session.getAddress());
super.processIQ(packet);
}
protected void processPresence(Presence packet) throws UnauthorizedException {
// Overwrite the FROM attribute to avoid spoofing
packet.setFrom(session.getAddress());
super.processPresence(packet);
}
protected void processMessage(Message packet) throws UnauthorizedException {
// Overwrite the FROM attribute to avoid spoofing
packet.setFrom(session.getAddress());
super.processMessage(packet);
}
/**
* Only packets of type Message, Presence and IQ can be processed by this class. Any other
* type of packet is unknown and thus rejected generating the connection to be closed.
*
* @param doc the unknown DOM element that was received
* @return always false.
*/
protected boolean processUnknowPacket(Element doc) {
return false;
}
boolean createSession(String namespace) throws UnauthorizedException, XmlPullParserException,
IOException {
if ("jabber:client".equals(namespace)) {
// The connected client is a regular client so create a ClientSession
session = ClientSession.createSession(serverName, reader, connection);
return true;
}
return false;
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import org.dom4j.Element;
import org.jivesoftware.messenger.ComponentSession;
import org.jivesoftware.messenger.PacketRouter;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.net.Socket;
/**
* A SocketReader specialized for component connections. This reader will be used when the open
* stream contains a jabber:component:accept namespace.
*
* @author Gaston Dombiak
*/
public class ComponentSocketReader extends SocketReader {
public ComponentSocketReader(PacketRouter router, String serverName, Socket socket,
SocketConnection connection) {
super(router, serverName, socket, connection);
}
/**
* Only packets of type Message, Presence and IQ can be processed by this class. Any other
* type of packet is unknown and thus rejected generating the connection to be closed.
*
* @param doc the unknown DOM element that was received
* @return always false.
*/
protected boolean processUnknowPacket(Element doc) {
return false;
}
boolean createSession(String namespace) throws UnauthorizedException, XmlPullParserException,
IOException {
if ("jabber:component:accept".equals(namespace)) {
// The connected client is a component so create a ComponentSession
session = ComponentSession.createSession(serverName, reader, connection);
return true;
}
return false;
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import org.dom4j.Element;
import org.jivesoftware.messenger.PacketRouter;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.interceptor.PacketRejectedException;
import org.jivesoftware.messenger.server.IncomingServerSession;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.*;
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* A SocketReader specialized for server connections. This reader will be used when the open
* stream contains a jabber:server namespace. Server-to-server communication requires two
* TCP connections between the servers where one is used for sending packets whilst the other
* connection is used for receiving packets. The connection used for receiving packets will use
* a ServerSocketReader since the other connection will not receive packets.<p>
*
* The received packets will be routed using another thread to ensure that many received packets
* could be routed at the same time. To avoid creating new threads every time a packet is received
* each <tt>ServerSocketReader</tt> instance uses a {@link ThreadPoolExecutor}. By default the
* maximum number of threads that the executor may have is 50. However, this value may be modified
* by changing the property <b>xmpp.server.processing.threads</b>.
*
* @author Gaston Dombiak
*/
public class ServerSocketReader extends SocketReader {
/**
* Pool of threads that are available for processing the requests.
*/
private ThreadPoolExecutor threadPool;
public ServerSocketReader(PacketRouter router, String serverName, Socket socket,
SocketConnection connection) {
super(router, serverName, socket, connection);
// Create a pool of threads that will process received packets. If more threads are
// required then the command will be executed on the SocketReader process
int maxThreads = JiveGlobals.getIntProperty("xmpp.server.processing.threads", 50);
threadPool =
new ThreadPoolExecutor(1, maxThreads, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
/**
* Processes the packet in another thread if the packet has not been rejected.
*
* @param packet the received packet.
*/
protected void processIQ(final IQ packet) throws UnauthorizedException {
try {
packetReceived(packet);
// Process the packet in another thread
threadPool.execute(new Runnable() {
public void run() {
try {
ServerSocketReader.super.processIQ(packet);
}
catch (UnauthorizedException e) {
Log.error("Error processing packet", e);
}
}
});
}
catch (PacketRejectedException e) {
// Do nothing
}
}
/**
* Processes the packet in another thread if the packet has not been rejected.
*
* @param packet the received packet.
*/
protected void processPresence(final Presence packet) throws UnauthorizedException {
try {
packetReceived(packet);
// Process the packet in another thread
threadPool.execute(new Runnable() {
public void run() {
try {
ServerSocketReader.super.processPresence(packet);
}
catch (UnauthorizedException e) {
Log.error("Error processing packet", e);
}
}
});
}
catch (PacketRejectedException e) {
// Do nothing
}
}
/**
* Processes the packet in another thread if the packet has not been rejected.
*
* @param packet the received packet.
*/
protected void processMessage(final Message packet) throws UnauthorizedException {
try {
packetReceived(packet);
// Process the packet in another thread
threadPool.execute(new Runnable() {
public void run() {
try {
ServerSocketReader.super.processMessage(packet);
}
catch (UnauthorizedException e) {
Log.error("Error processing packet", e);
}
}
});
}
catch (PacketRejectedException e) {
// Do nothing
}
}
/**
* Remote servers may send subsequent db:result packets so we need to process them in order
* to validate new domains.
*
* @param doc the unknown DOM element that was received
* @return true if the packet is a db:result packet otherwise false.
*/
protected boolean processUnknowPacket(Element doc) {
// Handle subsequent db:result packets
if ("db".equals(doc.getNamespacePrefix()) && "result".equals(doc.getName())) {
if (!((IncomingServerSession) session).validateSubsequentDomain(doc)) {
open = false;
}
return true;
}
return false;
}
/**
* Make sure that the received packet has a TO and FROM values defined and that it was sent
* from a previously validated domain. If the packet does not matches any of the above
* conditions then a PacketRejectedException will be thrown.
*
* @param packet the received packet.
* @throws PacketRejectedException if the packet does not include a TO or FROM or if the packet
* was sent from a domain that was not previously validated.
*/
private void packetReceived(Packet packet) throws PacketRejectedException {
if (packet.getTo() == null || packet.getFrom() == null) {
// Send a stream error saying that the packet includes no TO or FROM
StreamError error = new StreamError(StreamError.Condition.improper_addressing);
connection.deliverRawText(error.toXML());
// Close the underlying connection
connection.close();
open = false;
throw new PacketRejectedException("Packet with no TO or FROM attributes");
}
else if (!((IncomingServerSession) session).isValidDomain(packet.getFrom().getDomain())) {
// Send a stream error saying that the packet includes an invalid FROM
StreamError error = new StreamError(StreamError.Condition.invalid_from);
connection.deliverRawText(error.toXML());
// Close the underlying connection
connection.close();
open = false;
throw new PacketRejectedException("Packet with no TO or FROM attributes");
}
}
protected void shutdown() {
super.shutdown();
// Shutdown the pool of threads that are processing packets sent by
// the remote server
threadPool.shutdown();
}
boolean createSession(String namespace) throws UnauthorizedException, XmlPullParserException,
IOException {
if ("jabber:server".equals(namespace)) {
// The connected client is a server so create an IncomingServerSession
session = IncomingServerSession.createSession(serverName, reader, connection);
return true;
}
return false;
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.net;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParser;
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.interceptor.InterceptorManager;
import org.jivesoftware.messenger.interceptor.PacketRejectedException;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.LocaleUtils;
import org.dom4j.io.XPPPacketReader;
import org.dom4j.Element;
import org.xmpp.packet.*;
import java.net.Socket;
import java.net.SocketException;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.EOFException;
import java.io.Writer;
/**
* A SocketReader creates the appropriate {@link Session} based on the defined namespace in the
* stream element and will then keep reading and routing the received packets.
*
* @author Gaston Dombiak
*/
public abstract class SocketReader implements Runnable {
/**
* The utf-8 charset for decoding and encoding Jabber packet streams.
*/
private static String CHARSET = "UTF-8";
/**
* Reuse the same factory for all the connections.
*/
private static XmlPullParserFactory factory = null;
private Socket socket;
protected Session session;
protected SocketConnection connection;
protected String serverName;
/**
* Router used to route incoming packets to the correct channels.
*/
private PacketRouter router;
XPPPacketReader reader = null;
protected boolean open;
static {
try {
factory = XmlPullParserFactory.newInstance();
}
catch (XmlPullParserException e) {
Log.error("Error creating a parser factory", e);
}
}
/**
* Creates a dedicated reader for a socket.
*
* @param router the router for sending packets that were read.
* @param serverName the name of the server this socket is working for.
* @param socket the socket to read from.
* @param connection the connection being read.
*/
public SocketReader(PacketRouter router, String serverName, Socket socket,
SocketConnection connection) {
this.serverName = serverName;
this.router = router;
this.connection = connection;
this.socket = socket;
}
/**
* A dedicated thread loop for reading the stream and sending incoming
* packets to the appropriate router.
*/
public void run() {
try {
reader = new XPPPacketReader();
reader.setXPPFactory(factory);
reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
CHARSET));
// Read in the opening tag and prepare for packet stream
try {
createSession();
}
catch (IOException e) {
Log.debug("Error creating session", e);
throw e;
}
// Read the packet stream until it ends
if (session != null) {
readStream();
}
}
catch (EOFException eof) {
// Normal disconnect
}
catch (SocketException se) {
// The socket was closed. The server may close the connection for several
// reasons (e.g. user requested to remove his account). Do nothing here.
}
catch (XmlPullParserException ie) {
// It is normal for clients to abruptly cut a connection
// rather than closing the stream document. Since this is
// normal behavior, we won't log it as an error.
// Log.error(LocaleUtils.getLocalizedString("admin.disconnect"),ie);
}
catch (Exception e) {
if (session != null) {
Log.warn(LocaleUtils.getLocalizedString("admin.error.stream"), e);
}
}
finally {
if (session != null) {
Log.debug("Logging off " + session.getAddress() + " on " + connection);
try {
// Allow everything to settle down after a disconnect
// e.g. presence updates to avoid sending double
// presence unavailable's
Thread.sleep(3000);
session.getConnection().close();
}
catch (Exception e) {
Log.warn(LocaleUtils.getLocalizedString("admin.error.connection")
+ "\n" + socket.toString());
}
}
else {
Log.error(LocaleUtils.getLocalizedString("admin.error.connection")
+ "\n" + socket.toString());
}
shutdown();
}
}
/**
* Read the incoming stream until it ends.
*/
private void readStream() throws Exception {
open = true;
while (open) {
Element doc = reader.parseDocument().getRootElement();
if (doc == null) {
// Stop reading the stream since the client has sent an end of
// stream element and probably closed the connection.
return;
}
String tag = doc.getName();
if ("message".equals(tag)) {
Message packet = null;
try {
packet = new Message(doc);
}
catch(IllegalArgumentException e) {
// The original packet contains a malformed JID so answer with an error.
Message reply = new Message();
reply.setID(doc.attributeValue("id"));
reply.setTo(session.getAddress());
reply.getElement().addAttribute("from", doc.attributeValue("to"));
reply.setError(PacketError.Condition.jid_malformed);
session.process(reply);
continue;
}
processMessage(packet);
}
else if ("presence".equals(tag)) {
Presence packet = null;
try {
packet = new Presence(doc);
}
catch(IllegalArgumentException e) {
// The original packet contains a malformed JID so answer an error
Presence reply = new Presence();
reply.setID(doc.attributeValue("id"));
reply.setTo(session.getAddress());
reply.getElement().addAttribute("from", doc.attributeValue("to"));
reply.setError(PacketError.Condition.jid_malformed);
session.process(reply);
continue;
}
processPresence(packet);
}
else if ("iq".equals(tag)) {
IQ packet = null;
try {
packet = getIQ(doc);
}
catch(IllegalArgumentException e) {
// The original packet contains a malformed JID so answer an error
IQ reply = new IQ();
if (!doc.elements().isEmpty()) {
reply.setChildElement(((Element) doc.elements().get(0)).createCopy());
}
reply.setID(doc.attributeValue("id"));
reply.setTo(session.getAddress());
if (doc.attributeValue("to") != null) {
reply.getElement().addAttribute("from", doc.attributeValue("to"));
}
reply.setError(PacketError.Condition.jid_malformed);
session.process(reply);
continue;
}
processIQ(packet);
}
else {
if (!processUnknowPacket(doc)) {
throw new XmlPullParserException(LocaleUtils.getLocalizedString(
"admin.error.packet.tag") + tag);
}
}
}
}
/**
* Process the received IQ packet. Registered
* {@link org.jivesoftware.messenger.interceptor.PacketInterceptor} will be invoked before
* and after the packet was routed.<p>
*
* Subclasses may redefine this method for different reasons such as modifying the sender
* of the packet to avoid spoofing, rejecting the packet or even process the packet in
* another thread.
*
* @param packet the received packet.
*/
protected void processIQ(IQ packet) throws UnauthorizedException {
try {
// Invoke the interceptors before we process the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
false);
router.route(packet);
// Invoke the interceptors after we have processed the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
true);
session.incrementClientPacketCount();
}
catch (PacketRejectedException e) {
// An interceptor rejected this packet so answer a not_allowed error
IQ reply = new IQ();
reply.setChildElement(packet.getChildElement().createCopy());
reply.setID(packet.getID());
reply.setTo(session.getAddress());
reply.setFrom(packet.getTo());
reply.setError(PacketError.Condition.not_allowed);
session.process(reply);
}
}
/**
* Process the received Presence packet. Registered
* {@link org.jivesoftware.messenger.interceptor.PacketInterceptor} will be invoked before
* and after the packet was routed.<p>
*
* Subclasses may redefine this method for different reasons such as modifying the sender
* of the packet to avoid spoofing, rejecting the packet or even process the packet in
* another thread.
*
* @param packet the received packet.
*/
protected void processPresence(Presence packet) throws UnauthorizedException {
try {
// Invoke the interceptors before we process the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
false);
router.route(packet);
// Invoke the interceptors after we have processed the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
true);
session.incrementClientPacketCount();
}
catch (PacketRejectedException e) {
// An interceptor rejected this packet so answer a not_allowed error
Presence reply = new Presence();
reply.setID(packet.getID());
reply.setTo(session.getAddress());
reply.setFrom(packet.getTo());
reply.setError(PacketError.Condition.not_allowed);
session.process(reply);
}
}
/**
* Process the received Message packet. Registered
* {@link org.jivesoftware.messenger.interceptor.PacketInterceptor} will be invoked before
* and after the packet was routed.<p>
*
* Subclasses may redefine this method for different reasons such as modifying the sender
* of the packet to avoid spoofing, rejecting the packet or even process the packet in
* another thread.
*
* @param packet the received packet.
*/
protected void processMessage(Message packet) throws UnauthorizedException {
try {
// Invoke the interceptors before we process the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
false);
router.route(packet);
// Invoke the interceptors after we have processed the read packet
InterceptorManager.getInstance().invokeInterceptors(packet, session, true,
true);
session.incrementClientPacketCount();
}
catch (PacketRejectedException e) {
// An interceptor rejected this packet so answer a not_allowed error
Message reply = new Message();
reply.setID(packet.getID());
reply.setTo(session.getAddress());
reply.setFrom(packet.getTo());
reply.setError(PacketError.Condition.not_allowed);
session.process(reply);
}
}
/**
* Returns true if a received packet of an unkown type (i.e. not a Message, Presence
* or IQ) has been processed. If the packet was not processed then an exception will
* be thrown which will make the thread to stop processing further packets.
*
* @param doc the DOM element of an unkown type.
* @return true if a received packet has been processed.
*/
abstract boolean processUnknowPacket(Element doc);
private IQ getIQ(Element doc) {
Element query = doc.element("query");
if (query != null && "jabber:iq:roster".equals(query.getNamespaceURI())) {
return new Roster(doc);
}
else {
return new IQ(doc);
}
}
/**
* Uses the XPP to grab the opening stream tag and create an active session
* object. The session to create will depend on the sent namespace. In all
* cases, the method obtains the opening stream tag, checks for errors, and
* either creates a session or returns an error and kills the connection.
* If the connection remains open, the XPP will be set to be ready for the
* first packet. A call to next() should result in an START_TAG state with
* the first packet in the stream.
*/
private void createSession() throws UnauthorizedException, XmlPullParserException, IOException {
XmlPullParser xpp = reader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
// Create the correct session based on the sent namespace
if (!createSession(xpp.getNamespace(null))) {
// No session was created because of an invalid namespace prefix so answer a stream
// error and close the underlying connection
Writer writer = connection.getWriter();
StringBuilder sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
// Include the bad-namespace-prefix in the response
StreamError error = new StreamError(StreamError.Condition.bad_namespace_prefix);
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Close the underlying connection
connection.close();
}
}
/**
* Notification message indicating that the SocketReader is shutting down. The thread will
* stop reading and processing new requests. Subclasses may want to redefine this message
* for releasing any resource they might need.
*/
protected void shutdown() {
}
/**
* Creates the appropriate {@link Session} subclass based on the specified namespace.
*
* @param namespace the namespace sent in the stream element. eg. jabber:client.
* @return the created session or null.
* @throws UnauthorizedException
* @throws XmlPullParserException
* @throws IOException
*/
abstract boolean createSession(String namespace) throws UnauthorizedException,
XmlPullParserException, IOException;
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.server;
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.net.SocketConnection;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.xmpp.packet.Packet;
import org.dom4j.io.XPPPacketReader;
import org.dom4j.Element;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Collections;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>IncomingServerSession</tt> represents the connection to a remote server that will only
* be used for receiving packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating the remote server.
* Once the remote server has been authenticated incoming packets will be processed by this server.
* It is also possible for remote servers to authenticate more domains once the session has been
* established. For optimization reasons the existing connection is used between the servers.
* Therefore, the incoming server session holds the list of authenticated domains which are allowed
* to send packets to this server.<p>
*
* Using the Server Dialback method it is possible that this server may also act as the
* Authoritative Server. This implies that an incoming connection will be established with this
* server for authenticating a domain. This incoming connection will only last for a brief moment
* and after the domain has been authenticated the connection will be closed and no session will
* exist.
*
* @author Gaston Dombiak
*/
public class IncomingServerSession extends Session {
private Collection<String> validatedDomains = new ArrayList<String>();
/**
* Creates a new session that will receive packets. The new session will be authenticated
* before being returned. If the authentication process fails then the answer will be
* <tt>null</tt>.<p>
*
* Currently the Server Dialback method is the only way to authenticate a remote server. Since
* Server Dialback requires an Authoritative Server, it is possible for this server to receive
* an incoming connection that will only exist until the requested domain has been validated.
* In this case, this method will return <tt>null</tt> since the connection is closed after
* the domain was validated. See
* {@link ServerDialback#createIncomingSession(org.dom4j.io.XPPPacketReader)} for more
* information.
*
* @param serverName hostname of this server.
* @param reader reader on the new established connection with the remote server.
* @param connection the new established connection with the remote server.
* @return a new session that will receive packets or null if a problem occured while
* authenticating the remote server or when acting as the Authoritative Server during
* a Server Dialback authentication process.
* @throws XmlPullParserException if an error occurs while parsing the XML.
* @throws IOException if an input/output error occurs while using the connection.
*/
public static Session createSession(String serverName, XPPPacketReader reader,
SocketConnection connection) throws XmlPullParserException, IOException {
XmlPullParser xpp = reader.getXPPParser();
if (xpp.getNamespace("db") != null) {
ServerDialback method = new ServerDialback(connection, serverName);
return method.createIncomingSession(reader);
}
// Close the connection since we only support server dialback for s2s communication
connection.close();
return null;
}
public IncomingServerSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
}
public void process(Packet packet) throws UnauthorizedException, PacketException {
//TODO Should never be called? Should be passed to the outgoing connection?
}
/**
* Returns true if the request of a new domain was valid. Sessions may receive subsequent
* domain validation request. If the validation of the new domain fails then the session and
* the underlying TCP connection will be closed.<p>
*
* For optimization reasons, the same session may be servicing several domains of a
* remote server.
*
* @param dbResult the DOM stanza requesting the domain validation.
* @return true if the requested domain was valid.
*/
public boolean validateSubsequentDomain(Element dbResult) {
ServerDialback method = new ServerDialback(getConnection(), getServerName());
if (method.validateRemoteDomain(dbResult, getStreamID())) {
addValidatedDomain(dbResult.attributeValue("from"));
return true;
}
return false;
}
/**
* Returns true if the specified domain has been validated for this session. The remote
* server should send a "db:result" packet for registering new subdomains or even
* virtual hosts.<p>
*
* In the spirit of being flexible we allow remote servers to not register subdomains
* and even so consider subdomains that include the server domain in their domain part
* as valid domains.
*
* @param domain the domain to validate.
* @return true if the specified domain has been validated for this session.
*/
public boolean isValidDomain(String domain) {
// Check if the specified domain is contained in any of the validated domains
for (String validatedDomain : getValidatedDomains()) {
if (domain.contains(validatedDomain)) {
return true;
}
}
return false;
}
/**
* Returns a collection with all the domains, subdomains and virtual hosts that where
* validated. The remote server is allowed to send packets from any of these domains,
* subdomains and virtual hosts.
*
* @return domains, subdomains and virtual hosts that where validated.
*/
public Collection<String> getValidatedDomains() {
return Collections.unmodifiableCollection(validatedDomains);
}
/**
* Adds a new validated domain, subdomain or virtual host to the list of
* validated domains for the remote server.
*
* @param domain the new validated domain, subdomain or virtual host to add.
*/
public void addValidatedDomain(String domain) {
if (validatedDomains.add(domain)) {
// Register the new validated domain for this server session in SessionManager
SessionManager.getInstance().registerIncomingServerSession(domain, this);
}
}
/**
* Removes the previously validated domain from the list of validated domains. The remote
* server will no longer be able to send packets from the removed domain, subdomain or
* virtual host.
*
* @param domain the domain, subdomain or virtual host to remove from the list of
* validated domains.
*/
public void removeValidatedDomain(String domain) {
validatedDomains.remove(domain);
// Unregister the validated domain for this server session in SessionManager
SessionManager.getInstance().unregisterIncomingServerSession(domain);
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.server;
import org.dom4j.io.XPPPacketReader;
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.net.SocketAcceptThread;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>OutgoingServerSession</tt> represents the connection to a remote server that will only
* be used for sending packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating with the remote
* server. Use {@link #authenticateDomain(String, String)} to create a new connection to a remote
* server that will be used for sending packets to the remote server from the specified domain.
* Only the authenticated domains with the remote server will be able to effectively send packets
* to the remote server. The remote server will reject and close the connection if a
* non-authenticated domain tries to send a packet through this connection.<p>
*
* Once the connection has been established with the remote server and at least a domain has been
* authenticated then a new route will be added to the routing table for this connection. For
* optimization reasons the same outgoing connection will be used even if the remote server has
* several hostnames. However, different routes will be created in the routing table for each
* hostname of the remote server.
*
* @author Gaston Dombiak
*/
public class OutgoingServerSession extends Session {
private Collection<String> authenticatedDomains = new ArrayList<String>();
private Collection<String> hostnames = new ArrayList<String>();
private XPPPacketReader reader;
/**
* Creates a new outgoing connection to the specified hostname if no one exists. The port of
* the remote server could be configured by setting the <b>xmpp.server.socket.remotePort</b>
* property or otherwise the standard port 5269 will be used. Either a new connection was
* created or already existed the specified hostname will be authenticated with the remote
* server. Once authenticated the remote server will start accepting packets from the specified
* domain.<p>
*
* The Server Dialback method is currently the only implemented method for server-to-server
* authentication. This implies that the remote server will ask the Authoritative Server
* to verify the domain to authenticate. Most probably this server will act as the
* Authoritative Server. See {@link IncomingServerSession) for more information.
*
* @param domain the local domain to authenticate with the remote server.
* @param hostname the hostname of the remote server.
* @return True if the domain was authenticated by the remote server.
*/
public static boolean authenticateDomain(String domain, String hostname) {
try {
// TODO Check if the remote hostname is in the blacklist
// TODO Keep a list of ports to connect to for each hostname or use the default
// if none was defined
// Check if a session already exists to the desired hostname (i.e. remote server). If
// no one exists then create a new session. The same session will be used for the same
// hostname for all the domains to authenticate
SessionManager sessionManager = SessionManager.getInstance();
OutgoingServerSession session = sessionManager.getOutgoingServerSession(hostname);
if (session == null) {
// Try locating if the remote server has previously authenticated with this server
IncomingServerSession incomingSession = sessionManager.getIncomingServerSession(
hostname);
if (incomingSession != null) {
for (String otherHostname : incomingSession.getValidatedDomains()) {
session = sessionManager.getOutgoingServerSession(otherHostname);
if (session != null) {
// A session to the same remote server but with different hostname
// was found. Use this session and add the new hostname to the session
session.addHostname(hostname);
break;
}
}
}
}
if (session == null) {
int port = JiveGlobals.getIntProperty("xmpp.server.socket.remotePort",
SocketAcceptThread.DEFAULT_SERVER_PORT);
// No session was found to the remote server so make sure that only one is created
synchronized (hostname.intern()) {
session = sessionManager.getOutgoingServerSession(hostname);
if (session == null) {
session =
new ServerDialback().createOutgoingSession(domain, hostname, port);
if (session != null) {
// Add the new hostname to the list of names that the server may have
session.addHostname(hostname);
// Add the validated domain as an authenticated domain
session.addAuthenticatedDomain(domain);
return true;
}
else {
return false;
}
}
}
}
if (session.getAuthenticatedDomains().contains(domain)) {
// Do nothing since the domain has already been authenticated
return true;
}
// A session already exists so authenticate the domain using that session
ServerDialback method = new ServerDialback(session.getConnection(), domain);
if (method.authenticateDomain(session.reader, domain, hostname,
session.getStreamID().getID())) {
// Add the validated domain as an authenticated domain
session.addAuthenticatedDomain(domain);
return true;
}
}
catch (Exception e) {
Log.error("Error authenticating domain with remote server: " + hostname, e);
}
return false;
}
OutgoingServerSession(String serverName, Connection connection, XPPPacketReader reader,
StreamID streamID) {
super(serverName, connection, streamID);
this.reader = reader;
}
public void process(Packet packet) throws UnauthorizedException, PacketException {
if (conn != null && !conn.isClosed()) {
try {
conn.deliver(packet);
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
}
}
/**
* Returns a collection with all the domains, subdomains and virtual hosts that where
* authenticated. The remote server will accept packets sent from any of these domains,
* subdomains and virtual hosts.
*
* @return domains, subdomains and virtual hosts that where validated.
*/
public Collection<String> getAuthenticatedDomains() {
return Collections.unmodifiableCollection(authenticatedDomains);
}
/**
* Adds a new authenticated domain, subdomain or virtual host to the list of
* authenticated domains for the remote server. The remote server will accept packets
* sent from this new authenticated domain.
*
* @param domain the new authenticated domain, subdomain or virtual host to add.
*/
public void addAuthenticatedDomain(String domain) {
authenticatedDomains.add(domain);
}
/**
* Removes an authenticated domain from the list of authenticated domains. The remote
* server will no longer be able to accept packets sent from the removed domain, subdomain or
* virtual host.
*
* @param domain the domain, subdomain or virtual host to remove from the list of
* authenticated domains.
*/
public void removeAuthenticatedDomain(String domain) {
authenticatedDomains.remove(domain);
}
/**
* Returns the list of hostnames related to the remote server. This tracking is useful for
* reusing the same session for the same remote server even if the server has many names.
*
* @return the list of hostnames related to the remote server.
*/
public Collection<String> getHostnames() {
return Collections.unmodifiableCollection(hostnames);
}
/**
* Adds a new hostname to the list of known hostnames of the remote server. This tracking is
* useful for reusing the same session for the same remote server even if the server has
* many names.
*
* @param hostname the new known name of the remote server
*/
private void addHostname(String hostname) {
if (hostnames.add(hostname)) {
// Register the outgoing session in the SessionManager. If the session
// was already registered nothing happens
sessionManager.registerOutgoingServerSession(hostname, this);
// Add a new route for this new session
XMPPServer.getInstance().getRoutingTable().addRoute(new JID(hostname), this);
}
}
}
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.messenger.server;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.XPPPacketReader;
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.auth.AuthFactory;
import org.jivesoftware.messenger.net.DNSUtil;
import org.jivesoftware.messenger.net.SocketConnection;
import org.jivesoftware.messenger.spi.BasicStreamIDFactory;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.StreamError;
import javax.net.SocketFactory;
import java.io.*;
import java.net.Socket;
/**
* Implementation of the Server Dialback method as defined by the RFC3920.
*
* The dialback method follows the following logic to validate the remote server:
* <ol>
* <li>The Originating Server establishes a connection to the Receiving Server.</li>
* <li>The Originating Server sends a 'key' value over the connection to the Receiving
* Server.</li>
* <li>The Receiving Server establishes a connection to the Authoritative Server.</li>
* <li>The Receiving Server sends the same 'key' value to the Authoritative Server.</li>
* <li>The Authoritative Server replies that key is valid or invalid.</li>
* <li>The Receiving Server informs the Originating Server whether it is authenticated or
* not.</li>
* </ol>
*
* @author Gaston Dombiak
*/
class ServerDialback {
/**
* The utf-8 charset for decoding and encoding Jabber packet streams.
*/
protected static String CHARSET = "UTF-8";
/**
* Secret key to be used for encoding and decoding keys used for authentication.
*/
private static final String secretKey = StringUtils.randomString(10);
private Connection connection;
private String serverName;
private SessionManager sessionManager = SessionManager.getInstance();
/**
* Creates a new instance that will be used for creating {@link IncomingServerSession},
* validating subsequent domains or authenticatig new domains. Use
* {@link #createIncomingSession(org.dom4j.io.XPPPacketReader)} for creating a new server
* session used for receiving packets from the remote server. Use
* {@link #validateRemoteDomain(org.dom4j.Element, org.jivesoftware.messenger.StreamID)} for
* validating subsequent domains and use
* {@link #authenticateDomain(org.dom4j.io.XPPPacketReader, String, String, String)} for
* registering new domains that are allowed to send packets to the remote server.<p>
*
* For validating domains a new TCP connection will be established to the Authoritative Server.
* The Authoritative Server may be the same Originating Server or some other machine in the
* Originating Server's network. Once the remote domain gets validated the Originating Server
* will be allowed for sending packets to this server. However, this server will need to
* validate its domain/s with the Originating Server if this server needs to send packets to
* the Originating Server. Another TCP connection will be established for validation this
* server domain/s and for sending packets to the Originating Server.
*
* @param connection the connection created by the remote server.
* @param serverName the name of the local server.
*/
ServerDialback(Connection connection, String serverName) {
this.connection = connection;
this.serverName = serverName;
}
ServerDialback() {
}
/**
* Creates a new connection from the Originating Server to the Receiving Server for
* authenticating the specified domain.
*
* @param domain domain of the Originating Server to authenticate with the Receiving Server.
* @param hostname IP address or hostname of the Receiving Server.
* @param port port of the Receiving Server.
* @return an OutgoingServerSession if the domain was authenticated or <tt>null</tt> if none.
*/
public OutgoingServerSession createOutgoingSession(String domain, String hostname, int port) {
// TODO Check if the hostname is in the blacklist
try {
// Establish a TCP connection to the Receiving Server
Log.debug("OS - Trying to connect to " + hostname + ":" + port);
Socket socket = SocketFactory.getDefault().createSocket(hostname, port);
Log.debug("OS - Connection to " + hostname + ":" + port + " successfull");
connection =
new SocketConnection(XMPPServer.getInstance().getPacketDeliverer(), socket,
false);
// Get a writer for sending the open stream tag
// Send to the Receiving Server a stream header
StringBuilder stream = new StringBuilder();
stream.append("<stream:stream");
stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
stream.append(" xmlns=\"jabber:server\"");
stream.append(" xmlns:db=\"jabber:server:dialback\">");
connection.deliverRawText(stream.toString());
stream = null;
XPPPacketReader reader = new XPPPacketReader();
reader.setXPPFactory(XmlPullParserFactory.newInstance());
reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
CHARSET));
// Get the answer from the Receiving Server
XmlPullParser xpp = reader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) {
String id = xpp.getAttributeValue("", "id");
if (authenticateDomain(reader, domain, hostname, id)) {
// Domain was validated so create a new OutgoingServerSession
StreamID streamID = new BasicStreamIDFactory().createStreamID(id);
OutgoingServerSession session = new OutgoingServerSession(domain, connection,
reader, streamID);
connection.init(session);
// Set the hostname as the address of the session
session.setAddress(new JID(null, hostname, null));
return session;
}
else {
// Close the connection
connection.close();
}
}
else {
Log.debug("OS - Invalid namespace in packet: " + xpp.getText());
// Send an invalid-namespace stream error condition in the response
StreamError error = new StreamError(StreamError.Condition.invalid_namespace);
StringBuilder sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the connection
connection.close();
}
}
catch (Exception e) {
Log.error("Error connecting to the remote server", e);
// Close the connection
if (connection != null) {
connection.close();
}
}
return null;
}
/**
* Authenticates the Originating Server domain with the Receiving Server. Once the domain has
* been authenticated the Receiving Server will start accepting packets from the Originating
* Server.<p>
*
* The Receiving Server will connect to the Authoritative Server to verify the dialback key.
* Most probably the Originating Server machine will be the Authoritative Server too.
*
* @param reader the reader to use for reading the answer from the Receiving Server.
* @param domain the domain to authenticate.
* @param hostname the hostname of the remote server (i.e. Receiving Server).
* @param id the stream id to be used for creating the dialback key.
* @return true if the Receiving Server authenticated the domain with the Authoritative Server.
* @throws XmlPullParserException if a parsing error occured while reading the answer from
* the Receiving Server.
* @throws IOException if an input/output error occured while sending/receiving packets to/from
* the Receiving Server.
* @throws DocumentException if a parsing error occured while reading the answer from
* the Receiving Server.
*/
public boolean authenticateDomain(XPPPacketReader reader, String domain, String hostname,
String id) throws XmlPullParserException, IOException, DocumentException {
String key = AuthFactory.createDigest(id, secretKey);
Log.debug("OS - Sent dialback key to host: " + hostname + " id: " + id + " from domain: " +
domain);
synchronized (reader) {
// Send a dialback key to the Receiving Server
StringBuilder sb = new StringBuilder();
sb.append("<db:result");
sb.append(" from=\"" + domain + "\"");
sb.append(" to=\"" + hostname + "\">");
sb.append(key);
sb.append("</db:result>");
connection.deliverRawText(sb.toString());
sb = null;
// Process the answer from the Receiving Server
Element doc = reader.parseDocument().getRootElement();
if ("db".equals(doc.getNamespacePrefix()) && "result".equals(doc.getName())) {
boolean success = "valid".equals(doc.attributeValue("type"));
Log.debug("OS - Validation " + (success ? "GRANTED" : "FAILED") + " from: " +
hostname +
" id: " +
id +
" for domain: " +
domain);
return success;
}
else {
Log.debug("OS - Unexpected answer in validation from: " + hostname + " id: " + id +
" for domain: " +
domain +
" answer:" +
doc.asXML());
return false;
}
}
}
/**
* Returns a new {@link IncomingServerSession} with a domain validated by the Authoritative
* Server. New domains may be added to the returned IncomingServerSession after they have
* been validated. See
* {@link IncomingServerSession#validateSubsequentDomain(org.dom4j.Element)}. The remote
* server will be able to send packets through this session whose domains were previously
* validated.<p>
*
* When acting as an Authoritative Server this method will verify the requested key
* and will return null since the underlying TCP connection will be closed after sending the
* response to the Receiving Server.<p>
*
* @param reader reader of DOM documents on the connection to the remote server.
* @return an IncomingServerSession that was previously validated against the remote server.
* @throws IOException if an I/O error occurs while communicating with the remote server.
* @throws XmlPullParserException if an error occurs while parsing XML packets.
*/
public IncomingServerSession createIncomingSession(XPPPacketReader reader) throws IOException,
XmlPullParserException {
XmlPullParser xpp = reader.getXPPParser();
StringBuilder sb;
StreamError error;
if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) {
StreamID streamID = sessionManager.nextStreamID();
sb = new StringBuilder();
sb.append("<stream:stream");
sb.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
sb.append(" xmlns=\"jabber:server\" xmlns:db=\"jabber:server:dialback\"");
sb.append(" id=\"");
sb.append(streamID.toString());
sb.append("\">");
connection.deliverRawText(sb.toString());
try {
Element doc = reader.parseDocument().getRootElement();
if ("db".equals(doc.getNamespacePrefix()) && "result".equals(doc.getName())) {
if (validateRemoteDomain(doc, streamID)) {
String hostname = doc.attributeValue("from");
// Create a server Session for the remote server
IncomingServerSession session = sessionManager.
createIncomingServerSession(connection, streamID);
// Set the first validated domain as the address of the session
session.setAddress(new JID(null, hostname, null));
// Add the validated domain as a valid domain
session.addValidatedDomain(hostname);
return session;
}
}
else if ("db".equals(doc.getNamespacePrefix()) && "verify".equals(doc.getName())) {
// When acting as an Authoritative Server the Receiving Server will send a
// db:verify packet for verifying a key that was previously sent by this
// server when acting as the Originating Server
String verifyFROM = doc.attributeValue("from");
String verifyTO = doc.attributeValue("to");
String key = doc.getTextTrim();
String id = doc.attributeValue("id");
Log.debug("AS - Verifying key for host: " + verifyFROM + " id: " + id);
// TODO If the value of the 'to' address does not match a recognized hostname,
// then generate a <host-unknown/> stream error condition
// TODO If the value of the 'from' address does not match the hostname
// represented by the Receiving Server when opening the TCP connection, then
// generate an <invalid-from/> stream error condition
// Verify the received key
// Created the expected key based on the received ID value and the shared secret
String expectedKey = AuthFactory.createDigest(id, secretKey);
boolean verified = expectedKey.equals(key);
// Send the result of the key verification
sb = new StringBuilder();
sb.append("<db:verify");
sb.append(" from=\"" + verifyTO + "\"");
sb.append(" to=\"" + verifyFROM + "\"");
sb.append(" type=\"");
sb.append(verified ? "valid" : "invalid");
sb.append("\" id=\"" + id + "\"/>");
connection.deliverRawText(sb.toString());
Log.debug("AS - Key was: " + (verified ? "VALID" : "INVALID") + " for host: " +
verifyFROM +
" id: " +
id);
// Close the underlying connection
connection.close();
Log.debug("AS - Connection closed for host: " + verifyFROM + " id: " + id);
sb = null;
return null;
}
else {
// The remote server sent an invalid/unknown packet
error = new StreamError(StreamError.Condition.invalid_xml);
sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return null;
}
}
catch (Exception e) {
Log.error("An error occured while creating a server session", e);
// Close the underlying connection
connection.close();
return null;
}
}
else {
// Include the invalid-namespace stream error condition in the response
error = new StreamError(StreamError.Condition.invalid_namespace);
sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return null;
}
return null;
}
/**
* Returns true if the domain requested by the remote server was validated by the Authoritative
* Server. To validate the domain a new TCP connection will be established to the
* Authoritative Server. The Authoritative Server may be the same Originating Server or
* some other machine in the Originating Server's network.<p>
*
* If the domain was not valid or some error occured while validating the domain then the
* underlying TCP connection will be closed.
*
* @param doc the request for validating the new domain.
* @param streamID the stream id generated by this server for the Originating Server.
* @return true if the requested domain is valid.
*/
public boolean validateRemoteDomain(Element doc, StreamID streamID) {
StreamError error;
StringBuilder sb;
String recipient = doc.attributeValue("to");
String hostname = doc.attributeValue("from");
Log.debug("RS - Received dialback key from host: " + hostname + " to: " + recipient);
if (!serverName.equals(recipient)) {
// address does not match a recognized hostname
error = new StreamError(StreamError.Condition.host_unknown);
sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
Log.debug("RS - Error, hostname not recognized: " + recipient);
return false;
}
else {
if (sessionManager.getIncomingServerSession(hostname) != null) {
// Remote server already has a IncomingServerSession created
error = new StreamError(StreamError.Condition.not_authorized);
sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
Log.debug("RS - Error, incoming connection already exists from: " + hostname);
return false;
}
else {
String key = doc.getTextTrim();
DNSUtil.HostAddress address = DNSUtil.resolveXMPPServerDomain(hostname);
try {
boolean valid = verifyKey(key, streamID.toString(), hostname,
address.getHost(), address.getPort());
Log.debug("RS - Sending key verification result to OS: " + hostname);
sb = new StringBuilder();
sb.append("<db:result");
sb.append(" from=\"" + serverName + "\"");
sb.append(" to=\"" + hostname + "\"");
sb.append(" type=\"");
sb.append(valid ? "valid" : "invalid");
sb.append("\"/>");
connection.deliverRawText(sb.toString());
if (!valid) {
// Close the underlying connection
connection.close();
}
return valid;
}
catch (Exception e) {
Log.warn("Error verifying key", e);
// Send a <remote-connection-failed/> stream error condition
// and terminate both the XML stream and the underlying
// TCP connection
error =
new StreamError(StreamError.Condition.remote_connection_failed);
sb = new StringBuilder();
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return false;
}
}
}
}
/**
* Verifies the key with the Authoritative Server.
*/
private boolean verifyKey(String key, String streamID, String hostname, String host, int port)
throws IOException, XmlPullParserException, RemoteConnectionFailedException {
// TODO Check if the hostname is in the blacklist
XmlPullParserFactory factory = null;
XPPPacketReader reader = null;
Writer writer = null;
StreamError error;
// Establish a TCP connection back to the domain name asserted by the Originating Server
Log.debug("RS - Trying to connect to Authoritative Server: " + hostname + ":" + port);
Socket socket = SocketFactory.getDefault().createSocket(host, port);
Log.debug("RS - Connection to AS: " + hostname + ":" + port + " successfull");
try {
factory = XmlPullParserFactory.newInstance();
reader = new XPPPacketReader();
reader.setXPPFactory(factory);
reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
CHARSET));
// Get a writer for sending the open stream tag
writer =
new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),
CHARSET));
// Send the Authoritative Server a stream header
StringBuilder stream = new StringBuilder();
stream.append("<stream:stream");
stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
stream.append(" xmlns=\"jabber:server\"");
stream.append(" xmlns:db=\"jabber:server:dialback\">");
writer.write(stream.toString());
writer.flush();
stream = null;
// Get the answer from the Authoritative Server
XmlPullParser xpp = reader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
if ("jabber:server:dialback".equals(xpp.getNamespace("db"))) {
Log.debug("RS - Asking AS to verify dialback key for id" + streamID);
// Request for verification of the key
StringBuilder sb = new StringBuilder();
sb.append("<db:verify");
sb.append(" from=\"" + serverName + "\"");
sb.append(" to=\"" + hostname + "\"");
sb.append(" id=\"" + streamID + "\">");
sb.append(key);
sb.append("</db:verify>");
writer.write(sb.toString());
writer.flush();
sb = null;
try {
Element doc = reader.parseDocument().getRootElement();
if ("db".equals(doc.getNamespacePrefix()) && "verify".equals(doc.getName())) {
if (!streamID.equals(doc.attributeValue("id"))) {
// Include the invalid-id stream error condition in the response
error = new StreamError(StreamError.Condition.invalid_id);
sb = new StringBuilder();
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Thrown an error so <remote-connection-failed/> stream error
// condition is sent to the Originating Server
throw new RemoteConnectionFailedException("Invalid ID");
}
else if (!serverName.equals(doc.attributeValue("to"))) {
// Include the host-unknown stream error condition in the response
error = new StreamError(StreamError.Condition.host_unknown);
sb = new StringBuilder();
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Thrown an error so <remote-connection-failed/> stream error
// condition is sent to the Originating Server
throw new RemoteConnectionFailedException("Host unknown");
}
else if (!hostname.equals(doc.attributeValue("from"))) {
// Include the invalid-from stream error condition in the response
error = new StreamError(StreamError.Condition.invalid_from);
sb = new StringBuilder();
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Thrown an error so <remote-connection-failed/> stream error
// condition is sent to the Originating Server
throw new RemoteConnectionFailedException("Invalid From");
}
else {
boolean valid = "valid".equals(doc.attributeValue("type"));
Log.debug("RS - Key was " + (valid ? "" : "NOT ") +
"VERIFIED by the Authoritative Server for: " +
hostname);
return valid;
}
}
else {
Log.debug("db:verify answer was: " + doc.asXML());
}
}
catch (DocumentException e) {
Log.error("An error occured connecting to the Authoritative Server", e);
// Thrown an error so <remote-connection-failed/> stream error condition is
// sent to the Originating Server
throw new RemoteConnectionFailedException("Error connecting to the Authoritative Server");
}
}
else {
// Include the invalid-namespace stream error condition in the response
error = new StreamError(StreamError.Condition.invalid_namespace);
StringBuilder sb = new StringBuilder();
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Thrown an error so <remote-connection-failed/> stream error condition is
// sent to the Originating Server
throw new RemoteConnectionFailedException("Invalid namespace");
}
}
finally {
try {
Log.debug("RS - Closing connection to Authoritative Server: " + hostname);
// Close the stream
StringBuilder sb = new StringBuilder();
sb.append("</stream:stream>");
writer.write(sb.toString());
writer.flush();
// Close the TCP connection
socket.close();
}
catch (IOException ioe) {
}
}
return false;
}
}
<body>
Classes used for server-to-server communication.
</body>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment