/* * Jeti, a Java Jabber client, Copyright (C) 2003 E.S. de Boer * * This program 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 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For questions, comments etc, * use the website at http://jeti.jabberstudio.org * or mail/im me at jeti@jabber.org */ package nu.fw.jeti.backend; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.text.MessageFormat; import java.util.Iterator; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import javax.swing.JOptionPane; import nu.fw.jeti.events.DiscoveryListener; import nu.fw.jeti.events.LoginListener; import nu.fw.jeti.jabber.Backend; import nu.fw.jeti.jabber.JID; import nu.fw.jeti.jabber.elements.DiscoveryInfo; import nu.fw.jeti.jabber.elements.DiscoveryItem; import nu.fw.jeti.jabber.elements.IQAuth; import nu.fw.jeti.jabber.elements.IQAuthBuilder; import nu.fw.jeti.jabber.elements.IQExtension; import nu.fw.jeti.jabber.elements.IQPrivate; import nu.fw.jeti.jabber.elements.IQXRoster; import nu.fw.jeti.jabber.elements.InfoQuery; import nu.fw.jeti.jabber.elements.JetiPrivateExtension; import nu.fw.jeti.jabber.elements.JetiPrivateRosterExtension; import nu.fw.jeti.jabber.elements.Packet; import nu.fw.jeti.jabber.elements.Presence; import nu.fw.jeti.jabber.elements.PresenceBuilder; import nu.fw.jeti.jabber.elements.StreamError; import nu.fw.jeti.plugins.PluginsInfo; import nu.fw.jeti.plugins.XMPP; import nu.fw.jeti.util.I18N; import nu.fw.jeti.util.Log; import nu.fw.jeti.util.Utils; /** * @author E.S. de Boer */ //TODO stop packet start spul en de andere dingen in aparte class?? //TODO improve error reporting, make it translatable //class voor connectie public class Connect implements ConnectionPacketReceiver { private Output output; private Input input; private String authenticationId="yytr"; private JabberHandler jabberHandler; private LoginInfo loginInfo; private Backend backend; private static JID myJID = new JID("test","test","test"); private boolean authenticated = false; private boolean reconnecting = false; private long latestConnected=0; private int show; private String status; private String connectionID; private Discovery discovery; private OwnCapabilities capabilities; private Socket socket;//socket needed to close if abort private Thread connectThread;//login thread private volatile boolean abort = false;//abort login private IQTimerQueue iqTimerQueue; private Handlers handlers; private XMPP xmpp; public Connect(Backend backend,IQTimerQueue timerQueue,Handlers handlers) { this.backend = backend; iqTimerQueue = timerQueue; this.handlers = handlers; discovery = new Discovery(backend) ; capabilities = new OwnCapabilities(backend); } public void addCapability(String capability,String feature) { capabilities.addCapability(capability, feature); } public void removeCapability(String capability,String feature) { capabilities.removeCapability(capability, feature); } public int getStatus() { return show; } public void getItems(JID jid, DiscoveryListener listener, boolean useCache) { discovery.getItems(jid,listener,useCache); } public void getItems(JID jid, DiscoveryListener listener) { discovery.getItems(jid,listener); } public void getInfo(JID jid, DiscoveryListener listener) { discovery.getInfo(jid,listener); } public void getItems(JID jid,String node, DiscoveryListener listener) { discovery.getItems(jid,node,listener); } public void getInfo(JID jid,String node, DiscoveryListener listener) { discovery.getInfo(jid,node,listener); } public boolean isLoggedIn() { return authenticated; } public boolean isPasswordValid(String password) { return loginInfo.getPassword().equals(password); } public void login(LoginInfo info) { abort = false; loginInfo = info; connectThread = new Thread() { public void run() { connect(); } }; connectThread.start(); } public void autoLogin(LoginInfo info,final int tries) { if(System.currentTimeMillis() < latestConnected + 60000) {//prevent ping pong if things go wrong at login return; } abort = false; loginInfo = info; connectThread = new Thread() { int tel = 0; public void run() { boolean connected=false; while(tel < tries) { connected = connect(); if(connected) break; try { Thread.sleep(60000); } catch (InterruptedException e){} tel++; } } }; connectThread.start(); } public void abort() { abort = true; if(socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } connectThread.interrupt(); disconnect(); } public boolean isAborted() { return abort; } synchronized private boolean connect() { latestConnected = System.currentTimeMillis(); if( loginInfo == null ) return true; if( authenticated ) disconnect();//clear old connection if (loginInfo.useProxy(LoginInfo.SOCKS_PROXY)) { System.getProperties().put( "proxySet", "true" ); System.getProperties().setProperty("socksProxyHost", loginInfo.getProxyServer()); System.getProperties().setProperty("socksProxyPort", loginInfo.getProxyPort()); System.getProperties().setProperty("socksProxyUserName", loginInfo.getProxyUsername()); if(loginInfo.getProxyPassword()!=null)System.getProperties().setProperty("socksProxyPassword", loginInfo.getProxyPassword()); } jabberHandler = new JabberHandler(this,handlers); if( xmpp == null && PluginsInfo.isPluginLoaded("xmpp") ) { xmpp = (XMPP)PluginsInfo.newPluginInstance("xmpp"); handlers.loadExtraHandlers(xmpp.getXMPPHandlers()); } try { host = loginInfo.getHost(); if (host == null || host.length() == 0) { if(loginInfo.useProxy(LoginInfo.NO_PROXY) && PluginsInfo.isPluginLoaded("xmpp")) { host = xmpp.resolveXMPPDomain(loginInfo.getServer()); } else host = loginInfo.getServer(); } if(loginInfo.isSSl()) { if( loginInfo.isHTTPProxy() && loginInfo.getJavaProxy() ) { Socket tunnel = createHTTPTunel(host); socket = new DummySSLSocketFactory().createSocket(tunnel,host,loginInfo.getPort(),true); } else socket = new DummySSLSocketFactory().createSocket(host,loginInfo.getPort()); } else { socket = new Socket(host,loginInfo.getPort()); } } catch (UnknownHostException ex) { sendLoginError(MessageFormat.format(I18N.gettext("main.loginstatus.Server_{0}_could_not_be_found"), new Object[] { loginInfo.getHost() } )); return false; } catch (IOException ex) { sendLoginError(ex.getMessage()); return false; } if( abort ) return false; try { input = new Input(socket.getInputStream(),this,jabberHandler); } catch (IOException ex) { sendLoginError(I18N.gettext("main.loginstatus.Could_not_open_input_because") + " " + ex.getMessage()); return false; } if( abort ) return false; try { output = new Output(socket,loginInfo.getServer(),this,socket.getOutputStream()); } catch (IOException ex) { sendLoginError(I18N.gettext("main.loginstatus.Could_not_open_output_because") + " " +ex.getMessage()); return false; } return true; } String host; public boolean startTls(ConnectionPacketReceiver cpr) { if(abort)return false; try{ //TODO stop output output.disconnect(false); input.disconnect(); socket = new DummySSLSocketFactory().createSocket(socket,host,loginInfo.getPort(),true); jabberHandler = new JabberHandler(cpr,handlers); try{ input = new Input(socket.getInputStream(),this,jabberHandler); }catch (IOException ex) { sendLoginError(I18N.gettext("main.loginstatus.Could_not_open_input_because") + " " + ex.getMessage()); return false; } try{ output = new Output(socket,loginInfo.getServer(),this,socket.getOutputStream()); }catch (IOException ex) { sendLoginError(I18N.gettext("main.loginstatus.Could_not_open_output_because") + " " +ex.getMessage()); return false; } if(abort) return false; }catch (IOException e) {e.printStackTrace();} return true; } //used by compression plugin public Socket getSocket(){ return socket; } public boolean startCompressed(ConnectionPacketReceiver cpr,InputStream inputStream,OutputStream outputStream) { jabberHandler = new JabberHandler(cpr,handlers); if ( abort ) return false; output.disconnect(false); Input input2 = input; input = new Input(inputStream,this,jabberHandler); try { output = new Output(socket,loginInfo.getServer(),this,outputStream); } catch (IOException ex) { sendLoginError(I18N.gettext("main.loginstatus.Could_not_open_output_because") + " " +ex.getMessage()); ex.printStackTrace(); return false; } if ( abort ) return false; input2.disconnect(); //FIXME Using thread.stop because throwing an exception to stop the sax parser in the input thread fails input2.stop(); //throw new RuntimeException(); return true; } public void startSasl(ConnectionPacketReceiver cpr) { if( abort ) return; //kill old input and send new stream //input.disconnect(); jabberHandler = new JabberHandler(cpr,handlers); try { input = new Input(socket.getInputStream(),this,jabberHandler); } catch (IOException e) { e.printStackTrace(); } try { output.writeHeader(); } catch (IOException e) { e.printStackTrace(); } throw new UnsupportedOperationException("end xmlparser"); } private Socket createHTTPTunel(String host) throws IOException { String tunnelHost = loginInfo.getProxyServer(); int tunnelPort = Integer.valueOf(loginInfo.getProxyPort()).intValue(); Socket tunnel = new Socket(tunnelHost, tunnelPort); doTunnelHandshake(tunnel,host,loginInfo.getPort()); return tunnel; } private void doTunnelHandshake(Socket tunnel, String host, int port) throws IOException { OutputStream out = tunnel.getOutputStream(); String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\n" + "User-Agent: " + sun.net.www.protocol.http.HttpURLConnection.userAgent; if (loginInfo.getProxyUsername() != null && loginInfo.getProxyPassword() != null) { //add basic authentication header for the proxy sun.misc.BASE64Encoder enc = new sun.misc.BASE64Encoder(); String encodedPassword = enc.encode((loginInfo.getProxyUsername() + ":" + loginInfo.getProxyPassword()).getBytes()); msg = msg + "\nProxy-Authorization: Basic " + encodedPassword; } msg = msg + "\nContent-Length: 0" +"\nPragma: no-cache" +"\r\n\r\n"; byte b[]; try { /* * We really do want ASCII7 -- the http protocol doesn't change * with locale. */ b = msg.getBytes("ASCII7"); } catch (UnsupportedEncodingException ignored) { /* * If ASCII7 isn't there, something serious is wrong, but * Paranoia Is Good (tm) */ b = msg.getBytes(); } out.write(b); out.flush(); /* * We need to store the reply so we can create a detailed error * message to the user. */ byte reply[] = new byte[200]; int replyLen = 0; int newlinesSeen = 0; boolean headerDone = false; /* Done on first newline */ InputStream in = tunnel.getInputStream(); while ( newlinesSeen < 2 ) { int i = in.read(); if (i < 0) { throw new IOException("Unexpected EOF from proxy"); } if (i == '\n') { headerDone = true; ++newlinesSeen; } else if (i != '\r') { newlinesSeen = 0; if (!headerDone && replyLen < reply.length) { reply[replyLen++] = (byte) i; } } } /* * Converting the byte array to a string is slightly wasteful in the * case where the connection was successful, but it's insignificant * compared to the network overhead. */ String replyStr; try { replyStr = new String(reply, 0, replyLen, "ASCII7"); } catch (UnsupportedEncodingException ignored) { replyStr = new String(reply, 0, replyLen); } /* * We check for Connection Established because our proxy returns * HTTP/1.1 instead of 1.0 */ // if (!replyStr.startsWith("HTTP/1.0 200")) { if (replyStr.toLowerCase().indexOf("200 connection established") == -1) { String tunnelHost = System.getProperty("http.proxyHost"); String tunnelPort = System.getProperty("http.proxyPort"); throw new IOException("Unable to tunnel through " + tunnelHost + ":" + tunnelPort + ". Proxy returns \"" + replyStr + "\""); } /* tunneling Handshake was successful! */ } public void sendLoginError(String message) { for(Iterator j = backend.getListeners(LoginListener.class);j.hasNext();) { ((LoginListener)j.next()).loginError(message); } } public void sendUnauthorized() { for(Iterator j = backend.getListeners(LoginListener.class);j.hasNext();) { ((LoginListener)j.next()).unauthorized(); } } synchronized public void connected(String connectionID,String xmppVersion) { if(!abort) { if(xmppVersion!=null && PluginsInfo.isPluginLoaded("xmpp")) { ConnectionPacketReceiver cpr = xmpp.getConnectionPacketReceiver(loginInfo,this); jabberHandler.changePacketReceiver(cpr); cpr.connected(connectionID, xmppVersion); } else { jabberHandler.changePacketReceiver(this); socket = null; //clear socket reference this.connectionID = connectionID; // TODO remove lowercase if filetransfer bug fixed output.send(new InfoQuery(null,"get",new IQAuth(loginInfo.getUsername().toLowerCase(),null,null))); } } } public void authenticate(IQAuth iqAuth) { if(!abort) { authenticationId = "Jeti_Auth_" + new java.util.Date().getTime(); if(iqAuth.hasDigest()) { MessageDigest sha = null; try { sha = MessageDigest.getInstance("SHA"); } catch (Exception ex){ Log.error(I18N.gettext("main.loginstatus.Could_not_login_with_SHA")); // TODO remove lowercase if filetransfer bug fixed output.send(new InfoQuery(null,"set",authenticationId,new IQAuth(loginInfo.getUsername().toLowerCase(),loginInfo.getPassword() ,loginInfo.getResource()))); return; } sha.update(connectionID.getBytes()); String digest = Utils.toString(sha.digest(loginInfo.getPassword().getBytes())); IQAuthBuilder iqab = new IQAuthBuilder(); iqab.digest = digest; // TODO remove lowercase if filetransfer bug fixed iqab.username = loginInfo.getUsername().toLowerCase(); iqab.resource = loginInfo.getResource(); output.send(new InfoQuery(null,"set",authenticationId,(IQExtension)iqab.build())); } else { if(!loginInfo.isSSl()) { int option = JOptionPane.showConfirmDialog(null, I18N.gettext("main.loginstatus.Sending_password_as_plain_text_over_an_unencrypted_connection,_continue?"),"Plain text",JOptionPane.YES_NO_OPTION); if (option == JOptionPane.NO_OPTION) { sendLoginError("Sending password in plain not allowed"); return; } } // TODO remove lowercase if filetransfer bug fixed output.send(new InfoQuery(null,"set",authenticationId,new IQAuth(loginInfo.getUsername().toLowerCase(),loginInfo.getPassword() ,loginInfo.getResource()))); } } } public void authenticated(InfoQuery infoQuery) { if(!abort) { if(infoQuery.getType().equals("error")) { if(infoQuery.getErrorCode() == 401) { sendUnauthorized(); } else { sendLoginError(I18N.gettext("main.loginstatus.Not_logged_in_because") + " " + infoQuery.getErrorDescription()); } return; } // TODO remove lowercase if filetransfer bug fixed JID jid = new JID(loginInfo.getUsername().toLowerCase(),loginInfo.getServer() ,loginInfo.getResource()); authenticated(jid); } } public void authenticated(JID jid) { myJID = jid; iqTimerQueue.clear();//remove iqs from previous connections jabberHandler.changePacketReceiver(new Jabber(backend,capabilities,discovery,iqTimerQueue)); authenticated = true; reconnecting = false; output.setAuthenticated(); //output.send(new InfoQuery(new JID(server),"get",new IQBrowse())); //browse(new JID(loginInfo.getServer()),null); //TODO increase timeout getItems(new JID(loginInfo.getServer()), new DiscoveryListener() { public void discoveryItemResult(JID jid, DiscoveryItem item) {//cache disco items for this server if (item.hasItems()) { for(Iterator i = item.getItems();i.hasNext();) { DiscoveryItem di = (DiscoveryItem)i.next(); backend.getInfo(di.getJID(),null); } } } public void discoveryInfoResult(JID jid, DiscoveryInfo info) {} }); output.send(new InfoQuery("get",new IQPrivate(new JetiPrivateExtension()))); output.send(new InfoQuery("get",new IQXRoster())); if(show == Presence.NONE) { show = Presence.AVAILABLE; //status = Presence.toLongShow(show); } latestConnected =System.currentTimeMillis(); //moved to backend rosterloaded // for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) // {//online // ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(true); // } //changeStatus(show,status); } public void connected() { sendStatus(); send(new InfoQuery("get",new IQPrivate(new JetiPrivateRosterExtension()))); for( Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class) ; j.hasNext(); ) { //online ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(true); } } public void receivePackets(Packet packet) { if (packet instanceof StreamError) streamError((StreamError)packet); else if(authenticationId.equals(packet.getID())) authenticated((InfoQuery)packet); else if(packet instanceof InfoQuery) { IQExtension extension = packet.getIQExtension(); if(extension instanceof IQAuth) { if(((InfoQuery)packet).getType().equals("error")) { sendLoginError(packet.getErrorDescription()); } else authenticate((IQAuth) extension); } } } public void inputDeath() { //if(true)return; something todo with xmpp plugin, should not be here????? or server deads will not be detected try { if( socket != null ) socket.close(); } catch (IOException e) { e.printStackTrace(); } if((authenticated || reconnecting)) { authenticated = false; if (reconnecting) { try { Thread.sleep(10000); } catch (InterruptedException e) {}; } reconnecting = true; output.disconnect(true); for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) { //offline ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(false); } } sendLoginError(I18N.gettext("main.loginstatus.Lost_Input")); //else //io error while logging in //do something usefull } public void streamError(StreamError error) { //only when logging in if(authenticated) { authenticated = false; output.disconnect(true); for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) { //offline ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(false); } } sendLoginError(error.getErrorDescription()); } public void outputDeath() { authenticated = false; for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) { //offline ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(false); } } public static JID getMyJID() { return myJID; } public boolean getOnline() { return authenticated; } public void disconnect() { if( authenticated ) { send(new Presence(myJID, "unavailable")); authenticated = false; output.disconnect(true); } output = null; for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) { //offline ((nu.fw.jeti.events.StatusChangeListener)j.next()).connectionChanged(false); } } public void exit() { if(authenticated) { send(new nu.fw.jeti.jabber.elements.Presence(myJID,"unavailable")); authenticated = false; output.disconnect(true); } output = null; for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) {//exit ((nu.fw.jeti.events.StatusChangeListener)j.next()).exit(); } } public void send(Packet packet) { if(authenticated) output.send(packet); else nu.fw.jeti.util.Log.notSend(packet.toString()); } public void sendWhileConnecting(Packet packet) { output.send(packet); } public String getAccountInfo() { return MessageFormat.format(I18N.gettext("main.popup.logged_in_as_{0}_on_server_{1}_with_resource_{2}"),new Object[]{loginInfo.getUsername(),loginInfo.getServer(),loginInfo.getResource()}); } public void sendStatus() { if( authenticated ) { PresenceBuilder pb = new PresenceBuilder(); pb.show = show; pb.status = status; pb.priority = loginInfo.getPriority(); pb.addExtension(capabilities.getCaps()); try { send(pb.build()); } catch (InstantiationException e) { e.printStackTrace(); } for(Iterator j = backend.getListeners(nu.fw.jeti.events.StatusChangeListener.class);j.hasNext();) { ((nu.fw.jeti.events.StatusChangeListener)j.next()).ownPresenceChanged(show,status); } } } public void changeStatus(int show,String status) { this.show = show; this.status = status; sendStatus(); } public static class DummySSLSocketFactory extends SSLSocketFactory { private SSLSocketFactory factory; public DummySSLSocketFactory() { try { SSLContext sslcontent = SSLContext.getInstance("TLS"); sslcontent.init(null, // KeyManager not required new TrustManager[] {new DummyTrustManager()}, new java.security.SecureRandom()); factory = sslcontent.getSocketFactory(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyManagementException e) { e.printStackTrace(); } } public Socket createSocket(Socket socket, String s, int i,boolean flag) throws IOException { return factory.createSocket(socket, s, i, flag); } public Socket createSocket(InetAddress inaddr, int i,InetAddress inaddr2, int j) throws IOException { return factory.createSocket(inaddr, i, inaddr2, j); } public Socket createSocket(InetAddress inaddr, int i) throws IOException { return factory.createSocket(inaddr, i); } public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException { return factory.createSocket(s, i, inaddr, j); } public Socket createSocket(String s, int i) throws IOException { /* * register a callback for handshaking completion event */ Socket so = factory.createSocket(s, i); ((SSLSocket)so).addHandshakeCompletedListener( new HandshakeCompletedListener() { public void handshakeCompleted( HandshakeCompletedEvent event) { System.out.println("Handshake finished!"); System.out.println( "\t CipherSuite:" + event.getCipherSuite()); System.out.println( "\t SessionId " + event.getSession()); System.out.println( "\t PeerHost " + event.getSession().getPeerHost()); } } ); return so; } public String[] getDefaultCipherSuites() { return factory.getSupportedCipherSuites(); } public String[] getSupportedCipherSuites() { return factory.getSupportedCipherSuites(); } } /** * Trust manager which accepts certificates without any validation * except date validation. */ private static class DummyTrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) { } public void checkServerTrusted(X509Certificate[] chain, String authType) { try { chain[0].checkValidity(); } catch (CertificateExpiredException e) { } catch (CertificateNotYetValidException e) { } } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } } /* * Overrides for emacs * Local variables: * tab-width: 4 * End: */