/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.protocol.ssh;
import java.io.*;
import javax.swing.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.Logger;
import org.osgi.framework.*;
import com.jcraft.jsch.*;
/**
* A SSH implementation of the ProtocolProviderService.
*
* @author Shobhit Jindal
*/
public class ProtocolProviderServiceSSHImpl
extends AbstractProtocolProviderService
{
private static final Logger logger
= Logger.getLogger(ProtocolProviderServiceSSHImpl.class);
/**
* The name of this protocol.
*/
public static final String SSH_PROTOCOL_NAME = ProtocolNames.SSH;
// /**
// * The identifier for SSH Stack
// * Java Secure Channel JSch
// */
// JSch jsch = new JSch();
/**
* The test command given after each command to determine the reply length
* of the command
*/
//private final String testCommand =
// Resources.getString("testCommand");
/**
* A reference to the protocol provider of UIService
*/
private static ServiceReference ppUIServiceRef;
/**
* Connection timeout to a remote server in milliseconds
*/
private static int connectionTimeout = 30000;
/**
* A reference to UI Service
*/
private static UIService uiService;
/**
* The id of the account that this protocol provider represents.
*/
private AccountID accountID = null;
/**
* We use this to lock access to initialization.
*/
private final Object initializationLock = new Object();
private OperationSetBasicInstantMessagingSSHImpl basicInstantMessaging;
private OperationSetFileTransferSSHImpl fileTranfer;
/**
* Indicates whether or not the provider is initialized and ready for use.
*/
private boolean isInitialized = false;
/**
* The logo corresponding to the ssh protocol.
*/
private ProtocolIconSSHImpl sshIcon
= new ProtocolIconSSHImpl();
/**
* The registration state of SSH Provider is taken to be registered by
* default as it doesn't correspond to the state on remote server
*/
private RegistrationState currentRegistrationState
= RegistrationState.REGISTERED;
/**
* The default constructor for the SSH protocol provider.
*/
public ProtocolProviderServiceSSHImpl()
{
if (logger.isTraceEnabled())
logger.trace("Creating a ssh provider.");
try
{
// converting to milliseconds
connectionTimeout = Integer.parseInt(Resources.getString(
"connectionTimeout")) * 1000;
}
catch(NumberFormatException ex)
{
logger.error("Connection Timeout set to 30 seconds");
}
}
/**
* Initializes the service implementation, and puts it in a sate where it
* could interoperate with other services. It is strongly recomended that
* properties in this Map be mapped to property names as specified by
* AccountProperties.
*
* @param userID the user id of the ssh account we're currently
* initializing
* @param accountID the identifier of the account that this protocol
* provider represents.
*
* @see net.java.sip.communicator.service.protocol.AccountID
*/
protected void initialize(
String userID,
AccountID accountID)
{
synchronized(initializationLock)
{
this.accountID = accountID;
//initialize the presence operationset
OperationSetPersistentPresenceSSHImpl persistentPresence =
new OperationSetPersistentPresenceSSHImpl(this);
addSupportedOperationSet(
OperationSetPersistentPresence.class,
persistentPresence);
//register it once again for those that simply need presence and
//won't be smart enough to check for a persistent presence
//alternative
addSupportedOperationSet(
OperationSetPresence.class,
persistentPresence);
//initialize the IM operation set
basicInstantMessaging = new
OperationSetBasicInstantMessagingSSHImpl(
this);
addSupportedOperationSet(
OperationSetBasicInstantMessaging.class,
basicInstantMessaging);
//initialze the file transfer operation set
fileTranfer = new OperationSetFileTransferSSHImpl(this);
addSupportedOperationSet(
OperationSetFileTransfer.class,
fileTranfer);
isInitialized = true;
}
}
/**
* Determines whether a vaild session exists for the contact of remote
* machine.
*
* @param sshContact ID of SSH Contact
*
* @return true if the session is connected
* false otherwise
*/
public boolean isSessionValid(ContactSSH sshContact)
{
Session sshSession = sshContact.getSSHSession();
if( sshSession != null)
if(sshSession.isConnected())
return true;
// remove reference to an unconnected SSH Session, if any
sshContact.setSSHSession(null);
return false;
}
/**
* Determines whether the contact is connected to shell of remote machine
* as a precheck for any further operation
*
* @param sshContact ID of SSH Contact
*
* @return true if the contact is connected
* false if the contact is not connected
*/
public boolean isShellConnected(ContactSSH sshContact)
{
// a test command may also be run here
if(isSessionValid(sshContact))
{
return(sshContact.getShellChannel() != null);
}
/*
* Above should be return(sshContact.getShellChannel() != null
* && sshContact.getShellChannel().isConnected());
*
* but incorrect reply from stack for isConnected()
*/
return false;
}
/**
* Creates a shell channel to the remote machine
* a new jsch session is also created if the current one is invalid
*
* @param sshContact the contact of the remote machine
* @param firstMessage the first message
*/
public void connectShell(
final ContactSSH sshContact,
final Message firstMessage)
{
sshContact.setConnectionInProgress(true);
final Thread newConnection = new Thread((new Runnable()
{
public void run()
{
OperationSetPersistentPresenceSSHImpl persistentPresence
= (OperationSetPersistentPresenceSSHImpl)sshContact
.getParentPresenceOperationSet();
persistentPresence.changeContactPresenceStatus(
sshContact,
SSHStatusEnum.CONNECTING);
try
{
if(!isSessionValid(sshContact))
createSSHSessionAndLogin(sshContact);
createShellChannel(sshContact);
//initializing the reader and writers of ssh contact
persistentPresence.changeContactPresenceStatus(
sshContact,
SSHStatusEnum.CONNECTED);
showWelcomeMessage(sshContact);
sshContact.setMessageType(ContactSSH
.CONVERSATION_MESSAGE_RECEIVED);
sshContact.setConnectionInProgress(false);
Thread.sleep(1500);
sshContact.setCommandSent(true);
basicInstantMessaging.sendInstantMessage(
sshContact,
firstMessage);
}
// rigorous Exception Checking in future
catch (Exception ex)
{
persistentPresence.changeContactPresenceStatus(
sshContact,
SSHStatusEnum.NOT_AVAILABLE);
ex.printStackTrace();
}
finally
{
sshContact.setConnectionInProgress(false);
}
}
}));
newConnection.start();
}
/**
* Creates a channel for shell type in the current session
* channel types = shell, sftp, exec(X forwarding),
* direct-tcpip(stream forwarding) etc
*
* @param sshContact ID of SSH Contact
* @throws IOException if the shell channel cannot be created
*/
public void createShellChannel(ContactSSH sshContact)
throws IOException
{
try
{
Channel shellChannel = sshContact.getSSHSession()
.openChannel("shell");
//initalizing the reader and writers of ssh contact
sshContact.initializeShellIO(shellChannel.getInputStream(),
shellChannel.getOutputStream());
((ChannelShell)shellChannel).setPtyType(
sshContact.getSSHConfigurationForm().getTerminalType());
//initializing the shell
shellChannel.connect(1000);
sshContact.setShellChannel(shellChannel);
sshContact.sendLine("export PS1=");
}
catch (JSchException ex)
{
sshContact.setSSHSession(null);
throw new IOException("Unable to create shell channel to remote" +
" server");
}
}
/**
* Closes the Shell channel are associated IO Streams
*
* @param sshContact ID of SSH Contact
* @throws JSchException if something went wrong in JSch
* @throws IOException if I/O exception occurred
*/
public void closeShellChannel(ContactSSH sshContact) throws
JSchException,
IOException
{
sshContact.closeShellIO();
sshContact.getShellChannel().disconnect();
sshContact.setShellChannel(null);
}
/**
* Creates a SSH Session with a remote machine and tries to login
* according to the details specified by Contact
* An appropriate message is shown to the end user in case the login fails
*
* @param sshContact ID of SSH Contact
*
* @throws JSchException if a JSch is unable to create a SSH Session with
* the remote machine
* @throws InterruptedException if the thread is interrupted before session
* connected or is timed out
* @throws OperationFailedException if not of above reasons :-)
*/
public void createSSHSessionAndLogin(ContactSSH sshContact) throws
JSchException,
OperationFailedException,
InterruptedException
{
if (logger.isInfoEnabled())
logger.info("Creating a new SSH Session to "
+ sshContact.getHostName());
// creating a new JSch Stack identifier for contact
JSch jsch = new JSch();
String knownHosts =
accountID.getAccountPropertyString("KNOWN_HOSTS_FILE");
if(!knownHosts.equals("Optional"))
jsch.setKnownHosts(knownHosts);
String identitiyKey =
accountID.getAccountPropertyString("IDENTITY_FILE");
String userName = sshContact.getUserName();
// use the name of system user if the contact has not supplied SSH
// details
if(userName.equals(""))
userName = System.getProperty("user.name");
if(!identitiyKey.equals("Optional"))
jsch.addIdentity(identitiyKey);
// creating a new session for the contact
Session session = jsch.getSession(
userName,
sshContact.getHostName(),
sshContact.getSSHConfigurationForm().getPort());
/**
* Creating and associating User Info with the session
* User Info passes authentication from sshContact to SSH Stack
*/
SSHUserInfo sshUserInfo = new SSHUserInfo(sshContact);
session.setUserInfo(sshUserInfo);
/**
* initializing the session
*/
session.connect(connectionTimeout);
int count = 0;
// wait for session to get connected
while(!session.isConnected() && count<=30000)
{
Thread.sleep(1000);
count += 1000;
if (logger.isTraceEnabled())
logger.trace("SSH:" + sshContact.getHostName()
+ ": Sleep zzz .. " );
}
// if timeout have exceeded
if(count>30000)
{
sshContact.setSSHSession(null);
JOptionPane.showMessageDialog(
null,
"SSH Connection attempt to "
+ sshContact.getHostName() + " timed out");
// error codes are not defined yet
throw new OperationFailedException("SSH Connection attempt to " +
sshContact.getHostName() + " timed out", 2);
}
sshContact.setJSch(jsch);
sshContact.setSSHSession(session);
if (logger.isInfoEnabled())
logger.info("A new SSH Session to " + sshContact.getHostName()
+ " Created");
}
/**
* Closes the SSH Session associated with the contact
*
* @param sshContact ID of SSH Contact
*/
void closeSSHSession(ContactSSH sshContact)
{
sshContact.getSSHSession().disconnect();
sshContact.setSSHSession(null);
}
/**
* Presents the login welcome message to user
*
* @param sshContact ID of SSH Contact
* @throws IOException if I/O exception occurred
*/
public void showWelcomeMessage(ContactSSH sshContact)
throws IOException
{
/* //sending the command
sshContact.sendLine(testCommand);
String reply = "", line = "";
// message is extracted until the test Command ie echoed back
while(line.indexOf(testCommand) == -1)
{
reply += line + "\n";
line = sshContact.getLine();
}
uiService.getPopupDialog().showMessagePopupDialog
(reply,"Message from " + sshContact.getDisplayName(),
uiService.getPopupDialog().INFORMATION_MESSAGE);
if(line.startsWith(testCommand))
while(!sshContact.getLine().contains(testCommand));
//one line output of testCommand
sshContact.getLine();
*/
if (logger.isDebugEnabled())
logger.debug("SSH: Welcome message shown");
}
/**
* Returns a reference to UIServce for accessing UI related services
*
* @return uiService a reference to UIService
*/
public static UIService getUIService()
{
return uiService;
}
/**
* Returns the AccountID that uniquely identifies the account represented
* by this instance of the ProtocolProviderService.
*
* @return the id of the account represented by this provider.
*/
public AccountID getAccountID()
{
return accountID;
}
/**
* Returns the short name of the protocol that the implementation of this
* provider is based upon (like SIP, Jabber, ICQ/AIM, or others for
* example).
*
* @return a String containing the short name of the protocol this
* service is implementing (most often that would be a name in
* ProtocolNames).
*/
public String getProtocolName()
{
return SSH_PROTOCOL_NAME;
}
/**
* Returns the state of the registration of this protocol provider with
* the corresponding registration service.
*
* @return ProviderRegistrationState
*/
public RegistrationState getRegistrationState()
{
return currentRegistrationState;
}
/**
* Starts the registration process.
*
* @param authority the security authority that will be used for
* resolving any security challenges that may be returned during the
* registration or at any moment while wer're registered.
* @throws OperationFailedException with the corresponding code it the
* registration fails for some reason (e.g. a networking error or an
* implementation problem).
*/
public void register(SecurityAuthority authority)
throws OperationFailedException
{
RegistrationState oldState = currentRegistrationState;
currentRegistrationState = RegistrationState.REGISTERED;
//get a reference to UI Service via its Service Reference
ppUIServiceRef = SSHActivator.getBundleContext()
.getServiceReference(UIService.class.getName());
uiService = (UIService)SSHActivator.getBundleContext()
.getService(ppUIServiceRef);
fireRegistrationStateChanged(
oldState
, currentRegistrationState
, RegistrationStateChangeEvent.REASON_USER_REQUEST
, null);
}
/**
* Makes the service implementation close all open sockets and release
* any resources that it might have taken and prepare for
* shutdown/garbage collection.
*/
public void shutdown()
{
if(!isInitialized)
{
return;
}
if (logger.isTraceEnabled())
logger.trace("Killing the SSH Protocol Provider.");
if(isRegistered())
{
try
{
//do the unregistration
unregister();
}
catch (OperationFailedException ex)
{
//we're shutting down so we need to silence the exception here
logger.error(
"Failed to properly unregister before shutting down. "
+ getAccountID()
, ex);
}
}
isInitialized = false;
}
/**
* Ends the registration of this protocol provider with the current
* registration service.
*
* @throws OperationFailedException with the corresponding code it the
* registration fails for some reason (e.g. a networking error or an
* implementation problem).
*/
public void unregister()
throws OperationFailedException
{
RegistrationState oldState = currentRegistrationState;
currentRegistrationState = RegistrationState.UNREGISTERED;
fireRegistrationStateChanged(
oldState
, currentRegistrationState
, RegistrationStateChangeEvent.REASON_USER_REQUEST
, null);
}
/*
* (non-Javadoc)
*
* @see net.java.sip.communicator.service.protocol.ProtocolProviderService#
* isSignallingTransportSecure()
*/
public boolean isSignalingTransportSecure()
{
return false;
}
/**
* Returns the "transport" protocol of this instance used to carry the
* control channel for the current protocol service.
*
* @return The "transport" protocol of this instance: TCP.
*/
public TransportProtocol getTransportProtocol()
{
return TransportProtocol.TCP;
}
/**
* Returns the ssh protocol icon.
* @return the ssh protocol icon
*/
public ProtocolIcon getProtocolIcon()
{
return sshIcon;
}
}